Merge branch 'main' into aw-instance

This commit is contained in:
powellnorma 2024-10-11 12:23:06 +02:00
commit dd974db422
9 changed files with 961 additions and 928 deletions

1705
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -21,20 +21,20 @@ members = ["watchers"]
version = "0.3.0"
[workspace.dependencies]
anyhow = "1.0.83"
anyhow = "1.0.89"
log = { version = "0.4.21", features = ["std"] }
tokio = { version = "1.37.0" }
serde = "1.0.202"
[dev-dependencies]
rstest = "0.21.0"
rstest = "0.23.0"
tempfile = "3.10.1"
[dependencies]
watchers = { path = "./watchers", default-features = false }
chrono = "0.4.38"
toml = "0.8.13"
clap = { version = "4.5.4", features = ["string"] }
clap = { version = "4.5.20", features = ["string"] }
fern = { version = "0.6.2", features = ["colored"] }
log = { workspace = true }
anyhow = { workspace = true }

View File

@ -101,7 +101,8 @@ Copy the section as many times as needed for every given filter.
- `replace-title` replaces the window title with the provided value.
The first matching filter stops the replacement.
There should be at least 1 match field, and at least 1 replace field for a valid filter.
There should be at least 1 match field for a filter to be valid.
If the replacement is not specified, the data is not reported when matched.
Matches are case sensitive regular expressions between implicit ^ and $:
- `.` matches 1 any character
- `.*` matches any number of any characters

View File

@ -289,9 +289,12 @@ mod tests {
assert_eq!(manager.path_watchers[0].name(), "aw-test");
assert!(manager.path_watchers[0].handle.is_none()); // no starting in config
assert!(manager.start_watcher(&temp_dir.path().join("aw-test")));
let watcher_path = &temp_dir.path().join("aw-test");
assert!(manager.start_watcher(watcher_path));
assert!(manager.path_watchers[0].handle.is_some());
assert_autostart_content(&manager, &["aw-test"]);
manager.stop_watcher(watcher_path);
}
fn assert_autostart_content(manager: &Manager, watchers: &[&str]) {

View File

@ -10,7 +10,7 @@ crate-type = ["lib"]
path = "src/lib.rs"
[dev-dependencies]
rstest = "0.21.0"
rstest = "0.23.0"
tempfile = "3.10.1"
[dependencies]
@ -25,10 +25,10 @@ chrono = "0.4.38"
toml = "0.8.13"
dirs = "5.0.1"
serde = { workspace = true, features = ["derive"] }
serde_default = "0.1.0"
serde_default = "0.2.0"
serde_json = "1.0.117"
regex = "1.10.4"
gethostname = "0.4.3"
gethostname = "0.5.0"
log = { workspace = true }
anyhow = { workspace = true }
async-trait = "0.1.80"

View File

@ -2,9 +2,10 @@ pub mod defaults;
mod file_config;
mod filters;
use self::filters::{Filter, Replacement};
use self::filters::Filter;
use chrono::Duration;
pub use file_config::FileConfig;
pub use filters::FilterResult;
pub struct Config {
pub port: u16,
@ -17,13 +18,14 @@ pub struct Config {
}
impl Config {
pub fn window_data_replacement(&self, app_id: &str, title: &str) -> Replacement {
pub fn match_window_data(&self, app_id: &str, title: &str) -> FilterResult {
for filter in &self.filters {
if let Some(replacement) = filter.replacement(app_id, title) {
return replacement;
let result = filter.apply(app_id, title);
if matches!(result, FilterResult::Match | FilterResult::Replace(_)) {
return result;
}
}
Replacement::default()
FilterResult::Skip
}
}

View File

@ -147,6 +147,8 @@ impl FileConfig {
#[cfg(test)]
mod tests {
use crate::config::FilterResult;
use super::*;
use rstest::rstest;
use std::io::Write;
@ -192,16 +194,37 @@ replace-title = "Title"
assert_eq!(12, config.client.poll_time_window_seconds);
assert_eq!(2, config.client.filters.len());
let replacement1 = config.client.filters[0]
.replacement("firefox", "any")
.unwrap();
assert_eq!(None, replacement1.replace_app_id);
assert_eq!(Some("Unknown".to_string()), replacement1.replace_title);
let replacement2 = config.client.filters[1]
.replacement("code", "title")
.unwrap();
assert_eq!(Some("VSCode".to_string()), replacement2.replace_app_id);
assert_eq!(Some("Title".to_string()), replacement2.replace_title);
let replacement1 = config.client.filters[0].apply("firefox", "any");
assert!(matches!(replacement1, FilterResult::Replace(ref r) if
r.replace_app_id.is_none() &&
r.replace_title == Some("Unknown".to_string())
));
let replacement2 = config.client.filters[1].apply("code", "title");
assert!(matches!(replacement2, FilterResult::Replace(ref r) if
r.replace_app_id == Some("VSCode".to_string()) &&
r.replace_title == Some("Title".to_string())
));
}
#[rstest]
fn match_filter() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"
[[awatcher.filters]]
match-app-id = "firefox"
"#
)
.unwrap();
let config = FileConfig::new(Some(file.path().to_path_buf())).unwrap();
assert_eq!(1, config.client.filters.len());
let replacement1 = config.client.filters[0].apply("firefox", "any");
assert!(matches!(replacement1, FilterResult::Match));
}
#[rstest]

View File

@ -31,6 +31,13 @@ where
}
}
#[derive(Debug, PartialEq)]
pub enum FilterResult {
Replace(Replacement),
Match,
Skip,
}
#[derive(Default, Debug, PartialEq)]
pub struct Replacement {
pub replace_app_id: Option<String>,
@ -39,10 +46,7 @@ pub struct Replacement {
impl Filter {
fn is_valid(&self) -> bool {
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
self.match_app_id.is_some() || self.match_title.is_some()
}
fn is_match(&self, app_id: &str, title: &str) -> bool {
@ -70,9 +74,12 @@ impl Filter {
replacement.to_owned()
}
pub fn replacement(&self, app_id: &str, title: &str) -> Option<Replacement> {
pub fn apply(&self, app_id: &str, title: &str) -> FilterResult {
if !self.is_valid() || !self.is_match(app_id, title) {
return None;
return FilterResult::Skip;
}
if self.replace_app_id.is_none() && self.replace_title.is_none() {
return FilterResult::Match;
}
let mut replacement = Replacement::default();
@ -83,7 +90,7 @@ impl Filter {
if let Some(new_title) = &self.replace_title {
replacement.replace_title = Some(Self::replace(&self.match_title, title, new_title));
}
Some(replacement)
FilterResult::Replace(replacement)
}
}
@ -141,6 +148,12 @@ mod tests {
("org.kde.dolphin", "/home/user"),
None
)]
#[case::match_only(
(Some("org\\.kde\\.(.*)"), None),
(None, None),
("org.kde.dolphin", "/home/user"),
Some((None, None))
)]
fn replacement(
#[case] matches: (Option<&str>, Option<&str>),
#[case] replaces: (Option<&str>, Option<&str>),
@ -159,11 +172,15 @@ mod tests {
replace_title: replace_title.map(option_string),
};
let replacement = filter.replacement(app_id, title);
let expect_replacement = expect_replacement.map(|r| Replacement {
replace_app_id: r.0.map(option_string),
replace_title: r.1.map(option_string),
});
let replacement = filter.apply(app_id, title);
let expect_replacement = match expect_replacement {
None => FilterResult::Skip,
Some((None, None)) => FilterResult::Match,
Some((replace_app_id, replace_title)) => FilterResult::Replace(Replacement {
replace_app_id: replace_app_id.map(Into::into),
replace_title: replace_title.map(Into::into),
}),
};
assert_eq!(expect_replacement, replacement);
}
}

View File

@ -1,6 +1,5 @@
use super::config::{Config, FilterResult};
use crate::watchers::idle::Status;
use super::config::Config;
use anyhow::Context;
use aw_client_rust::{AwClient, Event as AwEvent};
use chrono::{DateTime, TimeDelta, Utc};
@ -106,31 +105,23 @@ impl ReportClient {
) -> anyhow::Result<()> {
let mut data = Map::new();
let replacement = self.config.window_data_replacement(app_id, title);
let inserted_app_id = if let Some(new_app_id) = replacement.replace_app_id {
trace!("Replacing app_id by {new_app_id}");
new_app_id
} else {
app_id.to_string()
};
let inserted_title = if let Some(new_title) = replacement.replace_title {
trace!("Replacing title of {inserted_app_id} by {new_title}");
new_title
} else {
title.to_string()
};
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));
if let Some((inserted_app_id, inserted_title)) = self.get_filtered_data(app_id, title) {
trace!(
"Reporting app_id: {}, title: {}",
inserted_app_id,
inserted_title
);
if let Some(extra) = extra_data {
for (key, value) in extra {
data.insert(key, Value::String(value));
data.insert("app".to_string(), Value::String(inserted_app_id));
data.insert("title".to_string(), Value::String(inserted_title));
if let Some(extra) = extra_data {
for (key, value) in extra {
data.insert(key, Value::String(value));
}
}
} else {
return Ok(());
}
let event = AwEvent {
@ -158,6 +149,33 @@ impl ReportClient {
.with_context(|| "Failed to send heartbeat for active window")
}
fn get_filtered_data(&self, app_id: &str, title: &str) -> Option<(String, String)> {
let filter_result = self.config.match_window_data(app_id, title);
match filter_result {
FilterResult::Replace(replacement) => {
let app_id = if let Some(replace_app_id) = replacement.replace_app_id {
trace!("Replacing app_id by {}", replace_app_id);
replace_app_id
} else {
app_id.to_string()
};
let title = if let Some(replace_title) = replacement.replace_title {
trace!("Replacing title by {}", replace_title);
replace_title
} else {
title.to_string()
};
Some((app_id, title))
}
FilterResult::Match => {
trace!("Matched a filter, not reported");
None
}
FilterResult::Skip => Some((app_id.to_string(), title.to_string())),
}
}
async fn create_bucket(
client: &AwClient,
bucket_name: &str,