diff --git a/komorebi/src/border_manager/border.rs b/komorebi/src/border_manager/border.rs index 017b0e71..2fd26c94 100644 --- a/komorebi/src/border_manager/border.rs +++ b/komorebi/src/border_manager/border.rs @@ -272,6 +272,7 @@ impl Border { WindowKind::Monocle, WindowKind::Unfocused, WindowKind::Floating, + WindowKind::UnfocusedLocked, ] { let color = window_kind_colour(window_kind); let color = D2D1_COLOR_F { diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 310ecf12..326e3d99 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -48,6 +48,8 @@ lazy_static! { AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245)))); pub static ref UNFOCUSED: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128)))); + pub static ref UNFOCUSED_LOCKED: AtomicU32 = + AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(158, 8, 8)))); pub static ref MONOCLE: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153)))); pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66)))); @@ -149,6 +151,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> { fn window_kind_colour(focus_kind: WindowKind) -> u32 { match focus_kind { WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed), + WindowKind::UnfocusedLocked => UNFOCUSED_LOCKED.load(Ordering::Relaxed), WindowKind::Single => FOCUSED.load(Ordering::Relaxed), WindowKind::Stack => STACK.load(Ordering::Relaxed), WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed), @@ -229,7 +232,11 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result let window_kind = if idx != ws.focused_container_idx() || monitor_idx != focused_monitor_idx { - WindowKind::Unfocused + if ws.locked_containers().contains(&idx) { + WindowKind::UnfocusedLocked + } else { + WindowKind::Unfocused + } } else if c.windows().len() > 1 { WindowKind::Stack } else { @@ -493,7 +500,11 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result || monitor_idx != focused_monitor_idx || focused_window_hwnd != foreground_window { - WindowKind::Unfocused + if ws.locked_containers().contains(&idx) { + WindowKind::UnfocusedLocked + } else { + WindowKind::Unfocused + } } else if c.windows().len() > 1 { WindowKind::Stack } else { diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 0cb88d72..d86e6435 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -84,6 +84,9 @@ pub enum SocketMessage { PromoteFocus, PromoteWindow(OperationDirection), EagerFocus(String), + LockMonitorWorkspaceContainer(usize, usize, usize), + UnlockMonitorWorkspaceContainer(usize, usize, usize), + ToggleLock, ToggleFloat, ToggleMonocle, ToggleMaximize, @@ -312,6 +315,7 @@ pub enum WindowKind { Monocle, #[default] Unfocused, + UnfocusedLocked, Floating, } diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index 4e6ecc81..774e027f 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -9,6 +9,7 @@ pub mod colour; pub mod container; pub mod core; pub mod focus_manager; +pub mod locked_deque; pub mod monitor; pub mod monitor_reconciliator; pub mod process_command; diff --git a/komorebi/src/locked_deque.rs b/komorebi/src/locked_deque.rs new file mode 100644 index 00000000..6586b647 --- /dev/null +++ b/komorebi/src/locked_deque.rs @@ -0,0 +1,367 @@ +use std::collections::HashSet; +use std::collections::VecDeque; + +pub struct LockedDeque<'a, T> { + deque: &'a mut VecDeque, + locked_indices: &'a mut HashSet, +} + +impl<'a, T: Clone + PartialEq> LockedDeque<'a, T> { + pub fn new(deque: &'a mut VecDeque, locked_indices: &'a mut HashSet) -> Self { + Self { + deque, + locked_indices, + } + } + + pub fn insert(&mut self, index: usize, value: T) -> usize { + insert_respecting_locks(self.deque, self.locked_indices, index, value) + } + + pub fn remove(&mut self, index: usize) -> Option { + remove_respecting_locks(self.deque, self.locked_indices, index) + } +} + +fn insert_respecting_locks( + deque: &mut VecDeque, + locked_indices: &mut HashSet, + index: usize, + value: T, +) -> usize { + if deque.is_empty() { + deque.push_back(value); + return 0; + } + + // Find the actual insertion point (first unlocked index >= requested index) + let mut actual_index = index; + while actual_index < deque.len() && locked_indices.contains(&actual_index) { + actual_index += 1; + } + + // If we're inserting at the end, just push_back + if actual_index >= deque.len() { + deque.push_back(value); + return actual_index; + } + + // Store original values at locked positions + let locked_values: Vec<(usize, T)> = locked_indices + .iter() + .filter_map(|&idx| { + if idx < deque.len() { + Some((idx, deque[idx].clone())) + } else { + None + } + }) + .collect(); + + // Store all original values + let original_values: Vec = deque.iter().cloned().collect(); + + // Create a new deque with the correct final size + let mut new_deque = VecDeque::with_capacity(deque.len() + 1); + for _ in 0..deque.len() + 1 { + new_deque.push_back(value.clone()); // Temporary placeholder + } + + // First, place the new value at the insertion point + new_deque[actual_index] = value.clone(); + + // Then, place all locked values at their original positions + for (idx, val) in &locked_values { + new_deque[*idx] = val.clone(); + } + + // Now, fill in all remaining positions with values from the original deque, + // accounting for the shift caused by insertion + let mut orig_idx = 0; + #[allow(clippy::needless_range_loop)] + for new_idx in 0..new_deque.len() { + // Skip positions that are already filled (insertion point and locked positions) + if new_idx == actual_index || locked_indices.contains(&new_idx) { + continue; + } + + // Skip original elements that were at locked positions + while orig_idx < original_values.len() && locked_indices.contains(&orig_idx) { + orig_idx += 1; + } + + // If we still have original elements to place + if orig_idx < original_values.len() { + new_deque[new_idx] = original_values[orig_idx].clone(); + orig_idx += 1; + } + } + + // Update the original deque + *deque = new_deque; + + actual_index +} + +fn remove_respecting_locks( + deque: &mut VecDeque, + locked_indices: &mut HashSet, + index: usize, +) -> Option { + if index >= deque.len() { + return None; + } + + let removed = deque[index].clone(); + + // If removing a locked index, just remove it and unlock + if locked_indices.contains(&index) { + locked_indices.remove(&index); + deque.remove(index); + + // Update locked indices after the removal point + let new_locked: HashSet = locked_indices + .iter() + .map(|&idx| if idx > index { idx - 1 } else { idx }) + .collect(); + *locked_indices = new_locked; + + return Some(removed); + } + + // Let's build a new deque with the correct order + let mut result = VecDeque::with_capacity(deque.len() - 1); + + // 1. First include all elements before the removal index + #[allow(clippy::needless_range_loop)] + for i in 0..index { + result.push_back(deque[i].clone()); + } + + // 2. Then for each element after the removal index + #[allow(clippy::needless_range_loop)] + for i in (index + 1)..deque.len() { + // If the previous index was locked, we need to swap this element + // with the previous one in our result + if locked_indices.contains(&(i - 1)) { + // Insert this element before the locked element + if !result.is_empty() { + let locked_element = result.pop_back().unwrap(); + result.push_back(deque[i].clone()); + result.push_back(locked_element); + } else { + // This shouldn't happen with valid inputs + result.push_back(deque[i].clone()); + } + } else { + // Normal case, just add the element + result.push_back(deque[i].clone()); + } + } + + // Update the original deque + *deque = result; + + // Important: Keep the same locked indices (don't update them) + // Only remove any that are now out of bounds + locked_indices.retain(|&idx| idx < deque.len()); + + Some(removed) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::collections::VecDeque; + + #[test] + fn test_insert_respecting_locks() { + // Test case 1: Basic insertion with locked index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + // Insert at index 0, should shift elements while keeping index 2 locked + insert_respecting_locks(&mut deque, &mut locked, 0, 99); + assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4])); + // Element '2' remains at index 2, element '1' that was at index 1 is now at index 3 + } + + // Test case 2: Insert at a locked index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + // Try to insert at locked index 2, should insert at index 3 instead + let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99); + assert_eq!(actual_index, 3); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4])); + } + + // Test case 3: Multiple locked indices + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(1); // Lock index 1 + locked.insert(3); // Lock index 3 + + // Insert at index 0, should maintain locked indices + insert_respecting_locks(&mut deque, &mut locked, 0, 99); + assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4])); + // Elements '1' and '3' remain at indices 1 and 3 + } + + // Test case 4: Insert at end + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + // Insert at end of deque + let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99); + assert_eq!(actual_index, 5); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99])); + } + + // Test case 5: Empty deque + { + let mut deque = VecDeque::new(); + let mut locked = HashSet::new(); + + // Insert into empty deque + let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99); + assert_eq!(actual_index, 0); + assert_eq!(deque, VecDeque::from(vec![99])); + } + + // Test case 6: All indices locked + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + for i in 0..5 { + locked.insert(i); // Lock all indices + } + + // Try to insert at index 2, should insert at the end + let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99); + assert_eq!(actual_index, 5); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99])); + } + + // Test case 7: Consecutive locked indices + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + locked.insert(3); // Lock index 3 + + // Insert at index 1, should maintain consecutive locked indices + insert_respecting_locks(&mut deque, &mut locked, 1, 99); + assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4])); + // Elements '2' and '3' remain at indices 2 and 3 + } + } + + #[test] + fn test_remove_respecting_locks() { + // Test case 1: Remove a non-locked index before a locked index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 0); + assert_eq!(removed, Some(0)); + assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4])); + assert!(locked.contains(&2)); // Index 2 should still be locked + } + + // Test case 2: Remove a locked index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 2); + assert_eq!(removed, Some(2)); + assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4])); + assert!(!locked.contains(&2)); // Index 2 should be unlocked + } + + // Test case 3: Remove an index after a locked index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(1); // Lock index 1 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 3); + assert_eq!(removed, Some(3)); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4])); + assert!(locked.contains(&1)); // Index 1 should still be locked + } + + // Test case 4: Multiple locked indices + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(1); // Lock index 1 + locked.insert(3); // Lock index 3 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 0); + assert_eq!(removed, Some(0)); + assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3])); + assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked + } + + // Test case 5: Remove the last element + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 4); + assert_eq!(removed, Some(4)); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3])); + assert!(locked.contains(&2)); // Index 2 should still be locked + } + + // Test case 6: Invalid index + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 10); + assert_eq!(removed, None); + assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged + assert!(locked.contains(&2)); // Lock unchanged + } + + // Test case 7: Remove enough elements to make a locked index invalid + { + let mut deque = VecDeque::from(vec![0, 1, 2]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + + remove_respecting_locks(&mut deque, &mut locked, 0); + assert_eq!(deque, VecDeque::from(vec![1, 2])); + assert!(!locked.contains(&2)); // Index 2 should now be invalid + } + + // Test case 8: Removing an element before multiple locked indices + { + let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]); + let mut locked = HashSet::new(); + locked.insert(2); // Lock index 2 + locked.insert(4); // Lock index 4 + + let removed = remove_respecting_locks(&mut deque, &mut locked, 1); + assert_eq!(removed, Some(1)); + assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4])); + assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked + } + } +} diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 54ee3e49..868c7e68 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -1,4 +1,5 @@ use color_eyre::eyre::anyhow; +use color_eyre::eyre::OptionExt; use color_eyre::Result; use miow::pipe::connect; use net2::TcpStreamExt; @@ -359,6 +360,41 @@ impl WindowManager { SocketMessage::Minimize => { Window::from(WindowsApi::foreground_window()?).minimize(); } + SocketMessage::LockMonitorWorkspaceContainer( + monitor_idx, + workspace_idx, + container_idx, + ) => { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_eyre("no monitor at the given index")?; + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_eyre("no workspace at the given index")?; + + workspace.locked_containers.insert(container_idx); + } + SocketMessage::UnlockMonitorWorkspaceContainer( + monitor_idx, + workspace_idx, + container_idx, + ) => { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_eyre("no monitor at the given index")?; + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_eyre("no workspace at the given index")?; + + workspace.locked_containers.remove(&container_idx); + } + SocketMessage::ToggleLock => self.toggle_lock()?, SocketMessage::ToggleFloat => self.toggle_float()?, SocketMessage::ToggleMonocle => self.toggle_monocle()?, SocketMessage::ToggleMaximize => self.toggle_maximize()?, @@ -1783,6 +1819,10 @@ impl WindowManager { WindowKind::Unfocused => { border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst); } + WindowKind::UnfocusedLocked => { + border_manager::UNFOCUSED_LOCKED + .store(Rgb::new(r, g, b).into(), Ordering::SeqCst); + } WindowKind::Floating => { border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst); } diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 432a2be8..ff53a559 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -500,6 +500,9 @@ pub enum KomorebiTheme { /// Border colour when the container is unfocused (default: Base) #[serde(skip_serializing_if = "Option::is_none")] unfocused_border: Option, + /// Border colour when the container is unfocused and locked (default: Red) + #[serde(skip_serializing_if = "Option::is_none")] + unfocused_locked_border: Option, /// Stackbar focused tab text colour (default: Green) #[serde(skip_serializing_if = "Option::is_none")] stackbar_focused_text: Option, @@ -532,6 +535,9 @@ pub enum KomorebiTheme { /// Border colour when the container is unfocused (default: Base01) #[serde(skip_serializing_if = "Option::is_none")] unfocused_border: Option, + /// Border colour when the container is unfocused and locked (default: Base08) + #[serde(skip_serializing_if = "Option::is_none")] + unfocused_locked_border: Option, /// Stackbar focused tab text colour (default: Base0B) #[serde(skip_serializing_if = "Option::is_none")] stackbar_focused_text: Option, diff --git a/komorebi/src/theme_manager.rs b/komorebi/src/theme_manager.rs index eb4790d7..be70a715 100644 --- a/komorebi/src/theme_manager.rs +++ b/komorebi/src/theme_manager.rs @@ -76,6 +76,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> { monocle_border, floating_border, unfocused_border, + unfocused_locked_border, stackbar_focused_text, stackbar_unfocused_text, stackbar_background, @@ -87,6 +88,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> { monocle_border, floating_border, unfocused_border, + unfocused_locked_border, stackbar_focused_text, stackbar_unfocused_text, stackbar_background, @@ -112,6 +114,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> { .unwrap_or(komorebi_themes::CatppuccinValue::Base) .color32(name.as_theme()); + let unfocused_locked_border = unfocused_locked_border + .unwrap_or(komorebi_themes::CatppuccinValue::Red) + .color32(name.as_theme()); + let stackbar_focused_text = stackbar_focused_text .unwrap_or(komorebi_themes::CatppuccinValue::Green) .color32(name.as_theme()); @@ -130,6 +136,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> { monocle_border, floating_border, unfocused_border, + unfocused_locked_border, stackbar_focused_text, stackbar_unfocused_text, stackbar_background, @@ -142,6 +149,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> { monocle_border, floating_border, unfocused_border, + unfocused_locked_border, stackbar_focused_text, stackbar_unfocused_text, stackbar_background, @@ -163,6 +171,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> { .unwrap_or(komorebi_themes::Base16Value::Base01) .color32(*name); + let unfocused_locked_border = unfocused_locked_border + .unwrap_or(komorebi_themes::Base16Value::Base08) + .color32(*name); + let floating_border = floating_border .unwrap_or(komorebi_themes::Base16Value::Base09) .color32(*name); @@ -185,6 +197,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> { monocle_border, floating_border, unfocused_border, + unfocused_locked_border, stackbar_focused_text, stackbar_unfocused_text, stackbar_background, @@ -198,6 +211,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> { border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst); border_manager::UNFOCUSED .store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst); + border_manager::UNFOCUSED_LOCKED.store( + u32::from(Colour::from(unfocused_locked_border)), + Ordering::SeqCst, + ); STACKBAR_TAB_BACKGROUND_COLOUR.store( u32::from(Colour::from(stackbar_background)), diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 1c677965..5138bcb1 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -342,6 +342,7 @@ impl From<&WindowManager> for State { float_override: workspace.float_override, layer: workspace.layer, globals: workspace.globals, + locked_containers: workspace.locked_containers.clone(), workspace_config: None, }) .collect::>(); @@ -2849,6 +2850,20 @@ impl WindowManager { self.update_focused_workspace(is_floating_window, true) } + #[tracing::instrument(skip(self))] + pub fn toggle_lock(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + let index = workspace.focused_container_idx(); + + if workspace.locked_containers().contains(&index) { + workspace.locked_containers_mut().remove(&index); + } else { + workspace.locked_containers_mut().insert(index); + } + + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn float_window(&mut self) -> Result<()> { tracing::info!("floating window"); diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index c1c7d2ed..e9b3aecd 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::collections::VecDeque; use std::fmt::Display; use std::fmt::Formatter; @@ -25,6 +26,7 @@ use crate::core::Rect; use crate::border_manager::BORDER_OFFSET; use crate::border_manager::BORDER_WIDTH; use crate::container::Container; +use crate::locked_deque::LockedDeque; use crate::ring::Ring; use crate::should_act; use crate::stackbar_manager; @@ -90,6 +92,8 @@ pub struct Workspace { pub globals: WorkspaceGlobals, #[getset(get = "pub", get_mut = "pub", set = "pub")] pub layer: WorkspaceLayer, + #[getset(get = "pub", get_mut = "pub", set = "pub")] + pub locked_containers: HashSet, #[serde(skip_serializing_if = "Option::is_none")] #[getset(get = "pub", set = "pub")] pub workspace_config: Option, @@ -139,6 +143,7 @@ impl Default for Workspace { layer: Default::default(), globals: Default::default(), workspace_config: None, + locked_containers: Default::default(), } } } @@ -912,9 +917,10 @@ impl Workspace { .ok_or_else(|| anyhow!("there is no window"))?; if container.windows().is_empty() { - self.containers_mut() - .remove(container_idx) - .ok_or_else(|| anyhow!("there is no container"))?; + let mut locked_containers = self.locked_containers().clone(); + let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers); + ld.remove(container_idx); + self.locked_containers = locked_containers; // Whenever a container is empty, we need to remove any resize dimensions for it too if self.resize_dimensions().get(container_idx).is_some() { @@ -1045,7 +1051,7 @@ impl Workspace { } pub fn new_container_for_window(&mut self, window: Window) { - let next_idx = if self.containers().is_empty() { + let mut next_idx = if self.containers().is_empty() { 0 } else { self.focused_container_idx() + 1 @@ -1057,7 +1063,10 @@ impl Workspace { if next_idx > self.containers().len() { self.containers_mut().push_back(container); } else { - self.containers_mut().insert(next_idx, container); + let mut locked_containers = self.locked_containers().clone(); + let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers); + next_idx = ld.insert(next_idx, container); + self.locked_containers = locked_containers; } if next_idx > self.resize_dimensions().len() { @@ -1640,6 +1649,82 @@ impl Workspace { mod tests { use super::*; + use crate::container::Container; + use crate::Window; + use std::collections::HashMap; + use std::collections::HashSet; + + #[test] + fn test_locked_containers_with_new_window() { + let mut ws = Workspace::default(); + + let mut state = HashMap::new(); + let mut locked = HashSet::new(); + + // add 3 containers + for i in 0..4 { + let container = Container::default(); + state.insert(i, container.id().to_string()); + ws.add_container_to_back(container); + } + assert_eq!(ws.containers().len(), 4); + + // set index 3 locked + locked.insert(3); + ws.locked_containers = locked; + + // focus container at index 2 + ws.focus_container(2); + + // simulate a new window being launched on this workspace + ws.new_container_for_window(Window::from(123)); + + // new length should be 5, with the focus on the new window at index 4 + assert_eq!(ws.containers().len(), 5); + assert_eq!(ws.focused_container_idx(), 4); + assert_eq!( + ws.focused_container() + .unwrap() + .focused_window() + .unwrap() + .hwnd, + 123 + ); + + // when inserting a new container at index 0, index 3's container should not change + ws.focus_container(0); + ws.new_container_for_window(Window::from(234)); + assert_eq!( + ws.containers()[3].id().to_string(), + state.get(&3).unwrap().to_string() + ); + } + + #[test] + fn test_locked_containers_remove_window() { + let mut ws = Workspace::default(); + + let mut locked = HashSet::new(); + + // add 4 containers + for i in 0..4 { + let mut container = Container::default(); + container.windows_mut().push_back(Window::from(i)); + ws.add_container_to_back(container); + } + assert_eq!(ws.containers().len(), 4); + + // set index 1 locked + locked.insert(1); + ws.locked_containers = locked; + + ws.remove_window(0).unwrap(); + assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2); + // index 1 should still be the same + assert_eq!(ws.containers()[1].focused_window().unwrap().hwnd, 1); + assert_eq!(ws.containers()[2].focused_window().unwrap().hwnd, 3); + } + #[test] fn test_contains_window() { // Create default workspace diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 185d6946..581703d2 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -1283,6 +1283,8 @@ enum SubCommand { ToggleMonocle, /// Toggle native maximization for the focused window ToggleMaximize, + /// Toggle a lock for the focused container, ensuring it will not be displaced by any new windows + ToggleLock, /// Restore all hidden windows (debugging command) RestoreWindows, /// Force komorebi to manage the focused window @@ -1947,6 +1949,9 @@ fn main() -> Result<()> { SubCommand::ToggleMaximize => { send_message(&SocketMessage::ToggleMaximize)?; } + SubCommand::ToggleLock => { + send_message(&SocketMessage::ToggleLock)?; + } SubCommand::WorkspaceLayout(arg) => { send_message(&SocketMessage::WorkspaceLayout( arg.monitor, diff --git a/schema.json b/schema.json index 26f6d345..f981dc9d 100644 --- a/schema.json +++ b/schema.json @@ -2211,6 +2211,38 @@ "Mantle", "Crust" ] + }, + "unfocused_locked_border": { + "description": "Border colour when the container is unfocused and locked (default: Red)", + "type": "string", + "enum": [ + "Rosewater", + "Flamingo", + "Pink", + "Mauve", + "Red", + "Maroon", + "Peach", + "Yellow", + "Green", + "Teal", + "Sky", + "Sapphire", + "Blue", + "Lavender", + "Text", + "Subtext1", + "Subtext0", + "Overlay2", + "Overlay1", + "Overlay0", + "Surface2", + "Surface1", + "Surface0", + "Base", + "Mantle", + "Crust" + ] } } }, @@ -2700,6 +2732,28 @@ "Base0E", "Base0F" ] + }, + "unfocused_locked_border": { + "description": "Border colour when the container is unfocused and locked (default: Base08)", + "type": "string", + "enum": [ + "Base00", + "Base01", + "Base02", + "Base03", + "Base04", + "Base05", + "Base06", + "Base07", + "Base08", + "Base09", + "Base0A", + "Base0B", + "Base0C", + "Base0D", + "Base0E", + "Base0F" + ] } } }