Implement idle watcher

This commit is contained in:
Demmie 2023-03-31 01:41:26 -04:00
commit 24254d5a51
No known key found for this signature in database
GPG Key ID: B06DAA3D432C6E9A
8 changed files with 1797 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1511
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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");
}