diff --git a/aw-client-rust/src/classes.rs b/aw-client-rust/src/classes.rs index 2d8453e0..fb5e2317 100644 --- a/aw-client-rust/src/classes.rs +++ b/aw-client-rust/src/classes.rs @@ -5,6 +5,7 @@ use log::warn; use rand::Rng; use serde::{Deserialize, Serialize}; +use serde_json; use super::blocking::AwClient as ActivityWatchClient; @@ -14,6 +15,7 @@ pub type CategoryId = Vec; pub struct CategorySpec { #[serde(rename = "type")] pub spec_type: String, + #[serde(default)] pub regex: String, #[serde(default)] pub ignore_case: bool, @@ -21,8 +23,12 @@ pub struct CategorySpec { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassSetting { + #[serde(default)] + pub id: Option, pub name: Vec, pub rule: CategorySpec, + #[serde(default)] + pub data: Option, } /// Returns the default categorization classes @@ -173,11 +179,16 @@ pub fn get_classes_from_server(host: &str, port: u16) -> Vec<(CategoryId, Catego return default_classes(); } - let class_settings: Vec = serde_json::from_value(setting_value) - .unwrap_or_else(|_| { - warn!("Failed to deserialize classes setting, using default classes"); - return vec![]; - }); + let class_settings: Vec = match serde_json::from_value(setting_value) { + Ok(classes) => classes, + Err(e) => { + warn!( + "Failed to deserialize classes setting: {}, using default classes", + e + ); + return default_classes(); + } + }; // Convert ClassSetting to (CategoryId, CategorySpec) format class_settings diff --git a/aw-client-rust/src/lib.rs b/aw-client-rust/src/lib.rs index d6bd0d1d..5c6be763 100644 --- a/aw-client-rust/src/lib.rs +++ b/aw-client-rust/src/lib.rs @@ -123,6 +123,8 @@ impl AwClient { .map(|(start, stop)| format!("{}/{}", start, stop)) .collect(); + let query_lines: Vec<&str> = query.split('\n').collect(); + // Result is a sequence, one element per timeperiod self.client .post(url) diff --git a/aw-client-rust/src/queries.rs b/aw-client-rust/src/queries.rs index 92648d3f..7129b9a9 100644 --- a/aw-client-rust/src/queries.rs +++ b/aw-client-rust/src/queries.rs @@ -165,21 +165,40 @@ impl QueryParams { } /// Helper function to serialize classes in the format expected by the categorize function +/// This version builds the query string directly without JSON serialization to avoid double-escaping fn serialize_classes(classes: &[ClassRule]) -> String { - // Convert Vec<(CategoryId, CategorySpec)> to the JSON format expected by categorize - let serialized_classes: Vec<(Vec, serde_json::Value)> = classes - .iter() - .map(|(category_id, category_spec)| { - let spec_json = serde_json::json!({ - "type": category_spec.spec_type, - "regex": category_spec.regex, - "ignore_case": category_spec.ignore_case - }); - (category_id.clone(), spec_json) - }) - .collect(); - - serde_json::to_string(&serialized_classes).unwrap_or_else(|_| "[]".to_string()) + let mut parts = Vec::new(); + + for (category_id, category_spec) in classes { + // Build category array string manually: ["Work", "Programming"] + let category_str = format!( + "[{}]", + category_id + .iter() + .map(|s| format!("\"{}\"", s)) + .collect::>() + .join(", ") + ); + + // Build spec object manually to avoid JSON escaping regex patterns + let mut spec_parts = Vec::new(); + spec_parts.push(format!("\"type\": \"{}\"", category_spec.spec_type)); + + // Only include regex for non-"none" types, and use raw pattern without escaping + if category_spec.spec_type != "none" { + spec_parts.push(format!("\"regex\": \"{}\"", category_spec.regex)); + } + + // Always include ignore_case field + spec_parts.push(format!("\"ignore_case\": {}", category_spec.ignore_case)); + + let spec_str = format!("{{{}}}", spec_parts.join(", ")); + + // Build the tuple [category, spec] + parts.push(format!("[{}, {}]", category_str, spec_str)); + } + + format!("[{}]", parts.join(", ")) } fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String { @@ -195,7 +214,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String { if params.base.filter_afk { query.push(format!( "not_afk = flood(query_bucket(find_bucket(\"{}\"))); - not_afk = filter_keyvals(not_afk, \"status\", [\"not-afk\"])", +not_afk = filter_keyvals(not_afk, \"status\", [\"not-afk\"])", escape_doublequote(¶ms.bid_afk) )); } @@ -207,7 +226,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String { if params.base.include_audible { query.push( "audible_events = filter_keyvals(browser_events, \"audible\", [true]); - not_afk = period_union(not_afk, audible_events)" +not_afk = period_union(not_afk, audible_events)" .to_string(), ); } @@ -221,7 +240,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String { // Add categorization if classes specified if !params.base.classes.is_empty() { query.push(format!( - "events = categorize(events, {})", + "events = categorize(events, {});", serialize_classes(¶ms.base.classes) )); } @@ -252,7 +271,7 @@ fn build_android_canonical_events(params: &AndroidQueryParams) -> String { // Add categorization if classes specified if !params.base.classes.is_empty() { query.push(format!( - "events = categorize(events, {})", + "events = categorize(events, {});", serialize_classes(¶ms.base.classes) )); } @@ -269,18 +288,19 @@ fn build_android_canonical_events(params: &AndroidQueryParams) -> String { } fn build_browser_events(params: &DesktopQueryParams) -> String { - let mut query = String::from("browser_events = [];\n"); + let mut query = String::from("browser_events = [];"); for browser_bucket in ¶ms.base.bid_browsers { for (browser_name, app_names) in BROWSER_APPNAMES.entries() { if browser_bucket.contains(browser_name) { query.push_str(&format!( - "events_{0} = flood(query_bucket(\"{1}\")); - window_{0} = filter_keyvals(events, \"app\", {2}); - events_{0} = filter_period_intersect(events_{0}, window_{0}); - events_{0} = split_url_events(events_{0}); - browser_events = concat(browser_events, events_{0}); - browser_events = sort_by_timestamp(browser_events);\n", + " +events_{0} = flood(query_bucket(\"{1}\")); +window_{0} = filter_keyvals(events, \"app\", {2}); +events_{0} = filter_period_intersect(events_{0}, window_{0}); +events_{0} = split_url_events(events_{0}); +browser_events = concat(browser_events, events_{0}); +browser_events = sort_by_timestamp(browser_events)", browser_name, escape_doublequote(browser_bucket), serde_json::to_string(app_names).unwrap() @@ -288,7 +308,6 @@ fn build_browser_events(params: &DesktopQueryParams) -> String { } } } - query } @@ -414,9 +433,9 @@ mod tests { assert!(serialized.contains("Programming")); assert!(serialized.contains("Google Docs")); assert!(serialized.contains("GitHub|vim")); - assert!(serialized.contains("\"type\":\"regex\"")); - assert!(serialized.contains("\"ignore_case\":false")); - assert!(serialized.contains("\"ignore_case\":true")); + assert!(serialized.contains("\"type\": \"regex\"")); + assert!(serialized.contains("\"ignore_case\": false")); + assert!(serialized.contains("\"ignore_case\": true")); } #[test] diff --git a/aw-webui b/aw-webui index 0cf78317..291da6f2 160000 --- a/aw-webui +++ b/aw-webui @@ -1 +1 @@ -Subproject commit 0cf7831771d9cad9e25954eaed99c77d5a7c6e10 +Subproject commit 291da6f2c5e7a6b896f23a4eec5ffed9874321ba