mirror of
https://github.com/2e3s/awatcher.git
synced 2025-06-06 19:45:30 +00:00
Implement idle watcher
This commit is contained in:
commit
24254d5a51
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
1511
Cargo.lock
generated
Normal file
1511
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "aw-watcher"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aw-client-rust = { git = "https://github.com/ActivityWatch/aw-server-rust" }
|
||||||
|
gethostname = "0.4.1"
|
||||||
|
wayland-client = "0.30.1"
|
||||||
|
wayland-scanner = "0.30"
|
||||||
|
wayland-backend = "0.1"
|
||||||
|
chrono = "0.4.24"
|
||||||
|
serde_json = "1.0.95"
|
17
src/config.rs
Normal file
17
src/config.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
pub struct Config {
|
||||||
|
pub port: u32,
|
||||||
|
pub host: String,
|
||||||
|
pub timeout_ms: u32,
|
||||||
|
pub poll_time: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
port: 5600,
|
||||||
|
host: String::from("localhost"),
|
||||||
|
timeout_ms: 3000,
|
||||||
|
poll_time: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
164
src/kwin_idle.rs
Normal file
164
src/kwin_idle.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use super::config::Config;
|
||||||
|
use super::wl_bindings;
|
||||||
|
use aw_client_rust::{AwClient, Event as AwEvent};
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::{thread, time};
|
||||||
|
use wayland_client::Proxy;
|
||||||
|
use wayland_client::{
|
||||||
|
globals::{registry_queue_init, GlobalListContents},
|
||||||
|
protocol::wl_registry,
|
||||||
|
protocol::wl_seat::WlSeat,
|
||||||
|
Connection, Dispatch, QueueHandle,
|
||||||
|
};
|
||||||
|
use wl_bindings::idle::org_kde_kwin_idle::OrgKdeKwinIdle;
|
||||||
|
use wl_bindings::idle::org_kde_kwin_idle_timeout::Event as OrgKdeKwinIdleTimeoutEvent;
|
||||||
|
use wl_bindings::idle::org_kde_kwin_idle_timeout::OrgKdeKwinIdleTimeout;
|
||||||
|
|
||||||
|
struct IdleState {
|
||||||
|
idle_timeout: OrgKdeKwinIdleTimeout,
|
||||||
|
start_time: DateTime<Utc>,
|
||||||
|
is_idle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for IdleState {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
println!("Releasing idle timeout");
|
||||||
|
self.idle_timeout.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdleState {
|
||||||
|
fn idle(&mut self) {
|
||||||
|
self.is_idle = true;
|
||||||
|
self.start_time = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resume(&mut self) {
|
||||||
|
self.is_idle = false;
|
||||||
|
self.start_time = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! subscribe_state {
|
||||||
|
($struct_name:ty, $data_name:ty) => {
|
||||||
|
impl Dispatch<$struct_name, $data_name> for IdleState {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &$struct_name,
|
||||||
|
_: <$struct_name as Proxy>::Event,
|
||||||
|
_: &$data_name,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe_state!(wl_registry::WlRegistry, ());
|
||||||
|
subscribe_state!(WlSeat, ());
|
||||||
|
subscribe_state!(OrgKdeKwinIdle, ());
|
||||||
|
subscribe_state!(wl_registry::WlRegistry, GlobalListContents);
|
||||||
|
|
||||||
|
impl Dispatch<OrgKdeKwinIdleTimeout, ()> for IdleState {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &OrgKdeKwinIdleTimeout,
|
||||||
|
event: <OrgKdeKwinIdleTimeout as Proxy>::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let OrgKdeKwinIdleTimeoutEvent::Idle = event {
|
||||||
|
state.idle();
|
||||||
|
println!("Idle");
|
||||||
|
}
|
||||||
|
if let OrgKdeKwinIdleTimeoutEvent::Resumed = event {
|
||||||
|
state.resume();
|
||||||
|
println!("Resumed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_heartbeat(client: &AwClient, state: &IdleState, bucket_name: &String, config: &Config) {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let timestamp = match state.is_idle {
|
||||||
|
true => now,
|
||||||
|
false => {
|
||||||
|
let last_guaranteed_activity = now - Duration::milliseconds(config.timeout_ms as i64);
|
||||||
|
match last_guaranteed_activity > state.start_time {
|
||||||
|
true => last_guaranteed_activity,
|
||||||
|
false => state.start_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data = Map::new();
|
||||||
|
let json_afk_state = match state.is_idle {
|
||||||
|
true => Value::String("afk".to_string()),
|
||||||
|
false => Value::String("not-afk".to_string()),
|
||||||
|
};
|
||||||
|
data.insert("status".to_string(), json_afk_state);
|
||||||
|
|
||||||
|
let event = AwEvent {
|
||||||
|
id: None,
|
||||||
|
timestamp,
|
||||||
|
duration: Duration::zero(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
let interval_margin: f64 = (config.poll_time + 1) as f64 / 1.0;
|
||||||
|
if client
|
||||||
|
.heartbeat(&bucket_name, &event, interval_margin)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
println!("Failed to send heartbeat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(client: &AwClient, conf: &Config) {
|
||||||
|
let hostname = gethostname::gethostname().into_string().unwrap();
|
||||||
|
let bucket_name = format!("aw-watcher-afk_{}", hostname);
|
||||||
|
|
||||||
|
client
|
||||||
|
.create_bucket_simple(&bucket_name, "afkstatus")
|
||||||
|
.expect("Failed to create afk bucket");
|
||||||
|
|
||||||
|
println!("Starting activity watcher");
|
||||||
|
|
||||||
|
let conn = Connection::connect_to_env().expect("Unable to connect to Wayland compositor");
|
||||||
|
let display = conn.display();
|
||||||
|
let (globals, mut event_queue) = registry_queue_init::<IdleState>(&conn).unwrap();
|
||||||
|
|
||||||
|
let qh = event_queue.handle();
|
||||||
|
|
||||||
|
let _registry = display.get_registry(&qh, ());
|
||||||
|
|
||||||
|
let seat: WlSeat = globals
|
||||||
|
.bind(&event_queue.handle(), 1..=WlSeat::interface().version, ())
|
||||||
|
.unwrap();
|
||||||
|
let idle: OrgKdeKwinIdle = globals
|
||||||
|
.bind(
|
||||||
|
&event_queue.handle(),
|
||||||
|
1..=OrgKdeKwinIdle::interface().version,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let idle_timeout = idle.get_idle_timeout(&seat, 3000, &qh, ());
|
||||||
|
|
||||||
|
let mut app_state = IdleState {
|
||||||
|
idle_timeout,
|
||||||
|
is_idle: false,
|
||||||
|
start_time: Utc::now(),
|
||||||
|
};
|
||||||
|
event_queue.roundtrip(&mut app_state).unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
event_queue.blocking_dispatch(&mut app_state).unwrap();
|
||||||
|
send_heartbeat(client, &app_state, &bucket_name, conf);
|
||||||
|
thread::sleep(time::Duration::from_secs(conf.poll_time as u64));
|
||||||
|
}
|
||||||
|
}
|
21
src/main.rs
Normal file
21
src/main.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// extern crate wayland_client;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod kwin_idle;
|
||||||
|
mod wl_bindings;
|
||||||
|
|
||||||
|
use aw_client_rust::AwClient;
|
||||||
|
use config::Config;
|
||||||
|
use kwin_idle::run as run_kwin_idle;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let conf = Config::default();
|
||||||
|
let client = AwClient::new(&conf.host, &conf.port.to_string(), "aw-watcher");
|
||||||
|
|
||||||
|
let idle_handler = thread::spawn(move || {
|
||||||
|
run_kwin_idle(&client, &conf);
|
||||||
|
});
|
||||||
|
|
||||||
|
idle_handler.join().expect("Error in the idle processing");
|
||||||
|
}
|
49
src/protocols/idle.xml
Normal file
49
src/protocols/idle.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="idle">
|
||||||
|
<copyright><![CDATA[
|
||||||
|
Copyright (C) 2015 Martin Gräßlin
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2.1 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]></copyright>
|
||||||
|
<interface name="org_kde_kwin_idle" version="1">
|
||||||
|
<description summary="User idle time manager">
|
||||||
|
This interface allows to monitor user idle time on a given seat. The interface
|
||||||
|
allows to register timers which trigger after no user activity was registered
|
||||||
|
on the seat for a given interval. It notifies when user activity resumes.
|
||||||
|
|
||||||
|
This is useful for applications wanting to perform actions when the user is not
|
||||||
|
interacting with the system, e.g. chat applications setting the user as away, power
|
||||||
|
management features to dim screen, etc..
|
||||||
|
</description>
|
||||||
|
<request name="get_idle_timeout">
|
||||||
|
<arg name="id" type="new_id" interface="org_kde_kwin_idle_timeout"/>
|
||||||
|
<arg name="seat" type="object" interface="wl_seat"/>
|
||||||
|
<arg name="timeout" type="uint" summary="The idle timeout in msec"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
<interface name="org_kde_kwin_idle_timeout" version="1">
|
||||||
|
<request name="release" type="destructor">
|
||||||
|
<description summary="release the timeout object"/>
|
||||||
|
</request>
|
||||||
|
<request name="simulate_user_activity">
|
||||||
|
<description summary="Simulates user activity for this timeout, behaves just like real user activity on the seat"/>
|
||||||
|
</request>
|
||||||
|
<event name="idle">
|
||||||
|
<description summary="Triggered when there has not been any user activity in the requested idle time interval"/>
|
||||||
|
</event>
|
||||||
|
<event name="resumed">
|
||||||
|
<description summary="Triggered on the first user activity after an idle event"/>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
21
src/wl_bindings.rs
Normal file
21
src/wl_bindings.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#![forbid(improper_ctypes, unsafe_op_in_unsafe_fn)]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||||
|
|
||||||
|
pub mod idle {
|
||||||
|
#![allow(dead_code,non_camel_case_types,unused_unsafe,unused_variables)]
|
||||||
|
#![allow(non_upper_case_globals,non_snake_case,unused_imports)]
|
||||||
|
#![allow(missing_docs, clippy::all)]
|
||||||
|
|
||||||
|
//! Client-side API of this protocol
|
||||||
|
use wayland_client;
|
||||||
|
use wayland_client::protocol::*;
|
||||||
|
|
||||||
|
pub mod __interfaces {
|
||||||
|
use wayland_client::protocol::__interfaces::*;
|
||||||
|
wayland_scanner::generate_interfaces!("src/protocols/idle.xml");
|
||||||
|
}
|
||||||
|
use self::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_client_code!("src/protocols/idle.xml");
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user