Skip to content

Commit

Permalink
feat(wm): add cross boundary behaviour options
Browse files Browse the repository at this point in the history
This commit introduces a new configuration option,
cross_boundary_behaviour, which allows the user to decide if they want
Focus and Move operations to operate across Workspace or Monitor
boundaries.

The default behaviour in komorebi has always been Monitor.  Setting this
to Workspace will make komorebi act a little like PaperWM, where
"komorebic focus left" and "komorebic focus right" will switch to the
next or previous workspace respectively if the currently focused window
as at either the left or right monitor boundary.

resolve #959
  • Loading branch information
LGUG2Z committed Aug 27, 2024
1 parent 3c03528 commit b799fd3
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 11 deletions.
10 changes: 10 additions & 0 deletions komorebi/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,16 @@ pub enum MoveBehaviour {
NoOp,
}

#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
Workspace,
/// Attempt to perform actions across a monitor boundary
Monitor,
}

#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
Expand Down
10 changes: 8 additions & 2 deletions komorebi/src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::core::Rect;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::OperationDirection;

#[derive(
Debug,
Expand Down Expand Up @@ -114,7 +115,7 @@ impl Monitor {
.ok_or_else(|| anyhow!("there is no workspace"))?
};

workspace.add_container(container);
workspace.add_container_to_back(container);

Ok(())
}
Expand Down Expand Up @@ -149,6 +150,7 @@ impl Monitor {
&mut self,
target_workspace_idx: usize,
follow: bool,
direction: Option<OperationDirection>,
) -> Result<()> {
let workspace = self
.focused_workspace_mut()
Expand All @@ -173,7 +175,11 @@ impl Monitor {
Some(workspace) => workspace,
};

target_workspace.add_container(container);
if matches!(direction, Some(OperationDirection::Right)) {
target_workspace.add_container_to_front(container);
} else {
target_workspace.add_container_to_back(container);
}

if follow {
self.focus_workspace(target_workspace_idx)?;
Expand Down
2 changes: 1 addition & 1 deletion komorebi/src/monitor_reconciliator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result

// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
focused_ws.add_container_to_back(container);
}

// Gotta reset the focus or the movement will feel "off"
Expand Down
8 changes: 4 additions & 4 deletions komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ impl WindowManager {
self.adjust_workspace_padding(sizing, adjustment)?;
}
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, true)?;
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::CycleMoveContainerToWorkspace(direction) => {
let focused_monitor = self
Expand All @@ -457,7 +457,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);

self.move_container_to_workspace(workspace_idx, true)?;
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, true)?;
Expand All @@ -475,7 +475,7 @@ impl WindowManager {
self.move_container_to_monitor(monitor_idx, None, true)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false)?;
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::CycleSendContainerToWorkspace(direction) => {
let focused_monitor = self
Expand All @@ -491,7 +491,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);

self.move_container_to_workspace(workspace_idx, false)?;
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, false)?;
Expand Down
8 changes: 8 additions & 0 deletions komorebi/src/static_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::CrossBoundaryBehaviour;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
Expand Down Expand Up @@ -265,6 +266,9 @@ pub struct StaticConfig {
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
/// Determine what happens when an action is called on a window at a monitor boundary (default: Monitor)
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_boundary_behaviour: Option<CrossBoundaryBehaviour>,
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
Expand Down Expand Up @@ -565,6 +569,7 @@ impl From<&WindowManager> for StaticConfig {
resize_delta: Option::from(value.resize_delta),
window_container_behaviour: Option::from(value.window_container_behaviour),
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
cross_boundary_behaviour: Option::from(value.cross_boundary_behaviour),
unmanaged_window_operation_behaviour: Option::from(
value.unmanaged_window_operation_behaviour,
),
Expand Down Expand Up @@ -866,6 +871,9 @@ impl StaticConfig {
cross_monitor_move_behaviour: value
.cross_monitor_move_behaviour
.unwrap_or(MoveBehaviour::Swap),
cross_boundary_behaviour: value
.cross_boundary_behaviour
.unwrap_or(CrossBoundaryBehaviour::Monitor),
unmanaged_window_operation_behaviour: value
.unmanaged_window_operation_behaviour
.unwrap_or(OperationBehaviour::Op),
Expand Down
83 changes: 80 additions & 3 deletions komorebi/src/window_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::BorderColours;
use crate::Colour;
use crate::CrossBoundaryBehaviour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::CUSTOM_FFM;
Expand Down Expand Up @@ -92,6 +93,7 @@ pub struct WindowManager {
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub cross_boundary_behaviour: CrossBoundaryBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
Expand Down Expand Up @@ -274,6 +276,7 @@ impl WindowManager {
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
cross_monitor_move_behaviour: MoveBehaviour::Swap,
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
resize_delta: 50,
focus_follows_mouse: None,
Expand Down Expand Up @@ -1119,7 +1122,7 @@ impl WindowManager {

if focused_monitor_idx == monitor_idx {
if let Some(workspace_idx) = workspace_idx {
return self.move_container_to_workspace(workspace_idx, follow);
return self.move_container_to_workspace(workspace_idx, follow, None);
}
}

Expand Down Expand Up @@ -1187,7 +1190,12 @@ impl WindowManager {
}

#[tracing::instrument(skip(self))]
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
pub fn move_container_to_workspace(
&mut self,
idx: usize,
follow: bool,
direction: Option<OperationDirection>,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;

tracing::info!("moving container");
Expand All @@ -1197,7 +1205,7 @@ impl WindowManager {
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;

monitor.move_container_to_workspace(idx, follow)?;
monitor.move_container_to_workspace(idx, follow, direction)?;
monitor.load_focused_workspace(mouse_follows_focus)?;

self.update_focused_workspace(mouse_follows_focus, true)?;
Expand Down Expand Up @@ -1239,6 +1247,7 @@ impl WindowManager {
self.handle_unmanaged_window_behaviour()?;

let workspace = self.focused_workspace()?;
let workspace_idx = self.focused_workspace_idx()?;

tracing::info!("focusing container");

Expand All @@ -1250,6 +1259,39 @@ impl WindowManager {

let mut cross_monitor_monocle = false;

if new_idx.is_none()
&& matches!(
self.cross_boundary_behaviour,
CrossBoundaryBehaviour::Workspace
)
&& matches!(
direction,
OperationDirection::Left | OperationDirection::Right
)
{
let workspace_count = if let Some(monitor) = self.focused_monitor() {
monitor.workspaces().len()
} else {
1
};

let next_idx = match direction {
OperationDirection::Left => match workspace_idx {
0 => workspace_count - 1,
n => n - 1,
},
OperationDirection::Right => match workspace_idx {
n if n == workspace_count - 1 => 0,
n => n + 1,
},
_ => workspace_idx,
};

self.focus_workspace(next_idx)?;

return Ok(());
}

// if there is no container in that direction for this workspace
match new_idx {
None => {
Expand Down Expand Up @@ -1292,6 +1334,7 @@ impl WindowManager {
self.handle_unmanaged_window_behaviour()?;

let workspace = self.focused_workspace()?;
let workspace_idx = self.focused_workspace_idx()?;

// removing this messes up the monitor / container / window index somewhere
// and results in the wrong window getting moved across the monitor boundary
Expand All @@ -1305,6 +1348,40 @@ impl WindowManager {
let origin_monitor_idx = self.focused_monitor_idx();
let target_container_idx = workspace.new_idx_for_direction(direction);

if target_container_idx.is_none()
&& matches!(
self.cross_boundary_behaviour,
CrossBoundaryBehaviour::Workspace
)
&& matches!(
direction,
OperationDirection::Left | OperationDirection::Right
)
{
let workspace_count = if let Some(monitor) = self.focused_monitor() {
monitor.workspaces().len()
} else {
1
};

let next_idx = match direction {
OperationDirection::Left => match workspace_idx {
0 => workspace_count - 1,
n => n - 1,
},
OperationDirection::Right => match workspace_idx {
n if n == workspace_count - 1 => 0,
n => n + 1,
},
_ => workspace_idx,
};

self.move_container_to_workspace(next_idx, true, Some(direction))?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;

return Ok(());
}

match target_container_idx {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
// in that direction if there is one
Expand Down
11 changes: 10 additions & 1 deletion komorebi/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,11 +656,16 @@ impl Workspace {
Ok(())
}

pub fn add_container(&mut self, container: Container) {
pub fn add_container_to_back(&mut self, container: Container) {
self.containers_mut().push_back(container);
self.focus_last_container();
}

pub fn add_container_to_front(&mut self, container: Container) {
self.containers_mut().push_front(container);
self.focus_first_container();
}

pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) {
self.containers_mut().insert(idx, container);
}
Expand Down Expand Up @@ -1442,4 +1447,8 @@ impl Workspace {
fn focus_last_container(&mut self) {
self.focus_container(self.containers().len().saturating_sub(1));
}

fn focus_first_container(&mut self) {
self.focus_container(0);
}
}

0 comments on commit b799fd3

Please sign in to comment.