diff --git a/src/renamer/formatter.rs b/src/renamer/formatter.rs index 4784149a..444ead2b 100644 --- a/src/renamer/formatter.rs +++ b/src/renamer/formatter.rs @@ -5,6 +5,7 @@ use hyprland::data::FullscreenMode; use std::collections::HashMap; use strfmt::strfmt; +#[derive(Clone)] pub struct AppWorkspace { pub id: i32, pub clients: Vec, diff --git a/src/renamer/mod.rs b/src/renamer/mod.rs index dfd023fc..71eee3d9 100644 --- a/src/renamer/mod.rs +++ b/src/renamer/mod.rs @@ -23,6 +23,7 @@ pub struct Renamer { known_workspaces: Mutex>, cfg: Mutex, args: Args, + workspace_strings_cache: Mutex>, } #[derive(Clone, Eq, Debug)] @@ -75,6 +76,7 @@ impl Renamer { known_workspaces: Mutex::new(HashSet::default()), cfg: Mutex::new(cfg), args, + workspace_strings_cache: Mutex::new(HashMap::new()), }) } @@ -93,15 +95,48 @@ impl Renamer { // Get workspaces based on open clients let workspaces = self.get_workspaces_from_clients(clients, active_client, config)?; + let workspace_ids: HashSet<_> = workspaces.iter().map(|w| w.id).collect(); // Generate workspace strings let workspaces_strings = self.generate_workspaces_string(workspaces, config); - // Render the workspaces - workspaces_strings.iter().for_each(|(&id, clients)| { - rename_cmd(id, clients, &config.format, &config.workspaces_name) + // Filter out unchanged workspaces + let altered_workspaces = self.get_altered_workspaces(&workspaces_strings)?; + + altered_workspaces.iter().for_each(|(&id, clients)| { + rename_cmd(id, clients, &config.format, &config.workspaces_name); }); + self.update_cache(&altered_workspaces, &workspace_ids)?; + + Ok(()) + } + + fn get_altered_workspaces( + &self, + workspaces_strings: &HashMap, + ) -> Result, Box> { + let cache = self.workspace_strings_cache.lock()?; + Ok(workspaces_strings.iter() + .filter_map(|(&id, new_string)| { + if cache.get(&id) != Some(new_string) { + Some((id, new_string.clone())) + } else { + None + } + }) + .collect()) + } + + fn update_cache(&self, workspaces_strings: &HashMap, workspace_ids: &HashSet) -> Result<(), Box> { + let mut cache = self.workspace_strings_cache.lock()?; + for (&id, new_string) in workspaces_strings { + cache.insert(id, new_string.clone()); + } + + // Remove cached entries for workspaces that no longer exist + cache.retain(|&id, _| workspace_ids.contains(&id)); + Ok(()) } @@ -149,6 +184,8 @@ impl Renamer { } pub fn reset_workspaces(&self, config: ConfigFile) -> Result<(), Box> { + self.workspace_strings_cache.lock()?.clear(); + self.known_workspaces .lock()? .iter() @@ -2226,6 +2263,167 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn test_workspace_cache() { + let mut config = crate::config::read_config_file(None, false, false).unwrap(); + config.class.push((Regex::new("kitty").unwrap(), "term".to_string())); + + let renamer = Renamer::new( + Config { + cfg_path: None, + config: config.clone(), + }, + Args { + verbose: false, + debug: false, + config: None, + dump: false, + migrate_config: false, + }, + ); + + // Initial state - cache should be empty + assert_eq!(renamer.workspace_strings_cache.lock().unwrap().len(), 0); + + let mut app_workspaces = vec![ + AppWorkspace { + id: 1, + clients: vec![AppClient { + initial_class: "kitty".to_string(), + class: "kitty".to_string(), + title: "term1".to_string(), + initial_title: "term1".to_string(), + is_active: false, + is_fullscreen: FullscreenMode::None, + matched_rule: renamer.parse_icon( + "kitty".to_string(), + "kitty".to_string(), + "term1".to_string(), + "term1".to_string(), + false, + &config, + ), + is_dedup_inactive_fullscreen: false, + }], + }, + AppWorkspace { + id: 2, + clients: vec![AppClient { + initial_class: "kitty".to_string(), + class: "kitty".to_string(), + title: "term2".to_string(), + initial_title: "term2".to_string(), + is_active: false, + is_fullscreen: FullscreenMode::None, + matched_rule: renamer.parse_icon( + "kitty".to_string(), + "kitty".to_string(), + "term2".to_string(), + "term2".to_string(), + false, + &config, + ), + is_dedup_inactive_fullscreen: false, + }], + }, + ]; + + let strings = renamer.generate_workspaces_string(app_workspaces.clone(), &config); + // Update cache and rename workspaces + let altered_strings = renamer.get_altered_workspaces(&strings).unwrap(); + assert_eq!(strings, altered_strings); + + let workspace_ids: HashSet<_> = app_workspaces.iter().map(|w| w.id).collect(); + renamer.update_cache(&altered_strings, &workspace_ids).unwrap(); + // Cache should now contain entries for all workspaces + { + let cache = renamer.workspace_strings_cache.lock().unwrap(); + assert_eq!(cache.len(), app_workspaces.len()); + assert_eq!(cache.get(&1), strings.get(&1)); + assert_eq!(cache.get(&2), strings.get(&2)); + } + + // Generate same workspaces again - nothing should be altered + let altered_strings2 = renamer.get_altered_workspaces(&strings).unwrap(); + assert!(altered_strings2.is_empty()); + + app_workspaces.push(AppWorkspace { + id: 3, + clients: vec![AppClient { + initial_class: "kitty".to_string(), + class: "kitty".to_string(), + title: "term3".to_string(), + initial_title: "term3".to_string(), + is_active: false, + is_fullscreen: FullscreenMode::None, + matched_rule: renamer.parse_icon( + "kitty".to_string(), + "kitty".to_string(), + "term3".to_string(), + "term3".to_string(), + false, + &config, + ), + is_dedup_inactive_fullscreen: false, + }], + }); + + let strings3 = renamer.generate_workspaces_string(app_workspaces.clone(), &config); + let altered_strings3 = renamer.get_altered_workspaces(&strings3).unwrap(); + + // Only the new workspace should be altered + assert_eq!(altered_strings3.len(), 1); + assert_eq!(altered_strings3.get(&3), strings3.get(&3)); + + + let workspace_ids: HashSet<_> = app_workspaces.iter().map(|w| w.id).collect(); + renamer.update_cache(&altered_strings3, &workspace_ids).unwrap(); + + // Generate different workspace set - should update cache + let app_workspaces2 = vec![ + AppWorkspace { + id: 4, + clients: vec![AppClient { + initial_class: "kitty".to_string(), + class: "kitty".to_string(), + title: "term3".to_string(), // Different title + initial_title: "term3".to_string(), + is_active: false, + is_fullscreen: FullscreenMode::None, + matched_rule: renamer.parse_icon( + "kitty".to_string(), + "kitty".to_string(), + "term3".to_string(), + "term3".to_string(), + false, + &config, + ), + is_dedup_inactive_fullscreen: false, + }], + }, + ]; + + let strings3 = renamer.generate_workspaces_string(app_workspaces2.clone(), &config); + let altered_strings3 = renamer.get_altered_workspaces(&strings3).unwrap(); + assert_eq!(strings3, altered_strings3); + + let workspace_ids: HashSet<_> = app_workspaces2.iter().map(|w| w.id).collect(); + renamer.update_cache(&altered_strings3, &workspace_ids).unwrap(); + + // Cache should be updated - workspace 2 removed, workspace 1 updated + { + let cache = renamer.workspace_strings_cache.lock().unwrap(); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(&1), strings3.get(&1)); + assert_eq!(cache.get(&2), None); + } + + // Test cache reset + renamer.reset_workspaces(config.clone()).unwrap(); + assert_eq!(renamer.workspace_strings_cache.lock().unwrap().len(), 0); + } + #[test] fn test_regex_capture_support() { let mut config = crate::config::read_config_file(None, false, false).unwrap();