diff --git a/default-plugins/status-bar/src/tip/data/mod.rs b/default-plugins/status-bar/src/tip/data/mod.rs index 1354b1ab4c..731fa77724 100644 --- a/default-plugins/status-bar/src/tip/data/mod.rs +++ b/default-plugins/status-bar/src/tip/data/mod.rs @@ -8,6 +8,7 @@ mod compact_layout; mod edit_scrollbuffer; mod floating_panes_mouse; mod move_focus_hjkl_tab_switch; +mod move_tabs; mod quicknav; mod send_mouse_click_to_terminal; mod sync_tab; @@ -88,5 +89,13 @@ lazy_static! { full: compact_layout::compact_layout_full, } ), + ( + "move_tabs", + TipBody { + short: move_tabs::move_tabs_short, + medium: move_tabs::move_tabs_medium, + full: move_tabs::move_tabs_full, + } + ) ]); } diff --git a/default-plugins/status-bar/src/tip/data/move_tabs.rs b/default-plugins/status-bar/src/tip/data/move_tabs.rs new file mode 100644 index 0000000000..6ad119e49c --- /dev/null +++ b/default-plugins/status-bar/src/tip/data/move_tabs.rs @@ -0,0 +1,69 @@ +use ansi_term::{ + unstyled_len, ANSIString, ANSIStrings, + Color::{Fixed, RGB}, + Style, +}; + +use zellij_tile::prelude::*; +use zellij_tile_utils::palette_match; + +use crate::LinePart; + +macro_rules! strings { + ($ANSIStrings:expr) => {{ + let strings: &[ANSIString] = $ANSIStrings; + + let ansi_strings = ANSIStrings(strings); + + LinePart { + part: format!("{}", ansi_strings), + len: unstyled_len(&ansi_strings), + } + }}; +} + +pub fn move_tabs_full(help: &ModeInfo) -> LinePart { + // Tip: Wrong order of tabs? You can move them to left and right with: + // Alt + i (left) and Alt + o (right) + let green_color = palette_match!(help.style.colors.green); + + let bits = vec![ + Style::new().paint(" Tip: "), + Style::new().paint("Wrong order of tabs? You can move them to left and right with: "), + Style::new().fg(green_color).bold().paint("Alt + i"), + Style::new().paint(" (left) and "), + Style::new().fg(green_color).bold().paint("Alt + o"), + Style::new().paint(" (right)"), + ]; + strings!(&bits) +} + +pub fn move_tabs_medium(help: &ModeInfo) -> LinePart { + // Tip: You can move tabs to left and right with: + // Alt + i (left) and Alt + o (right) + let green_color = palette_match!(help.style.colors.green); + + let bits = vec![ + Style::new().paint(" Tip: "), + Style::new().paint("You can move tabs to left and right with: "), + Style::new().fg(green_color).bold().paint("Alt + i"), + Style::new().paint(" (left) and "), + Style::new().fg(green_color).bold().paint("Alt + o"), + Style::new().paint(" (right)"), + ]; + strings!(&bits) +} + +pub fn move_tabs_short(help: &ModeInfo) -> LinePart { + // Move tabs with: Alt + i (left) and Alt + o (right) + let green_color = palette_match!(help.style.colors.green); + + let bits = vec![ + Style::new().paint(" Move tabs with: "), + Style::new().fg(green_color).bold().paint("Alt + i"), + Style::new().paint(" (left) and "), + Style::new().fg(green_color).bold().paint("Alt + o"), + Style::new().paint(" (right)"), + ]; + strings!(&bits) +} diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index be337ea921..f2dd093326 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -9,6 +9,13 @@ use regex::Regex; use std::fmt::Write; use std::path::Path; +use crate::tests::e2e::steps::{ + check_focus_on_second_tab, check_second_tab_opened, check_third_tab_is_left_wrapped, + check_third_tab_is_right_wrapped, check_third_tab_moved_left, + check_third_tab_moved_to_beginning, check_third_tab_opened, move_tab_left, move_tab_right, + new_tab, switch_focus_to_left_tab, type_second_tab_content, +}; + use super::remote_runner::{RemoteRunner, RemoteTerminal, Step}; pub const QUIT: [u8; 1] = [17]; // ctrl-q @@ -56,6 +63,9 @@ pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x pub const RENAME_TAB_MODE: [u8; 1] = [114]; // r +pub const MOVE_TAB_LEFT: [u8; 2] = [27, 105]; // Alt + i +pub const MOVE_TAB_RIGHT: [u8; 2] = [27, 111]; // Alt + o + pub const SESSION_MODE: [u8; 1] = [15]; // ctrl-o pub const DETACH_IN_SESSION_MODE: [u8; 1] = [100]; // d @@ -63,6 +73,9 @@ pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[ pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201 pub const SLEEP: [u8; 0] = []; +pub const SECOND_TAB_CONTENT: [u8; 14] = + [84, 97, 98, 32, 35, 50, 32, 99, 111, 110, 116, 101, 110, 116]; // Tab #2 content + pub fn sgr_mouse_report(position: Position, button: u8) -> Vec { // button: (release is with lower case m, not supported here yet) // 0 => left click @@ -511,6 +524,116 @@ pub fn close_tab() { assert!(!last_snapshot.contains("Tab #2")); } +#[test] +#[ignore] +pub fn move_tab_to_left() { + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size()); + let mut runner = RemoteRunner::new(fake_win_size()) + .add_step(new_tab()) + .add_step(check_second_tab_opened()) + .add_step(new_tab()) + .add_step(check_third_tab_opened()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#3) + .add_step(move_tab_left()); // now, it should be Tab#1 >> Tab#3 >> Tab#2 + + runner.run_all_steps(); + + let last_snapshot = runner.take_snapshot_after(check_third_tab_moved_left()); + if !runner.test_timed_out || test_attempts == 0 { + break last_snapshot; + } + test_attempts -= 1; + }; + assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); +} + +fn fake_win_size() -> Size { + Size { + cols: 120, + rows: 24, + } +} + +#[test] +#[ignore] +pub fn move_tab_to_right() { + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size()); + let mut runner = RemoteRunner::new(fake_win_size()) + .add_step(new_tab()) + .add_step(check_second_tab_opened()) + .add_step(type_second_tab_content()) // allows verifying the focus later + .add_step(new_tab()) + .add_step(check_third_tab_opened()) + .add_step(switch_focus_to_left_tab()) + .add_step(check_focus_on_second_tab()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#2) + .add_step(move_tab_right()); // now, it should be Tab#1 >> Tab#3 >> Tab#2 + + runner.run_all_steps(); + + let last_snapshot = runner.take_snapshot_after(check_third_tab_moved_left()); + if !runner.test_timed_out || test_attempts == 0 { + break last_snapshot; + } + test_attempts -= 1; + }; + assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); +} + +#[test] +#[ignore] +pub fn move_tab_to_left_until_it_wraps_around() { + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size()); + let mut runner = RemoteRunner::new(fake_win_size()) + .add_step(new_tab()) + .add_step(check_second_tab_opened()) + .add_step(new_tab()) + .add_step(check_third_tab_opened()) + .add_step(move_tab_left()) + .add_step(check_third_tab_moved_left()) + .add_step(move_tab_left()) + .add_step(check_third_tab_moved_to_beginning()) // should have Tab#3 >> Tab#1 >> Tab#2 (focused on Tab#3) + .add_step(move_tab_left()); // now, it should be Tab#2 >> Tab#1 >> Tab#3 + + runner.run_all_steps(); + + let last_snapshot = runner.take_snapshot_after(check_third_tab_is_left_wrapped()); + if !runner.test_timed_out || test_attempts == 0 { + break last_snapshot; + } + test_attempts -= 1; + }; + assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); +} + +#[test] +#[ignore] +pub fn move_tab_to_right_until_it_wraps_around() { + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size()); + let mut runner = RemoteRunner::new(fake_win_size()) + .add_step(new_tab()) + .add_step(check_second_tab_opened()) + .add_step(new_tab()) + .add_step(check_third_tab_opened()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#3) + .add_step(move_tab_right()); // now, it should be Tab#3 >> Tab#2 >> Tab#1 + + runner.run_all_steps(); + + let last_snapshot = runner.take_snapshot_after(check_third_tab_is_right_wrapped()); + if !runner.test_timed_out || test_attempts == 0 { + break last_snapshot; + } + test_attempts -= 1; + }; + assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); +} + #[test] #[ignore] pub fn close_pane() { diff --git a/src/tests/e2e/mod.rs b/src/tests/e2e/mod.rs index 3cf6354252..05f2cc8cdb 100644 --- a/src/tests/e2e/mod.rs +++ b/src/tests/e2e/mod.rs @@ -1,2 +1,3 @@ pub mod cases; mod remote_runner; +mod steps; diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left.snap new file mode 100644 index 0000000000..2e3add7846 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +assertion_line: 531 +expression: account_for_races_in_snapshot(last_snapshot) +--- + Zellij (e2e-test)  Tab #1  Tab #3  Tab #2  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left_until_it_wraps_around.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left_until_it_wraps_around.snap new file mode 100644 index 0000000000..9479e355c7 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_left_until_it_wraps_around.snap @@ -0,0 +1,28 @@ +--- +source: src/tests/e2e/cases.rs +expression: account_for_races_in_snapshot(last_snapshot) +--- + Zellij (e2e-test)  Tab #2  Tab #1  Tab #3  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right.snap new file mode 100644 index 0000000000..2aee51c514 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +assertion_line: 624 +expression: account_for_races_in_snapshot(last_snapshot) +--- + Zellij (e2e-test)  Tab #1  Tab #3  Tab #2  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ Tab #2 content█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right_until_it_wraps_around.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right_until_it_wraps_around.snap new file mode 100644 index 0000000000..9c2303a562 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__move_tab_to_right_until_it_wraps_around.snap @@ -0,0 +1,28 @@ +--- +source: src/tests/e2e/cases.rs +expression: account_for_races_in_snapshot(last_snapshot) +--- + Zellij (e2e-test)  Tab #3  Tab #2  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/steps.rs b/src/tests/e2e/steps.rs new file mode 100644 index 0000000000..c09fc9392d --- /dev/null +++ b/src/tests/e2e/steps.rs @@ -0,0 +1,153 @@ +use super::cases::{ + MOVE_FOCUS_LEFT_IN_NORMAL_MODE, MOVE_TAB_LEFT, MOVE_TAB_RIGHT, NEW_TAB_IN_TAB_MODE, + SECOND_TAB_CONTENT, TAB_MODE, +}; +use super::remote_runner::{RemoteTerminal, Step}; + +pub fn new_tab() -> Step { + Step { + name: "Open new tab", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() { + remote_terminal.send_key(&TAB_MODE); + remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); + step_is_complete = true; + } + step_is_complete + }, + } +} + +pub fn check_second_tab_opened() -> Step { + Step { + name: "Check second tab opened", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2") + }, + } +} + +pub fn move_tab_left() -> Step { + Step { + name: "Move tab left", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() { + remote_terminal.send_key(&MOVE_TAB_LEFT); + step_is_complete = true; + } + step_is_complete + }, + } +} + +pub fn check_third_tab_moved_left() -> Step { + Step { + name: "Check third tab is in the middle", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #1  Tab #3  Tab #2") + }, + } +} + +pub fn type_second_tab_content() -> Step { + Step { + name: "Type second tab content", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() { + remote_terminal.send_key(&SECOND_TAB_CONTENT); + step_is_complete = true; + } + step_is_complete + }, + } +} + +pub fn check_third_tab_opened() -> Step { + Step { + name: "Check third tab opened", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #3") + }, + } +} + +pub fn switch_focus_to_left_tab() -> Step { + Step { + name: "Move focus to tab on the left", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() { + remote_terminal.send_key(&MOVE_FOCUS_LEFT_IN_NORMAL_MODE); + step_is_complete = true; + } + step_is_complete + }, + } +} + +pub fn check_focus_on_second_tab() -> Step { + Step { + name: "Check focus is on the second tab", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2 content") + }, + } +} + +pub fn move_tab_right() -> Step { + Step { + name: "Move tab right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() { + remote_terminal.send_key(&MOVE_TAB_RIGHT); + step_is_complete = true; + } + step_is_complete + }, + } +} + +pub fn check_third_tab_moved_to_beginning() -> Step { + Step { + name: "Check third tab moved to beginning", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #3  Tab #1  Tab #2") + }, + } +} + +pub fn check_third_tab_is_left_wrapped() -> Step { + Step { + name: "Check third tab is in last position", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2  Tab #1  Tab #3") + }, + } +} + +pub fn check_third_tab_is_right_wrapped() -> Step { + Step { + name: "Check third tab is in last position", + instruction: |remote_terminal: RemoteTerminal| -> bool { + remote_terminal.status_bar_appears() + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #3  Tab #2  Tab #1") + }, + } +} diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 158659a42e..ba5fca1813 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -318,6 +318,7 @@ impl InputHandler { | Action::GoToPreviousTab | Action::CloseTab | Action::GoToTab(_) + | Action::MoveTab(_) | Action::GoToTabName(_, _) | Action::ToggleTab | Action::MoveFocusOrTab(_) => { diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 0dd2946ca2..120f8f500f 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -540,6 +540,16 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::UndoRenameTab(client_id)) .with_context(err_context)?; }, + Action::MoveTab(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::MoveTabLeft(client_id), + Direction::Right => ScreenInstruction::MoveTabRight(client_id), + _ => return Ok(false), + }; + senders + .send_to_screen(screen_instr) + .with_context(err_context)?; + }, Action::Quit => { senders .send_to_server(ServerInstruction::ClientExit(client_id)) diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index fa80300728..b8ebf4e316 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -7,6 +7,7 @@ use std::rc::Rc; use std::str; use std::time::Duration; +use log::{debug, warn}; use zellij_utils::data::{ Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo, }; @@ -238,6 +239,8 @@ pub enum ScreenInstruction { ToggleTab(ClientId), UpdateTabName(Vec, ClientId), UndoRenameTab(ClientId), + MoveTabLeft(ClientId), + MoveTabRight(ClientId), TerminalResize(Size), TerminalPixelDimensions(PixelDimensions), TerminalBackgroundColor(String), @@ -433,6 +436,8 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::GoToTabName(..) => ScreenContext::GoToTabName, ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName, ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab, + ScreenInstruction::MoveTabLeft(..) => ScreenContext::MoveTabLeft, + ScreenInstruction::MoveTabRight(..) => ScreenContext::MoveTabRight, ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize, ScreenInstruction::TerminalPixelDimensions(..) => { ScreenContext::TerminalPixelDimensions @@ -1571,6 +1576,91 @@ impl Screen { } } + pub fn move_active_tab_to_left(&mut self, client_id: ClientId) -> Result<()> { + if self.tabs.len() < 2 { + debug!("cannot move tab to left: only one tab exists"); + return Ok(()); + } + let Some(client_id) = self.client_id(client_id) else { + return Ok(()); + }; + let Some(&active_tab_idx) = self.active_tab_indices.get(&client_id) else { + return Ok(()); + }; + + // wraps around: [tab1, tab2, tab3] => [tab1, tab2, tab3] + // ^ ^ + // active_tab_idx left_tab_idx + let left_tab_idx = (active_tab_idx + self.tabs.len() - 1) % self.tabs.len(); + + self.switch_tabs(active_tab_idx, left_tab_idx, client_id); + self.log_and_report_session_state() + .context("failed to move tab to left")?; + Ok(()) + } + + fn client_id(&mut self, client_id: ClientId) -> Option { + if self.get_active_tab(client_id).is_ok() { + Some(client_id) + } else { + self.get_first_client_id() + } + } + + fn switch_tabs(&mut self, active_tab_idx: usize, other_tab_idx: usize, client_id: u16) { + if !self.tabs.contains_key(&active_tab_idx) || !self.tabs.contains_key(&other_tab_idx) { + warn!( + "failed to switch tabs: index {} or {} not found in {:?}", + active_tab_idx, + other_tab_idx, + self.tabs.keys() + ); + return; + } + + // NOTE: Can `expect` here, because we checked that the keys exist above + let mut active_tab = self + .tabs + .remove(&active_tab_idx) + .expect("active tab not found"); + let mut other_tab = self + .tabs + .remove(&other_tab_idx) + .expect("other tab not found"); + + std::mem::swap(&mut active_tab.index, &mut other_tab.index); + std::mem::swap(&mut active_tab.position, &mut other_tab.position); + + // now, `active_tab.index` is changed, so we need to update it + self.active_tab_indices.insert(client_id, active_tab.index); + + self.tabs.insert(active_tab.index, active_tab); + self.tabs.insert(other_tab.index, other_tab); + } + + pub fn move_active_tab_to_right(&mut self, client_id: ClientId) -> Result<()> { + if self.tabs.len() < 2 { + debug!("cannot move tab to right: only one tab exists"); + return Ok(()); + } + let Some(client_id) = self.client_id(client_id) else { + return Ok(()); + }; + let Some(&active_tab_idx) = self.active_tab_indices.get(&client_id) else { + return Ok(()); + }; + + // wraps around: [tab1, tab2, tab3] => [tab1, tab2, tab3] + // ^ ^ + // active_tab_idx right_tab_idx + let right_tab_idx = (active_tab_idx + 1) % self.tabs.len(); + + self.switch_tabs(active_tab_idx, right_tab_idx, client_id); + self.log_and_report_session_state() + .context("failed to move active tab to right")?; + Ok(()) + } + pub fn change_mode(&mut self, mut mode_info: ModeInfo, client_id: ClientId) -> Result<()> { if mode_info.session_name.as_ref() != Some(&self.session_name) { mode_info.session_name = Some(self.session_name.clone()); @@ -2975,6 +3065,16 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; screen.render(None)?; }, + ScreenInstruction::MoveTabLeft(client_id) => { + screen.move_active_tab_to_left(client_id)?; + screen.unblock_input()?; + screen.render(None)?; + }, + ScreenInstruction::MoveTabRight(client_id) => { + screen.move_active_tab_to_right(client_id)?; + screen.unblock_input()?; + screen.render(None)?; + }, ScreenInstruction::TerminalResize(new_size) => { screen.resize_to_screen(new_size)?; screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index de3faafce3..dc8939e6f8 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -717,6 +717,150 @@ fn move_focus_left_at_left_screen_edge_changes_tab() { ); } +#[test] +fn basic_move_of_active_tab_to_left() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + assert_eq!(screen.get_active_tab(1).unwrap().position, 1); + + screen.move_active_tab_to_left(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 0, + "Active tab moved to left" + ); +} + +fn create_fixed_size_screen() -> Screen { + create_new_screen(Size { + cols: 121, + rows: 20, + }) +} + +#[test] +fn move_of_active_tab_to_left_when_there_is_only_one_tab() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + assert_eq!(screen.get_active_tab(1).unwrap().position, 0); + + screen.move_active_tab_to_left(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 0, + "Active tab moved to left" + ); +} + +#[test] +fn move_of_active_tab_to_left_multiple_times() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); + assert_eq!(screen.get_active_tab(1).unwrap().position, 2); + + screen.move_active_tab_to_left(1).expect("TEST"); + screen.move_active_tab_to_left(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 0, + "Active tab moved to left twice" + ); +} + +#[test] +fn wrapping_move_of_active_tab_to_left() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); + screen.move_focus_left_or_previous_tab(1).expect("TEST"); + screen.move_focus_left_or_previous_tab(1).expect("TEST"); + assert_eq!(screen.get_active_tab(1).unwrap().position, 0); + + screen.move_active_tab_to_left(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 2, + "Active tab moved to left until wrapped around" + ); +} + +#[test] +fn basic_move_of_active_tab_to_right() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + screen.move_focus_left_or_previous_tab(1).expect("TEST"); + assert_eq!(screen.get_active_tab(1).unwrap().position, 0); + + screen.move_active_tab_to_right(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 1, + "Active tab moved to right" + ); +} + +#[test] +fn move_of_active_tab_to_right_when_there_is_only_one_tab() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + assert_eq!(screen.get_active_tab(1).unwrap().position, 0); + + screen.move_active_tab_to_right(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 0, + "Active tab moved to left" + ); +} + +#[test] +fn move_of_active_tab_to_right_multiple_times() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); + screen.move_focus_left_or_previous_tab(1).expect("TEST"); + screen.move_focus_left_or_previous_tab(1).expect("TEST"); + assert_eq!(screen.get_active_tab(1).unwrap().position, 0); + + screen.move_active_tab_to_right(1).expect("TEST"); + screen.move_active_tab_to_right(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 2, + "Active tab moved to right twice" + ); +} + +#[test] +fn wrapping_move_of_active_tab_to_right() { + let mut screen = create_fixed_size_screen(); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); + assert_eq!(screen.get_active_tab(1).unwrap().position, 2); + + screen.move_active_tab_to_right(1).expect("TEST"); + + assert_eq!( + screen.get_active_tab(1).unwrap().position, + 0, + "Active tab moved to right until wrapped around" + ); +} + #[test] fn move_focus_right_at_right_screen_edge_changes_tab() { let size = Size { diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 04879ecf42..f8dc9a756a 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -148,6 +148,8 @@ keybinds { bind "Ctrl g" { SwitchToMode "Locked"; } bind "Ctrl q" { Quit; } bind "Alt n" { NewPane; } + bind "Alt i" { MoveTab "Left"; } + bind "Alt o" { MoveTab "Right"; } bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } bind "Alt j" "Alt Down" { MoveFocus "Down"; } diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index a0911ddb6a..1b4fac46a7 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm index 6dba31da3d..b5056f248f 100755 Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm index 109987c0ff..00bb30ff38 100755 Binary files a/zellij-utils/assets/plugins/session-manager.wasm and b/zellij-utils/assets/plugins/session-manager.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 59859deef1..31916ecdb1 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index 9ba73f7ad6..11024a361b 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index 1b4ff1734a..b25aef5cfe 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 4096b07407..fe2c2144aa 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -5,7 +5,7 @@ pub struct Action { pub name: i32, #[prost( oneof = "action::OptionalPayload", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48" )] pub optional_payload: ::core::option::Option, } @@ -106,6 +106,8 @@ pub mod action { LaunchPluginPayload(super::LaunchOrFocusPluginPayload), #[prost(message, tag = "47")] MessagePayload(super::CliPipePayload), + #[prost(enumeration = "super::MoveTabDirection", tag = "48")] + MoveTabPayload(i32), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -341,6 +343,32 @@ impl SearchOption { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum MoveTabDirection { + Left = 0, + Right = 1, +} +impl MoveTabDirection { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + MoveTabDirection::Left => "Left", + MoveTabDirection::Right => "Right", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Left" => Some(Self::Left), + "Right" => Some(Self::Right), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum ActionName { Quit = 0, Write = 1, @@ -425,6 +453,7 @@ pub enum ActionName { RenameSession = 80, LaunchPlugin = 81, CliPipe = 82, + MoveTab = 83, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -516,6 +545,7 @@ impl ActionName { ActionName::RenameSession => "RenameSession", ActionName::LaunchPlugin => "LaunchPlugin", ActionName::CliPipe => "CliPipe", + ActionName::MoveTab => "MoveTab", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -604,6 +634,7 @@ impl ActionName { "RenameSession" => Some(Self::RenameSession), "LaunchPlugin" => Some(Self::LaunchPlugin), "CliPipe" => Some(Self::CliPipe), + "MoveTab" => Some(Self::MoveTab), _ => None, } } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 5b32339984..0677f611f4 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -291,6 +291,8 @@ pub enum ScreenContext { GoToTabName, UpdateTabName, UndoRenameTab, + MoveTabLeft, + MoveTabRight, TerminalResize, TerminalPixelDimensions, TerminalBackgroundColor, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index bb97a6eb93..b19529e48f 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -209,6 +209,7 @@ pub enum Action { ToggleTab, TabNameInput(Vec), UndoRenameTab, + MoveTab(Direction), /// Run specified command in new pane. Run(RunCommandAction), /// Detach session and exit diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 36d05aff04..1d72109070 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -453,6 +453,24 @@ impl Action { })?; Ok(Action::MoveFocusOrTab(direction)) }, + "MoveTab" => { + let direction = Direction::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + if direction.is_vertical() { + Err(ConfigError::new_kdl_error( + format!("Invalid horizontal direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + )) + } else { + Ok(Action::MoveTab(direction)) + } + }, "MovePane" => { if string.is_empty() { return Ok(Action::MovePane(None)); @@ -738,6 +756,11 @@ impl TryFrom<(&KdlNode, &Options)> for Action { action_arguments, kdl_action ), + "MoveTab" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), "MoveFocusOrTab" => parse_kdl_action_char_or_string_arguments!( action_name, action_arguments, diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 0ed5b3b7ae..d59da4ad60 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -54,6 +54,7 @@ message Action { string rename_session_payload = 45; LaunchOrFocusPluginPayload launch_plugin_payload = 46; CliPipePayload message_payload = 47; + MoveTabDirection move_tab_payload = 48; } } @@ -91,6 +92,11 @@ enum SearchOption { Wrap = 2; } +enum MoveTabDirection { + Left = 0; + Right = 1; +} + message LaunchOrFocusPluginPayload { string plugin_url = 1; bool should_float = 2; @@ -236,6 +242,7 @@ enum ActionName { RenameSession = 80; LaunchPlugin = 81; CliPipe = 82; + MoveTab = 83; } message Position { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index be54e6172a..ed65789774 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -2,12 +2,13 @@ pub use super::generated_api::api::{ action::{ action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, - LaunchOrFocusPluginPayload, MovePanePayload, NameAndValue as ProtobufNameAndValue, - NewFloatingPanePayload, NewPanePayload, NewPluginPanePayload, NewTiledPanePayload, - PaneIdAndShouldFloat, PluginConfiguration as ProtobufPluginConfiguration, - Position as ProtobufPosition, RunCommandAction as ProtobufRunCommandAction, - ScrollAtPayload, SearchDirection as ProtobufSearchDirection, - SearchOption as ProtobufSearchOption, SwitchToModePayload, WriteCharsPayload, WritePayload, + LaunchOrFocusPluginPayload, MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, + NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload, + NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat, + PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition, + RunCommandAction as ProtobufRunCommandAction, ScrollAtPayload, + SearchDirection as ProtobufSearchDirection, SearchOption as ProtobufSearchOption, + SwitchToModePayload, WriteCharsPayload, WritePayload, }, input_mode::InputMode as ProtobufInputMode, resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, @@ -358,6 +359,15 @@ impl TryFrom for Action { Some(_) => Err("UndoRenameTab should not have a payload"), None => Ok(Action::UndoRenameTab), }, + Some(ProtobufActionName::MoveTab) => match protobuf_action.optional_payload { + Some(OptionalPayload::MoveTabPayload(move_tab_payload)) => { + let direction: Direction = ProtobufMoveTabDirection::from_i32(move_tab_payload) + .ok_or("Malformed move tab direction for Action::MoveTab")? + .try_into()?; + Ok(Action::MoveTab(direction)) + }, + _ => Err("Wrong payload for Action::MoveTab"), + }, Some(ProtobufActionName::Run) => match protobuf_action.optional_payload { Some(OptionalPayload::RunPayload(run_command_action)) => { let run_command_action = run_command_action.try_into()?; @@ -994,6 +1004,13 @@ impl TryFrom for ProtobufAction { name: ProtobufActionName::UndoRenameTab as i32, optional_payload: None, }), + Action::MoveTab(direction) => { + let direction: ProtobufMoveTabDirection = direction.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MoveTab as i32, + optional_payload: Some(OptionalPayload::MoveTabPayload(direction as i32)), + }) + }, Action::Run(run_command_action) => { let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?; Ok(ProtobufAction { @@ -1311,6 +1328,29 @@ impl TryFrom for ProtobufSearchDirection { } } +impl TryFrom for Direction { + type Error = &'static str; + fn try_from( + protobuf_move_tab_direction: ProtobufMoveTabDirection, + ) -> Result { + match protobuf_move_tab_direction { + ProtobufMoveTabDirection::Left => Ok(Direction::Left), + ProtobufMoveTabDirection::Right => Ok(Direction::Right), + } + } +} + +impl TryFrom for ProtobufMoveTabDirection { + type Error = &'static str; + fn try_from(direction: Direction) -> Result { + match direction { + Direction::Left => Ok(ProtobufMoveTabDirection::Left), + Direction::Right => Ok(ProtobufMoveTabDirection::Right), + _ => Err("Wrong direction for ProtobufMoveTabDirection"), + } + } +} + impl TryFrom for RunCommandAction { type Error = &'static str; fn try_from( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index 35aa55a408..f73b753eff 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -59,6 +59,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -96,6 +105,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -400,6 +418,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -437,6 +464,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -743,6 +779,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -780,6 +825,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1132,6 +1186,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1169,6 +1232,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1404,6 +1476,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1441,6 +1522,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1616,6 +1706,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1653,6 +1752,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1907,6 +2015,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1944,6 +2061,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2119,6 +2245,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2156,6 +2291,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2328,6 +2472,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2365,6 +2518,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2566,6 +2728,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2603,6 +2774,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2843,6 +3023,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2880,6 +3069,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3049,6 +3247,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3086,6 +3293,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3423,6 +3639,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3460,6 +3685,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 636b8d0d55..b5c185c834 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -59,6 +59,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -96,6 +105,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -400,6 +418,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -437,6 +464,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -743,6 +779,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -780,6 +825,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1132,6 +1186,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1169,6 +1232,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1404,6 +1476,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1441,6 +1522,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1616,6 +1706,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1653,6 +1752,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1907,6 +2015,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1944,6 +2061,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2119,6 +2245,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2156,6 +2291,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2328,6 +2472,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2365,6 +2518,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2566,6 +2728,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2603,6 +2774,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2843,6 +3023,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2880,6 +3069,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3049,6 +3247,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3086,6 +3293,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3423,6 +3639,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3460,6 +3685,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap index f183a0fa1f..e055135e08 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap @@ -59,6 +59,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -96,6 +105,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -400,6 +418,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -437,6 +464,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -743,6 +779,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -780,6 +825,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1132,6 +1186,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1169,6 +1232,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1404,6 +1476,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1441,6 +1522,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1616,6 +1706,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1653,6 +1752,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1907,6 +2015,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1944,6 +2061,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2119,6 +2245,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2156,6 +2291,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2328,6 +2472,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2365,6 +2518,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2566,6 +2728,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2603,6 +2774,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2843,6 +3023,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2880,6 +3069,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3049,6 +3247,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3086,6 +3293,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3423,6 +3639,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3460,6 +3685,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index f70502e6e6..ba5df24dfb 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -59,6 +59,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -96,6 +105,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -400,6 +418,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -437,6 +464,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -743,6 +779,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -780,6 +825,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1132,6 +1186,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1169,6 +1232,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1404,6 +1476,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1441,6 +1522,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1616,6 +1706,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1653,6 +1752,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1907,6 +2015,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1944,6 +2061,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2119,6 +2245,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2156,6 +2291,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2328,6 +2472,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2365,6 +2518,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2566,6 +2728,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2603,6 +2774,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2843,6 +3023,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2880,6 +3069,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3049,6 +3247,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3086,6 +3293,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3423,6 +3639,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3460,6 +3685,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index b0329b3241..a6e7350eec 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -59,6 +59,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -96,6 +105,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -400,6 +418,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -437,6 +464,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -743,6 +779,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -780,6 +825,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1132,6 +1186,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1169,6 +1232,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1404,6 +1476,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1441,6 +1522,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1616,6 +1706,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1653,6 +1752,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -1907,6 +2015,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -1944,6 +2061,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2119,6 +2245,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2156,6 +2291,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2328,6 +2472,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2365,6 +2518,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2566,6 +2728,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2603,6 +2774,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -2843,6 +3023,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -2880,6 +3069,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3049,6 +3247,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3086,6 +3293,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left, @@ -3423,6 +3639,15 @@ Config { Left, ), ], + Alt( + Char( + 'i', + ), + ): [ + MoveTab( + Left, + ), + ], Alt( Char( 'j', @@ -3460,6 +3685,15 @@ Config { None, ), ], + Alt( + Char( + 'o', + ), + ): [ + MoveTab( + Right, + ), + ], Alt( Direction( Left,