Repository: zellij-org/zellij (language: rust)

Issue:
Switch to tab by name command
In the same flow as #2098 it would be nice to have a command that allows a user to switch to a tab by supplying a name.
Having a `--create `option to make the tab (like with session attaching) if it doesn't exist would be nice as well.

As far as duplicate tabs with the same name are concerned, I would imagine switching back to the tab with the name you were most recently in would be the "best" option, but if you don't track recency switching to the first tab with that name is also fairly logical.


Discussion / hints:

Fix patch:
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index eb711fa3a0..e764786c44 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -307,6 +307,7 @@ impl InputHandler {
             | Action::GoToPreviousTab
             | Action::CloseTab
             | Action::GoToTab(_)
+            | Action::GoToTabName(_, _)
             | Action::ToggleTab
             | Action::MoveFocusOrTab(_) => {
                 self.command_is_executing.blocking_input_thread();
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index 2ca58cc448..dce1c9ac0f 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -479,6 +479,16 @@ pub(crate) fn route_action(
                 .send_to_screen(ScreenInstruction::GoToTab(i, Some(client_id)))
                 .with_context(err_context)?;
         },
+        Action::GoToTabName(name, create) => {
+            session
+                .senders
+                .send_to_screen(ScreenInstruction::GoToTabName(
+                    name,
+                    create,
+                    Some(client_id),
+                ))
+                .with_context(err_context)?;
+        },
         Action::TabNameInput(c) => {
             session
                 .senders
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index fdca620d91..9a196f35db 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -196,6 +196,7 @@ pub enum ScreenInstruction {
     ToggleActiveSyncTab(ClientId),
     CloseTab(ClientId),
     GoToTab(u32, Option<ClientId>), // this Option is a hacky workaround, please do not copy this behaviour
+    GoToTabName(String, bool, Option<ClientId>),
     ToggleTab(ClientId),
     UpdateTabName(Vec<u8>, ClientId),
     UndoRenameTab(ClientId),
@@ -317,6 +318,7 @@ impl From<&ScreenInstruction> for ScreenContext {
             ScreenInstruction::SwitchTabPrev(..) => ScreenContext::SwitchTabPrev,
             ScreenInstruction::CloseTab(..) => ScreenContext::CloseTab,
             ScreenInstruction::GoToTab(..) => ScreenContext::GoToTab,
+            ScreenInstruction::GoToTabName(..) => ScreenContext::GoToTabName,
             ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName,
             ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab,
             ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize,
@@ -623,6 +625,18 @@ impl Screen {
         Ok(())
     }
 
+    /// A helper function to switch to a new tab with specified name. Return true if tab [name] has
+    /// been created, else false.
+    fn switch_active_tab_name(&mut self, name: String, client_id: ClientId) -> Result<bool> {
+        match self.tabs.values().find(|t| t.name == name) {
+            Some(new_tab) => {
+                self.switch_active_tab(new_tab.position, client_id)?;
+                Ok(true)
+            },
+            None => Ok(false),
+        }
+    }
+
     /// Sets this [`Screen`]'s active [`Tab`] to the next tab.
     pub fn switch_tab_next(&mut self, client_id: ClientId) -> Result<()> {
         let err_context = || format!("failed to switch to next tab for client {client_id}");
@@ -678,6 +692,10 @@ impl Screen {
         self.switch_active_tab(tab_index.saturating_sub(1), client_id)
     }
 
+    pub fn go_to_tab_name(&mut self, name: String, client_id: ClientId) -> Result<bool> {
+        self.switch_active_tab_name(name, client_id)
+    }
+
     fn close_tab_at_index(&mut self, tab_index: usize) -> Result<()> {
         let err_context = || format!("failed to close tab at index {tab_index:?}");
 
@@ -1981,6 +1999,39 @@ pub(crate) fn screen_thread_main(
                     screen.render()?;
                 }
             },
+            ScreenInstruction::GoToTabName(tab_name, create, client_id) => {
+                let client_id = if client_id.is_none() {
+                    None
+                } else if screen
+                    .active_tab_indices
+                    .contains_key(&client_id.expect("This is checked above"))
+                {
+                    client_id
+                } else {
+                    screen.active_tab_indices.keys().next().copied()
+                };
+                if let Some(client_id) = client_id {
+                    if let Ok(tab_exists) = screen.go_to_tab_name(tab_name.clone(), client_id) {
+                        screen.unblock_input()?;
+                        screen.render()?;
+                        if create && !tab_exists {
+                            let tab_index = screen.get_new_tab_index();
+                            screen.new_tab(tab_index, client_id)?;
+                            screen
+                                .bus
+                                .senders
+                                .send_to_plugin(PluginInstruction::NewTab(
+                                    None,
+                                    None,
+                                    vec![],
+                                    Some(tab_name),
+                                    tab_index,
+                                    client_id,
+                                ))?;
+                        }
+                    }
+                }
+            },
             ScreenInstruction::UpdateTabName(c, client_id) => {
                 screen.update_active_tab_name(c, client_id)?;
                 screen.unblock_input()?;
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 82c34eecd6..1ef43e27fd 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -315,6 +315,13 @@ pub enum CliAction {
     CloseTab,
     /// Go to tab with index [index]
     GoToTab { index: u32 },
+    /// Go to tab with name [name]
+    GoToTabName {
+        name: String,
+        /// Create a tab if one does not exist.
+        #[clap(short, long, value_parser)]
+        create: bool,
+    },
     /// Renames the focused pane
     RenameTab { name: String },
     /// Remove a previously set tab name
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 50c8f9b28f..a7986de8d0 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -285,6 +285,7 @@ pub enum ScreenContext {
     SwitchTabPrev,
     CloseTab,
     GoToTab,
+    GoToTabName,
     UpdateTabName,
     UndoRenameTab,
     TerminalResize,
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 1207023aad..b5d6acb13a 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -174,6 +174,7 @@ pub enum Action {
     /// Close the current tab.
     CloseTab,
     GoToTab(u32),
+    GoToTabName(String, bool),
     ToggleTab,
     TabNameInput(Vec<u8>),
     UndoRenameTab,
@@ -321,6 +322,7 @@ impl Action {
             CliAction::GoToPreviousTab => Ok(vec![Action::GoToPreviousTab]),
             CliAction::CloseTab => Ok(vec![Action::CloseTab]),
             CliAction::GoToTab { index } => Ok(vec![Action::GoToTab(index)]),
+            CliAction::GoToTabName { name, create } => Ok(vec![Action::GoToTabName(name, create)]),
             CliAction::RenameTab { name } => Ok(vec![
                 Action::TabNameInput(vec![0]),
                 Action::TabNameInput(name.as_bytes().to_vec()),


Test patch:
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index e9b399618a..7b1be55e05 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -546,6 +546,40 @@ pub fn switch_to_next_tab() {
     );
 }
 
+#[test]
+pub fn switch_to_tab_name() {
+    let size = Size {
+        cols: 121,
+        rows: 20,
+    };
+    let mut screen = create_new_screen(size);
+
+    new_tab(&mut screen, 1, 1);
+    new_tab(&mut screen, 2, 2);
+
+    assert_eq!(
+        screen
+            .switch_active_tab_name("Tab #1".to_string(), 1)
+            .expect("TEST"),
+        false,
+        "Active tab switched to tab by name"
+    );
+    assert_eq!(
+        screen
+            .switch_active_tab_name("Tab #2".to_string(), 1)
+            .expect("TEST"),
+        true,
+        "Active tab switched to tab by name"
+    );
+    assert_eq!(
+        screen
+            .switch_active_tab_name("Tab #3".to_string(), 1)
+            .expect("TEST"),
+        true,
+        "Active tab switched to tab by name"
+    );
+}
+
 #[test]
 pub fn close_tab() {
     let size = Size {