From 4e7d0e337ca14faf136b5d041c99a5ce42cd8f7a Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Mon, 4 May 2026 18:38:38 -0700 Subject: [PATCH] feat(wm): add monocle_focus_behaviour to gate monocle cycling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5f629e1f made directional focus cycle the monocle container in the underlying ring. Useful on a single monitor, but undesired when focusing across monitors — the focus stays trapped on the monocle workspace instead of crossing the boundary. This commit adds a new top-level monocle_focus_behaviour option with two variants, Cycle (the post-5f629e1f behaviour) and NoOp (the pre-5f629e1f behaviour, now the default), along with a ToggleMonocleFocusBehaviour socket message and corresponding komorebic subcommands to flip between them at runtime. --- komorebi-client/src/lib.rs | 1 + komorebi/src/core/mod.rs | 15 +++++++++++++++ komorebi/src/process_command.rs | 10 ++++++++++ komorebi/src/state.rs | 7 +++++++ komorebi/src/static_config.rs | 8 ++++++++ komorebi/src/window_manager.rs | 18 ++++++++++++------ komorebic/src/main.rs | 18 ++++++++++++++++++ 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/komorebi-client/src/lib.rs b/komorebi-client/src/lib.rs index 02bfd96e..db8bbeb6 100644 --- a/komorebi-client/src/lib.rs +++ b/komorebi-client/src/lib.rs @@ -57,6 +57,7 @@ pub use komorebi::core::FloatingLayerBehaviour; pub use komorebi::core::FocusFollowsMouseImplementation; pub use komorebi::core::HidingBehaviour; pub use komorebi::core::Layout; +pub use komorebi::core::MonocleFocusBehaviour; pub use komorebi::core::MoveBehaviour; pub use komorebi::core::OperationBehaviour; pub use komorebi::core::OperationDirection; diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index e66e023b..120069f6 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -113,6 +113,8 @@ pub enum SocketMessage { WindowHidingBehaviour(HidingBehaviour), ToggleCrossMonitorMoveBehaviour, CrossMonitorMoveBehaviour(MoveBehaviour), + ToggleMonocleFocusBehaviour, + MonocleFocusBehaviour(MonocleFocusBehaviour), UnmanagedWindowOperationBehaviour(OperationBehaviour), // Current Workspace Commands ManageFocusedWindow, @@ -530,6 +532,19 @@ pub enum CrossBoundaryBehaviour { Monitor, } +#[derive( + Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq, +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// Behaviour when focusing in a direction while a monocle container is active +pub enum MonocleFocusBehaviour { + /// Cycle the monocle container to the next/previous container in the workspace + Cycle, + /// Do nothing, allowing focus to fall through to cross-monitor logic + #[default] + NoOp, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Window hiding behaviour diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 4ac4f0f0..b1ee3d88 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -61,6 +61,7 @@ use crate::core::BorderImplementation; use crate::core::FocusFollowsMouseImplementation; use crate::core::Layout; use crate::core::LayoutOptions; +use crate::core::MonocleFocusBehaviour; use crate::core::MoveBehaviour; use crate::core::OperationDirection; use crate::core::Rect; @@ -2068,6 +2069,15 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue)) SocketMessage::CrossMonitorMoveBehaviour(behaviour) => { self.cross_monitor_move_behaviour = behaviour; } + SocketMessage::ToggleMonocleFocusBehaviour => { + self.monocle_focus_behaviour = match self.monocle_focus_behaviour { + MonocleFocusBehaviour::Cycle => MonocleFocusBehaviour::NoOp, + MonocleFocusBehaviour::NoOp => MonocleFocusBehaviour::Cycle, + }; + } + SocketMessage::MonocleFocusBehaviour(behaviour) => { + self.monocle_focus_behaviour = behaviour; + } SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => { self.unmanaged_window_operation_behaviour = behaviour; } diff --git a/komorebi/src/state.rs b/komorebi/src/state.rs index 8bc8a373..a935d044 100644 --- a/komorebi/src/state.rs +++ b/komorebi/src/state.rs @@ -13,6 +13,7 @@ use crate::IGNORE_IDENTIFIERS; use crate::LAYERED_WHITELIST; use crate::MANAGE_IDENTIFIERS; use crate::MONITOR_INDEX_PREFERENCES; +use crate::MonocleFocusBehaviour; use crate::MoveBehaviour; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; use crate::OperationBehaviour; @@ -61,6 +62,7 @@ pub struct State { pub new_window_behaviour: WindowContainerBehaviour, pub float_override: bool, pub cross_monitor_move_behaviour: MoveBehaviour, + pub monocle_focus_behaviour: MonocleFocusBehaviour, pub unmanaged_window_operation_behaviour: OperationBehaviour, pub work_area_offset: Option, pub focus_follows_mouse: Option, @@ -93,6 +95,10 @@ impl State { return true; } + if self.monocle_focus_behaviour != new.monocle_focus_behaviour { + return true; + } + if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour { return true; } @@ -301,6 +307,7 @@ impl From<&WindowManager> for State { new_window_behaviour: wm.window_management_behaviour.current_behaviour, float_override: wm.window_management_behaviour.float_override, cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour, + monocle_focus_behaviour: wm.monocle_focus_behaviour, focus_follows_mouse: wm.focus_follows_mouse, mouse_follows_focus: wm.mouse_follows_focus, has_pending_raise_op: wm.has_pending_raise_op, diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 08debea8..776f6d6a 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -58,6 +58,7 @@ use crate::core::HidingBehaviour; use crate::core::Layout; use crate::core::LayoutDefaultEntry; use crate::core::LayoutOptions; +use crate::core::MonocleFocusBehaviour; use crate::core::MoveBehaviour; use crate::core::OperationBehaviour; use crate::core::Rect; @@ -529,6 +530,10 @@ pub struct StaticConfig { #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schemars", schemars(extend("default" = CrossBoundaryBehaviour::Monitor)))] pub cross_boundary_behaviour: Option, + /// Determine what happens when focusing in a direction while a monocle container is active + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "schemars", schemars(extend("default" = MonocleFocusBehaviour::NoOp)))] + pub monocle_focus_behaviour: Option, /// Determine what happens when commands are sent while an unmanaged window is in the foreground #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schemars", schemars(extend("default" = OperationBehaviour::Op)))] @@ -886,6 +891,7 @@ impl From<&WindowManager> for StaticConfig { ), cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour), cross_boundary_behaviour: Option::from(value.cross_boundary_behaviour), + monocle_focus_behaviour: Option::from(value.monocle_focus_behaviour), unmanaged_window_operation_behaviour: Option::from( value.unmanaged_window_operation_behaviour, ), @@ -1348,6 +1354,7 @@ impl StaticConfig { cross_boundary_behaviour: value .cross_boundary_behaviour .unwrap_or(CrossBoundaryBehaviour::Monitor), + monocle_focus_behaviour: value.monocle_focus_behaviour.unwrap_or_default(), unmanaged_window_operation_behaviour: value .unmanaged_window_operation_behaviour .unwrap_or(OperationBehaviour::Op), @@ -1749,6 +1756,7 @@ impl StaticConfig { value.float_rule_placement.unwrap_or(Placement::None); wm.cross_monitor_move_behaviour = value.cross_monitor_move_behaviour.unwrap_or_default(); wm.cross_boundary_behaviour = value.cross_boundary_behaviour.unwrap_or_default(); + wm.monocle_focus_behaviour = value.monocle_focus_behaviour.unwrap_or_default(); wm.unmanaged_window_operation_behaviour = value .unmanaged_window_operation_behaviour .unwrap_or_default(); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index ecfbce24..d862a6db 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -33,6 +33,7 @@ use crate::core::CycleDirection; use crate::core::DefaultLayout; use crate::core::FocusFollowsMouseImplementation; use crate::core::Layout; +use crate::core::MonocleFocusBehaviour; use crate::core::MoveBehaviour; use crate::core::OperationBehaviour; use crate::core::OperationDirection; @@ -81,6 +82,7 @@ pub struct WindowManager { pub window_management_behaviour: WindowManagementBehaviour, pub cross_monitor_move_behaviour: MoveBehaviour, pub cross_boundary_behaviour: CrossBoundaryBehaviour, + pub monocle_focus_behaviour: MonocleFocusBehaviour, pub unmanaged_window_operation_behaviour: OperationBehaviour, pub focus_follows_mouse: Option, pub mouse_follows_focus: bool, @@ -158,6 +160,7 @@ impl WindowManager { window_management_behaviour: WindowManagementBehaviour::default(), cross_monitor_move_behaviour: MoveBehaviour::Swap, cross_boundary_behaviour: CrossBoundaryBehaviour::Monitor, + monocle_focus_behaviour: MonocleFocusBehaviour::default(), unmanaged_window_operation_behaviour: OperationBehaviour::Op, resize_delta: 50, focus_follows_mouse: None, @@ -2110,7 +2113,9 @@ impl WindowManager { tracing::info!("focusing container"); - if workspace.monocle_container.is_some() { + if workspace.monocle_container.is_some() + && matches!(self.monocle_focus_behaviour, MonocleFocusBehaviour::Cycle) + { let cycle_direction = match direction { OperationDirection::Left | OperationDirection::Down => CycleDirection::Previous, OperationDirection::Right | OperationDirection::Up => CycleDirection::Next, @@ -2118,11 +2123,12 @@ impl WindowManager { return self.cycle_monocle(cycle_direction); } - let new_idx = if workspace.maximized_window.is_some() { - None - } else { - workspace.new_idx_for_direction(direction) - }; + let new_idx = + if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() { + None + } else { + workspace.new_idx_for_direction(direction) + }; let mut cross_monitor_monocle_or_max = false; diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 00fa2318..74b40a0a 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -737,6 +737,13 @@ struct BorderStyle { style: komorebi_client::BorderStyle, } +#[derive(Parser)] +struct MonocleFocusBehaviour { + /// Desired monocle focus behaviour + #[clap(value_enum)] + behaviour: komorebi_client::MonocleFocusBehaviour, +} + #[derive(Parser)] struct BorderImplementation { /// Desired border implementation @@ -1418,6 +1425,11 @@ enum SubCommand { CrossMonitorMoveBehaviour(CrossMonitorMoveBehaviour), /// Toggle the behaviour when moving windows across monitor boundaries ToggleCrossMonitorMoveBehaviour, + /// Set the behaviour when focusing in a direction while a monocle container is active + #[clap(arg_required_else_help = true)] + MonocleFocusBehaviour(MonocleFocusBehaviour), + /// Toggle the behaviour when focusing in a direction while a monocle container is active + ToggleMonocleFocusBehaviour, /// Set the operation behaviour when the focused window is not managed #[clap(arg_required_else_help = true)] UnmanagedWindowOperationBehaviour(UnmanagedWindowOperationBehaviour), @@ -3251,6 +3263,12 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { SubCommand::ToggleCrossMonitorMoveBehaviour => { send_message(&SocketMessage::ToggleCrossMonitorMoveBehaviour)?; } + SubCommand::MonocleFocusBehaviour(args) => { + send_message(&SocketMessage::MonocleFocusBehaviour(args.behaviour))?; + } + SubCommand::ToggleMonocleFocusBehaviour => { + send_message(&SocketMessage::ToggleMonocleFocusBehaviour)?; + } SubCommand::UnmanagedWindowOperationBehaviour(args) => { send_message(&SocketMessage::UnmanagedWindowOperationBehaviour( args.operation_behaviour,