Support captures in replacements

This commit is contained in:
Demmie 2023-05-13 14:56:21 -04:00
parent 59b2c88c65
commit 78c66181b1
No known key found for this signature in database
GPG Key ID: B06DAA3D432C6E9A
4 changed files with 87 additions and 49 deletions

View File

@ -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, 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. 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 foundation is [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 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. 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 any number of any characters
- `.+` matches 1 or more any characters. - `.+` matches 1 or more any characters.
- `word` is an exact match. - `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 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. to see what application names and titles are reported to the server.
``` ```
$ awatcher -vvv --no-server $ awatcher -vvv --no-server
``` ```

View File

@ -7,6 +7,48 @@ use crate::config::defaults;
use super::filters::Filter; 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)] #[derive(Deserialize, DefaultFromSerde)]
pub struct ServerConfig { pub struct ServerConfig {
#[serde(default = "defaults::port")] #[serde(default = "defaults::port")]
@ -69,36 +111,7 @@ impl FileConfig {
toml::from_str(&config_content)? toml::from_str(&config_content)?
} else { } else {
let config = format!( let config = default_config();
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 error = std::fs::create_dir(config_path.parent().unwrap()); let error = std::fs::create_dir(config_path.parent().unwrap());
if let Err(e) = error { if let Err(e) = error {
if e.kind() != ErrorKind::AlreadyExists { if e.kind() != ErrorKind::AlreadyExists {

View File

@ -39,8 +39,10 @@ pub struct Replacement {
impl Filter { impl Filter {
fn is_valid(&self) -> bool { fn is_valid(&self) -> bool {
(self.match_app_id.is_some() || self.match_title.is_some()) let is_match_set = self.match_app_id.is_some() || self.match_title.is_some();
&& (self.replace_app_id.is_some() || self.replace_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 { fn is_match(&self, app_id: &str, title: &str) -> bool {
@ -58,22 +60,29 @@ impl Filter {
true true
} }
fn replace(regex: &Option<Regex>, 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<Replacement> { pub fn replacement(&self, app_id: &str, title: &str) -> Option<Replacement> {
if !self.is_valid() { if !self.is_valid() || !self.is_match(app_id, title) {
return None; return None;
} }
if self.is_match(app_id, title) { let mut replacement = Replacement::default();
let mut replacement = Replacement::default(); if let Some(new_app_id) = &self.replace_app_id {
if let Some(new_app_id) = &self.replace_app_id { replacement.replace_app_id =
replacement.replace_app_id = Some(new_app_id.to_string()); Some(Self::replace(&self.match_app_id, app_id, new_app_id));
}
if let Some(new_title) = &self.replace_title {
replacement.replace_title = Some(new_title.to_string());
}
Some(replacement)
} else {
None
} }
if let Some(new_title) = &self.replace_title {
replacement.replace_title = Some(Self::replace(&self.match_title, title, new_title));
}
Some(replacement)
} }
} }

View File

@ -76,7 +76,11 @@ impl ReportClient {
} else { } else {
title.to_string() 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("app".to_string(), Value::String(inserted_app_id));
data.insert("title".to_string(), Value::String(inserted_title)); data.insert("title".to_string(), Value::String(inserted_title));
let event = AwEvent { let event = AwEvent {