feat(ux): tmux mode (#1073)

* work

* basic tmux move and functionality

* tmux mode ui

* rustfmt
This commit is contained in:
Aram Drevekenin 2022-02-21 15:52:42 +01:00 committed by GitHub
parent 8aef32863f
commit a0a0a7e5c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 282 additions and 5 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -369,5 +369,20 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
colored_elements, colored_elements,
separator, separator,
), ),
InputMode::Tmux => key_indicators(
max_len,
&[
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Pane),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
separator,
),
} }
} }

View file

@ -208,6 +208,7 @@ fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => tip(help.palette), InputMode::Normal => tip(help.palette),
InputMode::Locked => locked_interface_indication(help.palette), InputMode::Locked => locked_interface_indication(help.palette),
InputMode::Tmux => full_tmux_mode_indication(help),
InputMode::RenamePane => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help), InputMode::RenamePane => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help),
_ => full_shortcut_list_nonstandard_mode(confirm_pane_selection)(help), _ => full_shortcut_list_nonstandard_mode(confirm_pane_selection)(help),
} }
@ -234,6 +235,7 @@ fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => tip(help.palette), InputMode::Normal => tip(help.palette),
InputMode::Locked => locked_interface_indication(help.palette), InputMode::Locked => locked_interface_indication(help.palette),
InputMode::Tmux => short_tmux_mode_indication(help),
InputMode::RenamePane => { InputMode::RenamePane => {
shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help) shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help)
} }
@ -266,6 +268,22 @@ fn best_effort_shortcut_list_nonstandard_mode(
} }
} }
fn best_effort_tmux_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart {
let mut line_part = tmux_mode_indication(help);
for (i, (letter, description)) in help.keybinds.iter().enumerate() {
let shortcut = first_word_shortcut(i == 0, letter, description, help.palette);
if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len {
// TODO: better
line_part.part = format!("{}{}", line_part.part, MORE_MSG);
line_part.len += MORE_MSG.chars().count();
break;
}
line_part.len += shortcut.len;
line_part.part = format!("{}{}", line_part.part, shortcut);
}
line_part
}
fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => { InputMode::Normal => {
@ -284,6 +302,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> Lin
LinePart::default() LinePart::default()
} }
} }
InputMode::Tmux => best_effort_tmux_shortcut_list(help, max_len),
InputMode::RenamePane => { InputMode::RenamePane => {
best_effort_shortcut_list_nonstandard_mode(select_pane_shortcut)(help, max_len) best_effort_shortcut_list_nonstandard_mode(select_pane_shortcut)(help, max_len)
} }
@ -377,6 +396,90 @@ pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> Line
} }
} }
pub fn tmux_mode_indication(help: &ModeInfo) -> LinePart {
let white_color = match help.palette.white {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match help.palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" (");
let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): ");
let tmux_mode_text = "TMUX MODE";
let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text);
let line_part = LinePart {
part: format!(
"{}{}{}",
shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator
),
len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space
};
line_part
}
pub fn full_tmux_mode_indication(help: &ModeInfo) -> LinePart {
let white_color = match help.palette.white {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match help.palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" (");
let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): ");
let tmux_mode_text = "TMUX MODE";
let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text);
let mut line_part = LinePart {
part: format!(
"{}{}{}",
shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator
),
len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space
};
for (i, (letter, description)) in help.keybinds.iter().enumerate() {
let shortcut = full_length_shortcut(i == 0, letter, description, help.palette);
line_part.len += shortcut.len;
line_part.part = format!("{}{}", line_part.part, shortcut,);
}
line_part
}
pub fn short_tmux_mode_indication(help: &ModeInfo) -> LinePart {
let white_color = match help.palette.white {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match help.palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" (");
let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): ");
let tmux_mode_text = "TMUX MODE";
let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text);
let mut line_part = LinePart {
part: format!(
"{}{}{}",
shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator
),
len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space
};
for (i, (letter, description)) in help.keybinds.iter().enumerate() {
let shortcut = first_word_shortcut(i == 0, letter, description, help.palette);
line_part.len += shortcut.len;
line_part.part = format!("{}{}", line_part.part, shortcut);
}
line_part
}
pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart {
let white_color = match palette.white { let white_color = match palette.white {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),

View file

@ -19,10 +19,12 @@ pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const TMUX_MODE: [u8; 1] = [2]; // ctrl-b
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const SPLIT_RIGHT_IN_TMUX_MODE: [u8; 1] = [37]; // %
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const TOGGLE_FLOATING_PANES: [u8; 1] = [119]; // w pub const TOGGLE_FLOATING_PANES: [u8; 1] = [119]; // w
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
@ -1748,3 +1750,50 @@ pub fn focus_tab_with_layout() {
}; };
assert_snapshot!(last_snapshot); assert_snapshot!(last_snapshot);
} }
#[test]
#[ignore]
pub fn tmux_mode() {
let fake_win_size = Size {
cols: 120,
rows: 24,
};
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(Step {
name: "Split pane to the right",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&TMUX_MODE);
remote_terminal.send_key(&SPLIT_RIGHT_IN_TMUX_MODE);
// back to normal mode after split
step_is_complete = true;
}
step_is_complete
},
});
runner.run_all_steps();
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for new pane to appear",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
step_is_complete = true;
}
step_is_complete
},
});
if runner.test_timed_out && test_attempts > 0 {
test_attempts -= 1;
continue;
} else {
break last_snapshot;
}
};
assert_snapshot!(last_snapshot);
}

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: last_snapshot
---
Zellij (e2e-test)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
│$ ││$ █ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -329,7 +329,7 @@ impl Pane for PluginPane {
.unwrap(); .unwrap();
} }
fn clear_scroll(&mut self) { fn clear_scroll(&mut self) {
unimplemented!(); // noop
} }
fn start_selection(&mut self, start: &Position, client_id: ClientId) { fn start_selection(&mut self, start: &Position, client_id: ClientId) {
self.send_plugin_instructions self.send_plugin_instructions

View file

@ -120,6 +120,9 @@ pub enum InputMode {
/// `Prompt` mode allows interacting with active prompts. /// `Prompt` mode allows interacting with active prompts.
#[serde(alias = "prompt")] #[serde(alias = "prompt")]
Prompt, Prompt,
/// `Tmux` mode allows for basic tmux keybindings functionality
#[serde(alias = "tmux")]
Tmux,
} }
impl Default for InputMode { impl Default for InputMode {
@ -164,6 +167,7 @@ impl FromStr for InputMode {
"renametab" => Ok(InputMode::RenameTab), "renametab" => Ok(InputMode::RenameTab),
"session" => Ok(InputMode::Session), "session" => Ok(InputMode::Session),
"move" => Ok(InputMode::Move), "move" => Ok(InputMode::Move),
"tmux" => Ok(InputMode::Tmux),
"prompt" => Ok(InputMode::Prompt), "prompt" => Ok(InputMode::Prompt),
"renamepane" => Ok(InputMode::RenamePane), "renamepane" => Ok(InputMode::RenamePane),
e => Err(e.to_string().into()), e => Err(e.to_string().into()),

View file

@ -22,6 +22,8 @@ keybinds:
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [Quit,] - action: [Quit,]
key: [Ctrl: 'q',] key: [Ctrl: 'q',]
- action: [NewPane: ] - action: [NewPane: ]
@ -62,6 +64,8 @@ keybinds:
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [Quit] - action: [Quit]
key: [Ctrl: 'q'] key: [Ctrl: 'q']
- action: [Resize: Left,] - action: [Resize: Left,]
@ -113,6 +117,8 @@ keybinds:
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [Quit,] - action: [Quit,]
key: [Ctrl: 'q',] key: [Ctrl: 'q',]
- action: [MoveFocus: Left,] - action: [MoveFocus: Left,]
@ -223,6 +229,8 @@ keybinds:
key: [Ctrl: 's'] key: [Ctrl: 's']
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [SwitchToMode: Session,] - action: [SwitchToMode: Session,]
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: RenameTab, TabNameInput: [0],] - action: [SwitchToMode: RenameTab, TabNameInput: [0],]
@ -290,6 +298,8 @@ keybinds:
key: [Ctrl: 'p',] key: [Ctrl: 'p',]
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [SwitchToMode: Session,] - action: [SwitchToMode: Session,]
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: Resize,] - action: [SwitchToMode: Resize,]
@ -389,6 +399,8 @@ keybinds:
key: [Ctrl: 'p',] key: [Ctrl: 'p',]
- action: [SwitchToMode: Move,] - action: [SwitchToMode: Move,]
key: [Ctrl: 'h',] key: [Ctrl: 'h',]
- action: [SwitchToMode: Tmux,]
key: [Ctrl: 'b',]
- action: [SwitchToMode: Tab,] - action: [SwitchToMode: Tab,]
key: [Ctrl: 't',] key: [Ctrl: 't',]
- action: [SwitchToMode: Normal,] - action: [SwitchToMode: Normal,]
@ -419,6 +431,65 @@ keybinds:
key: [ Alt: '+'] key: [ Alt: '+']
- action: [Resize: Decrease,] - action: [Resize: Decrease,]
key: [ Alt: '-'] key: [ Alt: '-']
tmux:
- action: [SwitchToMode: Locked,]
key: [Ctrl: 'g']
- action: [SwitchToMode: Resize,]
key: [Ctrl: 'n',]
- action: [SwitchToMode: Pane,]
key: [Ctrl: 'p',]
- action: [SwitchToMode: Move,]
key: [Ctrl: 'h',]
- action: [SwitchToMode: Tab,]
key: [Ctrl: 't',]
- action: [SwitchToMode: Normal,]
key: [Ctrl: 'o', Char: "\n", Char: ' ', Esc]
- action: [SwitchToMode: Scroll,]
key: [Ctrl: 's']
- action: [Quit,]
key: [Ctrl: 'q',]
- action: [NewPane: Down, SwitchToMode: Normal,]
key: [Char: "\"",]
- action: [NewPane: Right, SwitchToMode: Normal,]
key: [Char: '%',]
- action: [ToggleFocusFullscreen, SwitchToMode: Normal,]
key: [Char: 'z',]
- action: [NewTab: , SwitchToMode: Normal,]
key: [ Char: 'c',]
- action: [SwitchToMode: RenameTab, TabNameInput: [0],]
key: [Char: ',']
- action: [GoToPreviousTab, SwitchToMode: Normal,]
key: [ Char: 'p']
- action: [GoToNextTab, SwitchToMode: Normal,]
key: [ Char: 'n']
- action: [MoveFocus: Left, SwitchToMode: Normal,]
key: [ Left,]
- action: [MoveFocus: Right, SwitchToMode: Normal,]
key: [ Right,]
- action: [MoveFocus: Down, SwitchToMode: Normal,]
key: [ Down,]
- action: [MoveFocus: Up, SwitchToMode: Normal,]
key: [ Up,]
- action: [NewPane: ,]
key: [ Alt: 'n',]
- action: [MoveFocus: Left,]
key: [ Alt: 'h',]
- action: [MoveFocus: Right,]
key: [ Alt: 'l',]
- action: [MoveFocus: Down,]
key: [ Alt: 'j',]
- action: [MoveFocus: Up,]
key: [ Alt: 'k',]
- action: [FocusPreviousPane,]
key: [ Alt: '[',]
- action: [FocusNextPane,]
key: [ Alt: ']',]
- action: [Resize: Increase,]
key: [ Alt: '=']
- action: [Resize: Increase,]
key: [ Alt: '+']
- action: [Resize: Decrease,]
key: [ Alt: '-']
plugins: plugins:
- path: tab-bar - path: tab-bar
tag: tab-bar tag: tab-bar

View file

@ -199,10 +199,6 @@ impl Keybinds {
.0 .0
.get(mode) .get(mode)
.unwrap_or({ .unwrap_or({
log::warn!(
"The following mode has no action associated with it: {:?}",
mode
);
// create a dummy mode to recover from // create a dummy mode to recover from
&ModeKeybinds::new() &ModeKeybinds::new()
}) })

View file

@ -60,6 +60,16 @@ pub fn get_mode_info(
InputMode::RenameTab => vec![("Enter".to_string(), "when done".to_string())], InputMode::RenameTab => vec![("Enter".to_string(), "when done".to_string())],
InputMode::RenamePane => vec![("Enter".to_string(), "when done".to_string())], InputMode::RenamePane => vec![("Enter".to_string(), "when done".to_string())],
InputMode::Session => vec![("d".to_string(), "Detach".to_string())], InputMode::Session => vec![("d".to_string(), "Detach".to_string())],
InputMode::Tmux => vec![
("←↓↑→".to_string(), "Move focus".to_string()),
("\"".to_string(), "Split Down".to_string()),
("%".to_string(), "Split Right".to_string()),
("z".to_string(), "Fullscreen".to_string()),
("c".to_string(), "New Tab".to_string()),
(",".to_string(), "Rename Tab".to_string()),
("p".to_string(), "Previous Tab".to_string()),
("n".to_string(), "Next Tab".to_string()),
],
}; };
let session_name = envs::get_session_name().ok(); let session_name = envs::get_session_name().ok();