diff --git a/Cargo.lock b/Cargo.lock index 18eb074..b0c4e81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,10 +219,14 @@ dependencies = [ "aw-client-rust", "chrono", "clap", + "dirs 5.0.0", "fern", "gethostname 0.4.1", "log", + "serde", + "serde_default", "serde_json", + "toml", "wayland-backend", "wayland-client", "wayland-scanner", @@ -472,6 +476,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "derivative" version = "2.2.0" @@ -499,7 +538,16 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ - "dirs-sys", + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" +dependencies = [ + "dirs-sys 0.4.0", ] [[package]] @@ -513,6 +561,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +dependencies = [ + "libc", + "redox_users", + "windows-sys 0.45.0", +] + [[package]] name = "dlib" version = "0.5.0" @@ -895,6 +954,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -1483,18 +1548,30 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.159" +name = "serde_default" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "9fd4c77b86d9fb10363e52607ca6dc3043d8dfde6c790b702ed4ffafb34e7b99" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", @@ -1534,6 +1611,15 @@ dependencies = [ "syn 2.0.12", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1724,11 +1810,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1737,6 +1838,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -2268,7 +2371,7 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "dirs", + "dirs 4.0.0", "enumflags2", "event-listener", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index be29cde..6a11ec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,11 @@ wayland-backend = "0.1" chrono = "0.4.24" serde_json = "1.0.95" zbus = "3.11.1" -clap = "4.2.1" +clap = { version = "4.2.1", features = ["string"] } log = { version = "0.4.17", features = ["std"] } fern = { version = "0.6.2", features = ["colored"] } x11rb = { version = "0.11.1", features = ["screensaver"] } +toml = "0.7.3" +dirs = "5.0.0" +serde = { version = "1.0.160", features = ["derive"] } +serde_default = "0.1.0" diff --git a/src/config.rs b/src/config.rs index 5d41f2c..401e9b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,13 @@ -use std::{path::PathBuf, time::Duration}; +use clap::{arg, parser::ValueSource, value_parser, ArgMatches, Command}; +use serde::Deserialize; +use serde_default::DefaultFromSerde; +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, + time::Duration, +}; -use clap::{arg, value_parser, Command}; +use crate::BoxedError; pub struct Config { pub port: u32, @@ -12,8 +19,134 @@ pub struct Config { pub active_window_bucket_name: String, } +fn default_idle_timeout_seconds() -> u32 { + 180 +} +fn default_poll_time_idle_seconds() -> u32 { + 5 +} +fn default_poll_time_window_seconds() -> u32 { + 1 +} +fn default_port() -> u32 { + 5600 +} +fn default_host() -> String { + "localhost".to_string() +} + +#[derive(Deserialize, DefaultFromSerde)] +struct ServerConfig { + #[serde(default = "default_port")] + port: u32, + #[serde(default = "default_host")] + host: String, +} + +#[derive(Deserialize, DefaultFromSerde)] +struct ClientConfig { + #[serde(default = "default_idle_timeout_seconds")] + idle_timeout_seconds: u32, + #[serde(default = "default_poll_time_idle_seconds")] + poll_time_idle_seconds: u32, + #[serde(default = "default_poll_time_window_seconds")] + poll_time_window_seconds: u32, +} + +#[derive(Deserialize, Default)] +struct FileConfig { + #[serde(default)] + server: ServerConfig, + #[serde(default)] + client: ClientConfig, +} + +impl FileConfig { + fn new(config_path: &Path) -> Result { + if config_path.exists() { + debug!("Reading config at {}", config_path.display()); + let config_content = std::fs::read_to_string(config_path) + .map_err(|e| format!("Impossible to read config file: {e}"))?; + + Ok(toml::from_str(&config_content)?) + } else { + let config = format!( + r#"# The commented values are the defaults on the file creation + [server] + # port = {} + # host = "{}" + [awatcher] + # idle-timeout-seconds={} + # poll-time-idle-seconds={} + # poll-time-window-seconds={} + "#, + default_port(), + default_host(), + default_idle_timeout_seconds(), + default_poll_time_idle_seconds(), + default_poll_time_window_seconds(), + ); + let error = std::fs::create_dir(config_path.parent().unwrap()); + if let Err(e) = error { + if e.kind() != ErrorKind::AlreadyExists { + Err(e)?; + } + } + debug!("Creading config at {}", config_path.display()); + std::fs::write(config_path, config)?; + + Ok(Self::default()) + } + } + + fn merge_cli(&mut self, matches: &ArgMatches) { + self.client.poll_time_idle_seconds = get_arg_value( + "poll-time-idle", + matches, + self.client.poll_time_idle_seconds, + ); + self.client.poll_time_window_seconds = get_arg_value( + "poll-time-window", + matches, + self.client.poll_time_window_seconds, + ); + self.client.idle_timeout_seconds = + get_arg_value("idle-timeout", matches, self.client.idle_timeout_seconds); + + self.server.port = get_arg_value("port", matches, self.server.port); + self.server.host = get_arg_value("host", matches, self.server.host.clone()); + } + + fn get_idle_timeout(&self) -> Duration { + Duration::from_secs(u64::from(self.client.idle_timeout_seconds)) + } + + fn get_poll_time_idle(&self) -> Duration { + Duration::from_secs(u64::from(self.client.poll_time_idle_seconds)) + } + + fn get_poll_time_window(&self) -> Duration { + Duration::from_secs(u64::from(self.client.poll_time_window_seconds)) + } +} + +fn get_arg_value(id: &str, matches: &ArgMatches, config_value: T) -> T +where + T: Clone + Send + Sync + 'static, +{ + if let Some(ValueSource::CommandLine) = matches.value_source(id) { + matches.get_one::(id).unwrap().clone() + } else { + config_value + } +} + impl Config { - pub fn from_cli() -> Self { + pub fn from_cli() -> Result { + let mut config_path: PathBuf = dirs::config_dir().ok_or("Config directory is unknown")?; + config_path.push("awatcher"); + config_path.push("config.toml"); + let matches = Command::new("Activity Watcher") .version("0.1.0") .about("A set of ActivityWatch desktop watchers") @@ -21,37 +154,37 @@ impl Config { arg!(-c --config "Custom config file").value_parser(value_parser!(PathBuf)), arg!(--port "Custom server port") .value_parser(value_parser!(u32)) - .default_value("5600"), + .default_value(default_port().to_string()), arg!(--host "Custom server host") .value_parser(value_parser!(String)) - .default_value("localhost"), + .default_value(default_host()), arg!(--"idle-timeout" "Time of inactivity to consider the user idle") .value_parser(value_parser!(u32)) - .default_value("180"), + .default_value(default_idle_timeout_seconds().to_string()), arg!(--"poll-time-idle" "Period between sending heartbeats to the server for idle activity") .value_parser(value_parser!(u32)) - .default_value("5"), + .default_value(default_poll_time_idle_seconds().to_string()), arg!(--"poll-time-window" "Period between sending heartbeats to the server for idle activity") .value_parser(value_parser!(u32)) - .default_value("1"), + .default_value(default_poll_time_window_seconds().to_string()), ]) .get_matches(); + let mut config = FileConfig::new(config_path.as_path())?; + config.merge_cli(&matches); + 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::("poll-time-idle").unwrap(); - let poll_seconds_window = *matches.get_one::("poll-time-window").unwrap(); - let idle_timeout_seconds = *matches.get_one::("idle-timeout").unwrap(); - Self { - port: *matches.get_one("port").unwrap(), - host: String::clone(matches.get_one("host").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)), + Ok(Self { + port: config.server.port, + host: config.server.host.clone(), + idle_timeout: config.get_idle_timeout(), + poll_time_idle: config.get_poll_time_idle(), + poll_time_window: config.get_poll_time_window(), idle_bucket_name, active_window_bucket_name, - } + }) } } diff --git a/src/main.rs b/src/main.rs index a828490..74e31f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,7 +103,7 @@ fn setup_logger() -> Result<(), fern::InitError> { fn main() -> Result<(), BoxedError> { setup_logger()?; - let client = ReportClient::new(Config::from_cli())?; + let client = ReportClient::new(Config::from_cli()?)?; let client = Arc::new(client); info!( @@ -115,7 +115,11 @@ fn main() -> Result<(), BoxedError> { client.config.idle_timeout.as_secs() ); info!( - "Polling period: {} seconds", + "Idle polling period: {} seconds", + client.config.poll_time_idle.as_secs() + ); + info!( + "Window polling period: {} seconds", client.config.poll_time_idle.as_secs() ); diff --git a/src/x11_window.rs b/src/x11_window.rs index 0d523db..ef4b3f3 100644 --- a/src/x11_window.rs +++ b/src/x11_window.rs @@ -1,4 +1,4 @@ -use std::{thread, time::Duration}; +use std::thread; use crate::{report_client::ReportClient, x11_connection::X11Connection, BoxedError, Watcher};