aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirottu <56396750+Kirottu@users.noreply.github.com>2025-01-25 10:49:51 +0200
committerGitHub <noreply@github.com>2025-01-25 08:49:51 +0000
commit852da5714affd067de731599136ed619dc3bba40 (patch)
treee42514f81d8a7f74f5437746503367082917777a
parent4f793038117b4fef38f491e665a66589eb896e0a (diff)
downloadniri-852da5714affd067de731599136ed619dc3bba40.tar.gz
niri-852da5714affd067de731599136ed619dc3bba40.tar.bz2
niri-852da5714affd067de731599136ed619dc3bba40.zip
Add move-workspace-to-index and move-workspace-to-monitor actions (#1007)
* Added move-workspace-to-index and move-workspace-to-monitor IPC actions * Added redraws to the workspace handling actions, fixed tests that panicked, fixed other mentioned problems. * Fixed workspace focusing and handling numbered workspaces with `move-workspace-to-index` * Fixed more inconsistencies with move-workspace-to-monitor * Added back `self.workspace_switch = None` * Reordered some workspace cleanup logic * Fix formatting * Add missing blank lines * Fix moving workspace to same monitor and wrong current index updating * Move function up and add fixme comment --------- Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
-rw-r--r--niri-config/src/lib.rs34
-rw-r--r--niri-ipc/src/lib.rs32
-rw-r--r--src/input/mod.rs43
-rw-r--r--src/layout/mod.rs247
-rw-r--r--src/layout/monitor.rs47
5 files changed, 401 insertions, 2 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 62c91b34..b70499d1 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -1358,6 +1358,18 @@ pub enum Action {
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveWorkspaceDown,
MoveWorkspaceUp,
+ MoveWorkspaceToIndex(#[knuffel(argument)] usize),
+ #[knuffel(skip)]
+ MoveWorkspaceToIndexByRef {
+ new_idx: usize,
+ reference: WorkspaceReference,
+ },
+ #[knuffel(skip)]
+ MoveWorkspaceToMonitorByRef {
+ output_name: String,
+ reference: WorkspaceReference,
+ },
+ MoveWorkspaceToMonitor(#[knuffel(argument)] String),
SetWorkspaceName(#[knuffel(argument)] String),
#[knuffel(skip)]
SetWorkspaceNameByRef {
@@ -1612,6 +1624,28 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::MoveWorkspaceToMonitorPrevious {} => {
Self::MoveWorkspaceToMonitorPrevious
}
+ niri_ipc::Action::MoveWorkspaceToIndex {
+ index,
+ reference: Some(reference),
+ } => Self::MoveWorkspaceToIndexByRef {
+ new_idx: index,
+ reference: WorkspaceReference::from(reference),
+ },
+ niri_ipc::Action::MoveWorkspaceToIndex {
+ index,
+ reference: None,
+ } => Self::MoveWorkspaceToIndex(index),
+ niri_ipc::Action::MoveWorkspaceToMonitor {
+ output,
+ reference: Some(reference),
+ } => Self::MoveWorkspaceToMonitorByRef {
+ output_name: output,
+ reference: WorkspaceReference::from(reference),
+ },
+ niri_ipc::Action::MoveWorkspaceToMonitor {
+ output,
+ reference: None,
+ } => Self::MoveWorkspaceToMonitor(output),
niri_ipc::Action::MoveWorkspaceToMonitorNext {} => Self::MoveWorkspaceToMonitorNext,
niri_ipc::Action::ToggleDebugTint {} => Self::ToggleDebugTint,
niri_ipc::Action::DebugToggleOpaqueRegions {} => Self::DebugToggleOpaqueRegions,
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 513da578..44ef1d94 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -365,6 +365,22 @@ pub enum Action {
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
+ /// Move a workspace to a specific index on its monitor.
+ #[cfg_attr(
+ feature = "clap",
+ clap(about = "Move the focused workspace to a specific index on its monitor")
+ )]
+ MoveWorkspaceToIndex {
+ /// New index for the workspace.
+ #[cfg_attr(feature = "clap", arg())]
+ index: usize,
+
+ /// Reference (index or name) of the workspace to move.
+ ///
+ /// If `None`, uses the focused workspace.
+ #[cfg_attr(feature = "clap", arg(long))]
+ reference: Option<WorkspaceReferenceArg>,
+ },
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
@@ -519,6 +535,22 @@ pub enum Action {
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
+ /// Move a workspace to a specific monitor.
+ #[cfg_attr(
+ feature = "clap",
+ clap(about = "Move the focused workspace to a specific monitor")
+ )]
+ MoveWorkspaceToMonitor {
+ /// The target output name.
+ #[cfg_attr(feature = "clap", arg())]
+ output: String,
+
+ // Reference (index or name) of the workspace to move.
+ ///
+ /// If `None`, uses the focused workspace.
+ #[cfg_attr(feature = "clap", arg(long))]
+ reference: Option<WorkspaceReferenceArg>,
+ },
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 12746376..c2691739 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -1163,6 +1163,18 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
+ Action::MoveWorkspaceToIndex(new_idx) => {
+ self.niri.layout.move_workspace_to_idx(None, new_idx);
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWorkspaceToIndexByRef { new_idx, reference } => {
+ if let Some(res) = self.niri.find_output_and_workspace_index(reference) {
+ self.niri.layout.move_workspace_to_idx(Some(res), new_idx);
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ }
Action::SetWorkspaceName(name) => {
self.niri.layout.set_workspace_name(name, None);
}
@@ -1497,6 +1509,37 @@ impl State {
}
}
}
+ Action::MoveWorkspaceToMonitor(new_output) => {
+ if let Some(new_output) = self.niri.output_by_name_match(&new_output).cloned() {
+ if self.niri.layout.move_workspace_to_output(&new_output)
+ && !self.maybe_warp_cursor_to_focus_centered()
+ {
+ self.move_cursor_to_output(&new_output);
+ }
+ }
+ }
+ Action::MoveWorkspaceToMonitorByRef {
+ output_name,
+ reference,
+ } => {
+ if let Some((output, old_idx)) =
+ self.niri.find_output_and_workspace_index(reference)
+ {
+ if let Some(new_output) = self.niri.output_by_name_match(&output_name).cloned()
+ {
+ if self.niri.layout.move_workspace_to_output_by_id(
+ old_idx,
+ output,
+ new_output.clone(),
+ ) {
+ // Cursor warp already calls `queue_redraw_all`
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&new_output);
+ }
+ }
+ }
+ }
+ }
Action::ToggleWindowFloating => {
self.niri.layout.toggle_window_floating(None);
// FIXME: granular
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 63bedc7f..8eb4c85b 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -3032,17 +3032,23 @@ impl<W: LayoutElement> Layout<W> {
}
}
- pub fn move_workspace_to_output(&mut self, output: &Output) {
+ pub fn move_workspace_to_output(&mut self, output: &Output) -> bool {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
- return;
+ return false;
};
let current = &mut monitors[*active_monitor_idx];
+
+ // Do not do anything if the output is already correct
+ if &current.output == output {
+ return false;
+ }
+
if current.active_workspace_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
current.add_workspace_bottom();
@@ -3080,6 +3086,96 @@ impl<W: LayoutElement> Layout<W> {
target.clean_up_workspaces();
*active_monitor_idx = target_idx;
+
+ true
+ }
+
+ // FIXME: accept workspace by id and deduplicate logic with move_workspace_to_output()
+ pub fn move_workspace_to_output_by_id(
+ &mut self,
+ old_idx: usize,
+ old_output: Option<Output>,
+ new_output: Output,
+ ) -> bool {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = &mut self.monitor_set
+ else {
+ return false;
+ };
+
+ let current_idx = if let Some(old_output) = old_output {
+ monitors
+ .iter()
+ .position(|mon| mon.output == old_output)
+ .unwrap()
+ } else {
+ *active_monitor_idx
+ };
+ let target_idx = monitors
+ .iter()
+ .position(|mon| mon.output == new_output)
+ .unwrap();
+
+ if current_idx == target_idx {
+ return false;
+ }
+
+ let current = &mut monitors[current_idx];
+ let current_active_ws_idx = current.active_workspace_idx;
+
+ if old_idx == current.workspaces.len() - 1 {
+ // Insert a new empty workspace.
+ current.add_workspace_bottom();
+ }
+
+ let mut ws = current.workspaces.remove(old_idx);
+
+ if current.options.empty_workspace_above_first && old_idx == 0 {
+ current.add_workspace_top();
+ }
+
+ if old_idx < current.active_workspace_idx {
+ current.active_workspace_idx -= 1;
+ }
+ current.workspace_switch = None;
+ current.clean_up_workspaces();
+
+ ws.set_output(Some(new_output.clone()));
+ ws.original_output = OutputId::new(&new_output);
+
+ let target = &mut monitors[target_idx];
+
+ target.previous_workspace_id = Some(target.workspaces[target.active_workspace_idx].id());
+
+ if target.options.empty_workspace_above_first && target.workspaces.len() == 1 {
+ // Insert a new empty workspace on top to prepare for insertion of new workspce.
+ target.add_workspace_top();
+ }
+ // Insert the workspace after the currently active one. Unless the currently active one is
+ // the last empty workspace, then insert before.
+ let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
+ target.workspaces.insert(target_ws_idx, ws);
+
+ // Only switch active monitor if the workspace moved was the currently focused one on the
+ // current monitor
+ let res = if current_idx == *active_monitor_idx && old_idx == current_active_ws_idx {
+ *active_monitor_idx = target_idx;
+ target.active_workspace_idx = target_ws_idx;
+ true
+ } else {
+ if target_ws_idx <= target.active_workspace_idx {
+ target.active_workspace_idx += 1;
+ }
+ false
+ };
+
+ target.workspace_switch = None;
+ target.clean_up_workspaces();
+
+ res
}
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
@@ -3792,6 +3888,37 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_workspace_up();
}
+ pub fn move_workspace_to_idx(
+ &mut self,
+ reference: Option<(Option<Output>, usize)>,
+ new_idx: usize,
+ ) {
+ let (monitor, old_idx) = if let Some((output, old_idx)) = reference {
+ let monitor = if let Some(output) = output {
+ let Some(monitor) = self.monitor_for_output_mut(&output) else {
+ return;
+ };
+ monitor
+ } else {
+ // In case a numbered workspace reference is used, assume the active monitor
+ let Some(monitor) = self.active_monitor() else {
+ return;
+ };
+ monitor
+ };
+
+ (monitor, old_idx)
+ } else {
+ let Some(monitor) = self.active_monitor() else {
+ return;
+ };
+ let index = monitor.active_workspace_idx;
+ (monitor, index)
+ };
+
+ monitor.move_workspace_to_idx(old_idx, new_idx);
+ }
+
pub fn set_workspace_name(&mut self, name: String, reference: Option<WorkspaceReference>) {
// ignore the request if the name is already used by another workspace
if self.find_workspace_by_name(&name).is_some() {
@@ -4607,6 +4734,18 @@ mod tests {
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
MoveWorkspaceDown,
MoveWorkspaceUp,
+ MoveWorkspaceToIndex {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ #[proptest(strategy = "0..=4usize")]
+ target_idx: usize,
+ },
+ MoveWorkspaceToMonitor {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ #[proptest(strategy = "0..=5usize")]
+ output_id: usize,
+ },
SetWorkspaceName {
#[proptest(strategy = "1..=5usize")]
new_ws_name: usize,
@@ -5179,6 +5318,78 @@ mod tests {
}
Op::MoveWorkspaceDown => layout.move_workspace_down(),
Op::MoveWorkspaceUp => layout.move_workspace_up(),
+ Op::MoveWorkspaceToIndex {
+ ws_name: Some(ws_name),
+ target_idx,
+ } => {
+ let MonitorSet::Normal { monitors, .. } = &mut layout.monitor_set else {
+ return;
+ };
+
+ let Some((old_idx, old_output)) = monitors.iter().find_map(|monitor| {
+ monitor
+ .workspaces
+ .iter()
+ .enumerate()
+ .find_map(|(i, ws)| {
+ if ws.name == Some(format!("ws{ws_name}")) {
+ Some(i)
+ } else {
+ None
+ }
+ })
+ .map(|i| (i, monitor.output.clone()))
+ }) else {
+ return;
+ };
+
+ layout.move_workspace_to_idx(Some((Some(old_output), old_idx)), target_idx)
+ }
+ Op::MoveWorkspaceToIndex {
+ ws_name: None,
+ target_idx,
+ } => layout.move_workspace_to_idx(None, target_idx),
+ Op::MoveWorkspaceToMonitor {
+ ws_name: None,
+ output_id: id,
+ } => {
+ let name = format!("output{id}");
+ let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
+ return;
+ };
+ layout.move_workspace_to_output(&output);
+ }
+ Op::MoveWorkspaceToMonitor {
+ ws_name: Some(ws_name),
+ output_id: id,
+ } => {
+ let name = format!("output{id}");
+ let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
+ return;
+ };
+ let MonitorSet::Normal { monitors, .. } = &mut layout.monitor_set else {
+ return;
+ };
+
+ let Some((old_idx, old_output)) = monitors.iter().find_map(|monitor| {
+ monitor
+ .workspaces
+ .iter()
+ .enumerate()
+ .find_map(|(i, ws)| {
+ if ws.name == Some(format!("ws{ws_name}")) {
+ Some(i)
+ } else {
+ None
+ }
+ })
+ .map(|i| (i, monitor.output.clone()))
+ }) else {
+ return;
+ };
+
+ layout.move_workspace_to_output_by_id(old_idx, Some(old_output), output);
+ }
Op::SwitchPresetColumnWidth => layout.toggle_width(),
Op::SwitchPresetWindowWidth { id } => {
let id = id.filter(|id| layout.has_window(id));
@@ -6986,6 +7197,38 @@ mod tests {
check_ops(&ops);
}
+ #[test]
+ fn move_workspace_to_same_monitor_doesnt_reorder() {
+ let ops = [
+ Op::AddOutput(0),
+ Op::SetWorkspaceName {
+ new_ws_name: 0,
+ ws_name: None,
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(0),
+ },
+ Op::FocusWorkspaceDown,
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::MoveWorkspaceToMonitor {
+ ws_name: Some(0),
+ output_id: 0,
+ },
+ ];
+
+ let layout = check_ops(&ops);
+ let counts: Vec<_> = layout
+ .workspaces()
+ .map(|(_, _, ws)| ws.windows().count())
+ .collect();
+ assert_eq!(counts, &[1, 2, 0]);
+ }
+
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
if parent_id == id {
return true;
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index 8bb92a33..3eaac97f 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -858,6 +858,53 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
+ pub fn move_workspace_to_idx(&mut self, old_idx: usize, new_idx: usize) {
+ let mut new_idx = new_idx.clamp(0, self.workspaces.len() - 1);
+ if old_idx == new_idx {
+ return;
+ }
+
+ let ws = self.workspaces.remove(old_idx);
+ self.workspaces.insert(new_idx, ws);
+
+ if new_idx > old_idx {
+ if new_idx == self.workspaces.len() - 1 {
+ // Insert a new empty workspace.
+ self.add_workspace_bottom();
+ }
+
+ if self.options.empty_workspace_above_first && old_idx == 0 {
+ self.add_workspace_top();
+ new_idx += 1;
+ }
+ } else {
+ if old_idx == self.workspaces.len() - 1 {
+ // Insert a new empty workspace.
+ self.add_workspace_bottom();
+ }
+
+ if self.options.empty_workspace_above_first && new_idx == 0 {
+ self.add_workspace_top();
+ new_idx += 1;
+ }
+ }
+
+ // Only refocus the workspace if it was already focused
+ if self.active_workspace_idx == old_idx {
+ self.active_workspace_idx = new_idx;
+ // If the workspace order was switched so that the current workspace moved down the
+ // workspace stack, focus correctly
+ } else if new_idx <= self.active_workspace_idx && old_idx > self.active_workspace_idx {
+ self.active_workspace_idx += 1;
+ } else if new_idx >= self.active_workspace_idx && old_idx < self.active_workspace_idx {
+ self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
+ }
+
+ self.workspace_switch = None;
+
+ self.clean_up_workspaces();
+ }
+
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.