feat(wm): add locked containers per workspace

This commit adds the concept of locked container indexes to komorebi
workspaces.

When a container index is locked, it can only be displaced by manual
user actual - usually when another container is moved there, and when
this happens, that container becomes the locked container.

In the locked state, the container at the locked index should never be
displaced by new windows opening or existing windows around it being
closed.

When the total number of containers on a workspace falls below the
number of the locked index, the locked index will be removed.

A locked index can be identified by a special border color linked to the
new WindowKind::UnfocusedLocked variant.

The implementation of locked container indexes is backed by a new data
structure called a LockedDeque, which is a VecDeque with an auxiliary
HashSet which keeps track of locked indices.

A new komorebic command "toggle-lock" has been added to support
programmatic use of this feature, as well as the
LockMonitorWorkspaceContainer and UnlockMonitorWorkspaceContainer
SocketMessage variants which can be used by status bars.
This commit is contained in:
LGUG2Z
2025-03-06 19:30:16 -08:00
parent edf1943157
commit 6ba19d3ea2
12 changed files with 613 additions and 7 deletions

View File

@@ -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 {

View File

@@ -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<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> 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 {

View File

@@ -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,
}

View File

@@ -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;

View File

@@ -0,0 +1,367 @@
use std::collections::HashSet;
use std::collections::VecDeque;
pub struct LockedDeque<'a, T> {
deque: &'a mut VecDeque<T>,
locked_indices: &'a mut HashSet<usize>,
}
impl<'a, T: Clone + PartialEq> LockedDeque<'a, T> {
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut HashSet<usize>) -> 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<T> {
remove_respecting_locks(self.deque, self.locked_indices, index)
}
}
fn insert_respecting_locks<T: Clone>(
deque: &mut VecDeque<T>,
locked_indices: &mut HashSet<usize>,
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<T> = 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<T: Clone>(
deque: &mut VecDeque<T>,
locked_indices: &mut HashSet<usize>,
index: usize,
) -> Option<T> {
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<usize> = 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
}
}
}

View File

@@ -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);
}

View File

@@ -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<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused and locked (default: Red)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
@@ -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<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,

View File

@@ -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)),

View File

@@ -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::<VecDeque<_>>();
@@ -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");

View File

@@ -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<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
pub workspace_config: Option<WorkspaceConfig>,
@@ -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

View File

@@ -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,

View File

@@ -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"
]
}
}
}