feat: add moving tab to other position (#3047)

* feat: add moving tab to other position

* docs(changelog): revert changes

* test: update config snapshots

* refactor: get rid of HorizontalDirection enum

* refactor: cleanup code order

* refactor: use debug! instead of info!

* refactor: use more defensive way to switch tabs

* refactor: revert tip changes

* refactor: code formatting

* refactor: improve invalid input notification

* refactor: inline fns for calculating target index

---------

Co-authored-by: Jae-Heon Ji <atx6419@gmail.com>
This commit is contained in:
Bartosz Zbytniewski 2024-02-18 19:40:03 +01:00 committed by GitHub
parent b677ffe75f
commit dd5ea26cc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 2007 additions and 7 deletions

View file

@ -8,6 +8,7 @@ mod compact_layout;
mod edit_scrollbuffer; mod edit_scrollbuffer;
mod floating_panes_mouse; mod floating_panes_mouse;
mod move_focus_hjkl_tab_switch; mod move_focus_hjkl_tab_switch;
mod move_tabs;
mod quicknav; mod quicknav;
mod send_mouse_click_to_terminal; mod send_mouse_click_to_terminal;
mod sync_tab; mod sync_tab;
@ -88,5 +89,13 @@ lazy_static! {
full: compact_layout::compact_layout_full, 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,
}
)
]); ]);
} }

View file

@ -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)
}

View file

@ -9,6 +9,13 @@ use regex::Regex;
use std::fmt::Write; use std::fmt::Write;
use std::path::Path; 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}; use super::remote_runner::{RemoteRunner, RemoteTerminal, Step};
pub const QUIT: [u8; 1] = [17]; // ctrl-q 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 CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
pub const RENAME_TAB_MODE: [u8; 1] = [114]; // r 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 SESSION_MODE: [u8; 1] = [15]; // ctrl-o
pub const DETACH_IN_SESSION_MODE: [u8; 1] = [100]; // d 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 BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
pub const SLEEP: [u8; 0] = []; 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<u8> { pub fn sgr_mouse_report(position: Position, button: u8) -> Vec<u8> {
// button: (release is with lower case m, not supported here yet) // button: (release is with lower case m, not supported here yet)
// 0 => left click // 0 => left click
@ -511,6 +524,116 @@ pub fn close_tab() {
assert!(!last_snapshot.contains("Tab #2")); 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] #[test]
#[ignore] #[ignore]
pub fn close_pane() { pub fn close_pane() {

View file

@ -1,2 +1,3 @@
pub mod cases; pub mod cases;
mod remote_runner; mod remote_runner;
mod steps;

View file

@ -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 + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.

View file

@ -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 + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.

View file

@ -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 + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.

View file

@ -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 + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.

153
src/tests/e2e/steps.rs Normal file
View file

@ -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")
},
}
}

View file

@ -318,6 +318,7 @@ impl InputHandler {
| Action::GoToPreviousTab | Action::GoToPreviousTab
| Action::CloseTab | Action::CloseTab
| Action::GoToTab(_) | Action::GoToTab(_)
| Action::MoveTab(_)
| Action::GoToTabName(_, _) | Action::GoToTabName(_, _)
| Action::ToggleTab | Action::ToggleTab
| Action::MoveFocusOrTab(_) => { | Action::MoveFocusOrTab(_) => {

View file

@ -540,6 +540,16 @@ pub(crate) fn route_action(
.send_to_screen(ScreenInstruction::UndoRenameTab(client_id)) .send_to_screen(ScreenInstruction::UndoRenameTab(client_id))
.with_context(err_context)?; .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 => { Action::Quit => {
senders senders
.send_to_server(ServerInstruction::ClientExit(client_id)) .send_to_server(ServerInstruction::ClientExit(client_id))

View file

@ -7,6 +7,7 @@ use std::rc::Rc;
use std::str; use std::str;
use std::time::Duration; use std::time::Duration;
use log::{debug, warn};
use zellij_utils::data::{ use zellij_utils::data::{
Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo, Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo,
}; };
@ -238,6 +239,8 @@ pub enum ScreenInstruction {
ToggleTab(ClientId), ToggleTab(ClientId),
UpdateTabName(Vec<u8>, ClientId), UpdateTabName(Vec<u8>, ClientId),
UndoRenameTab(ClientId), UndoRenameTab(ClientId),
MoveTabLeft(ClientId),
MoveTabRight(ClientId),
TerminalResize(Size), TerminalResize(Size),
TerminalPixelDimensions(PixelDimensions), TerminalPixelDimensions(PixelDimensions),
TerminalBackgroundColor(String), TerminalBackgroundColor(String),
@ -433,6 +436,8 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::GoToTabName(..) => ScreenContext::GoToTabName, ScreenInstruction::GoToTabName(..) => ScreenContext::GoToTabName,
ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName, ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName,
ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab, ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab,
ScreenInstruction::MoveTabLeft(..) => ScreenContext::MoveTabLeft,
ScreenInstruction::MoveTabRight(..) => ScreenContext::MoveTabRight,
ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize, ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize,
ScreenInstruction::TerminalPixelDimensions(..) => { ScreenInstruction::TerminalPixelDimensions(..) => {
ScreenContext::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<u16> {
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<()> { 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) { if mode_info.session_name.as_ref() != Some(&self.session_name) {
mode_info.session_name = Some(self.session_name.clone()); mode_info.session_name = Some(self.session_name.clone());
@ -2975,6 +3065,16 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.render(None)?; 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) => { ScreenInstruction::TerminalResize(new_size) => {
screen.resize_to_screen(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 screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins

View file

@ -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] #[test]
fn move_focus_right_at_right_screen_edge_changes_tab() { fn move_focus_right_at_right_screen_edge_changes_tab() {
let size = Size { let size = Size {

View file

@ -148,6 +148,8 @@ keybinds {
bind "Ctrl g" { SwitchToMode "Locked"; } bind "Ctrl g" { SwitchToMode "Locked"; }
bind "Ctrl q" { Quit; } bind "Ctrl q" { Quit; }
bind "Alt n" { NewPane; } bind "Alt n" { NewPane; }
bind "Alt i" { MoveTab "Left"; }
bind "Alt o" { MoveTab "Right"; }
bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; }
bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; }
bind "Alt j" "Alt Down" { MoveFocus "Down"; } bind "Alt j" "Alt Down" { MoveFocus "Down"; }

View file

@ -5,7 +5,7 @@ pub struct Action {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "action::OptionalPayload", 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<action::OptionalPayload>, pub optional_payload: ::core::option::Option<action::OptionalPayload>,
} }
@ -106,6 +106,8 @@ pub mod action {
LaunchPluginPayload(super::LaunchOrFocusPluginPayload), LaunchPluginPayload(super::LaunchOrFocusPluginPayload),
#[prost(message, tag = "47")] #[prost(message, tag = "47")]
MessagePayload(super::CliPipePayload), MessagePayload(super::CliPipePayload),
#[prost(enumeration = "super::MoveTabDirection", tag = "48")]
MoveTabPayload(i32),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -341,6 +343,32 @@ impl SearchOption {
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)] #[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<Self> {
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 { pub enum ActionName {
Quit = 0, Quit = 0,
Write = 1, Write = 1,
@ -425,6 +453,7 @@ pub enum ActionName {
RenameSession = 80, RenameSession = 80,
LaunchPlugin = 81, LaunchPlugin = 81,
CliPipe = 82, CliPipe = 82,
MoveTab = 83,
} }
impl ActionName { impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -516,6 +545,7 @@ impl ActionName {
ActionName::RenameSession => "RenameSession", ActionName::RenameSession => "RenameSession",
ActionName::LaunchPlugin => "LaunchPlugin", ActionName::LaunchPlugin => "LaunchPlugin",
ActionName::CliPipe => "CliPipe", ActionName::CliPipe => "CliPipe",
ActionName::MoveTab => "MoveTab",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -604,6 +634,7 @@ impl ActionName {
"RenameSession" => Some(Self::RenameSession), "RenameSession" => Some(Self::RenameSession),
"LaunchPlugin" => Some(Self::LaunchPlugin), "LaunchPlugin" => Some(Self::LaunchPlugin),
"CliPipe" => Some(Self::CliPipe), "CliPipe" => Some(Self::CliPipe),
"MoveTab" => Some(Self::MoveTab),
_ => None, _ => None,
} }
} }

View file

@ -291,6 +291,8 @@ pub enum ScreenContext {
GoToTabName, GoToTabName,
UpdateTabName, UpdateTabName,
UndoRenameTab, UndoRenameTab,
MoveTabLeft,
MoveTabRight,
TerminalResize, TerminalResize,
TerminalPixelDimensions, TerminalPixelDimensions,
TerminalBackgroundColor, TerminalBackgroundColor,

View file

@ -209,6 +209,7 @@ pub enum Action {
ToggleTab, ToggleTab,
TabNameInput(Vec<u8>), TabNameInput(Vec<u8>),
UndoRenameTab, UndoRenameTab,
MoveTab(Direction),
/// Run specified command in new pane. /// Run specified command in new pane.
Run(RunCommandAction), Run(RunCommandAction),
/// Detach session and exit /// Detach session and exit

View file

@ -453,6 +453,24 @@ impl Action {
})?; })?;
Ok(Action::MoveFocusOrTab(direction)) 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" => { "MovePane" => {
if string.is_empty() { if string.is_empty() {
return Ok(Action::MovePane(None)); return Ok(Action::MovePane(None));
@ -738,6 +756,11 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
action_arguments, action_arguments,
kdl_action kdl_action
), ),
"MoveTab" => parse_kdl_action_char_or_string_arguments!(
action_name,
action_arguments,
kdl_action
),
"MoveFocusOrTab" => parse_kdl_action_char_or_string_arguments!( "MoveFocusOrTab" => parse_kdl_action_char_or_string_arguments!(
action_name, action_name,
action_arguments, action_arguments,

View file

@ -54,6 +54,7 @@ message Action {
string rename_session_payload = 45; string rename_session_payload = 45;
LaunchOrFocusPluginPayload launch_plugin_payload = 46; LaunchOrFocusPluginPayload launch_plugin_payload = 46;
CliPipePayload message_payload = 47; CliPipePayload message_payload = 47;
MoveTabDirection move_tab_payload = 48;
} }
} }
@ -91,6 +92,11 @@ enum SearchOption {
Wrap = 2; Wrap = 2;
} }
enum MoveTabDirection {
Left = 0;
Right = 1;
}
message LaunchOrFocusPluginPayload { message LaunchOrFocusPluginPayload {
string plugin_url = 1; string plugin_url = 1;
bool should_float = 2; bool should_float = 2;
@ -236,6 +242,7 @@ enum ActionName {
RenameSession = 80; RenameSession = 80;
LaunchPlugin = 81; LaunchPlugin = 81;
CliPipe = 82; CliPipe = 82;
MoveTab = 83;
} }
message Position { message Position {

View file

@ -2,12 +2,13 @@ pub use super::generated_api::api::{
action::{ action::{
action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName,
DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName,
LaunchOrFocusPluginPayload, MovePanePayload, NameAndValue as ProtobufNameAndValue, LaunchOrFocusPluginPayload, MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection,
NewFloatingPanePayload, NewPanePayload, NewPluginPanePayload, NewTiledPanePayload, NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload,
PaneIdAndShouldFloat, PluginConfiguration as ProtobufPluginConfiguration, NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat,
Position as ProtobufPosition, RunCommandAction as ProtobufRunCommandAction, PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition,
ScrollAtPayload, SearchDirection as ProtobufSearchDirection, RunCommandAction as ProtobufRunCommandAction, ScrollAtPayload,
SearchOption as ProtobufSearchOption, SwitchToModePayload, WriteCharsPayload, WritePayload, SearchDirection as ProtobufSearchDirection, SearchOption as ProtobufSearchOption,
SwitchToModePayload, WriteCharsPayload, WritePayload,
}, },
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection},
@ -358,6 +359,15 @@ impl TryFrom<ProtobufAction> for Action {
Some(_) => Err("UndoRenameTab should not have a payload"), Some(_) => Err("UndoRenameTab should not have a payload"),
None => Ok(Action::UndoRenameTab), 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(ProtobufActionName::Run) => match protobuf_action.optional_payload {
Some(OptionalPayload::RunPayload(run_command_action)) => { Some(OptionalPayload::RunPayload(run_command_action)) => {
let run_command_action = run_command_action.try_into()?; let run_command_action = run_command_action.try_into()?;
@ -994,6 +1004,13 @@ impl TryFrom<Action> for ProtobufAction {
name: ProtobufActionName::UndoRenameTab as i32, name: ProtobufActionName::UndoRenameTab as i32,
optional_payload: None, 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) => { Action::Run(run_command_action) => {
let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?; let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?;
Ok(ProtobufAction { Ok(ProtobufAction {
@ -1311,6 +1328,29 @@ impl TryFrom<SearchDirection> for ProtobufSearchDirection {
} }
} }
impl TryFrom<ProtobufMoveTabDirection> for Direction {
type Error = &'static str;
fn try_from(
protobuf_move_tab_direction: ProtobufMoveTabDirection,
) -> Result<Self, &'static str> {
match protobuf_move_tab_direction {
ProtobufMoveTabDirection::Left => Ok(Direction::Left),
ProtobufMoveTabDirection::Right => Ok(Direction::Right),
}
}
}
impl TryFrom<Direction> for ProtobufMoveTabDirection {
type Error = &'static str;
fn try_from(direction: Direction) -> Result<Self, &'static str> {
match direction {
Direction::Left => Ok(ProtobufMoveTabDirection::Left),
Direction::Right => Ok(ProtobufMoveTabDirection::Right),
_ => Err("Wrong direction for ProtobufMoveTabDirection"),
}
}
}
impl TryFrom<ProtobufRunCommandAction> for RunCommandAction { impl TryFrom<ProtobufRunCommandAction> for RunCommandAction {
type Error = &'static str; type Error = &'static str;
fn try_from( fn try_from(

View file

@ -59,6 +59,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -96,6 +105,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -400,6 +418,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -437,6 +464,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -743,6 +779,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -780,6 +825,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1132,6 +1186,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1169,6 +1232,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1404,6 +1476,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1441,6 +1522,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1616,6 +1706,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1653,6 +1752,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1907,6 +2015,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1944,6 +2061,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2119,6 +2245,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2156,6 +2291,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2328,6 +2472,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2365,6 +2518,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2566,6 +2728,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2603,6 +2774,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2843,6 +3023,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2880,6 +3069,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3049,6 +3247,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3086,6 +3293,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3423,6 +3639,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3460,6 +3685,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,

View file

@ -59,6 +59,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -96,6 +105,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -400,6 +418,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -437,6 +464,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -743,6 +779,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -780,6 +825,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1132,6 +1186,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1169,6 +1232,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1404,6 +1476,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1441,6 +1522,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1616,6 +1706,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1653,6 +1752,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1907,6 +2015,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1944,6 +2061,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2119,6 +2245,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2156,6 +2291,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2328,6 +2472,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2365,6 +2518,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2566,6 +2728,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2603,6 +2774,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2843,6 +3023,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2880,6 +3069,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3049,6 +3247,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3086,6 +3293,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3423,6 +3639,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3460,6 +3685,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,

View file

@ -59,6 +59,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -96,6 +105,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -400,6 +418,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -437,6 +464,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -743,6 +779,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -780,6 +825,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1132,6 +1186,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1169,6 +1232,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1404,6 +1476,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1441,6 +1522,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1616,6 +1706,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1653,6 +1752,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1907,6 +2015,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1944,6 +2061,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2119,6 +2245,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2156,6 +2291,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2328,6 +2472,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2365,6 +2518,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2566,6 +2728,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2603,6 +2774,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2843,6 +3023,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2880,6 +3069,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3049,6 +3247,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3086,6 +3293,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3423,6 +3639,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3460,6 +3685,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,

View file

@ -59,6 +59,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -96,6 +105,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -400,6 +418,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -437,6 +464,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -743,6 +779,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -780,6 +825,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1132,6 +1186,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1169,6 +1232,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1404,6 +1476,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1441,6 +1522,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1616,6 +1706,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1653,6 +1752,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1907,6 +2015,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1944,6 +2061,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2119,6 +2245,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2156,6 +2291,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2328,6 +2472,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2365,6 +2518,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2566,6 +2728,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2603,6 +2774,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2843,6 +3023,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2880,6 +3069,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3049,6 +3247,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3086,6 +3293,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3423,6 +3639,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3460,6 +3685,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,

View file

@ -59,6 +59,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -96,6 +105,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -400,6 +418,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -437,6 +464,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -743,6 +779,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -780,6 +825,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1132,6 +1186,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1169,6 +1232,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1404,6 +1476,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1441,6 +1522,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1616,6 +1706,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1653,6 +1752,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -1907,6 +2015,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -1944,6 +2061,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2119,6 +2245,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2156,6 +2291,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2328,6 +2472,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2365,6 +2518,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2566,6 +2728,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2603,6 +2774,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -2843,6 +3023,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -2880,6 +3069,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3049,6 +3247,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3086,6 +3293,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,
@ -3423,6 +3639,15 @@ Config {
Left, Left,
), ),
], ],
Alt(
Char(
'i',
),
): [
MoveTab(
Left,
),
],
Alt( Alt(
Char( Char(
'j', 'j',
@ -3460,6 +3685,15 @@ Config {
None, None,
), ),
], ],
Alt(
Char(
'o',
),
): [
MoveTab(
Right,
),
],
Alt( Alt(
Direction( Direction(
Left, Left,