From d90e3d4cacbafd395a9df7ee3a02d5e908b5df2d Mon Sep 17 00:00:00 2001 From: Kunal Mohan <44079328+kunalmohan@users.noreply.github.com> Date: Tue, 19 Oct 2021 20:20:28 +0530 Subject: [PATCH] Feature: Move panes directionally (#762) * Feature: Move panes directionally * change keybinds * Fix active pane after move * Add a separate 'Move' mode * Add tests * Add more tests * Send resize message to pty * wrap set_terminal_size_using_fd() in macro * change keybind for Move mode * cargo fmt * fix test * move render functions from tab.rs to screen.rs * undo wrong keybinds --- default-plugins/status-bar/src/first_line.rs | 25 ++ zellij-server/src/route.rs | 9 + zellij-server/src/screen.rs | 31 +++ zellij-server/src/tab.rs | 190 ++++++++++++++- zellij-server/src/unit/tab_tests.rs | 238 +++++++++++++++++++ zellij-tile/src/data.rs | 4 + zellij-utils/assets/config/default.yaml | 51 ++++ zellij-utils/src/errors.rs | 4 + zellij-utils/src/input/actions.rs | 1 + zellij-utils/src/input/mod.rs | 1 + zellij-utils/src/input/unit/keybinds_test.rs | 4 +- 11 files changed, 546 insertions(+), 12 deletions(-) diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index 18465d4f..5dc4f08a 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -23,6 +23,7 @@ enum CtrlKeyAction { Scroll, Quit, Session, + Move, } enum CtrlKeyMode { @@ -41,6 +42,7 @@ impl CtrlKeyShortcut { CtrlKeyAction::Scroll => String::from("SCROLL"), CtrlKeyAction::Quit => String::from("QUIT"), CtrlKeyAction::Session => String::from("SESSION"), + CtrlKeyAction::Move => String::from("MOVE"), } } pub fn letter_shortcut(&self) -> char { @@ -52,6 +54,7 @@ impl CtrlKeyShortcut { CtrlKeyAction::Scroll => 's', CtrlKeyAction::Quit => 'q', CtrlKeyAction::Session => 'o', + CtrlKeyAction::Move => 'h', } } } @@ -253,6 +256,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Pane), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Resize), + CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Move), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Scroll), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Quit), @@ -267,6 +271,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Pane), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Selected, 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), @@ -281,6 +286,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Selected, 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), @@ -295,6 +301,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Pane), CtrlKeyShortcut::new(CtrlKeyMode::Selected, 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), @@ -309,6 +316,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { 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::Selected, CtrlKeyAction::Scroll), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), @@ -316,6 +324,21 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { colored_elements, separator, ), + InputMode::Move => 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::Selected, CtrlKeyAction::Move), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), + ], + colored_elements, + separator, + ), InputMode::Normal => key_indicators( max_len, &[ @@ -323,6 +346,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { 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), @@ -337,6 +361,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { 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::Selected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index fa659faa..4206cf78 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -116,6 +116,15 @@ fn route_action( }; session.senders.send_to_screen(screen_instr).unwrap(); } + Action::MovePane(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::MovePaneLeft, + Direction::Right => ScreenInstruction::MovePaneRight, + Direction::Up => ScreenInstruction::MovePaneUp, + Direction::Down => ScreenInstruction::MovePaneDown, + }; + session.senders.send_to_screen(screen_instr).unwrap(); + } Action::ScrollUp => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 1c344f54..580626f2 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -44,6 +44,10 @@ pub(crate) enum ScreenInstruction { MoveFocusUp, MoveFocusRight, MoveFocusRightOrNextTab, + MovePaneUp, + MovePaneDown, + MovePaneRight, + MovePaneLeft, Exit, ScrollUp, ScrollUpAt(Position), @@ -100,6 +104,10 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight, ScreenInstruction::MoveFocusRightOrNextTab => ScreenContext::MoveFocusRightOrNextTab, + ScreenInstruction::MovePaneDown => ScreenContext::MovePaneDown, + ScreenInstruction::MovePaneUp => ScreenContext::MovePaneUp, + ScreenInstruction::MovePaneRight => ScreenContext::MovePaneRight, + ScreenInstruction::MovePaneLeft => ScreenContext::MovePaneLeft, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::ScrollUp => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown => ScreenContext::ScrollDown, @@ -637,6 +645,29 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::MovePaneDown => { + screen.get_active_tab_mut().unwrap().move_active_pane_down(); + + screen.render(); + } + ScreenInstruction::MovePaneUp => { + screen.get_active_tab_mut().unwrap().move_active_pane_up(); + + screen.render(); + } + ScreenInstruction::MovePaneRight => { + screen + .get_active_tab_mut() + .unwrap() + .move_active_pane_right(); + + screen.render(); + } + ScreenInstruction::MovePaneLeft => { + screen.get_active_tab_mut().unwrap().move_active_pane_left(); + + screen.render(); + } ScreenInstruction::ScrollUp => { screen .get_active_tab_mut() diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index 81372fdf..ec181023 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -275,6 +275,20 @@ pub trait Pane { fn borderless(&self) -> bool; } +macro_rules! resize_pty { + ($pane:expr, $os_input:expr) => { + if let PaneId::Terminal(ref pid) = $pane.pid() { + // FIXME: This `set_terminal_size_using_fd` call would be best in + // `TerminalPane::reflow_lines` + $os_input.set_terminal_size_using_fd( + *pid, + $pane.get_content_columns() as u16, + $pane.get_content_rows() as u16, + ); + } + }; +} + impl Tab { // FIXME: Still too many arguments for clippy to be happy... #[allow(clippy::too_many_arguments)] @@ -756,7 +770,7 @@ impl Tab { self.draw_pane_frames = draw_pane_frames; self.should_clear_display_before_rendering = true; let viewport = self.viewport; - for (pane_id, pane) in self.panes.iter_mut() { + for pane in self.panes.values_mut() { if !pane.borderless() { pane.set_frame(draw_pane_frames); } @@ -783,15 +797,7 @@ impl Tab { pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); } - // FIXME: This, and all other `set_terminal_size_using_fd` calls, would be best in - // `TerminalPane::reflow_lines` - if let PaneId::Terminal(pid) = pane_id { - self.os_api.set_terminal_size_using_fd( - *pid, - pane.get_content_columns() as u16, - pane.get_content_rows() as u16, - ); - } + resize_pty!(pane, self.os_api); } } pub fn render(&mut self) -> Option { @@ -2031,6 +2037,170 @@ impl Tab { self.active_terminal = updated_active_terminal; false } + pub fn move_active_pane_down(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + if let Some(active) = self.get_active_pane() { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_below(active) && c.vertically_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.active_at()) + .map(|(_, (pid, _))| pid); + if let Some(&p) = next_index { + let current_position = self.panes.get(&self.active_terminal.unwrap()).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&p).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.get_geom_override(geom); + } + resize_pty!(new_position, self.os_api); + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&self.active_terminal.unwrap()).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.get_geom_override(geom); + } + resize_pty!(current_position, self.os_api); + current_position.set_should_render(true); + } + } + } + pub fn move_active_pane_up(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + if let Some(active) = self.get_active_pane() { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_above(active) && c.vertically_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.active_at()) + .map(|(_, (pid, _))| pid); + if let Some(&p) = next_index { + let current_position = self.panes.get(&self.active_terminal.unwrap()).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&p).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.get_geom_override(geom); + } + resize_pty!(new_position, self.os_api); + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&self.active_terminal.unwrap()).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.get_geom_override(geom); + } + resize_pty!(current_position, self.os_api); + current_position.set_should_render(true); + } + } + } + pub fn move_active_pane_right(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + if let Some(active) = self.get_active_pane() { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_right_of(active) && c.horizontally_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.active_at()) + .map(|(_, (pid, _))| pid); + if let Some(&p) = next_index { + let current_position = self.panes.get(&self.active_terminal.unwrap()).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&p).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.get_geom_override(geom); + } + resize_pty!(new_position, self.os_api); + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&self.active_terminal.unwrap()).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.get_geom_override(geom); + } + resize_pty!(current_position, self.os_api); + current_position.set_should_render(true); + } + } + } + pub fn move_active_pane_left(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + if let Some(active) = self.get_active_pane() { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_left_of(active) && c.horizontally_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.active_at()) + .map(|(_, (pid, _))| pid); + if let Some(&p) = next_index { + let current_position = self.panes.get(&self.active_terminal.unwrap()).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&p).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.get_geom_override(geom); + } + resize_pty!(new_position, self.os_api); + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&self.active_terminal.unwrap()).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.get_geom_override(geom); + } + resize_pty!(current_position, self.os_api); + current_position.set_should_render(true); + } + } + } fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet { terminals.iter().fold(HashSet::new(), |mut borders, t| { let terminal = self.panes.get(t).unwrap(); diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index 8c26421c..41e243cd 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -2525,6 +2525,244 @@ pub fn move_focus_right_to_the_most_recently_used_pane() { ); } +#[test] +pub fn move_active_pane_down() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + + tab.horizontal_split(new_pane_id); + tab.move_focus_up(); + tab.move_active_pane_down(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 10, + "Active pane is the bottom one" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(1), + "Active pane is the bottom one" + ); +} + +#[test] +pub fn move_active_pane_down_to_the_most_recently_used_position() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + + tab.horizontal_split(new_pane_id_1); + tab.vertical_split(new_pane_id_2); + tab.vertical_split(new_pane_id_3); + tab.move_focus_up(); + tab.move_active_pane_down(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 10, + "Active pane y position" + ); + assert_eq!( + tab.get_active_pane().unwrap().x(), + 91, + "Active pane x position" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(1), + "Active pane PaneId" + ); +} + +#[test] +pub fn move_active_pane_up() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + + tab.horizontal_split(new_pane_id); + tab.move_active_pane_up(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 0, + "Active pane is the top one" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(2), + "Active pane is the top one" + ); +} + +#[test] +pub fn move_active_pane_up_to_the_most_recently_used_position() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + + tab.horizontal_split(new_pane_id_1); + tab.move_focus_up(); + tab.vertical_split(new_pane_id_2); + tab.vertical_split(new_pane_id_3); + tab.move_focus_down(); + tab.move_active_pane_up(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 0, + "Active pane y position" + ); + assert_eq!( + tab.get_active_pane().unwrap().x(), + 91, + "Active pane x position" + ); + + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(2), + "Active pane PaneId" + ); +} + +#[test] +pub fn move_active_pane_left() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + + tab.vertical_split(new_pane_id); + tab.move_active_pane_left(); + + assert_eq!( + tab.get_active_pane().unwrap().x(), + 0, + "Active pane is the left one" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(2), + "Active pane is the left one" + ); +} + +#[test] +pub fn move_active_pane_left_to_the_most_recently_used_position() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + + tab.vertical_split(new_pane_id_1); + tab.move_focus_left(); + tab.horizontal_split(new_pane_id_2); + tab.horizontal_split(new_pane_id_3); + tab.move_focus_right(); + tab.move_active_pane_left(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 15, + "Active pane y position" + ); + assert_eq!( + tab.get_active_pane().unwrap().x(), + 0, + "Active pane x position" + ); + + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(2), + "Active pane PaneId" + ); +} + +#[test] +pub fn move_active_pane_right() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + + tab.vertical_split(new_pane_id); + tab.move_focus_left(); + tab.move_active_pane_right(); + + assert_eq!( + tab.get_active_pane().unwrap().x(), + 61, + "Active pane is the right one" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(1), + "Active pane is the right one" + ); +} + +#[test] +pub fn move_active_pane_right_to_the_most_recently_used_position() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + + tab.vertical_split(new_pane_id_1); + tab.horizontal_split(new_pane_id_2); + tab.horizontal_split(new_pane_id_3); + tab.move_focus_left(); + tab.move_active_pane_right(); + + assert_eq!( + tab.get_active_pane().unwrap().y(), + 15, + "Active pane y position" + ); + assert_eq!( + tab.get_active_pane().unwrap().x(), + 61, + "Active pane x position" + ); + assert_eq!( + tab.get_active_pane().unwrap().pid(), + PaneId::Terminal(1), + "Active pane Paneid" + ); +} + #[test] pub fn resize_down_with_pane_above() { // ┌───────────┐ ┌───────────┐ diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 4d6bba30..9696b356 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -81,6 +81,9 @@ pub enum InputMode { /// `Session` mode allows detaching sessions #[serde(alias = "session")] Session, + /// `Move` mode allows moving the different existing panes within a tab + #[serde(alias = "move")] + Move, } impl Default for InputMode { @@ -124,6 +127,7 @@ impl FromStr for InputMode { "scroll" => Ok(InputMode::Scroll), "renametab" => Ok(InputMode::RenameTab), "session" => Ok(InputMode::Session), + "move" => Ok(InputMode::Move), e => Err(e.to_string().into()), } } diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 007cd4b4..323c96e9 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -20,6 +20,8 @@ keybinds: key: [Ctrl: 's',] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [Quit,] key: [Ctrl: 'q',] - action: [NewPane: ] @@ -52,6 +54,8 @@ keybinds: key: [Ctrl: 's'] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [Quit] key: [Ctrl: 'q'] - action: [Resize: Left,] @@ -89,6 +93,8 @@ keybinds: key: [Ctrl: 's'] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [Quit,] key: [Ctrl: 'q',] - action: [MoveFocus: Left,] @@ -117,6 +123,45 @@ keybinds: key: [ Alt: '[',] - action: [FocusNextPane,] key: [ Alt: ']',] + move: + - action: [SwitchToMode: Locked,] + key: [Ctrl: 'g'] + - action: [SwitchToMode: Pane,] + key: [Ctrl: 'p',] + - action: [SwitchToMode: Tab,] + key: [Ctrl: 't',] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] + - action: [SwitchToMode: Normal,] + key: [Ctrl: 'h', Char: "\n", Char: ' ',] + - action: [SwitchToMode: Scroll,] + key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] + - action: [Quit] + key: [Ctrl: 'q'] + - action: [MovePane: Left,] + key: [Char: 'h', Left,] + - action: [MovePane: Down,] + key: [Char: 'j', Down,] + - action: [MovePane: Up,] + key: [Char: 'k', Up, ] + - action: [MovePane: Right,] + key: [Char: 'l', Right,] + - 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: ']',] tab: - action: [SwitchToMode: Locked,] key: [Ctrl: 'g'] @@ -128,6 +173,8 @@ keybinds: key: [Ctrl: 't', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] - action: [SwitchToMode: RenameTab, TabNameInput: [0],] @@ -186,6 +233,8 @@ keybinds: key: [Ctrl: 'g',] - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] - action: [SwitchToMode: Resize,] @@ -244,6 +293,8 @@ keybinds: key: [Ctrl: 'n',] - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] + - action: [SwitchToMode: Move,] + key: [Ctrl: 'h',] - action: [SwitchToMode: Tab,] key: [Ctrl: 't',] - action: [SwitchToMode: Normal,] diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 1859a904..b6353c5b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -226,6 +226,10 @@ pub enum ScreenContext { MoveFocusUp, MoveFocusRight, MoveFocusRightOrNextTab, + MovePaneDown, + MovePaneUp, + MovePaneRight, + MovePaneLeft, Exit, ScrollUp, ScrollUpAt, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 915f2dad..ed1865f7 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -41,6 +41,7 @@ pub enum Action { /// Tries to move the focus pane in specified direction. /// If there is no pane in the direction, move to previous/next Tab. MoveFocusOrTab(Direction), + MovePane(Direction), /// Scroll up in focus pane. ScrollUp, /// Scroll up at point diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 409c9afa..80e5e451 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -23,6 +23,7 @@ pub fn get_mode_info( let keybinds = match mode { InputMode::Normal | InputMode::Locked => Vec::new(), InputMode::Resize => vec![("←↓↑→".to_string(), "Resize".to_string())], + InputMode::Move => vec![("←↓↑→".to_string(), "Move".to_string())], InputMode::Pane => vec![ ("←↓↑→".to_string(), "Move focus".to_string()), ("p".to_string(), "Next".to_string()), diff --git a/zellij-utils/src/input/unit/keybinds_test.rs b/zellij-utils/src/input/unit/keybinds_test.rs index 6800bdf5..2cf84b70 100644 --- a/zellij-utils/src/input/unit/keybinds_test.rs +++ b/zellij-utils/src/input/unit/keybinds_test.rs @@ -378,7 +378,7 @@ fn unbind_multiple_keybinds_all_modes() { let result_normal_2 = mode_keybinds_normal .expect("ModeKeybinds shouldn't be empty") .0 - .get(&Key::Ctrl('h')); + .get(&Key::Ctrl('f')); let result_resize_1 = mode_keybinds_resize .expect("ModeKeybinds shouldn't be empty") .0 @@ -386,7 +386,7 @@ fn unbind_multiple_keybinds_all_modes() { let result_resize_2 = mode_keybinds_resize .expect("ModeKeybinds shouldn't be empty") .0 - .get(&Key::Ctrl('h')); + .get(&Key::Ctrl('f')); assert!(result_normal_1.is_none()); assert!(result_resize_1.is_none()); assert!(result_normal_2.is_none());