diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 8e65df49..03d223b2 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -190,6 +190,7 @@ impl InputHandler { | Action::GoToPreviousTab | Action::CloseTab | Action::GoToTab(_) + | Action::ToggleTab | Action::MoveFocusOrTab(_) => { self.command_is_executing.blocking_input_thread(); self.os_input diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 20f14c3b..54ddfe1b 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -28,6 +28,12 @@ fn route_action( .send_to_plugin(PluginInstruction::Update(None, Event::InputReceived)) .unwrap(); match action { + Action::ToggleTab => { + session + .senders + .send_to_screen(ScreenInstruction::ToggleTab) + .unwrap(); + } Action::Write(val) => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 4a62dad0..d0e02e80 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -69,6 +69,7 @@ pub(crate) enum ScreenInstruction { ToggleActiveSyncTab, CloseTab, GoToTab(u32), + ToggleTab, UpdateTabName(Vec), TerminalResize(PositionAndSize), ChangeMode(ModeInfo), @@ -135,6 +136,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MouseRelease(_) => ScreenContext::MouseRelease, ScreenInstruction::MouseHold(_) => ScreenContext::MouseHold, ScreenInstruction::Copy => ScreenContext::Copy, + ScreenInstruction::ToggleTab => ScreenContext::ToggleTab, } } } @@ -152,6 +154,7 @@ pub(crate) struct Screen { position_and_size: PositionAndSize, /// The index of this [`Screen`]'s active [`Tab`]. active_tab_index: Option, + tab_history: Vec>, mode_info: ModeInfo, colors: Palette, session_state: Arc>, @@ -175,6 +178,7 @@ impl Screen { colors: client_attributes.palette, active_tab_index: None, tabs: BTreeMap::new(), + tab_history: Vec::with_capacity(32), mode_info, session_state, draw_pane_frames, @@ -200,6 +204,7 @@ impl Screen { self.session_state.clone(), self.draw_pane_frames, ); + self.tab_history.push(self.active_tab_index); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); self.update_tabs(); @@ -225,6 +230,8 @@ impl Screen { for tab in self.tabs.values_mut() { if tab.position == new_tab_pos { tab.set_force_render(); + self.tab_history.retain(|&e| e != Some(tab.index)); + self.tab_history.push(self.active_tab_index); self.active_tab_index = Some(tab.index); break; } @@ -244,6 +251,8 @@ impl Screen { for tab in self.tabs.values_mut() { if tab.position == new_tab_pos { tab.set_force_render(); + self.tab_history.retain(|&e| e != Some(tab.index)); + self.tab_history.push(self.active_tab_index); self.active_tab_index = Some(tab.index); break; } @@ -258,6 +267,8 @@ impl Screen { if let Some(t) = self.tabs.values_mut().find(|t| t.position == tab_index) { if t.index != active_tab_index { t.set_force_render(); + self.tab_history.retain(|&e| e != Some(t.index)); + self.tab_history.push(self.active_tab_index); self.active_tab_index = Some(t.index); self.update_tabs(); self.render(); @@ -269,9 +280,6 @@ impl Screen { /// to be the last tab. pub fn close_tab(&mut self) { let active_tab_index = self.active_tab_index.unwrap(); - if self.tabs.len() > 1 { - self.switch_tab_prev(); - } let active_tab = self.tabs.remove(&active_tab_index).unwrap(); let pane_ids = active_tab.get_pane_ids(); // below we don't check the result of sending the CloseTab instruction to the pty thread @@ -290,6 +298,7 @@ impl Screen { .unwrap(); } } else { + self.active_tab_index = self.tab_history.pop().unwrap(); for t in self.tabs.values_mut() { if t.position > active_tab.position { t.position -= 1; @@ -335,6 +344,17 @@ impl Screen { } } + /// Returns an immutable reference to this [`Screen`]'s previous active [`Tab`]. + /// Consumes the last entry in tab history. + pub fn get_previous_tab(&mut self) -> Option<&Tab> { + let last = self.tab_history.pop(); + last?; + match last.unwrap() { + Some(tab) => self.tabs.get(&tab), + None => None, + } + } + /// Returns a mutable reference to this [`Screen`]'s active [`Tab`]. pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> { match self.active_tab_index { @@ -368,6 +388,7 @@ impl Screen { self.draw_pane_frames, ); tab.apply_layout(layout, new_pids, tab_index); + self.tab_history.push(self.active_tab_index); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); self.update_tabs(); @@ -425,6 +446,16 @@ impl Screen { self.switch_tab_next(); } } + pub fn toggle_tab(&mut self) { + let tab = self.get_previous_tab(); + if let Some(t) = tab { + let position = t.position; + self.go_to_tab(position + 1); + }; + + self.update_tabs(); + self.render(); + } } // The box is here in order to make the @@ -768,6 +799,14 @@ pub(crate) fn screen_thread_main( ScreenInstruction::Exit => { break; } + ScreenInstruction::ToggleTab => { + screen.toggle_tab(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } } } } diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 0ce5cff8..6248ded2 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -202,7 +202,7 @@ pub fn close_the_middle_tab() { assert_eq!(screen.tabs.len(), 2, "Two tabs left"); assert_eq!( screen.get_active_tab().unwrap().position, - 0, + 1, "Active tab switched to previous tab" ); } @@ -254,3 +254,186 @@ fn move_focus_right_at_right_screen_edge_changes_tab() { "Active tab switched to next" ); } + +#[test] +pub fn toggle_to_previous_tab_simple() { + let position_and_size = PositionAndSize { + cols: 121, + rows: 20, + x: 0, + y: 0, + ..Default::default() + }; + let mut screen = create_new_screen(position_and_size); + + screen.new_tab(1); + screen.new_tab(2); + screen.go_to_tab(1); + screen.go_to_tab(2); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 0, + "Active tab toggler to previous tab" + ); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 1, + "Active tab toggler to previous tab" + ); +} + +#[test] +pub fn toggle_to_previous_tab_create_tabs_only() { + let position_and_size = PositionAndSize { + cols: 121, + rows: 20, + x: 0, + y: 0, + ..Default::default() + }; + let mut screen = create_new_screen(position_and_size); + + screen.new_tab(1); + screen.new_tab(2); + screen.new_tab(3); + + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1)], + "Tab history is invalid" + ); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 1, + "Active tab toggler to previous tab" + ); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(2)], + "Tab history is invalid" + ); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 2, + "Active tab toggler to previous tab" + ); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1)], + "Tab history is invalid" + ); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 1, + "Active tab toggler to previous tab" + ); +} + +#[test] +pub fn toggle_to_previous_tab_delete() { + let position_and_size = PositionAndSize { + cols: 121, + rows: 20, + x: 0, + y: 0, + ..Default::default() + }; + let mut screen = create_new_screen(position_and_size); + + screen.new_tab(1); // 0 + screen.new_tab(2); // 1 + screen.new_tab(3); // 2 + screen.new_tab(4); // 3 + + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1), Some(2)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 3, + "Active tab toggler to previous tab" + ); + + screen.toggle_tab(); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1), Some(3)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 2, + "Active tab toggler to previous tab" + ); + + screen.toggle_tab(); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1), Some(2)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 3, + "Active tab toggler to previous tab" + ); + + screen.switch_tab_prev(); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(1), Some(3)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 2, + "Active tab toggler to previous tab" + ); + screen.switch_tab_prev(); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(3), Some(2)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 1, + "Active tab toggler to previous tab" + ); + + screen.close_tab(); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(3)], + "Tab history is invalid" + ); + assert_eq!( + screen.get_active_tab().unwrap().position, + 1, + "Active tab toggler to previous tab" + ); + + screen.toggle_tab(); + assert_eq!( + screen.get_active_tab().unwrap().position, + 2, + "Active tab toggler to previous tab" + ); + assert_eq!( + screen.tab_history, + vec![None, Some(0), Some(2)], + "Tab history is invalid" + ); +} diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 4654cee9..94f12f33 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -168,6 +168,8 @@ keybinds: key: [ Char: '8',] - action: [GoToTab: 9,] key: [ Char: '9',] + - action: [ToggleTab] + key: [ Char: "\t" ] scroll: - action: [SwitchToMode: Normal,] key: [Ctrl: 'r', Ctrl: 's', Char: ' ', diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index c0891d52..65a54c56 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -228,6 +228,7 @@ pub enum ScreenContext { MouseRelease, MouseHold, Copy, + ToggleTab, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 3e86409b..23c01eef 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -76,6 +76,7 @@ pub enum Action { /// Close the current tab. CloseTab, GoToTab(u32), + ToggleTab, TabNameInput(Vec), /// Run speficied command in new pane. Run(RunCommandAction), diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 87d72db8..68aa9dd7 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -38,6 +38,7 @@ pub fn get_mode_info( ("x".to_string(), "Close".to_string()), ("r".to_string(), "Rename".to_string()), ("s".to_string(), "Sync".to_string()), + ("Tab".to_string(), "Toggle".to_string()), ], InputMode::Scroll => vec![ ("↓↑".to_string(), "Scroll".to_string()),