feat(wm): add direction preselection

This commit adds a new feature to preselect the direction of the next
spawned window with a corresponding komorebic preselect-direction
command which takes an OperationDirection.

If the OperationDirection is valid from the current position, it will be
stored in the Workspace state, and then read, applied, and deleted when
the next manage-able window is spawned.

Direction preselection does not (yet?) support the Grid layout.
This commit is contained in:
LGUG2Z
2025-10-31 15:08:55 -07:00
parent 18ee667896
commit adbb6c1cb0
8 changed files with 93 additions and 1 deletions

View File

@@ -0,0 +1,16 @@
# preselect-direction
```
Preselect the specified direction for the next window to be spawned on supported layouts
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
Arguments:
<OPERATION_DIRECTION>
[possible values: left, right, up, down]
Options:
-h, --help
Print help
```

View File

@@ -55,6 +55,7 @@ pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
MoveWindow(OperationDirection),
PreselectDirection(OperationDirection),
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),

View File

@@ -303,6 +303,12 @@ impl WindowManager {
}
}
}
SocketMessage::PreselectDirection(direction) => {
let focused_workspace = self.focused_workspace()?;
if matches!(focused_workspace.layer, WorkspaceLayer::Tiling) {
self.preselect_container_in_direction(direction)?;
}
}
SocketMessage::MoveWindow(direction) => {
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer {

View File

@@ -272,6 +272,7 @@ impl From<&WindowManager> for State {
globals: workspace.globals,
wallpaper: workspace.wallpaper.clone(),
workspace_config: None,
preselected_container_idx: None,
})
.collect::<VecDeque<_>>();
ws.focus(monitor.workspaces.focused_idx());

View File

@@ -2042,6 +2042,53 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn preselect_container_in_direction(
&mut self,
direction: OperationDirection,
) -> eyre::Result<()> {
let workspace = self.focused_workspace_mut()?;
let focused_idx = workspace.focused_container_idx();
if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) {
tracing::warn!("preselection is not supported on the grid layout");
return Ok(());
}
tracing::info!("preselecting container");
let new_idx =
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
match new_idx {
Some(new_idx) => {
let adjusted_idx = match direction {
OperationDirection::Left | OperationDirection::Up => {
if focused_idx.abs_diff(new_idx) == 1 {
new_idx + 1
} else {
new_idx
}
}
_ => new_idx,
};
workspace.preselect_container_index(adjusted_idx);
}
None => {
tracing::debug!(
"this is not a valid preselection direction from the current position"
)
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_direction(
&mut self,

View File

@@ -78,6 +78,8 @@ pub struct Workspace {
pub wallpaper: Option<Wallpaper>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_config: Option<WorkspaceConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preselected_container_idx: Option<usize>,
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -129,6 +131,7 @@ impl Default for Workspace {
globals: Default::default(),
workspace_config: None,
wallpaper: None,
preselected_container_idx: None,
}
}
}
@@ -976,6 +979,10 @@ impl Workspace {
container
}
pub fn preselect_container_index(&mut self, insertion_index: usize) {
self.preselected_container_idx = Some(insertion_index);
}
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
let len = NonZeroUsize::new(self.containers().len())?;
@@ -1075,7 +1082,13 @@ impl Workspace {
}
pub fn new_container_for_window(&mut self, window: Window) {
let next_idx = if self.containers().is_empty() {
let next_idx = if let Some(idx) = self.preselected_container_idx {
let next = idx;
self.preselected_container_idx = None;
next
} else if self.containers().is_empty() {
0
} else {
self.focused_container_idx() + 1

View File

@@ -151,6 +151,7 @@ macro_rules! gen_enum_subcommand_args {
gen_enum_subcommand_args! {
Focus: OperationDirection,
Move: OperationDirection,
PreselectDirection: OperationDirection,
CycleFocus: CycleDirection,
CycleMove: CycleDirection,
CycleMoveToWorkspace: CycleDirection,
@@ -1095,6 +1096,9 @@ enum SubCommand {
/// Move the focused window in the specified direction
#[clap(arg_required_else_help = true)]
Move(Move),
/// Preselect the specified direction for the next window to be spawned on supported layouts
#[clap(arg_required_else_help = true)]
PreselectDirection(PreselectDirection),
/// Minimize the focused window
Minimize,
/// Close the focused window
@@ -2038,6 +2042,9 @@ fn main() -> eyre::Result<()> {
SubCommand::Move(arg) => {
send_message(&SocketMessage::MoveWindow(arg.operation_direction))?;
}
SubCommand::PreselectDirection(arg) => {
send_message(&SocketMessage::PreselectDirection(arg.operation_direction))?;
}
SubCommand::CycleFocus(arg) => {
send_message(&SocketMessage::CycleFocusWindow(arg.cycle_direction))?;
}

View File

@@ -111,6 +111,7 @@ nav:
- cli/load-resize.md
- cli/focus.md
- cli/move.md
- cli/preselect-direction.md
- cli/minimize.md
- cli/close.md
- cli/force-focus.md