mirror of
https://github.com/2e3s/awatcher.git
synced 2025-06-06 11:35:46 +00:00
Implement idle watcher for X11
This commit is contained in:
parent
65ce731f03
commit
5c1b98d75f
76
Cargo.lock
generated
76
Cargo.lock
generated
@ -194,7 +194,7 @@ source = "git+https://github.com/ActivityWatch/aw-server-rust#1f4f07d86c06de89de
|
||||
dependencies = [
|
||||
"aw-models",
|
||||
"chrono",
|
||||
"gethostname",
|
||||
"gethostname 0.4.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -220,12 +220,13 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"fern",
|
||||
"gethostname",
|
||||
"gethostname 0.4.1",
|
||||
"log",
|
||||
"serde_json",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
"x11rb",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@ -713,6 +714,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.4.1"
|
||||
@ -1008,6 +1019,15 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.7.1"
|
||||
@ -1053,6 +1073,19 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.2"
|
||||
@ -1062,7 +1095,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.7.1",
|
||||
"pin-utils",
|
||||
"static_assertions",
|
||||
]
|
||||
@ -1927,7 +1960,7 @@ dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"io-lifetimes",
|
||||
"nix",
|
||||
"nix 0.26.2",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
@ -1940,7 +1973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85bde68449abab1a808e5227b6e295f4ae3680911eb7711b4a2cb90141edb780"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"nix",
|
||||
"nix 0.26.2",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
@ -2002,6 +2035,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-wsapoll"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@ -2188,6 +2230,28 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdf3c79412dd91bae7a7366b8ad1565a85e35dd049affc3a6a2c549e97419617"
|
||||
dependencies = [
|
||||
"gethostname 0.2.3",
|
||||
"nix 0.25.1",
|
||||
"winapi",
|
||||
"winapi-wsapoll",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1513b141123073ce54d5bb1d33f801f17508fbd61e02060b1214e96d39c56"
|
||||
dependencies = [
|
||||
"nix 0.25.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "3.11.1"
|
||||
@ -2211,7 +2275,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.26.2",
|
||||
"once_cell",
|
||||
"ordered-stream",
|
||||
"rand",
|
||||
|
@ -15,3 +15,4 @@ zbus = "3.11.1"
|
||||
clap = "4.2.1"
|
||||
log = { version = "0.4.17", features = ["std"] }
|
||||
fern = { version = "0.6.2", features = ["colored"] }
|
||||
x11rb = { version = "0.11.1", features = ["screensaver"] }
|
||||
|
@ -1,13 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use clap::{arg, value_parser, Command};
|
||||
|
||||
pub struct Config {
|
||||
pub port: u32,
|
||||
pub host: String,
|
||||
pub idle_timeout: u32,
|
||||
pub poll_time_idle: u32,
|
||||
pub poll_time_window: u32,
|
||||
pub idle_timeout: Duration,
|
||||
pub poll_time_idle: Duration,
|
||||
pub poll_time_window: Duration,
|
||||
pub idle_bucket_name: String,
|
||||
pub active_window_bucket_name: String,
|
||||
}
|
||||
@ -40,13 +40,16 @@ impl Config {
|
||||
let hostname = gethostname::gethostname().into_string().unwrap();
|
||||
let idle_bucket_name = format!("aw-watcher-afk_{hostname}");
|
||||
let active_window_bucket_name = format!("aw-watcher-window_{hostname}");
|
||||
let poll_seconds_idle = *matches.get_one::<u32>("poll-time-idle").unwrap();
|
||||
let poll_seconds_window = *matches.get_one::<u32>("poll-time-window").unwrap();
|
||||
let idle_timeout_seconds = *matches.get_one::<u32>("idle-timeout").unwrap();
|
||||
|
||||
Self {
|
||||
port: *matches.get_one("port").unwrap(),
|
||||
host: String::clone(matches.get_one("host").unwrap()),
|
||||
idle_timeout: *matches.get_one("idle-timeout").unwrap(),
|
||||
poll_time_idle: *matches.get_one("poll-time-idle").unwrap(),
|
||||
poll_time_window: *matches.get_one("poll-time-window").unwrap(),
|
||||
idle_timeout: Duration::from_secs(u64::from(idle_timeout_seconds)),
|
||||
poll_time_idle: Duration::from_secs(u64::from(poll_seconds_idle)),
|
||||
poll_time_window: Duration::from_secs(u64::from(poll_seconds_window)),
|
||||
idle_bucket_name,
|
||||
active_window_bucket_name,
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ use std::env::temp_dir;
|
||||
use std::path::Path;
|
||||
use std::sync::{mpsc::channel, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use zbus::blocking::{Connection, ConnectionBuilder};
|
||||
use zbus::dbus_interface;
|
||||
|
||||
@ -196,9 +195,7 @@ impl Watcher for WindowWatcher {
|
||||
if let Err(error) = send_active_window(client, &active_window) {
|
||||
error!("Error on sending active window heartbeat: {error}");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
client.config.poll_time_window,
|
||||
)));
|
||||
thread::sleep(client.config.poll_time_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
src/main.rs
15
src/main.rs
@ -10,6 +10,7 @@ mod wl_bindings;
|
||||
mod wl_connection;
|
||||
mod wl_foreign_toplevel;
|
||||
mod wl_kwin_idle;
|
||||
mod x11_screensaver_idle;
|
||||
|
||||
use config::Config;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
@ -18,6 +19,7 @@ use report_client::ReportClient;
|
||||
use std::env;
|
||||
use std::{error::Error, str::FromStr, sync::Arc, thread};
|
||||
use wl_kwin_idle::IdleWatcher as WlKwinIdleWatcher;
|
||||
use x11_screensaver_idle::IdleWatcher as X11IdleWatcher;
|
||||
|
||||
use crate::wl_foreign_toplevel::WindowWatcher as WlrForeignToplevelWindowWatcher;
|
||||
|
||||
@ -59,7 +61,8 @@ macro_rules! watcher {
|
||||
};
|
||||
}
|
||||
|
||||
const IDLE_WATCHERS: &[WatcherConstructor] = &[watcher!(WlKwinIdleWatcher)];
|
||||
const IDLE_WATCHERS: &[WatcherConstructor] =
|
||||
&[watcher!(WlKwinIdleWatcher), watcher!(X11IdleWatcher)];
|
||||
|
||||
const ACTIVE_WINDOW_WATCHERS: &[WatcherConstructor] = &[
|
||||
watcher!(WlrForeignToplevelWindowWatcher),
|
||||
@ -103,8 +106,14 @@ fn main() -> Result<(), BoxedError> {
|
||||
"Sending to server {}:{}",
|
||||
client.config.host, client.config.port
|
||||
);
|
||||
info!("Idle timeout: {} seconds", client.config.idle_timeout);
|
||||
info!("Polling period: {} seconds", client.config.poll_time_idle);
|
||||
info!(
|
||||
"Idle timeout: {} seconds",
|
||||
client.config.idle_timeout.as_secs()
|
||||
);
|
||||
info!(
|
||||
"Polling period: {} seconds",
|
||||
client.config.poll_time_idle.as_secs()
|
||||
);
|
||||
|
||||
let mut thread_handlers = Vec::new();
|
||||
|
||||
|
@ -39,7 +39,7 @@ impl ReportClient {
|
||||
data,
|
||||
};
|
||||
|
||||
let pulsetime = f64::from(self.config.idle_timeout + self.config.poll_time_idle);
|
||||
let pulsetime = (self.config.idle_timeout + self.config.poll_time_idle).as_secs_f64();
|
||||
self.client
|
||||
.heartbeat(&self.config.idle_bucket_name, &event, pulsetime)
|
||||
.map_err(|_| "Failed to send heartbeat")?;
|
||||
@ -58,7 +58,7 @@ impl ReportClient {
|
||||
data,
|
||||
};
|
||||
|
||||
let interval_margin: f64 = f64::from(self.config.poll_time_idle + 1);
|
||||
let interval_margin = self.config.poll_time_idle.as_secs_f64() + 1.0;
|
||||
self.client
|
||||
.heartbeat(
|
||||
&self.config.active_window_bucket_name,
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{sync::Arc, thread, time};
|
||||
use std::{sync::Arc, thread};
|
||||
|
||||
use crate::{wl_connection::subscribe_state, Watcher};
|
||||
|
||||
@ -160,9 +160,7 @@ impl Watcher for WindowWatcher {
|
||||
error!("Error on iteration: {e}");
|
||||
}
|
||||
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
client.config.poll_time_window,
|
||||
)));
|
||||
thread::sleep(client.config.poll_time_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use super::wl_bindings;
|
||||
use super::wl_connection::{subscribe_state, WlEventConnection};
|
||||
use super::BoxedError;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use std::{sync::Arc, thread, time};
|
||||
use std::{sync::Arc, thread};
|
||||
use wayland_client::{
|
||||
globals::GlobalListContents,
|
||||
protocol::{wl_registry, wl_seat::WlSeat},
|
||||
@ -131,9 +131,10 @@ impl Watcher for IdleWatcher {
|
||||
}
|
||||
|
||||
fn watch(&mut self, client: &Arc<ReportClient>) {
|
||||
let timeout = u32::try_from(client.config.idle_timeout.as_secs() * 1000);
|
||||
let mut idle_state = IdleState::new(
|
||||
self.connection
|
||||
.get_kwin_idle_timeout(client.config.idle_timeout * 1000)
|
||||
.get_kwin_idle_timeout(timeout.unwrap())
|
||||
.unwrap(),
|
||||
Arc::clone(client),
|
||||
);
|
||||
@ -149,9 +150,7 @@ impl Watcher for IdleWatcher {
|
||||
} else if let Err(e) = idle_state.send_ping() {
|
||||
error!("Error on idle iteration: {e}");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
client.config.poll_time_idle,
|
||||
)));
|
||||
thread::sleep(client.config.poll_time_idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
93
src/x11_screensaver_idle.rs
Normal file
93
src/x11_screensaver_idle.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use std::{env, sync::Arc, thread};
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::screensaver::ConnectionExt;
|
||||
use x11rb::rust_connection::RustConnection;
|
||||
|
||||
use crate::{report_client::ReportClient, BoxedError, Watcher};
|
||||
|
||||
pub struct IdleWatcher {
|
||||
connection: RustConnection,
|
||||
screen_root: u32,
|
||||
}
|
||||
|
||||
impl IdleWatcher {
|
||||
fn seconds_since_last_input(&self) -> Result<u32, BoxedError> {
|
||||
let a = self.connection.screensaver_query_info(self.screen_root)?;
|
||||
let b = a.reply()?;
|
||||
|
||||
Ok(b.ms_since_user_input / 1000)
|
||||
}
|
||||
|
||||
fn run(&self, is_idle: bool, client: &Arc<ReportClient>) -> Result<bool, BoxedError> {
|
||||
// The logic is rewritten from the original Python code:
|
||||
// https://github.com/ActivityWatch/aw-watcher-afk/blob/ef531605cd8238e00138bbb980e5457054e05248/aw_watcher_afk/afk.py#L73
|
||||
let duration_1ms: Duration = Duration::milliseconds(1);
|
||||
let duration_zero: Duration = Duration::zero();
|
||||
|
||||
let seconds_since_input = self.seconds_since_last_input()?;
|
||||
let now = Utc::now();
|
||||
let time_since_input = Duration::seconds(i64::from(seconds_since_input));
|
||||
let last_input = now - time_since_input;
|
||||
let mut is_idle_again = is_idle;
|
||||
|
||||
if is_idle && u64::from(seconds_since_input) < client.config.idle_timeout.as_secs() {
|
||||
debug!("No longer idle");
|
||||
client.ping(is_idle, last_input, duration_zero)?;
|
||||
is_idle_again = false;
|
||||
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
|
||||
client.ping(is_idle, last_input + duration_1ms, duration_zero)?;
|
||||
} else if !is_idle && u64::from(seconds_since_input) >= client.config.idle_timeout.as_secs()
|
||||
{
|
||||
debug!("Idle again");
|
||||
client.ping(is_idle, last_input, duration_zero)?;
|
||||
is_idle_again = true;
|
||||
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
|
||||
client.ping(is_idle, last_input + duration_1ms, time_since_input)?;
|
||||
} else {
|
||||
// Send a heartbeat if no state change was made
|
||||
if is_idle {
|
||||
trace!("Reporting as idle");
|
||||
client.ping(is_idle, last_input, time_since_input)?;
|
||||
} else {
|
||||
trace!("Reporting as not idle");
|
||||
client.ping(is_idle, last_input, duration_zero)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(is_idle_again)
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher for IdleWatcher {
|
||||
fn new() -> Result<Self, BoxedError> {
|
||||
if env::var("DISPLAY").is_err() {
|
||||
warn!("DISPLAY is not set, setting to the default value \":0\"");
|
||||
env::set_var("DISPLAY", ":0");
|
||||
}
|
||||
|
||||
let (connection, screen_num) = x11rb::connect(None)?;
|
||||
let screen_root = connection.setup().roots[screen_num].root;
|
||||
|
||||
Ok(IdleWatcher {
|
||||
connection,
|
||||
screen_root,
|
||||
})
|
||||
}
|
||||
|
||||
fn watch(&mut self, client: &Arc<ReportClient>) {
|
||||
info!("Starting idle watcher");
|
||||
let mut is_idle = false;
|
||||
loop {
|
||||
match self.run(is_idle, client) {
|
||||
Ok(is_idle_again) => {
|
||||
is_idle = is_idle_again;
|
||||
}
|
||||
Err(e) => error!("Error on idle iteration: {e}"),
|
||||
};
|
||||
|
||||
thread::sleep(client.config.poll_time_idle);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user