diff --git a/README.md b/README.md index 6fd7a151..299f5e2c 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,9 @@ one in the layout, or to the next one in the workspace's order, and `move` and again. `change-layout`, `cycle-layout` and `flip-layout` tile the workspace in another layout, `layout-ratios` and `scrolling-layout-columns` tune it, `toggle-tiling` stops it moving windows and starts it again, -`preselect-direction` reserves a tile for the next window, and `resize-edge` -and `resize-axis` move the edges of the focused tile. The scrolling layout +`preselect-direction` reserves a tile for the next window, `resize-edge` and +`resize-axis` move the edges of the focused tile, and `promote` brings the +focused window to the largest tile. The scrolling layout is a strip of columns that runs past the edges of the monitor and scrolls to the window that has the focus. diff --git a/docs/cli/promote-focus.md b/docs/cli/promote-focus.md new file mode 100644 index 00000000..5b021a73 --- /dev/null +++ b/docs/cli/promote-focus.md @@ -0,0 +1,10 @@ +```text +Promote the user focus to the largest window container + +Usage: promote-focus + +Options: + -h, --help + Print help + +``` diff --git a/docs/cli/promote-swap.md b/docs/cli/promote-swap.md new file mode 100644 index 00000000..79ba6e19 --- /dev/null +++ b/docs/cli/promote-swap.md @@ -0,0 +1,10 @@ +```text +Promote the focused window to the largest tile by swapping container indices with the largest tile + +Usage: promote-swap + +Options: + -h, --help + Print help + +``` diff --git a/docs/cli/promote-window.md b/docs/cli/promote-window.md new file mode 100644 index 00000000..7a4f51ea --- /dev/null +++ b/docs/cli/promote-window.md @@ -0,0 +1,14 @@ +```text +Promote the window in the specified direction + +Usage: promote-window + +Arguments: + + [possible values: left, right, up, down] + +Options: + -h, --help + Print help + +``` diff --git a/docs/cli/promote.md b/docs/cli/promote.md new file mode 100644 index 00000000..465b293a --- /dev/null +++ b/docs/cli/promote.md @@ -0,0 +1,10 @@ +```text +Promote the focused window to the largest tile via container removal and re-insertion + +Usage: promote + +Options: + -h, --help + Print help + +``` diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c2c60932..f656e2fb 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -285,6 +285,15 @@ focused container has changed, which scrolls the strip to it. KWin keeps part of resizes on an output, so the bridge asks again for the position of a window KWin pulled back within a second of the command, three times at most ([`bridge.js`]'s `settle`). +`Promote` moves the focused container to index 0, the largest tile of every layout but the grid, +through `LockableSequence`, so the containers before it move one index on +([`WindowManager::promote_container_to_front`]). `PromoteSwap` swaps it with the container at +index 0 and records the index it came from in [`Workspace::promotion_swap_container_idx`], so +that from index 0 it swaps back ([`WindowManager::promote_container_swap`]). `PromoteFocus` +focuses the container at index 0 ([`WindowManager::promote_focus_to_front`]), and +`PromoteWindow` focuses the container in a direction and promotes it. On the grid layout they +change nothing. + `ResizeWindowEdge` and `ResizeWindowAxis` resize the focused container ([`WindowManager::resize_window`]). A resize is an adjustment of the container's tile, kept in [`Workspace::resize_dimensions`], one per container, which the layout applies when it calculates @@ -352,8 +361,8 @@ module's unit. changes its containers. - [`window_manager`](../../komorebi/src/window_manager.rs): the [`WindowManager`] and the operations on it (tile the workspace in front, tile every monitor, take a minimized - window out, move the focus, move the focused container, change the layout, resize the - focused container). + window out, move the focus, move the focused container, change the layout, resize or + promote the focused container). - [`desktop`](../../komorebi/src/desktop.rs): loads the monitors and the open windows from the backend. - [`window_manager_event`](../../komorebi/src/window_manager_event.rs) and @@ -553,7 +562,8 @@ A window is shown, and ends up on a tile: commands against the fake; [`komorebi/tests/layout_options.rs`](../../komorebi/tests/layout_options.rs) and [`komorebi/tests/scrolling.rs`](../../komorebi/tests/scrolling.rs), the layout options and the - scrolling layout; [`komorebi/tests/resize.rs`](../../komorebi/tests/resize.rs), resizing. + scrolling layout; [`komorebi/tests/resize.rs`](../../komorebi/tests/resize.rs), resizing; + [`komorebi/tests/promote.rs`](../../komorebi/tests/promote.rs), promoting. - [`nix/tests/vm/suites/scrolling`](../../nix/tests/vm/suites/scrolling/test.py): the scrolling strip in a real KWin, with its columns out of view off the screen. - [`nix/tests/vm/suites/commands`](../../nix/tests/vm/suites/commands/test.py): every layout, @@ -593,6 +603,10 @@ A window is shown, and ends up on a tile: [`Workspace::layout_options`]: ../../komorebi/src/workspace.rs [`Workspace::resize_dimensions`]: ../../komorebi/src/workspace.rs [`WindowManager::resize_window`]: ../../komorebi/src/window_manager.rs +[`WindowManager::promote_container_to_front`]: ../../komorebi/src/window_manager.rs +[`WindowManager::promote_container_swap`]: ../../komorebi/src/window_manager.rs +[`WindowManager::promote_focus_to_front`]: ../../komorebi/src/window_manager.rs +[`Workspace::promotion_swap_container_idx`]: ../../komorebi/src/workspace.rs [`WindowManager::resize_delta`]: ../../komorebi/src/window_manager.rs [`Workspace::latest_layout`]: ../../komorebi/src/workspace.rs [`LayoutOptions`]: ../../komorebi/src/core/mod.rs diff --git a/docs/usage/focusing-windows.md b/docs/usage/focusing-windows.md index 599d306f..ee9ed845 100644 --- a/docs/usage/focusing-windows.md +++ b/docs/usage/focusing-windows.md @@ -33,6 +33,11 @@ komorebic cycle-focus next komorebic cycle-focus previous ``` +## The largest tile + +[`komorebic promote-focus`](../cli/promote-focus.md) focuses the window on the first tile of the +layout, which is its largest ([Moving windows](moving-windows.md#to-the-largest-tile)). + ## The pointer By default the pointer moves to the middle of the window that is given the focus. komorebi asks diff --git a/docs/usage/moving-windows.md b/docs/usage/moving-windows.md index 09237554..75010824 100644 --- a/docs/usage/moving-windows.md +++ b/docs/usage/moving-windows.md @@ -32,6 +32,29 @@ komorebic cycle-move next komorebic cycle-move previous ``` +## To the largest tile + +The first tile of a layout is the largest one: the left half in BSP and the vertical stack, the +top half in the horizontal stack, the middle of the ultrawide vertical stack and the right half +in the right main vertical stack. In the columns, rows and scrolling layouts it is the first +column or row. The grid layout is left alone: these commands do nothing there. + +[`komorebic promote`](../cli/promote.md) moves the focused window to the first tile, and the +windows that were before it each move one tile on. +[`komorebic promote-swap`](../cli/promote-swap.md) swaps the focused window with the window on +the first tile instead, and from the first tile swaps it back with the window it was last +swapped with. [`komorebic promote-window`](../cli/promote-window.md) promotes the window on one +side of the focused one. + +```sh +komorebic promote +komorebic promote-swap +komorebic promote-window left +``` + +[`komorebic promote-focus`](../cli/promote-focus.md) gives the focus to the window on the first +tile and moves nothing. + ## The pointer The pointer follows the window that moved, unless diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 8198e213..4ff6ec9d 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -348,6 +348,10 @@ pub enum SocketMessage { CycleMoveWindow(CycleDirection), ResizeWindowEdge(OperationDirection, Sizing), ResizeWindowAxis(Axis, Sizing), + Promote, + PromoteSwap, + PromoteFocus, + PromoteWindow(OperationDirection), // Current Workspace Commands ChangeLayout(DefaultLayout), CycleLayout(CycleDirection), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 319e058c..9df4f6e8 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -20,6 +20,11 @@ //! of it, or with the next or the previous one, and tile the workspace again //! ([`WindowManager::move_container_in_direction`], //! [`WindowManager::move_container_in_cycle_direction`]). +//! - **Promote** moves the focused container to the largest tile, **PromoteSwap** swaps it with +//! the container there (and back), **PromoteFocus** gives that container the focus, and +//! **PromoteWindow** promotes the container on one side of the focused one +//! ([`WindowManager::promote_container_to_front`], [`WindowManager::promote_container_swap`], +//! [`WindowManager::promote_focus_to_front`]). //! - **ChangeLayout**, **CycleLayout** and **FlipLayout** give the focused workspace another //! layout, or mirror it, and **ToggleTiling** turns its tiling off and on //! ([`WindowManager::change_workspace_layout_default`], [`WindowManager::cycle_layout`], @@ -138,7 +143,7 @@ impl WindowManager { /// [`WindowManager::move_container_in_direction`] and /// [`WindowManager::move_container_in_cycle_direction`] give. /// - There is no focused workspace to change the layout or the tiling of, to preselect a - /// place in, or to resize a container of, or it cannot be tiled again. + /// place in, or to resize or promote a container of, or it cannot be tiled again. /// - The file of `QuickSave` or `Save` cannot be written, or the file of `QuickLoad` or /// `Load` cannot be read or is not a list of resize adjustments. /// - The window manager fails to retile, for one of the reasons @@ -159,6 +164,13 @@ impl WindowManager { let initial_state = State::from(self.as_ref()); match message { + SocketMessage::Promote => self.promote_container_to_front()?, + SocketMessage::PromoteSwap => self.promote_container_swap()?, + SocketMessage::PromoteFocus => self.promote_focus_to_front()?, + SocketMessage::PromoteWindow(direction) => { + self.focus_container_in_direction(direction)?; + self.promote_container_to_front()? + } SocketMessage::FocusWindow(direction) => { self.focus_container_in_direction(direction)?; } diff --git a/komorebi/src/state.rs b/komorebi/src/state.rs index 0ee1f767..7795aad3 100644 --- a/komorebi/src/state.rs +++ b/komorebi/src/state.rs @@ -90,6 +90,7 @@ impl From<&WindowManager> for State { tile: workspace.tile, globals: workspace.globals, preselected_container_idx: None, + promotion_swap_container_idx: None, }) .collect::>(); ws.focus(monitor.workspaces.focused_idx()); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 630d9058..7ef9be02 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -606,6 +606,92 @@ impl WindowManager { Ok(()) } + /// Moves the focused container to the first index of the focused workspace, which is the + /// largest tile, and tiles the workspace again: the containers before it each move one index + /// on + /// + /// The grid layout has no largest tile, and nothing changes there. + /// + /// # Errors + /// + /// There is no focused workspace, it has no containers, or it cannot be tiled again. + #[tracing::instrument(skip(self))] + pub fn promote_container_to_front(&mut self) -> eyre::Result<()> { + let workspace = self.focused_workspace_mut()?; + + if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) { + tracing::debug!("ignoring promote command for grid layout"); + return Ok(()); + } + + tracing::info!("promoting container"); + + workspace.promote_container()?; + self.update_focused_workspace(self.mouse_follows_focus, true) + } + + /// Swaps the focused container with the one at the first index, the largest tile, and tiles + /// the workspace again; when the focused container is the first already, swaps it back with + /// the container it was swapped with last + /// + /// The grid layout has no largest tile, and nothing changes there. + /// + /// # Errors + /// + /// There is no focused workspace, or it cannot be tiled again. + #[tracing::instrument(skip(self))] + pub fn promote_container_swap(&mut self) -> eyre::Result<()> { + let workspace = self.focused_workspace_mut()?; + let focused_container_idx = workspace.focused_container_idx(); + + let primary_idx = match workspace.layout { + Layout::Default(_) => 0, + }; + + if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) { + tracing::debug!("ignoring promote-swap command for grid layout"); + return Ok(()); + } + + let primary_tile_is_focused = focused_container_idx == primary_idx; + + if primary_tile_is_focused && let Some(swap_idx) = workspace.promotion_swap_container_idx { + workspace.swap_containers(focused_container_idx, swap_idx); + } else { + workspace.promotion_swap_container_idx = Some(focused_container_idx); + workspace.swap_containers(focused_container_idx, primary_idx); + } + + self.update_focused_workspace(self.mouse_follows_focus, true) + } + + /// Gives the focus to the container at the first index, the largest tile, without moving + /// anything + /// + /// The grid layout has no largest tile, and nothing changes there. + /// + /// # Errors + /// + /// There is no focused workspace, or the backend refuses to focus the window. + #[tracing::instrument(skip(self))] + pub fn promote_focus_to_front(&mut self) -> eyre::Result<()> { + let workspace = self.focused_workspace_mut()?; + + if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) { + tracing::info!("ignoring promote focus command for grid layout"); + return Ok(()); + } + + tracing::info!("promoting focus"); + + let target_idx = match workspace.layout { + Layout::Default(_) => 0, + }; + + workspace.focus_container(target_idx); + self.update_focused_workspace(self.mouse_follows_focus, true) + } + /// Mirrors the layout of the focused workspace along `layout_flip`, on top of the way it is /// mirrored already, and tiles the workspace again /// diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 468a070f..31eecb0b 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -39,6 +39,8 @@ pub struct Workspace { pub globals: WorkspaceGlobals, #[serde(skip_serializing_if = "Option::is_none")] pub preselected_container_idx: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub promotion_swap_container_idx: Option, } impl_ring_elements!(Workspace, Container); @@ -57,6 +59,7 @@ impl Default for Workspace { globals: Default::default(), tile: true, preselected_container_idx: None, + promotion_swap_container_idx: None, } } } @@ -340,6 +343,31 @@ impl Workspace { container } + /// Moves the focused container to the first index, the largest tile in every layout but the + /// grid, and keeps the focus on it + /// + /// # Errors + /// + /// The workspace has no containers. + pub fn promote_container(&mut self) -> eyre::Result<()> { + let focused_idx = self.focused_container_idx(); + let container = self + .containers_mut() + .remove_respecting_locks(focused_idx) + .ok_or_eyre("there is no container")?; + + let primary_idx = match &self.layout { + Layout::Default(_) => 0, + }; + + let insertion_idx = self + .containers_mut() + .insert_respecting_locks(primary_idx, container); + self.focus_container(insertion_idx); + + Ok(()) + } + pub fn add_container_to_back(&mut self, container: Container) { self.containers_mut().push_back(container); self.focus_last_container(); diff --git a/komorebi/tests/promote.rs b/komorebi/tests/promote.rs new file mode 100644 index 00000000..46def693 --- /dev/null +++ b/komorebi/tests/promote.rs @@ -0,0 +1,153 @@ +//! Bringing a window to the largest tile with `promote`, `promote-swap`, `promote-window`, and the +//! focus to it with `promote-focus`, against an in-memory desktop: the messages go in through +//! [`WindowManager::process_command`], and what comes out is the order of the containers, where +//! the backend is asked to put each window, and which window has the focus. +//! +//! Window ids are in the 24_000s. + +use komorebi::backend::fake::Command; +use komorebi::backend::fake::FakeBackend; +use komorebi::backend::with_backend; +use komorebi::core::DefaultLayout; +use komorebi::core::OperationDirection; +use komorebi::core::SocketMessage; +use komorebi::window::Window; +use komorebi::window_manager::WindowManager; +use komorebi::window_manager_event::KWinNotification; +use komorebi::window_manager_event::SystemNotification; +use komorebi::window_manager_event::WindowManagerEvent; +use std::sync::Arc; + +fn added(id: u32) -> WindowManagerEvent { + WindowManagerEvent::Show( + SystemNotification::KWin(KWinNotification::WindowAdded), + Window::from(id), + ) +} + +/// A window manager over a fake desktop with one 1920x1080 monitor in `layout`, and a window for +/// each of `ids`, opened in that order. The last one has the focus, and the commands it took to +/// get there are cleared. +fn desktop_with(layout: DefaultLayout, ids: &[u32]) -> (Arc, WindowManager) { + let fake = Arc::new(FakeBackend::with_one_monitor()); + let (_sender, receiver) = crossbeam_channel::bounded(1); + // unix socket paths are limited to ~104 bytes on macOS, which the temp dir alone can exceed + static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let socket = std::path::PathBuf::from(format!( + "/tmp/kmp-{}-{}.sock", + std::process::id(), + NEXT.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + )); + + let mut wm = with_backend(fake.clone(), || { + let mut wm = WindowManager::new(receiver, Some(socket)).unwrap(); + wm.init().unwrap(); + wm + }); + send(&fake, &mut wm, SocketMessage::ChangeLayout(layout)); + for &id in ids { + fake.open_window(id, "foot", "foot", &format!("terminal {id}")); + with_backend(fake.clone(), || wm.process_event(added(id))).unwrap(); + } + + fake.clear_commands(); + (fake, wm) +} + +fn send(fake: &Arc, wm: &mut WindowManager, message: SocketMessage) { + with_backend(fake.clone(), || wm.process_command(message, Vec::new())).unwrap(); +} + +/// The window of each container of the focused workspace, in order +fn order(wm: &WindowManager) -> Vec { + wm.focused_workspace() + .unwrap() + .containers() + .iter() + .map(|container| container.focused_window().unwrap().id) + .collect() +} + +fn focused(wm: &WindowManager) -> u32 { + wm.focused_window().unwrap().id +} + +#[test] +fn promote_moves_the_focused_window_to_the_largest_tile_and_the_others_along() { + let (fake, mut wm) = desktop_with(DefaultLayout::BSP, &[24_001, 24_002, 24_003]); + let largest = fake.rect_of(24_001); + + send(&fake, &mut wm, SocketMessage::Promote); + + assert_eq!(order(&wm), [24_003, 24_001, 24_002]); + assert_eq!(focused(&wm), 24_003); + assert_eq!(fake.rect_of(24_003), largest); +} + +#[test] +fn promote_swap_swaps_with_the_largest_tile_and_swaps_back() { + let (fake, mut wm) = desktop_with(DefaultLayout::BSP, &[24_011, 24_012, 24_013]); + + send(&fake, &mut wm, SocketMessage::PromoteSwap); + + assert_eq!(order(&wm), [24_013, 24_012, 24_011]); + assert_eq!(focused(&wm), 24_013); + + send(&fake, &mut wm, SocketMessage::PromoteSwap); + + assert_eq!( + order(&wm), + [24_011, 24_012, 24_013], + "the largest tile is swapped back" + ); + assert_eq!(focused(&wm), 24_013); +} + +#[test] +fn promote_focus_gives_the_focus_to_the_largest_tile_and_moves_nothing() { + let ids = [24_021, 24_022, 24_023]; + let (fake, mut wm) = desktop_with(DefaultLayout::BSP, &ids); + let tiles: Vec<_> = ids.iter().map(|&id| fake.rect_of(id)).collect(); + + send(&fake, &mut wm, SocketMessage::PromoteFocus); + + assert_eq!(focused(&wm), 24_021); + assert_eq!(order(&wm), ids); + assert_eq!( + ids.iter().map(|&id| fake.rect_of(id)).collect::>(), + tiles + ); + assert!( + fake.commands().contains(&Command::Focus(24_021, true)), + "{:?}", + fake.commands() + ); +} + +#[test] +fn promote_window_promotes_the_window_on_that_side() { + let (fake, mut wm) = desktop_with(DefaultLayout::Columns, &[24_031, 24_032, 24_033]); + + send( + &fake, + &mut wm, + SocketMessage::PromoteWindow(OperationDirection::Left), + ); + + assert_eq!(order(&wm), [24_032, 24_031, 24_033]); + assert_eq!(focused(&wm), 24_032); +} + +#[test] +fn nothing_is_promoted_in_the_grid_layout() { + let ids = [24_041, 24_042, 24_043]; + let (fake, mut wm) = desktop_with(DefaultLayout::Grid, &ids); + + send(&fake, &mut wm, SocketMessage::Promote); + send(&fake, &mut wm, SocketMessage::PromoteSwap); + send(&fake, &mut wm, SocketMessage::PromoteFocus); + + assert_eq!(order(&wm), ids); + assert_eq!(focused(&wm), 24_043); + assert!(fake.commands().is_empty(), "{:?}", fake.commands()); +} diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 8fd4942a..d6c65017 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -14,6 +14,8 @@ //! workspace, `scrolling-layout-columns` and `layout-ratios` tune it, `toggle-tiling` turns its //! tiling off and on, and `preselect-direction` and `cancel-preselect` reserve the place of the //! next window and give it up. +//! - `promote`, `promote-swap`, `promote-focus` and `promote-window` bring a window to the largest +//! tile, or the focus to it. //! - `resize-edge` and `resize-axis` resize the focused window by the `resize-delta`, and //! `quick-save-resize`, `quick-load-resize`, `save-resize` and `load-resize` save the sizes of //! the tiles of the focused workspace and load them back. @@ -95,6 +97,7 @@ gen_enum_subcommand_args! { // WatchConfiguration: BooleanState, MouseFollowsFocus: BooleanState, Query: StateQuery, + PromoteWindow: OperationDirection, } #[derive(Parser)] @@ -289,6 +292,14 @@ enum SubCommand { /// Flip the layout on the focused workspace #[clap(arg_required_else_help = true)] FlipLayout(FlipLayout), + /// Promote the focused window to the largest tile via container removal and re-insertion + Promote, + /// Promote the focused window to the largest tile by swapping container indices with the largest tile + PromoteSwap, + /// Promote the user focus to the largest window container + PromoteFocus, + /// Promote the window in the specified direction + PromoteWindow(PromoteWindow), /// Force the retiling of all managed windows Retile, /// Toggle window tiling on the focused workspace @@ -451,6 +462,18 @@ fn main() -> eyre::Result<()> { SubCommand::Retile => { send_message(&SocketMessage::Retile)?; } + SubCommand::Promote => { + send_message(&SocketMessage::Promote)?; + } + SubCommand::PromoteSwap => { + send_message(&SocketMessage::PromoteSwap)?; + } + SubCommand::PromoteFocus => { + send_message(&SocketMessage::PromoteFocus)?; + } + SubCommand::PromoteWindow(args) => { + send_message(&SocketMessage::PromoteWindow(args.operation_direction))?; + } SubCommand::ToggleTiling => { send_message(&SocketMessage::ToggleTiling)?; } diff --git a/komorebic/tests/cli.rs b/komorebic/tests/cli.rs index 2e3847a9..1440500b 100644 --- a/komorebic/tests/cli.rs +++ b/komorebic/tests/cli.rs @@ -284,6 +284,17 @@ fn the_resize_subcommands_send_what_they_are_given() { ); } +#[test] +fn the_promote_subcommands_send_their_message() { + sends(&["promote"], r#"{"type":"Promote"}"#); + sends(&["promote-swap"], r#"{"type":"PromoteSwap"}"#); + sends(&["promote-focus"], r#"{"type":"PromoteFocus"}"#); + sends( + &["promote-window", "right"], + r#"{"type":"PromoteWindow","content":"Right"}"#, + ); +} + #[test] fn mouse_follows_focus_is_set_with_enable_and_disable_or_toggled() { sends( @@ -355,6 +366,10 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() { "mouse-follows-focus.md", "move.md", "preselect-direction.md", + "promote-focus.md", + "promote-swap.md", + "promote-window.md", + "promote.md", "query.md", "quick-load-resize.md", "quick-save-resize.md", diff --git a/nix/tests/vm/suites/commands/test.py b/nix/tests/vm/suites/commands/test.py index a29f4dcc..97e240fc 100644 --- a/nix/tests/vm/suites/commands/test.py +++ b/nix/tests/vm/suites/commands/test.py @@ -165,6 +165,10 @@ resize-axis horizontal increase resize-axis vertical decrease resize-delta 50 resize-delta 100 +promote +promote-swap +promote-focus +promote-window left retile flip-layout horizontal flip-layout vertical