aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-11 13:26:33 +0100
committerGitHub <noreply@github.com>2026-02-11 13:26:33 +0100
commit13a82eea6d74d8168e538b8755131ed793a7f054 (patch)
treebb8d2c1b6cd24bf46b84860360c48e6249e21ba1
parentAllow writing characters, bytes and keys to specific panes from the CLI (#4691) (diff)
downloadsl0ppij-13a82eea6d74d8168e538b8755131ed793a7f054.tar.gz
sl0ppij-13a82eea6d74d8168e538b8755131ed793a7f054.tar.xz
sl0ppij-13a82eea6d74d8168e538b8755131ed793a7f054.zip
CLI commands to query tab ids, names and info (#4695)
* add equivalent tab_id commands to tab_position commands (cli and plugins) * add list-tabs and current-tab-info cli commands * rustfmt * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs137
-rw-r--r--zellij-server/src/route.rs267
-rw-r--r--zellij-server/src/screen.rs153
-rw-r--r--zellij-server/src/unit/screen_tests.rs256
-rw-r--r--zellij-utils/assets/prost/api.action.rs25
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs80
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs54
-rw-r--r--zellij-utils/src/cli.rs49
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto29
-rw-r--r--zellij-utils/src/data.rs8
-rw-r--r--zellij-utils/src/errors.rs6
-rw-r--r--zellij-utils/src/input/actions.rs42
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs108
-rw-r--r--zellij-utils/src/plugin_api/action.proto11
-rw-r--r--zellij-utils/src/plugin_api/action.rs12
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto40
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs148
18 files changed, 1346 insertions, 80 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 216f0d5f..2c2f44a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* refactor: canonicalize tab_id vs. tab_position and expose tab_id to plugins (https://github.com/zellij-org/zellij/pull/4685)
* feat: `zellij action list-panes` + return pane/tab IDs from plugin and CLI methods creating them (https://github.com/zellij-org/zellij/pull/4690)
* feat: allow sending keys to other panes from the CLI (https://github.com/zellij-org/zellij/pull/4691)
+* feat: allow querying tab info (id, name and position) from the CLI as well as general info about all tabs (https://github.com/zellij-org/zellij/pull/4695)
## [0.43.1] - 2025-08-08
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 6968741a..650892f4 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -21,18 +21,18 @@ use std::{
use tokio::sync::oneshot;
use wasmi::{Caller, Linker};
use zellij_utils::data::{
- BreakPanesToNewTabResponse, BreakPanesToTabWithIndexResponse, CommandType, ConnectToSession,
- DeleteLayoutResponse, EditLayoutResponse, Event, FloatingPaneCoordinates,
- FocusOrCreateTabResponse, GetFocusedPaneInfoResponse, GetPaneCwdResponse, GetPanePidResponse,
- GetPaneRunningCommandResponse, HttpVerb, KeyWithModifier, LayoutInfo, LayoutMetadata,
- LayoutParsingError, MessageToPlugin, NewPanePlacement, NewTabResponse, NewTabsResponse,
- OpenCommandPaneBackgroundResponse, OpenCommandPaneFloatingNearPluginResponse,
- OpenCommandPaneFloatingResponse, OpenCommandPaneInPlaceOfPluginResponse,
- OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse,
- OpenFileFloatingNearPluginResponse, OpenFileFloatingResponse, OpenFileInPlaceOfPluginResponse,
- OpenFileInPlaceResponse, OpenFileNearPluginResponse, OpenFileResponse,
- OpenTerminalFloatingNearPluginResponse, OpenTerminalFloatingResponse,
- OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
+ BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse,
+ CommandType, ConnectToSession, DeleteLayoutResponse, EditLayoutResponse, Event,
+ FloatingPaneCoordinates, FocusOrCreateTabResponse, GetFocusedPaneInfoResponse,
+ GetPaneCwdResponse, GetPanePidResponse, GetPaneRunningCommandResponse, HttpVerb,
+ KeyWithModifier, LayoutInfo, LayoutMetadata, LayoutParsingError, MessageToPlugin,
+ NewPanePlacement, NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse,
+ OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse,
+ OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse,
+ OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, OpenFileFloatingNearPluginResponse,
+ OpenFileFloatingResponse, OpenFileInPlaceOfPluginResponse, OpenFileInPlaceResponse,
+ OpenFileNearPluginResponse, OpenFileResponse, OpenTerminalFloatingNearPluginResponse,
+ OpenTerminalFloatingResponse, OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
OpenTerminalNearPluginResponse, OpenTerminalResponse, OriginatingPlugin,
PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission,
RenameLayoutResponse, SaveLayoutResponse, TabMetadata,
@@ -73,13 +73,14 @@ use zellij_utils::{
plugin_command::{
dump_layout_response, dump_session_layout_response, parse_layout_response,
save_session_response, ProtobufBreakPanesToNewTabResponse,
- ProtobufBreakPanesToTabWithIndexResponse, ProtobufDeleteLayoutResponse,
- ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse,
- ProtobufEditLayoutResponse, ProtobufFocusOrCreateTabResponse,
- ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
- ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse,
- ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
- ProtobufGetTabInfoResponse, ProtobufNewTabResponse, ProtobufNewTabsResponse,
+ ProtobufBreakPanesToTabWithIdResponse, ProtobufBreakPanesToTabWithIndexResponse,
+ ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse,
+ ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
+ ProtobufFocusOrCreateTabResponse, ProtobufGenerateRandomNameResponse,
+ ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse,
+ ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse, ProtobufGetPanePidResponse,
+ ProtobufGetPaneRunningCommandResponse, ProtobufGetTabInfoResponse,
+ ProtobufNewTabResponse, ProtobufNewTabsResponse,
ProtobufOpenCommandPaneBackgroundResponse,
ProtobufOpenCommandPaneFloatingNearPluginResponse,
ProtobufOpenCommandPaneFloatingResponse,
@@ -520,6 +521,20 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
tab_index,
should_change_focus_to_new_tab,
),
+ PluginCommand::SwitchTabToId(tab_id) => switch_tab_to_id(env, tab_id),
+ PluginCommand::GoToTabWithId(tab_id) => go_to_tab_with_id(env, tab_id),
+ PluginCommand::CloseTabWithId(tab_id) => close_tab_with_id(env, tab_id),
+ PluginCommand::RenameTabWithId(tab_id, new_name) => {
+ rename_tab_with_id(env, tab_id, &new_name)
+ },
+ PluginCommand::BreakPanesToTabWithId(pane_ids, tab_id, should_change_focus) => {
+ break_panes_to_tab_with_id(
+ env,
+ pane_ids.into_iter().map(|p_id| p_id.into()).collect(),
+ should_change_focus,
+ tab_id,
+ )
+ },
PluginCommand::ReloadPlugin(plugin_id) => reload_plugin(env, plugin_id),
PluginCommand::LoadNewPlugin {
url,
@@ -3925,6 +3940,85 @@ fn break_panes_to_tab_with_index(
wasi_write_object(env, &response.encode_to_vec()).non_fatal();
}
+fn switch_tab_to_id(env: &PluginEnv, tab_id: u64) {
+ let tab_id = tab_id as usize;
+ env.senders
+ .send_to_screen(ScreenInstruction::GoToTabWithId(
+ tab_id,
+ Some(env.client_id),
+ None,
+ ))
+ .with_context(|| {
+ format!(
+ "failed to switch to tab {} from plugin {}",
+ tab_id,
+ env.name()
+ )
+ })
+ .non_fatal();
+}
+
+fn go_to_tab_with_id(env: &PluginEnv, tab_id: u64) {
+ let tab_id = tab_id as usize;
+ env.senders
+ .send_to_screen(ScreenInstruction::GoToTabWithId(
+ tab_id,
+ Some(env.client_id),
+ None,
+ ))
+ .with_context(|| format!("failed to go to tab {} from plugin {}", tab_id, env.name()))
+ .non_fatal();
+}
+
+fn close_tab_with_id(env: &PluginEnv, tab_id: u64) {
+ let tab_id = tab_id as usize;
+ let _ = env
+ .senders
+ .send_to_screen(ScreenInstruction::CloseTabWithId(tab_id, None));
+}
+
+fn rename_tab_with_id(env: &PluginEnv, tab_id: u64, new_name: &str) {
+ let tab_id = tab_id as usize;
+ let rename_tab_action =
+ ScreenInstruction::RenameTabWithId(tab_id, new_name.as_bytes().to_vec(), None);
+ env.senders
+ .send_to_screen(rename_tab_action)
+ .with_context(|| format!("Failed to rename tab {}", tab_id))
+ .non_fatal();
+}
+
+fn break_panes_to_tab_with_id(
+ env: &PluginEnv,
+ pane_ids: Vec<PaneId>,
+ should_change_focus_to_target_tab: bool,
+ tab_id: u64,
+) {
+ let tab_id = tab_id as usize;
+
+ // Create completion channel to receive tab ID
+ let (tx, rx) = tokio::sync::oneshot::channel();
+ let completion_tx = Some(NotificationEnd::new(tx));
+
+ let result = env
+ .senders
+ .send_to_screen(ScreenInstruction::BreakPanesToTabWithId {
+ pane_ids,
+ tab_id,
+ should_change_focus_to_target_tab,
+ client_id: env.client_id,
+ completion_tx,
+ });
+
+ let result_tab_id: BreakPanesToTabWithIdResponse = if result.is_ok() {
+ wait_for_action_completion(rx, "break_panes_to_tab_with_id", false).affected_tab_id
+ } else {
+ None
+ };
+
+ let response = ProtobufBreakPanesToTabWithIdResponse::from(result_tab_id);
+ wasi_write_object(env, &response.encode_to_vec()).non_fatal();
+}
+
fn reload_plugin(env: &PluginEnv, plugin_id: u32) {
let _ = env
.senders
@@ -4461,6 +4555,11 @@ fn check_command_permission(
| PluginCommand::CloseTabWithIndex(..)
| PluginCommand::BreakPanesToNewTab(..)
| PluginCommand::BreakPanesToTabWithIndex(..)
+ | PluginCommand::SwitchTabToId(..)
+ | PluginCommand::GoToTabWithId(..)
+ | PluginCommand::CloseTabWithId(..)
+ | PluginCommand::RenameTabWithId(..)
+ | PluginCommand::BreakPanesToTabWithId(..)
| PluginCommand::ReloadPlugin(..)
| PluginCommand::LoadNewPlugin { .. }
| PluginCommand::SetFloatingPanePinned(..)
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index a81f9ecc..be3cf492 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -19,7 +19,8 @@ use zellij_utils::{
channels::SenderWithContext,
data::{
BareKey, ConnectToSession, Direction, Event, InputMode, KeyModifier, ListPanesResponse,
- NewPanePlacement, PaneListEntry, PluginCapabilities, ResizeStrategy, UnblockCondition,
+ ListTabsResponse, NewPanePlacement, PaneListEntry, PluginCapabilities, ResizeStrategy,
+ TabInfo, UnblockCondition,
},
envs,
errors::prelude::*,
@@ -1024,6 +1025,32 @@ pub(crate) fn route_action(
))
.with_context(err_context)?;
},
+ Action::GoToTabById { id } => {
+ senders
+ .send_to_screen(ScreenInstruction::GoToTabWithId(
+ id as usize,
+ Some(client_id),
+ Some(NotificationEnd::new(completion_tx)),
+ ))
+ .with_context(err_context)?;
+ },
+ Action::CloseTabById { id } => {
+ senders
+ .send_to_screen(ScreenInstruction::CloseTabWithId(
+ id as usize,
+ Some(NotificationEnd::new(completion_tx)),
+ ))
+ .with_context(err_context)?;
+ },
+ Action::RenameTabById { id, name } => {
+ senders
+ .send_to_screen(ScreenInstruction::RenameTabWithId(
+ id as usize,
+ name.into_bytes(),
+ Some(NotificationEnd::new(completion_tx)),
+ ))
+ .with_context(err_context)?;
+ },
Action::MoveTab { direction } => {
let screen_instr = match direction {
Direction::Left | Direction::Up => ScreenInstruction::MoveTabLeft(
@@ -1588,6 +1615,59 @@ pub(crate) fn route_action(
}
drop(NotificationEnd::new(completion_tx));
},
+ Action::ListTabs {
+ show_state,
+ show_dimensions,
+ show_panes,
+ show_layout,
+ show_all,
+ output_json,
+ } => {
+ let maybe_tabs =
+ request_tabs_from_screen(&senders, client_id).with_context(err_context)?;
+
+ if let Some(tab_infos) = maybe_tabs {
+ let output_lines = if output_json {
+ format_tabs_as_json(&tab_infos)
+ } else {
+ format_tabs_table(
+ &tab_infos,
+ show_state || show_all,
+ show_dimensions || show_all,
+ show_panes || show_all,
+ show_layout || show_all,
+ )
+ };
+
+ send_output_to_client(cli_client_id, os_input.as_ref(), output_lines);
+ } else {
+ send_error_to_client(cli_client_id, os_input.as_ref(), "Timeout listing tabs");
+ }
+ drop(NotificationEnd::new(completion_tx));
+ },
+ Action::CurrentTabInfo { output_json } => {
+ let maybe_tab_info = request_current_tab_info_from_screen(&senders, client_id)
+ .with_context(err_context)?;
+
+ match maybe_tab_info {
+ Some(tab_info) => {
+ let output_lines = if output_json {
+ format_current_tab_info_as_json(&tab_info)
+ } else {
+ format_current_tab_info_plain(&tab_info)
+ };
+ send_output_to_client(cli_client_id, os_input.as_ref(), output_lines);
+ },
+ None => {
+ send_error_to_client(
+ cli_client_id,
+ os_input.as_ref(),
+ "No active tab found for current client",
+ );
+ },
+ }
+ drop(NotificationEnd::new(completion_tx));
+ },
Action::TogglePanePinned => {
senders
.send_to_screen(ScreenInstruction::TogglePanePinned(
@@ -2238,6 +2318,58 @@ fn request_panes_from_screen(
}
}
+fn request_tabs_from_screen(
+ senders: &ThreadSenders,
+ client_id: ClientId,
+) -> Result<Option<ListTabsResponse>> {
+ use crossbeam::channel::{unbounded, RecvTimeoutError};
+ use std::time::Duration;
+
+ let (response_sender, response_receiver) = unbounded();
+ senders.send_to_screen(ScreenInstruction::ListTabs {
+ client_id,
+ response_channel: response_sender,
+ })?;
+
+ match response_receiver.recv_timeout(Duration::from_secs(1)) {
+ Ok(entries) => Ok(Some(entries)),
+ Err(RecvTimeoutError::Timeout) => {
+ log::error!("ListTabs timed out waiting for Screen response");
+ Ok(None)
+ },
+ Err(RecvTimeoutError::Disconnected) => {
+ log::error!("ListTabs channel disconnected");
+ Ok(None)
+ },
+ }
+}
+
+fn request_current_tab_info_from_screen(
+ senders: &ThreadSenders,
+ client_id: ClientId,
+) -> Result<Option<TabInfo>> {
+ use crossbeam::channel::{unbounded, RecvTimeoutError};
+ use std::time::Duration;
+
+ let (response_sender, response_receiver) = unbounded();
+ senders.send_to_screen(ScreenInstruction::GetCurrentTabInfo {
+ client_id,
+ response_channel: response_sender,
+ })?;
+
+ match response_receiver.recv_timeout(Duration::from_secs(1)) {
+ Ok(tab_info_opt) => Ok(tab_info_opt),
+ Err(RecvTimeoutError::Timeout) => {
+ log::error!("GetCurrentTabInfo timed out waiting for Screen response");
+ Ok(None)
+ },
+ Err(RecvTimeoutError::Disconnected) => {
+ log::error!("GetCurrentTabInfo channel disconnected");
+ Ok(None)
+ },
+ }
+}
+
fn enrich_panes_with_pty_data(
pane_entries: &mut [PaneListEntry],
senders: &ThreadSenders,
@@ -2445,6 +2577,139 @@ fn extract_cwd(entry: &PaneListEntry) -> String {
.to_string()
}
+fn format_tabs_as_json(tab_infos: &[TabInfo]) -> Vec<String> {
+ vec![serde_json::to_string_pretty(tab_infos).unwrap_or_else(|_| "[]".to_string())]
+}
+
+fn format_tabs_table(
+ tabs: &[TabInfo],
+ show_state: bool,
+ show_dimensions: bool,
+ show_panes: bool,
+ show_layout: bool,
+) -> Vec<String> {
+ let mut lines = Vec::new();
+ lines.push(build_tabs_table_header(
+ show_state,
+ show_dimensions,
+ show_panes,
+ show_layout,
+ ));
+
+ for tab_info in tabs {
+ lines.push(build_tabs_table_row(
+ tab_info,
+ show_state,
+ show_dimensions,
+ show_panes,
+ show_layout,
+ ));
+ }
+
+ lines
+}
+
+fn build_tabs_table_header(
+ show_state: bool,
+ show_dimensions: bool,
+ show_panes: bool,
+ show_layout: bool,
+) -> String {
+ let mut header = Vec::new();
+
+ // Core fields (always shown)
+ header.push("TAB_ID");
+ header.push("POSITION");
+ header.push("NAME");
+
+ if show_state {
+ header.push("ACTIVE");
+ header.push("FULLSCREEN");
+ header.push("SYNC_PANES");
+ header.push("FLOATING_VIS");
+ }
+
+ if show_dimensions {
+ header.push("VP_ROWS");
+ header.push("VP_COLS");
+ header.push("DA_ROWS");
+ header.push("DA_COLS");
+ }
+
+ if show_panes {
+ header.push("TILED_PANES");
+ header.push("FLOAT_PANES");
+ header.push("HIDDEN_PANES");
+ }
+
+ if show_layout {
+ header.push("SWAP_LAYOUT");
+ header.push("LAYOUT_DIRTY");
+ }
+
+ header.join(" ")
+}
+
+fn build_tabs_table_row(
+ tab_info: &TabInfo,
+ show_state: bool,
+ show_dimensions: bool,
+ show_panes: bool,
+ show_layout: bool,
+) -> String {
+ let mut row = Vec::new();
+
+ // Core fields
+ row.push(tab_info.tab_id.to_string());
+ row.push(tab_info.position.to_string());
+ row.push(tab_info.name.clone());
+
+ if show_state {
+ row.push(tab_info.active.to_string());
+ row.push(tab_info.is_fullscreen_active.to_string());
+ row.push(tab_info.is_sync_panes_active.to_string());
+ row.push(tab_info.are_floating_panes_visible.to_string());
+ }
+
+ if show_dimensions {
+ row.push(tab_info.viewport_rows.to_string());
+ row.push(tab_info.viewport_columns.to_string());
+ row.push(tab_info.display_area_rows.to_string());
+ row.push(tab_info.display_area_columns.to_string());
+ }
+
+ if show_panes {
+ row.push(tab_info.selectable_tiled_panes_count.to_string());
+ row.push(tab_info.selectable_floating_panes_count.to_string());
+ row.push(tab_info.panes_to_hide.to_string());
+ }
+
+ if show_layout {
+ row.push(
+ tab_info
+ .active_swap_layout_name
+ .as_deref()
+ .unwrap_or("-")
+ .to_string(),
+ );
+ row.push(tab_info.is_swap_layout_dirty.to_string());
+ }
+
+ row.join(" ")
+}
+
+fn format_current_tab_info_as_json(tab_info: &TabInfo) -> Vec<String> {
+ vec![serde_json::to_string_pretty(tab_info).unwrap_or_else(|_| "{}".to_string())]
+}
+
+fn format_current_tab_info_plain(tab_info: &TabInfo) -> Vec<String> {
+ vec![
+ format!("name: {}", tab_info.name),
+ format!("id: {}", tab_info.tab_id),
+ format!("position: {}", tab_info.position),
+ ]
+}
+
fn send_error_to_client(
cli_client_id: Option<ClientId>,
os_input: Option<&Box<dyn ServerOsApi>>,
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 40200523..7089fb64 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -41,9 +41,9 @@ use crate::route::NotificationEnd;
use log::{debug, warn};
use zellij_utils::data::{
CommandOrPlugin, Direction, FloatingPaneCoordinates, GetFocusedPaneInfoResponse,
- KeyWithModifier, LayoutInfo, ListPanesResponse, NewPanePlacement, PaneContents, PaneInfo,
- PaneListEntry, PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy,
- SessionInfo, Styling, WebSharing,
+ KeyWithModifier, LayoutInfo, ListPanesResponse, ListTabsResponse, NewPanePlacement,
+ PaneContents, PaneInfo, PaneListEntry, PaneManifest, PaneScrollbackResponse, PluginPermission,
+ Resize, ResizeStrategy, SessionInfo, Styling, TabInfo, WebSharing,
};
use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
@@ -84,7 +84,7 @@ use crate::{
ClientId, ServerInstruction,
};
use zellij_utils::{
- data::{Event, InputMode, ModeInfo, Palette, PaletteColor, PluginCapabilities, Style, TabInfo},
+ data::{Event, InputMode, ModeInfo, Palette, PaletteColor, PluginCapabilities, Style},
errors::{ContextType, ScreenContext},
input::get_mode_info,
ipc::{ClientAttributes, PixelDimensions, ServerToClientMsg},
@@ -389,6 +389,16 @@ pub enum ScreenInstruction {
UndoRenameTab(ClientId, Option<NotificationEnd>),
MoveTabLeft(ClientId, Option<NotificationEnd>),
MoveTabRight(ClientId, Option<NotificationEnd>),
+ GoToTabWithId(usize, Option<ClientId>, Option<NotificationEnd>),
+ CloseTabWithId(usize, Option<NotificationEnd>),
+ RenameTabWithId(usize, Vec<u8>, Option<NotificationEnd>),
+ BreakPanesToTabWithId {
+ pane_ids: Vec<PaneId>,
+ tab_id: usize,
+ should_change_focus_to_target_tab: bool,
+ client_id: ClientId,
+ completion_tx: Option<NotificationEnd>,
+ },
TerminalResize(Size),
TerminalPixelDimensions(PixelDimensions),
TerminalBackgroundColor(String),
@@ -542,6 +552,14 @@ pub enum ScreenInstruction {
show_all: bool,
response_channel: crossbeam::channel::Sender<ListPanesResponse>,
},
+ ListTabs {
+ client_id: ClientId,
+ response_channel: crossbeam::channel::Sender<ListTabsResponse>,
+ },
+ GetCurrentTabInfo {
+ client_id: ClientId,
+ response_channel: crossbeam::channel::Sender<Option<TabInfo>>,
+ },
Reconfigure {
client_id: ClientId,
keybinds: Keybinds,
@@ -729,6 +747,10 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab,
ScreenInstruction::MoveTabLeft(..) => ScreenContext::MoveTabLeft,
ScreenInstruction::MoveTabRight(..) => ScreenContext::MoveTabRight,
+ ScreenInstruction::GoToTabWithId(..) => ScreenContext::GoToTabWithId,
+ ScreenInstruction::CloseTabWithId(..) => ScreenContext::CloseTabWithId,
+ ScreenInstruction::RenameTabWithId(..) => ScreenContext::RenameTabWithId,
+ ScreenInstruction::BreakPanesToTabWithId { .. } => ScreenContext::BreakPanesToTabWithId,
ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize,
ScreenInstruction::TerminalPixelDimensions(..) => {
ScreenContext::TerminalPixelDimensions
@@ -812,6 +834,8 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession,
ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata,
ScreenInstruction::ListPanes { .. } => ScreenContext::ListPanes,
+ ScreenInstruction::ListTabs { .. } => ScreenContext::ListTabs,
+ ScreenInstruction::GetCurrentTabInfo { .. } => ScreenContext::GetCurrentTabInfo,
ScreenInstruction::Reconfigure { .. } => ScreenContext::Reconfigure,
ScreenInstruction::RerunCommandPane { .. } => ScreenContext::RerunCommandPane,
ScreenInstruction::ResizePaneWithId(..) => ScreenContext::ResizePaneWithId,
@@ -2304,6 +2328,28 @@ impl Screen {
Ok(pane_entries)
}
+ fn collect_tab_list(&self, _client_id: ClientId) -> Result<ListTabsResponse> {
+ let mut tab_infos = Vec::new();
+
+ for tab in self.tabs.values() {
+ if let Some(tab_info) = self.get_tab_info(tab.id) {
+ tab_infos.push(tab_info);
+ }
+ }
+
+ // Sort by position (display order)
+ tab_infos.sort_by_key(|t| t.position);
+
+ Ok(tab_infos)
+ }
+
+ fn get_current_tab_info(&self, client_id: ClientId) -> Result<Option<TabInfo>> {
+ match self.active_tab_ids.get(&client_id) {
+ Some(active_tab_id) => Ok(self.get_tab_info(*active_tab_id)),
+ None => Ok(None),
+ }
+ }
+
fn log_and_report_session_state(&mut self, skip_querying_layouts: bool) -> Result<()> {
let err_context = || format!("Failed to log and report session state");
@@ -4570,6 +4616,26 @@ pub(crate) fn screen_thread_main(
.with_context(err_context)?;
let _ = response_channel.send(pane_entries);
},
+ ScreenInstruction::ListTabs {
+ client_id,
+ response_channel,
+ } => {
+ let err_context = || "Failed to list tabs";
+ let tab_infos = screen
+ .collect_tab_list(client_id)
+ .with_context(err_context)?;
+ let _ = response_channel.send(tab_infos);
+ },
+ ScreenInstruction::GetCurrentTabInfo {
+ client_id,
+ response_channel,
+ } => {
+ let err_context = || "Failed to get current tab info";
+ let tab_info = screen
+ .get_current_tab_info(client_id)
+ .with_context(err_context)?;
+ let _ = response_channel.send(tab_info);
+ },
ScreenInstruction::DumpLayoutToPlugin {
plugin_id,
tab_index,
@@ -6379,6 +6445,85 @@ pub(crate) fn screen_thread_main(
}
screen.log_and_report_session_state(false)?;
},
+ ScreenInstruction::GoToTabWithId(tab_id, client_id, _completion_tx) => {
+ let client_id_to_switch = client_id
+ .and_then(|cid| {
+ if screen.active_tab_ids.contains_key(&cid) {
+ Some(cid)
+ } else {
+ screen.active_tab_ids.keys().next().copied()
+ }
+ })
+ .or_else(|| screen.active_tab_ids.keys().next().copied());
+
+ if let Some(client_id) = client_id_to_switch {
+ // Get the position from the ID
+ if let Some(tab_position) = screen.get_tab_position_by_id(tab_id) {
+ // switch_active_tab expects 0-based position
+ screen.switch_active_tab(tab_position, None, true, client_id)?;
+ screen.render(None)?;
+
+ screen
+ .tab_history
+ .entry(client_id)
+ .or_insert_with(Vec::new)
+ .push(tab_id);
+ } else {
+ log::error!("Tab with ID {} not found", tab_id);
+ }
+ }
+ },
+ ScreenInstruction::RenameTabWithId(tab_id, new_name, _completion_tx) => {
+ // Use get_tab_by_id_mut() helper method
+ if let Some(tab) = screen.get_tab_by_id_mut(tab_id) {
+ tab.name = String::from_utf8_lossy(&new_name).to_string();
+ screen.log_and_report_session_state(false)?;
+ } else {
+ log::error!("Failed to find tab with ID: {}", tab_id);
+ }
+ },
+ ScreenInstruction::CloseTabWithId(tab_id, _completion_tx) => {
+ if screen.get_tab_by_id(tab_id).is_some() {
+ screen.close_tab_by_id(tab_id).non_fatal();
+ } else {
+ log::error!("Failed to find tab with ID: {}", tab_id);
+ }
+ },
+ ScreenInstruction::BreakPanesToTabWithId {
+ pane_ids,
+ tab_id,
+ should_change_focus_to_target_tab,
+ client_id,
+ mut completion_tx,
+ } => {
+ // Verify tab exists
+ if screen.get_tab_by_id(tab_id).is_none() {
+ log::error!("Tab with ID {} not found", tab_id);
+ // Don't set affected_tab_id, it will remain None to signal failure
+ } else {
+ // break_multiple_panes_to_tab_with_index uses tab ID
+ screen.break_multiple_panes_to_tab_with_index(
+ pane_ids,
+ tab_id,
+ should_change_focus_to_target_tab,
+ client_id,
+ )?;
+ // Set affected tab ID (tab_id is the ID here)
+ completion_tx
+ .as_mut()
+ .map(|c| c.set_affected_tab_id(tab_id));
+ let pane_group = screen.get_client_pane_group(&client_id);
+ if !pane_group.is_empty() {
+ let _ = screen.bus.senders.send_to_background_jobs(
+ BackgroundJob::HighlightPanesWithMessage(
+ pane_group.iter().copied().collect(),
+ "BROKEN OUT".to_owned(),
+ ),
+ );
+ }
+ screen.clear_pane_group(&client_id);
+ }
+ },
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
let all_tabs = screen.get_tabs_mut();
let found = all_tabs.values_mut().any(|tab| {
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index 6af94ddd..059235a1 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -4592,3 +4592,259 @@ pub fn send_cli_change_floating_pane_coordinates_action() {
}
assert_snapshot!(format!("{}", snapshot_count));
}
+
+#[test]
+pub fn go_to_tab_by_id_verifies_screen_state() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 1;
+ let mut screen = create_new_screen(size, true, true);
+
+ // Create multiple tabs with known IDs
+ new_tab(&mut screen, 1, 0); // ID 0
+ new_tab(&mut screen, 2, 1); // ID 1
+ new_tab(&mut screen, 3, 2); // ID 2
+
+ // Active tab should be the last one created (ID 2)
+ assert_eq!(screen.get_active_tab(client_id).unwrap().id, 2);
+
+ // Switch to tab with ID 0
+ if let Some(tab_position) = screen.get_tab_position_by_id(0) {
+ screen
+ .switch_active_tab(tab_position, None, true, client_id)
+ .expect("TEST");
+ }
+
+ // Verify active tab is now ID 0
+ assert_eq!(
+ screen.get_active_tab(client_id).unwrap().id,
+ 0,
+ "Active tab should be tab with ID 0"
+ );
+
+ // Switch to tab with ID 1
+ if let Some(tab_position) = screen.get_tab_position_by_id(1) {
+ screen
+ .switch_active_tab(tab_position, None, true, client_id)
+ .expect("TEST");
+ }
+
+ // Verify active tab is now ID 1
+ assert_eq!(
+ screen.get_active_tab(client_id).unwrap().id,
+ 1,
+ "Active tab should be tab with ID 1"
+ );
+}
+
+#[test]
+pub fn send_cli_go_to_tab_by_id_action() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 10;
+ let mut mock_screen = MockScreen::new(size);
+ let session_metadata = mock_screen.clone_session_metadata();
+
+ // Create tabs
+ mock_screen.new_tab(TiledPaneLayout::default());
+ mock_screen.new_tab(TiledPaneLayout::default());
+
+ let screen_thread = mock_screen.run(None, vec![]);
+ let received_server_instructions = Arc::new(Mutex::new(vec![]));
+ let server_receiver = mock_screen.server_receiver.take().unwrap();
+ let server_thread = log_actions_in_thread!(
+ received_server_instructions,
+ ServerInstruction::KillSession,
+ server_receiver
+ );
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ // Send CLI action
+ let cli_action = CliAction::GoToTabById { id: 1 };
+ send_cli_action_to_server(&session_metadata, cli_action, client_id);
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ mock_screen.teardown(vec![server_thread, screen_thread]);
+
+ // Verify that CLI action caused screen updates (Render instructions sent)
+ let render_count = received_server_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .filter(|instr| matches!(instr, ServerInstruction::Render(_)))
+ .count();
+
+ assert!(
+ render_count > 0,
+ "GoToTabById CLI action should trigger screen renders"
+ );
+}
+
+#[test]
+pub fn rename_tab_by_id_verifies_screen_state() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let mut screen = create_new_screen(size, true, true);
+
+ // Create tabs with known IDs
+ new_tab(&mut screen, 1, 0); // ID 0
+ new_tab(&mut screen, 2, 1); // ID 1
+
+ // Verify initial tab names
+ assert_eq!(screen.get_tab_by_id(0).unwrap().name, "Tab #1");
+ assert_eq!(screen.get_tab_by_id(1).unwrap().name, "Tab #2");
+
+ // Rename tab with ID 1
+ if let Some(tab) = screen.get_tab_by_id_mut(1) {
+ tab.name = "CustomTabName".to_string();
+ }
+
+ // Verify the tab name changed
+ assert_eq!(
+ screen.get_tab_by_id(1).unwrap().name,
+ "CustomTabName",
+ "Tab with ID 1 should be renamed"
+ );
+
+ // Verify other tab name unchanged
+ assert_eq!(
+ screen.get_tab_by_id(0).unwrap().name,
+ "Tab #1",
+ "Tab with ID 0 should keep original name"
+ );
+}
+
+#[test]
+pub fn send_cli_rename_tab_by_id_action() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 10;
+ let mut mock_screen = MockScreen::new(size);
+ let session_metadata = mock_screen.clone_session_metadata();
+
+ mock_screen.new_tab(TiledPaneLayout::default());
+
+ let screen_thread = mock_screen.run(None, vec![]);
+ let received_server_instructions = Arc::new(Mutex::new(vec![]));
+ let server_receiver = mock_screen.server_receiver.take().unwrap();
+ let server_thread = log_actions_in_thread!(
+ received_server_instructions,
+ ServerInstruction::KillSession,
+ server_receiver
+ );
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ // Send CLI action
+ let cli_action = CliAction::RenameTabById {
+ id: 1,
+ name: "TestName".to_string(),
+ };
+ send_cli_action_to_server(&session_metadata, cli_action, client_id);
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ mock_screen.teardown(vec![server_thread, screen_thread]);
+
+ // Verify that CLI action was processed (no panics means routing worked)
+ // The action should complete successfully
+ assert!(true, "RenameTabById CLI action completed without errors");
+}
+
+#[test]
+pub fn close_tab_by_id_verifies_screen_state() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let mut screen = create_new_screen(size, true, true);
+
+ // Create multiple tabs with known IDs
+ new_tab(&mut screen, 1, 0); // ID 0
+ new_tab(&mut screen, 2, 1); // ID 1
+ new_tab(&mut screen, 3, 2); // ID 2
+
+ assert_eq!(screen.tabs.len(), 3, "Should have 3 tabs initially");
+
+ // Verify all tabs exist
+ assert!(
+ screen.get_tab_by_id(0).is_some(),
+ "Tab with ID 0 should exist"
+ );
+ assert!(
+ screen.get_tab_by_id(1).is_some(),
+ "Tab with ID 1 should exist"
+ );
+ assert!(
+ screen.get_tab_by_id(2).is_some(),
+ "Tab with ID 2 should exist"
+ );
+
+ // Close tab with ID 1
+ screen.close_tab_by_id(1).expect("TEST");
+
+ assert_eq!(screen.tabs.len(), 2, "Should have 2 tabs after closing one");
+
+ // Verify tab with ID 1 no longer exists
+ assert!(
+ screen.get_tab_by_id(1).is_none(),
+ "Tab with ID 1 should not exist"
+ );
+
+ // Verify other tabs still exist
+ assert!(
+ screen.get_tab_by_id(0).is_some(),
+ "Tab with ID 0 should still exist"
+ );
+ assert!(
+ screen.get_tab_by_id(2).is_some(),
+ "Tab with ID 2 should still exist"
+ );
+}
+
+#[test]
+pub fn send_cli_close_tab_by_id_action() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 10;
+ let mut mock_screen = MockScreen::new(size);
+ let session_metadata = mock_screen.clone_session_metadata();
+
+ mock_screen.new_tab(TiledPaneLayout::default());
+ mock_screen.new_tab(TiledPaneLayout::default());
+
+ let screen_thread = mock_screen.run(None, vec![]);
+ let received_server_instructions = Arc::new(Mutex::new(vec![]));
+ let server_receiver = mock_screen.server_receiver.take().unwrap();
+ let server_thread = log_actions_in_thread!(
+ received_server_instructions,
+ ServerInstruction::KillSession,
+ server_receiver
+ );
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ // Send CLI action
+ let cli_action = CliAction::CloseTabById { id: 1 };
+ send_cli_action_to_server(&session_metadata, cli_action, client_id);
+
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ mock_screen.teardown(vec![server_thread, screen_thread]);
+
+ // Verify that CLI action was processed (no panics means routing worked)
+ // The action should complete successfully
+ assert!(true, "CloseTabById CLI action completed without errors");
+}
diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs
index ee6d434f..7cc9043b 100644
--- a/zellij-utils/assets/prost/api.action.rs
+++ b/zellij-utils/assets/prost/api.action.rs
@@ -290,7 +290,7 @@ pub struct OverrideLayoutPayload {
pub struct Action {
#[prost(enumeration="ActionName", tag="1")]
pub name: i32,
- #[prost(oneof="action::OptionalPayload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54")]
+ #[prost(oneof="action::OptionalPayload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57")]
pub optional_payload: ::core::option::Option<action::OptionalPayload>,
}
/// Nested message and enum types in `Action`.
@@ -398,6 +398,12 @@ pub mod action {
OverrideLayoutPayload(super::OverrideLayoutPayload),
#[prost(message, tag="54")]
SetPaneBorderlessPayload(super::SetPaneBorderlessPayload),
+ #[prost(uint64, tag="55")]
+ GoToTabByIdPayload(u64),
+ #[prost(uint64, tag="56")]
+ CloseTabByIdPayload(u64),
+ #[prost(message, tag="57")]
+ RenameTabByIdPayload(super::TabIdAndName),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -422,6 +428,14 @@ pub struct IdAndName {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct TabIdAndName {
+ #[prost(uint64, tag="1")]
+ pub tab_id: u64,
+ #[prost(string, tag="2")]
+ pub name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PaneIdAndShouldFloat {
#[prost(uint32, tag="1")]
pub pane_id: u32,
@@ -1055,6 +1069,9 @@ pub enum ActionName {
NewInPlacePane = 92,
OverrideLayout = 93,
SetPaneBorderless = 94,
+ GoToTabById = 95,
+ CloseTabById = 96,
+ RenameTabById = 97,
}
impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1155,6 +1172,9 @@ impl ActionName {
ActionName::NewInPlacePane => "NewInPlacePane",
ActionName::OverrideLayout => "OverrideLayout",
ActionName::SetPaneBorderless => "SetPaneBorderless",
+ ActionName::GoToTabById => "GoToTabById",
+ ActionName::CloseTabById => "CloseTabById",
+ ActionName::RenameTabById => "RenameTabById",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -1252,6 +1272,9 @@ impl ActionName {
"NewInPlacePane" => Some(Self::NewInPlacePane),
"OverrideLayout" => Some(Self::OverrideLayout),
"SetPaneBorderless" => Some(Self::SetPaneBorderless),
+ "GoToTabById" => Some(Self::GoToTabById),
+ "CloseTabById" => Some(Self::CloseTabById),
+ "RenameTabById" => Some(Self::RenameTabById),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index 2be64791..90023ef1 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -3,7 +3,7 @@
pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")]
pub name: i32,
- #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142")]
+ #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147")]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
/// Nested message and enum types in `PluginCommand`.
@@ -267,6 +267,16 @@ pub mod plugin_command {
GetPaneRunningCommandPayload(super::GetPaneRunningCommandPayload),
#[prost(message, tag="142")]
GetPaneCwdPayload(super::GetPaneCwdPayload),
+ #[prost(message, tag="143")]
+ SwitchTabToIdPayload(super::SwitchTabToIdPayload),
+ #[prost(message, tag="144")]
+ GoToTabWithIdPayload(super::GoToTabWithIdPayload),
+ #[prost(message, tag="145")]
+ CloseTabWithIdPayload(super::CloseTabWithIdPayload),
+ #[prost(message, tag="146")]
+ RenameTabWithIdPayload(super::RenameTabWithIdPayload),
+ #[prost(message, tag="147")]
+ BreakPanesToTabWithIdPayload(super::BreakPanesToTabWithIdPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -934,6 +944,59 @@ pub struct FixedOrPercentValue {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SwitchTabToIdPayload {
+ #[prost(uint64, tag="1")]
+ pub tab_id: u64,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GoToTabWithIdPayload {
+ #[prost(uint64, tag="1")]
+ pub tab_id: u64,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct CloseTabWithIdPayload {
+ #[prost(uint64, tag="1")]
+ pub tab_id: u64,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct RenameTabWithIdPayload {
+ #[prost(uint64, tag="1")]
+ pub tab_id: u64,
+ #[prost(string, tag="2")]
+ pub new_name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct BreakPanesToTabWithIdPayload {
+ #[prost(message, repeated, tag="1")]
+ pub pane_ids: ::prost::alloc::vec::Vec<PaneId>,
+ #[prost(uint64, tag="2")]
+ pub tab_id: u64,
+ #[prost(bool, tag="3")]
+ pub should_change_focus_to_target_tab: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct BreakPanesToTabWithIdResponse {
+ #[prost(oneof="break_panes_to_tab_with_id_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<break_panes_to_tab_with_id_response::Result>,
+}
+/// Nested message and enum types in `BreakPanesToTabWithIdResponse`.
+pub mod break_panes_to_tab_with_id_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(uint64, tag="1")]
+ TabId(u64),
+ #[prost(bool, tag="2")]
+ None(bool),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CreateTokenResponse {
#[prost(string, optional, tag="1")]
pub token: ::core::option::Option<::prost::alloc::string::String>,
@@ -1735,6 +1798,11 @@ pub enum CommandName {
GetTabInfo = 189,
GetPaneRunningCommand = 190,
GetPaneCwd = 191,
+ SwitchTabToId = 192,
+ GoToTabWithId = 193,
+ CloseTabWithId = 194,
+ RenameTabWithId = 195,
+ BreakPanesToTabWithId = 196,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1914,6 +1982,11 @@ impl CommandName {
CommandName::GetTabInfo => "GetTabInfo",
CommandName::GetPaneRunningCommand => "GetPaneRunningCommand",
CommandName::GetPaneCwd => "GetPaneCwd",
+ CommandName::SwitchTabToId => "SwitchTabToId",
+ CommandName::GoToTabWithId => "GoToTabWithId",
+ CommandName::CloseTabWithId => "CloseTabWithId",
+ CommandName::RenameTabWithId => "RenameTabWithId",
+ CommandName::BreakPanesToTabWithId => "BreakPanesToTabWithId",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -2090,6 +2163,11 @@ impl CommandName {
"GetTabInfo" => Some(Self::GetTabInfo),
"GetPaneRunningCommand" => Some(Self::GetPaneRunningCommand),
"GetPaneCwd" => Some(Self::GetPaneCwd),
+ "SwitchTabToId" => Some(Self::SwitchTabToId),
+ "GoToTabWithId" => Some(Self::GoToTabWithId),
+ "CloseTabWithId" => Some(Self::CloseTabWithId),
+ "RenameTabWithId" => Some(Self::RenameTabWithId),
+ "BreakPanesToTabWithId" => Some(Self::BreakPanesToTabWithId),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index 90754a11..248810ab 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -119,7 +119,7 @@ pub struct RgbColor {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Action {
- #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100")]
+ #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105")]
pub action_type: ::core::option::Option<action::ActionType>,
}
/// Nested message and enum types in `Action`.
@@ -327,6 +327,16 @@ pub mod action {
WriteCharsToPaneId(super::WriteCharsToPaneIdAction),
#[prost(message, tag="100")]
WriteToPaneId(super::WriteToPaneIdAction),
+ #[prost(message, tag="101")]
+ GoToTabById(super::GoToTabByIdAction),
+ #[prost(message, tag="102")]
+ CloseTabById(super::CloseTabByIdAction),
+ #[prost(message, tag="103")]
+ RenameTabById(super::RenameTabByIdAction),
+ #[prost(message, tag="104")]
+ ListTabs(super::ListTabsAction),
+ #[prost(message, tag="105")]
+ CurrentTabInfo(super::CurrentTabInfoAction),
}
}
// Action message definitions (all 92 variants)
@@ -546,6 +556,28 @@ pub struct ListPanesAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct ListTabsAction {
+ #[prost(bool, tag="1")]
+ pub show_state: bool,
+ #[prost(bool, tag="2")]
+ pub show_dimensions: bool,
+ #[prost(bool, tag="3")]
+ pub show_panes: bool,
+ #[prost(bool, tag="4")]
+ pub show_layout: bool,
+ #[prost(bool, tag="5")]
+ pub show_all: bool,
+ #[prost(bool, tag="6")]
+ pub output_json: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct CurrentTabInfoAction {
+ #[prost(bool, tag="1")]
+ pub output_json: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TogglePanePinnedAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -961,6 +993,26 @@ pub struct RenameTabAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GoToTabByIdAction {
+ #[prost(uint64, tag="1")]
+ pub id: u64,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct CloseTabByIdAction {
+ #[prost(uint64, tag="1")]
+ pub id: u64,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct RenameTabByIdAction {
+ #[prost(uint64, tag="1")]
+ pub id: u64,
+ #[prost(string, tag="2")]
+ pub name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RenameSessionAction {
#[prost(string, tag="1")]
pub name: ::prost::alloc::string::String,
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 376eafa1..6e99766c 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -884,6 +884,19 @@ pub enum CliAction {
},
/// Remove a previously set tab name
UndoRenameTab,
+ /// Go to tab with stable ID
+ GoToTabById {
+ id: u64,
+ },
+ /// Close tab with stable ID
+ CloseTabById {
+ id: u64,
+ },
+ /// Rename tab by stable ID
+ RenameTabById {
+ id: u64,
+ name: String,
+ },
/// Create a new tab, optionally with a specified tab layout and name
///
/// Returns: The created tab's ID as a single number on stdout
@@ -1143,6 +1156,42 @@ tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://e
#[clap(short, long, value_parser)]
json: bool,
},
+ /// List all tabs with their information
+ ///
+ /// Returns: Tab information in table or JSON format
+ ListTabs {
+ /// Include state information (active, fullscreen, sync, floating visibility)
+ #[clap(short, long, value_parser)]
+ state: bool,
+
+ /// Include dimension information (viewport, display area)
+ #[clap(short, long, value_parser)]
+ dimensions: bool,
+
+ /// Include pane counts
+ #[clap(short, long, value_parser)]
+ panes: bool,
+
+ /// Include layout information (swap layout name and dirty state)
+ #[clap(short, long, value_parser)]
+ layout: bool,
+
+ /// Include all available fields
+ #[clap(short, long, value_parser)]
+ all: bool,
+
+ /// Output as JSON
+ #[clap(short, long, value_parser)]
+ json: bool,
+ },
+ /// Get information about the currently active tab
+ ///
+ /// Returns: Tab name and ID by default, or full info in JSON
+ CurrentTabInfo {
+ /// Output as JSON with full TabInfo
+ #[clap(short, long, value_parser)]
+ json: bool,
+ },
TogglePanePinned,
/// Stack pane ids
/// Ids are a space separated list of pane ids.
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index 2154dde4..512f52bd 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -236,6 +236,11 @@ message Action {
ListPanesAction list_panes = 98;
WriteCharsToPaneIdAction write_chars_to_pane_id = 99;
WriteToPaneIdAction write_to_pane_id = 100;
+ GoToTabByIdAction go_to_tab_by_id = 101;
+ CloseTabByIdAction close_tab_by_id = 102;
+ RenameTabByIdAction rename_tab_by_id = 103;
+ ListTabsAction list_tabs = 104;
+ CurrentTabInfoAction current_tab_info = 105;
}
}
@@ -307,6 +312,17 @@ message ListPanesAction {
bool show_all = 5;
bool output_json = 6;
}
+message ListTabsAction {
+ bool show_state = 1;
+ bool show_dimensions = 2;
+ bool show_panes = 3;
+ bool show_layout = 4;
+ bool show_all = 5;
+ bool output_json = 6;
+}
+message CurrentTabInfoAction {
+ bool output_json = 1;
+}
message TogglePanePinnedAction {}
message TogglePaneInGroupAction {}
message ToggleGroupMarkingAction {}
@@ -558,6 +574,19 @@ message RenameTabAction {
repeated uint32 name = 2;
}
+message GoToTabByIdAction {
+ uint64 id = 1;
+}
+
+message CloseTabByIdAction {
+ uint64 id = 1;
+}
+
+message RenameTabByIdAction {
+ uint64 id = 1;
+ string name = 2;
+}
+
message RenameSessionAction {
string name = 1;
}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index c6e95f9e..4ec7d654 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -2270,6 +2270,7 @@ pub struct PaneListEntry {
}
pub type ListPanesResponse = Vec<PaneListEntry>;
+pub type ListTabsResponse = Vec<TabInfo>;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct ClientInfo {
@@ -3352,6 +3353,12 @@ pub enum PluginCommand {
// the new tab
BreakPanesToTabWithIndex(Vec<PaneId>, usize, bool), // usize - tab_index, bool -
// should_change_focus_to_new_tab
+ SwitchTabToId(u64), // u64 - tab_id
+ GoToTabWithId(u64), // u64 - tab_id
+ CloseTabWithId(u64), // u64 - tab_id
+ RenameTabWithId(u64, String), // u64 - tab_id, String - new name
+ BreakPanesToTabWithId(Vec<PaneId>, u64, bool), // u64 - tab_id, bool -
+ // should_change_focus_to_target_tab
ReloadPlugin(u32), // u32 - plugin pane id
LoadNewPlugin {
url: String,
@@ -3445,6 +3452,7 @@ pub type NewTabsResponse = Vec<usize>;
pub type FocusOrCreateTabResponse = Option<usize>;
pub type BreakPanesToNewTabResponse = Option<usize>;
pub type BreakPanesToTabWithIndexResponse = Option<usize>;
+pub type BreakPanesToTabWithIdResponse = Option<usize>;
// Response types for plugin API methods that create panes
pub type OpenFileResponse = Option<PaneId>;
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 06bb6801..3b5822a8 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -297,6 +297,10 @@ pub enum ScreenContext {
UndoRenameTab,
MoveTabLeft,
MoveTabRight,
+ GoToTabWithId,
+ CloseTabWithId,
+ RenameTabWithId,
+ BreakPanesToTabWithId,
TerminalResize,
TerminalPixelDimensions,
TerminalBackgroundColor,
@@ -359,6 +363,8 @@ pub enum ScreenContext {
GetTabInfo,
ListClientsMetadata,
ListPanes,
+ ListTabs,
+ GetCurrentTabInfo,
Reconfigure,
RerunCommandPane,
ResizePaneWithId,
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index bf77fe4d..f53a5c69 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -432,6 +432,16 @@ pub enum Action {
tab_index: u32,
name: Vec<u8>,
},
+ GoToTabById {
+ id: u64,
+ },
+ CloseTabById {
+ id: u64,
+ },
+ RenameTabById {
+ id: u64,
+ name: String,
+ },
BreakPane,
BreakPaneRight,
BreakPaneLeft,
@@ -475,6 +485,17 @@ pub enum Action {
show_all: bool,
output_json: bool,
},
+ ListTabs {
+ show_state: bool,
+ show_dimensions: bool,
+ show_panes: bool,
+ show_layout: bool,
+ show_all: bool,
+ output_json: bool,
+ },
+ CurrentTabInfo {
+ output_json: bool,
+ },
TogglePanePinned,
StackPanes {
pane_ids: Vec<PaneId>,
@@ -933,6 +954,9 @@ impl Action {
},
]),
CliAction::UndoRenameTab => Ok(vec![Action::UndoRenameTab]),
+ CliAction::GoToTabById { id } => Ok(vec![Action::GoToTabById { id }]),
+ CliAction::CloseTabById { id } => Ok(vec![Action::CloseTabById { id }]),
+ CliAction::RenameTabById { id, name } => Ok(vec![Action::RenameTabById { id, name }]),
CliAction::NewTab {
name,
layout,
@@ -1331,6 +1355,24 @@ impl Action {
show_all: all,
output_json: json,
}]),
+ CliAction::ListTabs {
+ state,
+ dimensions,
+ panes,
+ layout,
+ all,
+ json,
+ } => Ok(vec![Action::ListTabs {
+ show_state: state,
+ show_dimensions: dimensions,
+ show_panes: panes,
+ show_layout: layout,
+ show_all: all,
+ output_json: json,
+ }]),
+ CliAction::CurrentTabInfo { json } => {
+ Ok(vec![Action::CurrentTabInfo { output_json: json }])
+ },
CliAction::TogglePanePinned => Ok(vec![Action::TogglePanePinned]),
CliAction::StackPanes { pane_ids } => {
let mut malformed_ids = vec![];
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index 3c127e78..656be5db 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -717,32 +717,32 @@ impl From<crate::input::actions::Action>
use crate::client_server_contract::client_server_contract::{
action::ActionType, BreakPaneAction, BreakPaneLeftAction, BreakPaneRightAction,
ChangeFloatingPaneCoordinatesAction, ClearScreenAction, CliPipeAction,
- CloseFocusAction, ClosePluginPaneAction, CloseTabAction, CloseTerminalPaneAction,
- ConfirmAction, CopyAction, DenyAction, DetachAction, DumpLayoutAction,
- DumpScreenAction, EditFileAction, EditScrollbackAction, FocusNextPaneAction,
- FocusPluginPaneWithIdAction, FocusPreviousPaneAction, FocusTerminalPaneWithIdAction,
- GoToNextTabAction, GoToPreviousTabAction, GoToTabAction, GoToTabNameAction,
- HalfPageScrollDownAction, HalfPageScrollUpAction, KeybindPipeAction,
- LaunchOrFocusPluginAction, LaunchPluginAction, ListClientsAction, ListPanesAction,
- MouseEventAction, MoveFocusAction, MoveFocusOrTabAction, MovePaneAction,
- MovePaneBackwardsAction, MoveTabAction, NewBlockingPaneAction, NewFloatingPaneAction,
- NewFloatingPluginPaneAction, NewInPlacePaneAction, NewInPlacePluginPaneAction,
- NewPaneAction, NewStackedPaneAction, NewTabAction, NewTiledPaneAction,
- NewTiledPluginPaneAction, NextSwapLayoutAction, NoOpAction, OverrideLayoutAction,
- PageScrollDownAction, PageScrollUpAction, PaneIdWithPlugin, PaneNameInputAction,
- PreviousSwapLayoutAction, QueryTabNamesAction, QuitAction, RenamePluginPaneAction,
- RenameSessionAction, RenameTabAction, RenameTerminalPaneAction, ResizeAction,
- RunAction, SaveSessionAction, ScrollDownAction, ScrollDownAtAction,
- ScrollToBottomAction, ScrollToTopAction, ScrollUpAction, ScrollUpAtAction,
- SearchAction, SearchInputAction, SearchToggleOptionAction, SetPaneBorderlessAction,
- SkipConfirmAction, StackPanesAction, StartOrReloadPluginAction, SwitchFocusAction,
- SwitchModeForAllClientsAction, SwitchSessionAction, SwitchToModeAction,
- TabNameInputAction, ToggleActiveSyncTabAction, ToggleFloatingPanesAction,
- ToggleFocusFullscreenAction, ToggleGroupMarkingAction, ToggleMouseModeAction,
- TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction, TogglePaneFramesAction,
- TogglePaneInGroupAction, TogglePanePinnedAction, ToggleTabAction, UndoRenamePaneAction,
- UndoRenameTabAction, WriteAction, WriteCharsAction, WriteCharsToPaneIdAction,
- WriteToPaneIdAction,
+ CloseFocusAction, ClosePluginPaneAction, CloseTabAction, CloseTabByIdAction,
+ CloseTerminalPaneAction, ConfirmAction, CopyAction, CurrentTabInfoAction, DenyAction,
+ DetachAction, DumpLayoutAction, DumpScreenAction, EditFileAction, EditScrollbackAction,
+ FocusNextPaneAction, FocusPluginPaneWithIdAction, FocusPreviousPaneAction,
+ FocusTerminalPaneWithIdAction, GoToNextTabAction, GoToPreviousTabAction, GoToTabAction,
+ GoToTabByIdAction, GoToTabNameAction, HalfPageScrollDownAction, HalfPageScrollUpAction,
+ KeybindPipeAction, LaunchOrFocusPluginAction, LaunchPluginAction, ListClientsAction,
+ ListPanesAction, ListTabsAction, MouseEventAction, MoveFocusAction,
+ MoveFocusOrTabAction, MovePaneAction, MovePaneBackwardsAction, MoveTabAction,
+ NewBlockingPaneAction, NewFloatingPaneAction, NewFloatingPluginPaneAction,
+ NewInPlacePaneAction, NewInPlacePluginPaneAction, NewPaneAction, NewStackedPaneAction,
+ NewTabAction, NewTiledPaneAction, NewTiledPluginPaneAction, NextSwapLayoutAction,
+ NoOpAction, OverrideLayoutAction, PageScrollDownAction, PageScrollUpAction,
+ PaneIdWithPlugin, PaneNameInputAction, PreviousSwapLayoutAction, QueryTabNamesAction,
+ QuitAction, RenamePluginPaneAction, RenameSessionAction, RenameTabAction,
+ RenameTabByIdAction, RenameTerminalPaneAction, ResizeAction, RunAction,
+ SaveSessionAction, ScrollDownAction, ScrollDownAtAction, ScrollToBottomAction,
+ ScrollToTopAction, ScrollUpAction, ScrollUpAtAction, SearchAction, SearchInputAction,
+ SearchToggleOptionAction, SetPaneBorderlessAction, SkipConfirmAction, StackPanesAction,
+ StartOrReloadPluginAction, SwitchFocusAction, SwitchModeForAllClientsAction,
+ SwitchSessionAction, SwitchToModeAction, TabNameInputAction, ToggleActiveSyncTabAction,
+ ToggleFloatingPanesAction, ToggleFocusFullscreenAction, ToggleGroupMarkingAction,
+ ToggleMouseModeAction, TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction,
+ TogglePaneFramesAction, TogglePaneInGroupAction, TogglePanePinnedAction,
+ ToggleTabAction, UndoRenamePaneAction, UndoRenameTabAction, WriteAction,
+ WriteCharsAction, WriteCharsToPaneIdAction, WriteToPaneIdAction,
};
use std::collections::HashMap;
@@ -1212,6 +1212,15 @@ impl From<crate::input::actions::Action>
name: name.into_iter().map(|b| b as u32).collect(),
})
},
+ crate::input::actions::Action::GoToTabById { id } => {
+ ActionType::GoToTabById(GoToTabByIdAction { id })
+ },
+ crate::input::actions::Action::CloseTabById { id } => {
+ ActionType::CloseTabById(CloseTabByIdAction { id })
+ },
+ crate::input::actions::Action::RenameTabById { id, name } => {
+ ActionType::RenameTabById(RenameTabByIdAction { id, name })
+ },
crate::input::actions::Action::BreakPane => ActionType::BreakPane(BreakPaneAction {}),
crate::input::actions::Action::BreakPaneRight => {
ActionType::BreakPaneRight(BreakPaneRightAction {})
@@ -1338,6 +1347,24 @@ impl From<crate::input::actions::Action>
crate::input::actions::Action::SaveSession => {
ActionType::SaveSession(SaveSessionAction {})
},
+ crate::input::actions::Action::ListTabs {
+ show_state,
+ show_dimensions,
+ show_panes,
+ show_layout,
+ show_all,
+ output_json,
+ } => ActionType::ListTabs(ListTabsAction {
+ show_state,
+ show_dimensions,
+ show_panes,
+ show_layout,
+ show_all,
+ output_json,
+ }),
+ crate::input::actions::Action::CurrentTabInfo { output_json } => {
+ ActionType::CurrentTabInfo(CurrentTabInfoAction { output_json })
+ },
};
Self {
@@ -1871,6 +1898,22 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
.collect(),
})
},
+ ActionType::GoToTabById(go_to_tab_by_id_action) => {
+ Ok(crate::input::actions::Action::GoToTabById {
+ id: go_to_tab_by_id_action.id,
+ })
+ },
+ ActionType::CloseTabById(close_tab_by_id_action) => {
+ Ok(crate::input::actions::Action::CloseTabById {
+ id: close_tab_by_id_action.id,
+ })
+ },
+ ActionType::RenameTabById(rename_tab_by_id_action) => {
+ Ok(crate::input::actions::Action::RenameTabById {
+ id: rename_tab_by_id_action.id,
+ name: rename_tab_by_id_action.name,
+ })
+ },
ActionType::BreakPane(_) => Ok(crate::input::actions::Action::BreakPane),
ActionType::BreakPaneRight(_) => Ok(crate::input::actions::Action::BreakPaneRight),
ActionType::BreakPaneLeft(_) => Ok(crate::input::actions::Action::BreakPaneLeft),
@@ -1936,6 +1979,19 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
output_json: list_panes_action.output_json,
})
},
+ ActionType::ListTabs(list_tabs_action) => Ok(crate::input::actions::Action::ListTabs {
+ show_state: list_tabs_action.show_state,
+ show_dimensions: list_tabs_action.show_dimensions,
+ show_panes: list_tabs_action.show_panes,
+ show_layout: list_tabs_action.show_layout,
+ show_all: list_tabs_action.show_all,
+ output_json: list_tabs_action.output_json,
+ }),
+ ActionType::CurrentTabInfo(current_tab_info_action) => {
+ Ok(crate::input::actions::Action::CurrentTabInfo {
+ output_json: current_tab_info_action.output_json,
+ })
+ },
ActionType::TogglePanePinned(_) => Ok(crate::input::actions::Action::TogglePanePinned),
ActionType::StackPanes(stack_panes_action) => {
Ok(crate::input::actions::Action::StackPanes {
diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto
index 802d0a0e..83181097 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -232,6 +232,9 @@ message Action {
NewInPlacePanePayload new_in_place_pane_payload = 52;
OverrideLayoutPayload override_layout_payload = 53;
SetPaneBorderlessPayload set_pane_borderless_payload = 54;
+ uint64 go_to_tab_by_id_payload = 55;
+ uint64 close_tab_by_id_payload = 56;
+ TabIdAndName rename_tab_by_id_payload = 57;
}
}
@@ -247,6 +250,11 @@ message IdAndName {
uint32 id = 2;
}
+message TabIdAndName {
+ uint64 tab_id = 1;
+ string name = 2;
+}
+
message PaneIdAndShouldFloat {
uint32 pane_id = 1;
bool should_float = 2;
@@ -451,6 +459,9 @@ enum ActionName {
NewInPlacePane = 92;
OverrideLayout = 93;
SetPaneBorderless = 94;
+ GoToTabById = 95;
+ CloseTabById = 96;
+ RenameTabById = 97;
}
message Position {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 92dc618c..047244d7 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -1030,8 +1030,12 @@ impl TryFrom<Action> for ProtobufAction {
chars: chars_to_write,
})),
}),
- Action::WriteToPaneId { .. } | Action::WriteCharsToPaneId { .. } => {
- Err("WriteToPaneId and WriteCharsToPaneId are CLI-only actions, not available in keybindings")
+ Action::WriteToPaneId { .. }
+ | Action::WriteCharsToPaneId { .. }
+ | Action::GoToTabById { .. }
+ | Action::CloseTabById { .. }
+ | Action::RenameTabById { .. } => {
+ Err("WriteToPaneId, WriteCharsToPaneId, GoToTabById, CloseTabById, and RenameTabById are CLI-only actions, not available in keybindings")
},
Action::SwitchToMode { input_mode } => {
let input_mode: ProtobufInputMode = input_mode.try_into()?;
@@ -1782,7 +1786,9 @@ impl TryFrom<Action> for ProtobufAction {
| Action::SetPaneBorderless { .. }
| Action::SkipConfirm { action: _ }
| Action::SwitchSession { .. }
- | Action::SaveSession => Err("Unsupported action"),
+ | Action::SaveSession
+ | Action::ListTabs { .. }
+ | Action::CurrentTabInfo { .. } => Err("Unsupported action"),
}
}
}
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index b450c13f..a2a25f99 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -184,6 +184,11 @@ enum CommandName {
GetTabInfo = 189;
GetPaneRunningCommand = 190;
GetPaneCwd = 191;
+ SwitchTabToId = 192;
+ GoToTabWithId = 193;
+ CloseTabWithId = 194;
+ RenameTabWithId = 195;
+ BreakPanesToTabWithId = 196;
}
message PluginCommand {
@@ -317,6 +322,11 @@ message PluginCommand {
GetTabInfoPayload get_tab_info_payload = 140;
GetPaneRunningCommandPayload get_pane_running_command_payload = 141;
GetPaneCwdPayload get_pane_cwd_payload = 142;
+ SwitchTabToIdPayload switch_tab_to_id_payload = 143;
+ GoToTabWithIdPayload go_to_tab_with_id_payload = 144;
+ CloseTabWithIdPayload close_tab_with_id_payload = 145;
+ RenameTabWithIdPayload rename_tab_with_id_payload = 146;
+ BreakPanesToTabWithIdPayload break_panes_to_tab_with_id_payload = 147;
}
}
@@ -749,6 +759,36 @@ enum FixedOrPercent {
Percent = 1;
}
+message SwitchTabToIdPayload {
+ uint64 tab_id = 1;
+}
+
+message GoToTabWithIdPayload {
+ uint64 tab_id = 1;
+}
+
+message CloseTabWithIdPayload {
+ uint64 tab_id = 1;
+}
+
+message RenameTabWithIdPayload {
+ uint64 tab_id = 1;
+ string new_name = 2;
+}
+
+message BreakPanesToTabWithIdPayload {
+ repeated PaneId pane_ids = 1;
+ uint64 tab_id = 2;
+ bool should_change_focus_to_target_tab = 3;
+}
+
+message BreakPanesToTabWithIdResponse {
+ oneof result {
+ uint64 tab_id = 1;
+ bool none = 2;
+ }
+}
+
message CreateTokenResponse {
optional string token = 1;
optional string token_label = 2;
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index 7871d15f..6ade3376 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -3,20 +3,23 @@ pub use super::generated_api::api::{
event::{EventNameList as ProtobufEventNameList, Header},
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
- break_panes_to_new_tab_response, break_panes_to_tab_with_index_response,
- delete_layout_response, dump_layout_response, dump_session_layout_response,
- edit_layout_response, focus_or_create_tab_response, get_focused_pane_info_response,
- get_pane_cwd_response, get_pane_pid_response, get_pane_running_command_response,
- new_tab_response, parse_layout_response, plugin_command::Payload, rename_layout_response,
- save_layout_response, save_session_response, BreakPanesToNewTabPayload,
+ break_panes_to_new_tab_response, break_panes_to_tab_with_id_response,
+ break_panes_to_tab_with_index_response, delete_layout_response, dump_layout_response,
+ dump_session_layout_response, edit_layout_response, focus_or_create_tab_response,
+ get_focused_pane_info_response, get_pane_cwd_response, get_pane_pid_response,
+ get_pane_running_command_response, new_tab_response, parse_layout_response,
+ plugin_command::Payload, rename_layout_response, save_layout_response,
+ save_session_response, BreakPanesToNewTabPayload,
BreakPanesToNewTabResponse as ProtobufBreakPanesToNewTabResponse,
+ BreakPanesToTabWithIdPayload,
+ BreakPanesToTabWithIdResponse as ProtobufBreakPanesToTabWithIdResponse,
BreakPanesToTabWithIndexPayload,
BreakPanesToTabWithIndexResponse as ProtobufBreakPanesToTabWithIndexResponse,
ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload,
ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload,
- CloseTabWithIndexPayload, CommandName, ContextItem, CopyToClipboardPayload,
- CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse,
- CurrentSessionLastSavedTimePayload,
+ CloseTabWithIdPayload, CloseTabWithIndexPayload, CommandName, ContextItem,
+ CopyToClipboardPayload, CreateTokenResponse as ProtobufCreateTokenResponse,
+ CreateTokenResponse, CurrentSessionLastSavedTimePayload,
CurrentSessionLastSavedTimeResponse as ProtobufCurrentSessionLastSavedTimeResponse,
CursorPosition, DeleteLayoutPayload, DeleteLayoutResponse as ProtobufDeleteLayoutResponse,
DumpLayoutPayload, DumpLayoutResponse as ProtobufDumpLayoutResponse,
@@ -39,13 +42,14 @@ pub use super::generated_api::api::{
GetPaneRunningCommandPayload as ProtobufGetPaneRunningCommandPayload,
GetPaneRunningCommandResponse as ProtobufGetPaneRunningCommandResponse,
GetPaneScrollbackPayload, GetTabInfoPayload,
- GetTabInfoResponse as ProtobufGetTabInfoResponse, GroupAndUngroupPanesPayload,
- HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb,
- IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse,
- LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload,
- MovePaneWithPaneIdPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
- NewTabPayload, NewTabResponse as ProtobufNewTabResponse,
- NewTabsResponse as ProtobufNewTabsResponse, NewTabsWithLayoutInfoPayload,
+ GetTabInfoResponse as ProtobufGetTabInfoResponse, GoToTabWithIdPayload,
+ GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload,
+ HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload,
+ ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload,
+ MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload,
+ NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload,
+ NewTabResponse as ProtobufNewTabResponse, NewTabsResponse as ProtobufNewTabsResponse,
+ NewTabsWithLayoutInfoPayload,
OpenCommandPaneBackgroundResponse as ProtobufOpenCommandPaneBackgroundResponse,
OpenCommandPaneFloatingNearPluginPayload,
OpenCommandPaneFloatingNearPluginResponse as ProtobufOpenCommandPaneFloatingNearPluginResponse,
@@ -77,9 +81,10 @@ pub use super::generated_api::api::{
ParseLayoutResponse as ProtobufParseLayoutResponse, PluginCommand as ProtobufPluginCommand,
PluginMessagePayload, RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload,
RenameLayoutPayload, RenameLayoutResponse as ProtobufRenameLayoutResponse,
- RenameWebLoginTokenPayload, RenameWebTokenResponse, ReplacePaneWithExistingPanePayload,
- RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload,
- ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
+ RenameTabWithIdPayload, RenameWebLoginTokenPayload, RenameWebTokenResponse,
+ ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload,
+ RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload,
+ RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
RunActionPayload, RunCommandPayload, RunningCommand as ProtobufRunningCommand,
SaveLayoutPayload, SaveLayoutResponse as ProtobufSaveLayoutResponse, SaveSessionPayload,
SaveSessionResponse as ProtobufSaveSessionResponse, ScrollDownInPaneIdPayload,
@@ -87,9 +92,9 @@ pub use super::generated_api::api::{
SetFloatingPanePinnedPayload, SetPaneBorderlessPayload,
SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowCursorPayload,
ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, SwitchSessionPayload,
- SwitchTabToPayload, TogglePaneBorderlessPayload, TogglePaneEmbedOrEjectForPaneIdPayload,
- TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload,
- WriteCharsToPaneIdPayload, WriteToPaneIdPayload,
+ SwitchTabToIdPayload, SwitchTabToPayload, TogglePaneBorderlessPayload,
+ TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload,
+ WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload,
},
plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction,
@@ -1679,6 +1684,44 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
)),
_ => Err("Mismatched payload for BreakPanesToTabWithIndex"),
},
+ Some(CommandName::SwitchTabToId) => match protobuf_plugin_command.payload {
+ Some(Payload::SwitchTabToIdPayload(payload)) => {
+ Ok(PluginCommand::SwitchTabToId(payload.tab_id))
+ },
+ _ => Err("Mismatched payload for SwitchTabToId"),
+ },
+ Some(CommandName::GoToTabWithId) => match protobuf_plugin_command.payload {
+ Some(Payload::GoToTabWithIdPayload(payload)) => {
+ Ok(PluginCommand::GoToTabWithId(payload.tab_id))
+ },
+ _ => Err("Mismatched payload for GoToTabWithId"),
+ },
+ Some(CommandName::CloseTabWithId) => match protobuf_plugin_command.payload {
+ Some(Payload::CloseTabWithIdPayload(payload)) => {
+ Ok(PluginCommand::CloseTabWithId(payload.tab_id))
+ },
+ _ => Err("Mismatched payload for CloseTabWithId"),
+ },
+ Some(CommandName::RenameTabWithId) => match protobuf_plugin_command.payload {
+ Some(Payload::RenameTabWithIdPayload(payload)) => Ok(
+ PluginCommand::RenameTabWithId(payload.tab_id, payload.new_name),
+ ),
+ _ => Err("Mismatched payload for RenameTabWithId"),
+ },
+ Some(CommandName::BreakPanesToTabWithId) => match protobuf_plugin_command.payload {
+ Some(Payload::BreakPanesToTabWithIdPayload(payload)) => {
+ Ok(PluginCommand::BreakPanesToTabWithId(
+ payload
+ .pane_ids
+ .into_iter()
+ .filter_map(|p_id| p_id.try_into().ok())
+ .collect(),
+ payload.tab_id,
+ payload.should_change_focus_to_target_tab,
+ ))
+ },
+ _ => Err("Mismatched payload for BreakPanesToTabWithId"),
+ },
Some(CommandName::ReloadPlugin) => match protobuf_plugin_command.payload {
Some(Payload::ReloadPluginPayload(reload_plugin_payload)) => {
Ok(PluginCommand::ReloadPlugin(reload_plugin_payload.plugin_id))
@@ -3128,6 +3171,48 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
},
)),
}),
+ PluginCommand::SwitchTabToId(tab_id) => Ok(ProtobufPluginCommand {
+ name: CommandName::SwitchTabToId as i32,
+ payload: Some(Payload::SwitchTabToIdPayload(SwitchTabToIdPayload {
+ tab_id,
+ })),
+ }),
+ PluginCommand::GoToTabWithId(tab_id) => Ok(ProtobufPluginCommand {
+ name: CommandName::GoToTabWithId as i32,
+ payload: Some(Payload::GoToTabWithIdPayload(GoToTabWithIdPayload {
+ tab_id,
+ })),
+ }),
+ PluginCommand::CloseTabWithId(tab_id) => Ok(ProtobufPluginCommand {
+ name: CommandName::CloseTabWithId as i32,
+ payload: Some(Payload::CloseTabWithIdPayload(CloseTabWithIdPayload {
+ tab_id,
+ })),
+ }),
+ PluginCommand::RenameTabWithId(tab_id, new_name) => Ok(ProtobufPluginCommand {
+ name: CommandName::RenameTabWithId as i32,
+ payload: Some(Payload::RenameTabWithIdPayload(RenameTabWithIdPayload {
+ tab_id,
+ new_name,
+ })),
+ }),
+ PluginCommand::BreakPanesToTabWithId(
+ pane_ids,
+ tab_id,
+ should_change_focus_to_target_tab,
+ ) => Ok(ProtobufPluginCommand {
+ name: CommandName::BreakPanesToTabWithId as i32,
+ payload: Some(Payload::BreakPanesToTabWithIdPayload(
+ BreakPanesToTabWithIdPayload {
+ pane_ids: pane_ids
+ .into_iter()
+ .filter_map(|p_id| p_id.try_into().ok())
+ .collect(),
+ tab_id,
+ should_change_focus_to_target_tab,
+ },
+ )),
+ }),
PluginCommand::ReloadPlugin(plugin_id) => Ok(ProtobufPluginCommand {
name: CommandName::ReloadPlugin as i32,
payload: Some(Payload::ReloadPluginPayload(ReloadPluginPayload {
@@ -3601,8 +3686,8 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
// Conversion implementations for tab creation response types
use crate::data::{
- BreakPanesToNewTabResponse, BreakPanesToTabWithIndexResponse, FocusOrCreateTabResponse,
- NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse,
+ BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse,
+ FocusOrCreateTabResponse, NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse,
OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse,
OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse,
OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, OpenFileFloatingNearPluginResponse,
@@ -3725,6 +3810,21 @@ impl From<BreakPanesToTabWithIndexResponse> for ProtobufBreakPanesToTabWithIndex
}
}
+impl From<BreakPanesToTabWithIdResponse> for ProtobufBreakPanesToTabWithIdResponse {
+ fn from(response: BreakPanesToTabWithIdResponse) -> Self {
+ match response {
+ Some(tab_id) => ProtobufBreakPanesToTabWithIdResponse {
+ result: Some(break_panes_to_tab_with_id_response::Result::TabId(
+ tab_id as u64,
+ )),
+ },
+ None => ProtobufBreakPanesToTabWithIdResponse {
+ result: Some(break_panes_to_tab_with_id_response::Result::None(true)),
+ },
+ }
+ }
+}
+
// Pane-creating command response conversions
impl TryFrom<ProtobufOpenFileResponse> for OpenFileResponse {