feat(controls): add quick navigation (#260)

* feat(input): quick navigation

* feat(ui): quick navigation

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2021-04-08 11:36:49 +02:00 committed by GitHub
parent c25eb04de9
commit 65c75ebb95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 292 additions and 33 deletions

View File

@ -63,8 +63,8 @@ impl ZellijTile for State {
let second_line = keybinds(&self.mode_info, cols);
// [48;5;238m is gray background, [0K is so that it fills the rest of the line
// [48;5;16m is black background, [0K is so that it fills the rest of the line
// [m is background reset, [0K is so that it clears the rest of the line
println!("{}\u{1b}[48;5;238m\u{1b}[0K", first_line);
println!("{}\u{1b}[48;5;16m\u{1b}[0K", second_line);
println!("\u{1b}[m{}\u{1b}[0K", second_line);
}
}

View File

@ -59,6 +59,122 @@ fn first_word_shortcut(is_first_shortcut: bool, letter: &str, description: &str)
len,
}
}
fn quicknav_full() -> LinePart {
let text_first_part = " Tip: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "n";
let text_third_part = " => open new pane. ";
let second_alt = "Alt";
let text_fourth_part = " + ";
let brackets_navigation = "[]";
let text_fifth_part = " or ";
let hjkl_navigation = "hjkl";
let text_sixths_part = " => navigate between panes.";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ second_alt.chars().count()
+ text_fourth_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count()
+ text_sixths_part.chars().count();
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(ORANGE).bold().paint(alt),
text_second_part,
Style::new().fg(GREEN).bold().paint(new_pane_shortcut),
text_third_part,
Style::new().fg(ORANGE).bold().paint(second_alt),
text_fourth_part,
Style::new().fg(GREEN).bold().paint(brackets_navigation),
text_fifth_part,
Style::new().fg(GREEN).bold().paint(hjkl_navigation),
text_sixths_part,
),
len,
}
}
fn quicknav_medium() -> LinePart {
let text_first_part = " Tip: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "n";
let text_third_part = " => new pane. ";
let second_alt = "Alt";
let text_fourth_part = " + ";
let brackets_navigation = "[]";
let text_fifth_part = " or ";
let hjkl_navigation = "hjkl";
let text_sixths_part = " => navigate.";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ second_alt.chars().count()
+ text_fourth_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count()
+ text_sixths_part.chars().count();
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(ORANGE).bold().paint(alt),
text_second_part,
Style::new().fg(GREEN).bold().paint(new_pane_shortcut),
text_third_part,
Style::new().fg(ORANGE).bold().paint(second_alt),
text_fourth_part,
Style::new().fg(GREEN).bold().paint(brackets_navigation),
text_fifth_part,
Style::new().fg(GREEN).bold().paint(hjkl_navigation),
text_sixths_part,
),
len,
}
}
fn quicknav_short() -> LinePart {
let text_first_part = " QuickNav: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "n";
let text_third_part = "/";
let brackets_navigation = "[]";
let text_fifth_part = "/";
let hjkl_navigation = "hjkl";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count();
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(ORANGE).bold().paint(alt),
text_second_part,
Style::new().fg(GREEN).bold().paint(new_pane_shortcut),
text_third_part,
Style::new().fg(GREEN).bold().paint(brackets_navigation),
text_fifth_part,
Style::new().fg(GREEN).bold().paint(hjkl_navigation),
),
len,
}
}
fn locked_interface_indication() -> LinePart {
let locked_text = " -- INTERFACE LOCKED -- ";
@ -99,7 +215,7 @@ fn select_pane_shortcut(is_first_shortcut: bool) -> LinePart {
fn full_shortcut_list(help: &ModeInfo) -> LinePart {
match help.mode {
InputMode::Normal => LinePart::default(),
InputMode::Normal => quicknav_full(),
InputMode::Locked => locked_interface_indication(),
_ => {
let mut line_part = LinePart::default();
@ -118,7 +234,7 @@ fn full_shortcut_list(help: &ModeInfo) -> LinePart {
fn shortened_shortcut_list(help: &ModeInfo) -> LinePart {
match help.mode {
InputMode::Normal => LinePart::default(),
InputMode::Normal => quicknav_medium(),
InputMode::Locked => locked_interface_indication(),
_ => {
let mut line_part = LinePart::default();
@ -137,7 +253,14 @@ fn shortened_shortcut_list(help: &ModeInfo) -> LinePart {
fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart {
match help.mode {
InputMode::Normal => LinePart::default(),
InputMode::Normal => {
let line_part = quicknav_short();
if line_part.len <= max_len {
line_part
} else {
LinePart::default()
}
}
InputMode::Locked => {
let line_part = locked_interface_indication();
if line_part.len <= max_len {
@ -157,7 +280,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart {
break;
}
line_part.len += shortcut.len;
line_part.part = format!("{}{}", line_part.part, shortcut,);
line_part.part = format!("{}{}", line_part.part, shortcut);
}
let select_pane_shortcut = select_pane_shortcut(help.keybinds.is_empty());
if line_part.len + select_pane_shortcut.len <= max_len {

View File

@ -21,9 +21,9 @@ pub mod boundary_type {
pub mod colors {
use ansi_term::Colour::{self, Fixed};
pub const WHITE: Colour = Fixed(255);
pub const GREEN: Colour = Fixed(154);
pub const GRAY: Colour = Fixed(238);
pub const ORANGE: Colour = Fixed(166);
}
pub type BoundaryType = &'static str; // easy way to refer to boundary_type above
@ -768,7 +768,7 @@ impl Boundaries {
let color = match color.is_some() {
true => match input_mode {
InputMode::Normal | InputMode::Locked => Some(colors::GREEN),
_ => Some(colors::WHITE),
_ => Some(colors::ORANGE),
},
false => None,
};

View File

@ -1809,6 +1809,62 @@ impl Tab {
}
self.render();
}
pub fn focus_next_pane(&mut self) {
if !self.has_selectable_panes() {
return;
}
if self.fullscreen_is_active {
return;
}
let active_pane_id = self.get_active_pane_id().unwrap();
let mut panes: Vec<(&PaneId, &Box<dyn Pane>)> = self.get_selectable_panes().collect();
panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| {
if a_pane.y() == b_pane.y() {
a_pane.x().cmp(&b_pane.x())
} else {
a_pane.y().cmp(&b_pane.y())
}
});
let first_pane = panes.get(0).unwrap();
let active_pane_position = panes
.iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap();
if let Some(next_pane) = panes.get(active_pane_position + 1) {
self.active_terminal = Some(*next_pane.0);
} else {
self.active_terminal = Some(*first_pane.0);
}
self.render();
}
pub fn focus_previous_pane(&mut self) {
if !self.has_selectable_panes() {
return;
}
if self.fullscreen_is_active {
return;
}
let active_pane_id = self.get_active_pane_id().unwrap();
let mut panes: Vec<(&PaneId, &Box<dyn Pane>)> = self.get_selectable_panes().collect();
panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| {
if a_pane.y() == b_pane.y() {
a_pane.x().cmp(&b_pane.x())
} else {
a_pane.y().cmp(&b_pane.y())
}
});
let last_pane = panes.last().unwrap();
let active_pane_position = panes
.iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap();
if active_pane_position == 0 {
self.active_terminal = Some(*last_pane.0);
} else {
self.active_terminal = Some(*panes.get(active_pane_position - 1).unwrap().0);
}
self.render();
}
pub fn move_focus_left(&mut self) {
if !self.has_selectable_panes() {
return;

View File

@ -178,7 +178,9 @@ pub enum ScreenContext {
ResizeRight,
ResizeDown,
ResizeUp,
MoveFocus,
SwitchFocus,
FocusNextPane,
FocusPreviousPane,
MoveFocusLeft,
MoveFocusDown,
MoveFocusUp,
@ -218,7 +220,9 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ResizeRight => ScreenContext::ResizeRight,
ScreenInstruction::ResizeDown => ScreenContext::ResizeDown,
ScreenInstruction::ResizeUp => ScreenContext::ResizeUp,
ScreenInstruction::MoveFocus => ScreenContext::MoveFocus,
ScreenInstruction::SwitchFocus => ScreenContext::SwitchFocus,
ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane,
ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane,
ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft,
ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown,
ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp,

View File

@ -24,8 +24,10 @@ pub enum Action {
/// Resize focus pane in specified direction.
Resize(Direction),
/// Switch focus to next pane in specified direction.
SwitchFocus(Direction),
FocusNextPane,
FocusPreviousPane,
/// Move the focus pane in specified direction.
SwitchFocus,
MoveFocus(Direction),
/// Scroll up in focus pane.
ScrollUp,

View File

@ -26,6 +26,7 @@ struct InputHandler {
send_pty_instructions: SenderWithContext<PtyInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_app_instructions: SenderWithContext<AppInstruction>,
should_exit: bool,
}
impl InputHandler {
@ -48,6 +49,7 @@ impl InputHandler {
send_pty_instructions,
send_plugin_instructions,
send_app_instructions,
should_exit: false,
}
}
@ -60,30 +62,29 @@ impl InputHandler {
self.send_app_instructions.update(err_ctx);
self.send_screen_instructions.update(err_ctx);
let keybinds = self.config.keybinds.clone();
'input_loop: loop {
//@@@ I think this should actually just iterate over stdin directly
let alt_left_bracket = vec![27, 91];
loop {
if self.should_exit {
break;
}
let stdin_buffer = self.os_input.read_from_stdin();
for key_result in stdin_buffer.events_and_raw() {
match key_result {
Ok((event, raw_bytes)) => match event {
termion::event::Event::Key(key) => {
let key = cast_termion_key(key);
// FIXME this explicit break is needed because the current test
// framework relies on it to not create dead threads that loop
// and eat up CPUs. Do not remove until the test framework has
// been revised. Sorry about this (@categorille)
let mut should_break = false;
for action in
Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds)
{
should_break |= self.dispatch_action(action);
}
if should_break {
break 'input_loop;
self.handle_key(&key, raw_bytes, &keybinds);
}
termion::event::Event::Unsupported(unsupported_key) => {
// we have to do this because of a bug in termion
// this should be a key event and not an unsupported event
if unsupported_key == alt_left_bracket {
let key = Key::Alt('[');
self.handle_key(&key, raw_bytes, &keybinds);
}
}
termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => {
// Mouse and unsupported events aren't implemented yet,
termion::event::Event::Mouse(_) => {
// Mouse events aren't implemented yet,
// use a NoOp untill then.
}
},
@ -92,6 +93,14 @@ impl InputHandler {
}
}
}
fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>, keybinds: &Keybinds) {
for action in Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) {
let should_exit = self.dispatch_action(action);
if should_exit {
self.should_exit = true;
}
}
}
/// Dispatches an [`Action`].
///
@ -144,9 +153,19 @@ impl InputHandler {
};
self.send_screen_instructions.send(screen_instr).unwrap();
}
Action::SwitchFocus(_) => {
Action::SwitchFocus => {
self.send_screen_instructions
.send(ScreenInstruction::MoveFocus)
.send(ScreenInstruction::SwitchFocus)
.unwrap();
}
Action::FocusNextPane => {
self.send_screen_instructions
.send(ScreenInstruction::FocusNextPane)
.unwrap();
}
Action::FocusPreviousPane => {
self.send_screen_instructions
.send(ScreenInstruction::FocusPreviousPane)
.unwrap();
}
Action::MoveFocus(direction) => {

View File

@ -91,6 +91,14 @@ impl Keybinds {
vec![Action::SwitchToMode(InputMode::Scroll)],
);
defaults.insert(Key::Ctrl('q'), vec![Action::Quit]);
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
InputMode::Locked => {
defaults.insert(
@ -133,6 +141,14 @@ impl Keybinds {
defaults.insert(Key::Down, vec![Action::Resize(Direction::Down)]);
defaults.insert(Key::Up, vec![Action::Resize(Direction::Up)]);
defaults.insert(Key::Right, vec![Action::Resize(Direction::Right)]);
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
InputMode::Pane => {
defaults.insert(
@ -173,7 +189,7 @@ impl Keybinds {
defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Char('p'), vec![Action::SwitchFocus(Direction::Right)]);
defaults.insert(Key::Char('p'), vec![Action::SwitchFocus]);
defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]);
defaults.insert(
@ -182,6 +198,14 @@ impl Keybinds {
);
defaults.insert(Key::Char('x'), vec![Action::CloseFocus]);
defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]);
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
InputMode::Tab => {
defaults.insert(
@ -240,6 +264,13 @@ impl Keybinds {
for i in '1'..='9' {
defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]);
}
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
InputMode::Scroll => {
defaults.insert(
@ -272,6 +303,14 @@ impl Keybinds {
defaults.insert(Key::Down, vec![Action::ScrollDown]);
defaults.insert(Key::Up, vec![Action::ScrollUp]);
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
InputMode::RenameTab => {
defaults.insert(Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]);
@ -286,6 +325,14 @@ impl Keybinds {
Action::SwitchToMode(InputMode::Tab),
],
);
defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]);
defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]);
defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]);
defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]);
defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]);
defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]);
defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]);
}
}
ModeKeybinds(defaults)

View File

@ -326,9 +326,15 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
ScreenInstruction::ResizeUp => {
screen.get_active_tab_mut().unwrap().resize_up();
}
ScreenInstruction::MoveFocus => {
ScreenInstruction::SwitchFocus => {
screen.get_active_tab_mut().unwrap().move_focus();
}
ScreenInstruction::FocusNextPane => {
screen.get_active_tab_mut().unwrap().focus_next_pane();
}
ScreenInstruction::FocusPreviousPane => {
screen.get_active_tab_mut().unwrap().focus_previous_pane();
}
ScreenInstruction::MoveFocusLeft => {
screen.get_active_tab_mut().unwrap().move_focus_left();
}
@ -589,7 +595,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
}
ApiCommand::MoveFocus => {
send_screen_instructions
.send(ScreenInstruction::MoveFocus)
.send(ScreenInstruction::FocusNextPane)
.unwrap();
}
}

View File

@ -28,7 +28,9 @@ pub enum ScreenInstruction {
ResizeRight,
ResizeDown,
ResizeUp,
MoveFocus,
SwitchFocus,
FocusNextPane,
FocusPreviousPane,
MoveFocusLeft,
MoveFocusDown,
MoveFocusUp,