diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-02-11 13:26:33 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-11 13:26:33 +0100 |
| commit | 13a82eea6d74d8168e538b8755131ed793a7f054 (patch) | |
| tree | bb8d2c1b6cd24bf46b84860360c48e6249e21ba1 | |
| parent | Allow writing characters, bytes and keys to specific panes from the CLI (#4691) (diff) | |
| download | sl0ppij-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.md | 1 | ||||
| -rw-r--r-- | zellij-server/src/plugins/zellij_exports.rs | 137 | ||||
| -rw-r--r-- | zellij-server/src/route.rs | 267 | ||||
| -rw-r--r-- | zellij-server/src/screen.rs | 153 | ||||
| -rw-r--r-- | zellij-server/src/unit/screen_tests.rs | 256 | ||||
| -rw-r--r-- | zellij-utils/assets/prost/api.action.rs | 25 | ||||
| -rw-r--r-- | zellij-utils/assets/prost/api.plugin_command.rs | 80 | ||||
| -rw-r--r-- | zellij-utils/assets/prost_ipc/client_server_contract.rs | 54 | ||||
| -rw-r--r-- | zellij-utils/src/cli.rs | 49 | ||||
| -rw-r--r-- | zellij-utils/src/client_server_contract/common_types.proto | 29 | ||||
| -rw-r--r-- | zellij-utils/src/data.rs | 8 | ||||
| -rw-r--r-- | zellij-utils/src/errors.rs | 6 | ||||
| -rw-r--r-- | zellij-utils/src/input/actions.rs | 42 | ||||
| -rw-r--r-- | zellij-utils/src/ipc/protobuf_conversion.rs | 108 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/action.proto | 11 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/action.rs | 12 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.proto | 40 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.rs | 148 |
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 { |
