diff --git a/README.md b/README.md index de1091e..03916b3 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Awatcher is a window activity and idle watcher with an optional tray and UI for The goal is to compensate the fragmentation of desktop environments on Linux by supporting all reportable environments, to add more flexibility to reports with filters, and to have better UX with the distribution by a single executable. -The foundation is taken from [ActivityWatch](https://github.com/ActivityWatch), which includes the server and web UI. -The unbundled watcher can replace the original idle and active window watchers in the original distribution if necessary. +The foundation is [ActivityWatch](https://github.com/ActivityWatch), which includes the server and web UI. +The unbundled watcher is supposed to replace the original idle and active window watchers from the original distribution. +The bundled executable can be used independently as it contains the server, UI and tray. The crate also provides a library with watchers which can send the data to the server. @@ -109,10 +110,21 @@ Matches are case sensitive regular expressions between implici ^ and $: - `.*` matches any number of any characters - `.+` matches 1 or more any characters. - `word` is an exact match. -- Use escapes to match special characters, e.g. `org\.kde\.Dolpin` +- Use escapes `\` to match special characters, e.g. `org\.kde\.Dolpin` + +The replacements in filters also support regexp captures. +A captures takes a string in parentheses from the match and replaces `$N` in the replacement, where `N` is the number of parentheses. +Example filter to remove the changed file indicator in Visual Studio Code: +```toml +[[awatcher.filters]] +match-app-id = "code" +match-title = "● (.*)" +# Inserts the content within 1st parentheses, this can be in any form, e.g. "App $1 - $2/$3" +replace-title = "$1" +``` Run the command with "debug" or "trace" verbosity and without reporting to server in the terminal to see what application names and titles are reported to the server. ``` $ awatcher -vvv --no-server -``` \ No newline at end of file +``` diff --git a/watchers/src/config/file_config.rs b/watchers/src/config/file_config.rs index 404f125..a2524e6 100644 --- a/watchers/src/config/file_config.rs +++ b/watchers/src/config/file_config.rs @@ -7,6 +7,48 @@ use crate::config::defaults; use super::filters::Filter; +pub fn default_config() -> String { + 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={} + +# Add as many filters as needed. The first matching filter stops the replacement. +# There should be at least 1 match field, and at least 1 replace field. +# Matches are case sensitive regular expressions between implici ^ and $, e.g. +# - "." matches 1 any character +# - ".*" matches any number of any characters +# - ".+" matches 1 or more any characters. +# - "word" is an exact match. +# [[awatcher.filters]] +# match-app-id = "navigator" +# match-title = ".*Firefox.*" +# replace-app-id = "firefox" +# replace-title = "Unknown" + +# Use captures for app-id or title in the regular form to use parts of the original text +# (parentheses for a capture, $1, $2 etc for each capture). +# The example rule removes the changed file indicator from the title in Visual Studio Code: +# "● file_config.rs - awatcher - Visual Studio Code" to "file_config.rs - awatcher - Visual Studio Code". +# [[awatcher.filters]] +# match-app-id = "code" +# match-title = "● (.*)" +# replace-title = "$1" +"#, + defaults::port(), + defaults::host(), + defaults::idle_timeout_seconds(), + defaults::poll_time_idle_seconds(), + defaults::poll_time_window_seconds(), + ) +} + #[derive(Deserialize, DefaultFromSerde)] pub struct ServerConfig { #[serde(default = "defaults::port")] @@ -69,36 +111,7 @@ impl FileConfig { 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={} - -# Add as many filters as needed. The first matching filter stops the replacement. -# There should be at least 1 match field, and at least 1 replace field. -# Matches are case sensitive regular expressions between implici ^ and $, e.g. -# - "." matches 1 any character -# - ".*" matches any number of any characters -# - ".+" matches 1 or more any characters. -# - "word" is an exact match. -# [[awatcher.filters]] -# match-app-id = "navigator" -# match-title = ".*Firefox.*" -# replace-app-id = "firefox" -# replace-title = "Unknown" -"#, - defaults::port(), - defaults::host(), - defaults::idle_timeout_seconds(), - defaults::poll_time_idle_seconds(), - defaults::poll_time_window_seconds(), - ); + let config = default_config(); let error = std::fs::create_dir(config_path.parent().unwrap()); if let Err(e) = error { if e.kind() != ErrorKind::AlreadyExists { diff --git a/watchers/src/config/filters.rs b/watchers/src/config/filters.rs index 75591f5..36b3b6c 100644 --- a/watchers/src/config/filters.rs +++ b/watchers/src/config/filters.rs @@ -39,8 +39,10 @@ pub struct Replacement { impl Filter { fn is_valid(&self) -> bool { - (self.match_app_id.is_some() || self.match_title.is_some()) - && (self.replace_app_id.is_some() || self.replace_title.is_some()) + let is_match_set = self.match_app_id.is_some() || self.match_title.is_some(); + let is_replacement_set = self.replace_app_id.is_some() || self.replace_title.is_some(); + + is_match_set && is_replacement_set } fn is_match(&self, app_id: &str, title: &str) -> bool { @@ -58,22 +60,29 @@ impl Filter { true } + fn replace(regex: &Option, source: &str, replacement: &str) -> String { + if let Some(regex) = regex { + // Avoid using the more expensive regexp replacements when unnecessary. + if regex.captures_len() > 1 { + return regex.replace(source, replacement).to_string(); + } + } + replacement.to_owned() + } + pub fn replacement(&self, app_id: &str, title: &str) -> Option { - if !self.is_valid() { + if !self.is_valid() || !self.is_match(app_id, title) { return None; } - if self.is_match(app_id, title) { - let mut replacement = Replacement::default(); - if let Some(new_app_id) = &self.replace_app_id { - replacement.replace_app_id = Some(new_app_id.to_string()); - } - if let Some(new_title) = &self.replace_title { - replacement.replace_title = Some(new_title.to_string()); - } - Some(replacement) - } else { - None + let mut replacement = Replacement::default(); + if let Some(new_app_id) = &self.replace_app_id { + replacement.replace_app_id = + Some(Self::replace(&self.match_app_id, app_id, new_app_id)); } + if let Some(new_title) = &self.replace_title { + replacement.replace_title = Some(Self::replace(&self.match_title, title, new_title)); + } + Some(replacement) } } diff --git a/watchers/src/report_client.rs b/watchers/src/report_client.rs index defe758..a033f3d 100644 --- a/watchers/src/report_client.rs +++ b/watchers/src/report_client.rs @@ -76,7 +76,11 @@ impl ReportClient { } else { title.to_string() }; - trace!("Reporting app_id: {}, title: {}", app_id, title); + trace!( + "Reporting app_id: {}, title: {}", + inserted_app_id, + inserted_title + ); data.insert("app".to_string(), Value::String(inserted_app_id)); data.insert("title".to_string(), Value::String(inserted_title)); let event = AwEvent {