diff options
| author | divens <divens@users.noreply.github.com> | 2026-04-06 10:34:23 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-06 10:34:23 +0200 |
| commit | 2b3ae1e69cbf6a84599415ad80de456970199961 (patch) | |
| tree | 66d13c802e4a1fdbb5af26c1faa7ea15e167d2f2 | |
| parent | docs(changelog): update recent PRs (diff) | |
| download | sl0ppij-2b3ae1e69cbf6a84599415ad80de456970199961.tar.gz sl0ppij-2b3ae1e69cbf6a84599415ad80de456970199961.tar.xz sl0ppij-2b3ae1e69cbf6a84599415ad80de456970199961.zip | |
fix: respect --name flag when creating new panes (#4979)
* fix: respect --name flag when creating and renaming panes
Three issues fixed:
1. Pane creation: --name was silently discarded for default shell panes
because use_terminal_title was true. Now user-provided names take
priority. Also pass initial_pane_title as pane_name to TerminalPane
constructor (was hardcoded to empty string).
2. CLI rename without --pane-id: was routed through PaneNameInput which
appends to existing name. Now uses a dedicated RenameActivePane action
that fully replaces the name, working for any name length including
single characters.
3. Interactive rename: entering rename mode preserves the existing name
for editing (append/backspace). Esc reverts to the name before rename
mode was entered.
Adds 28 tests covering rename semantics, interactive editing, undo,
OSC title interaction, and a wire-level IPC round-trip test.
* refactor: route active-pane rename via Option<PaneId>
Replace the dedicated Action::RenameActivePane variant with an
Option<PaneId> on the existing Action::RenamePaneByPaneId. route.rs
dispatches None to the server-internal ScreenInstruction::RenameActivePane
and Some(id) to the existing RenamePaneWithPaneId path.
Drops all proto/conversion boilerplate for the removed variant: plugin
API additions, IPC tag 137, and both directions of the wire conversion.
---------
Co-authored-by: divens <divens.dev@gmail.com>
| -rw-r--r-- | zellij-server/src/pty.rs | 8 | ||||
| -rw-r--r-- | zellij-server/src/route.rs | 14 | ||||
| -rw-r--r-- | zellij-server/src/screen.rs | 13 | ||||
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 15 | ||||
| -rw-r--r-- | zellij-server/src/tab/unit/tab_tests.rs | 392 | ||||
| -rw-r--r-- | zellij-server/src/unit/screen_tests.rs | 91 | ||||
| -rw-r--r-- | zellij-utils/src/errors.rs | 1 | ||||
| -rw-r--r-- | zellij-utils/src/input/actions.rs | 43 | ||||
| -rw-r--r-- | zellij-utils/src/ipc/protobuf_conversion.rs | 7 | ||||
| -rw-r--r-- | zellij-utils/src/ipc/tests/roundtrip_tests.rs | 61 |
10 files changed, 609 insertions, 36 deletions
diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index bb9c800c..0fdcddc4 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -226,10 +226,14 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> { Some(TerminalAction::RunCommand(run_command)) => ( run_command.hold_on_close, Some(run_command.clone()), - if run_command.use_terminal_title { + if name.is_some() { + // User explicitly provided a name — use it regardless + // of use_terminal_title + name + } else if run_command.use_terminal_title { None } else { - Some(name.unwrap_or_else(|| run_command.to_string())) + Some(run_command.to_string()) }, None, ), diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 2ffbd5cb..b060511a 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -1981,12 +1981,20 @@ pub(crate) fn route_action( .with_context(err_context)?; }, Action::RenamePaneByPaneId { pane_id, name } => { - senders - .send_to_screen(ScreenInstruction::RenamePaneWithPaneId( + let instruction = match pane_id { + Some(pane_id) => ScreenInstruction::RenamePaneWithPaneId( pane_id.into(), name, Some(NotificationEnd::new(completion_tx)), - )) + ), + None => ScreenInstruction::RenameActivePane( + name, + client_id, + Some(NotificationEnd::new(completion_tx)), + ), + }; + senders + .send_to_screen(instruction) .with_context(err_context)?; }, Action::UndoRenamePaneByPaneId { pane_id } => { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 6f1bf70b..f1be251c 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -625,6 +625,7 @@ pub enum ScreenInstruction { // should_float_if_hidden, // should_be_in_place_if_hidden RenamePane(PaneId, Vec<u8>, Option<NotificationEnd>), + RenameActivePane(Vec<u8>, ClientId, Option<NotificationEnd>), RenameTab(usize, Vec<u8>, Option<NotificationEnd>), RequestPluginPermissions( u32, // u32 - plugin_id @@ -991,6 +992,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::UnsuppressOrExpandPane(..) => ScreenContext::UnsuppressOrExpandPane, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, + ScreenInstruction::RenameActivePane(..) => ScreenContext::RenameActivePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, ScreenInstruction::RequestPluginPermissions(..) => { ScreenContext::RequestPluginPermissions @@ -7633,6 +7635,17 @@ pub(crate) fn screen_thread_main( } screen.log_and_report_session_state()?; }, + ScreenInstruction::RenameActivePane(new_name, client_id, _completion_tx) => { + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, client_id: ClientId| tab + .rename_active_pane(new_name, client_id), + ? + ); + screen.render(None)?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::RenameTab( tab_index, new_name, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index c78395bf..0eb953c7 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1556,7 +1556,7 @@ impl Tab { PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, - String::new(), + initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), @@ -1669,7 +1669,7 @@ impl Tab { PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, - String::new(), + initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), @@ -1771,7 +1771,7 @@ impl Tab { PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, - String::new(), + initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), @@ -1917,7 +1917,7 @@ impl Tab { PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, - String::new(), + initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), @@ -4766,6 +4766,13 @@ impl Tab { Ok(()) } + pub fn rename_active_pane(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> { + if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { + active_pane.rename(buf); + } + Ok(()) + } + pub fn undo_active_rename_pane(&mut self, client_id: ClientId) -> Result<()> { if let Some(active_terminal_id) = self.get_active_terminal_id(client_id) { let active_terminal = if self.are_floating_panes_visible() { diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 6a2b82e2..512b4a66 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -15587,3 +15587,395 @@ pub fn scroll_up_nonexistent_pane_id_does_not_panic() { assert!(!tab.has_pane_with_pid(&pane_id)); tab.scroll_up_by_pane_id(pane_id); } + +#[test] +pub fn rename_pane_sets_current_title() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "flame"); +} + +#[test] +pub fn rename_pane_replaces_existing_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + let _ = tab.rename_pane_by_pane_id(pane_id, "spark".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} + +#[test] +pub fn rename_pane_to_empty_clears_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + let _ = tab.rename_pane_by_pane_id(pane_id, "".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + // Empty name should fall through to the fallback title + assert_ne!(pane.current_title(), "flame"); +} + +#[test] +pub fn rename_pane_single_char() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "x".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "x"); +} + +#[test] +pub fn rename_pane_with_spaces() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "my pane".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "my pane"); +} + +#[test] +pub fn rename_pane_with_special_chars() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "pane#1 (dev)".as_bytes().to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "pane#1 (dev)"); +} + +#[test] +pub fn named_pane_not_overridden_by_osc_title() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // Simulate shell sending OSC 0 title + let osc_title = b"\x1b]0;user@host: ~/code\x07"; + let _ = tab.handle_pty_bytes(1, osc_title.to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "flame"); +} + +#[test] +pub fn unnamed_pane_shows_osc_title() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + // Send OSC 0 title without renaming the pane + let osc_title = b"\x1b]0;user@host: ~/code\x07"; + let _ = tab.handle_pty_bytes(1, osc_title.to_vec()); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "user@host: ~/code"); +} + +#[test] +pub fn undo_rename_clears_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let title_before = tab.get_pane_with_id(pane_id).unwrap().current_title(); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + tab.undo_rename_pane_by_pane_id(pane_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), title_before); +} + +#[test] +pub fn undo_rename_on_unnamed_pane_is_noop() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let title_before = tab.get_pane_with_id(pane_id).unwrap().current_title(); + tab.undo_rename_pane_by_pane_id(pane_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), title_before); +} + +#[test] +pub fn interactive_rename_appends_to_empty_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let _ = tab.update_active_pane_name(vec![b's'], client_id); + let _ = tab.update_active_pane_name(vec![b'p'], client_id); + let _ = tab.update_active_pane_name(vec![b'a'], client_id); + let _ = tab.update_active_pane_name(vec![b'r'], client_id); + let _ = tab.update_active_pane_name(vec![b'k'], client_id); + let pane = tab.get_pane_with_id(PaneId::Terminal(1)).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} + +#[test] +pub fn interactive_rename_appends_to_existing_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // Simulate entering rename mode + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + let _ = tab.update_active_pane_name(vec![b's'], client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "flames"); +} + +#[test] +pub fn interactive_rename_backspace_removes_chars() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + // Backspace 3 times (DEL = 0x7F) + let _ = tab.update_active_pane_name(vec![0x7f], client_id); + let _ = tab.update_active_pane_name(vec![0x7f], client_id); + let _ = tab.update_active_pane_name(vec![0x7f], client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "fl"); +} + +#[test] +pub fn interactive_rename_backspace_all_then_retype() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + // Backspace 5 times to clear "flame" + for _ in 0..5 { + let _ = tab.update_active_pane_name(vec![0x7f], client_id); + } + // Type "new" + let _ = tab.update_active_pane_name(vec![b'n'], client_id); + let _ = tab.update_active_pane_name(vec![b'e'], client_id); + let _ = tab.update_active_pane_name(vec![b'w'], client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "new"); +} + +#[test] +pub fn interactive_rename_esc_reverts() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // Enter rename mode + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + // Type some chars + let _ = tab.update_active_pane_name(vec![b'x'], client_id); + let _ = tab.update_active_pane_name(vec![b'y'], client_id); + // Esc — undo + let _ = tab.undo_active_rename_pane(client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "flame"); +} + +#[test] +pub fn interactive_rename_esc_on_unnamed_stays_unnamed() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let title_before = tab.get_pane_with_id(pane_id).unwrap().current_title(); + // Enter rename mode on unnamed pane + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + let _ = tab.update_active_pane_name(vec![b'a'], client_id); + let _ = tab.update_active_pane_name(vec![b'b'], client_id); + // Esc + let _ = tab.undo_active_rename_pane(client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), title_before); +} + +#[test] +pub fn cli_rename_then_interactive_esc_restores_cli_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + // CLI rename + let _ = tab.rename_pane_by_pane_id(pane_id, "spark".as_bytes().to_vec()); + // Enter interactive rename mode + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + let _ = tab.update_active_pane_name(vec![b'!'], client_id); + // Esc — should restore "spark" + let _ = tab.undo_active_rename_pane(client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} + +#[test] +pub fn cli_rename_then_undo_clears_to_fallback() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let pane_id = PaneId::Terminal(1); + let fallback_title = tab.get_pane_with_id(pane_id).unwrap().current_title(); + let _ = tab.rename_pane_by_pane_id(pane_id, "spark".as_bytes().to_vec()); + tab.undo_rename_pane_by_pane_id(pane_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), fallback_title); +} + +#[test] +pub fn cli_rename_active_pane_replaces_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + // Set initial name + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // CLI rename (focused pane) — full replacement + let _ = tab.rename_active_pane("spark".as_bytes().to_vec(), client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} + +#[test] +pub fn cli_rename_active_pane_single_char() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // CLI rename with single character — should replace, not append + let _ = tab.rename_active_pane("x".as_bytes().to_vec(), client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "x"); +} + +#[test] +pub fn cli_rename_active_pane_on_unnamed_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + // Pane has no name — CLI rename should set it + let _ = tab.rename_active_pane("spark".as_bytes().to_vec(), client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} + +#[test] +pub fn cli_rename_active_pane_to_empty_clears_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + let fallback = tab.get_pane_with_id(pane_id).unwrap().current_title(); + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + // CLI rename to empty — should clear name + let _ = tab.rename_active_pane("".as_bytes().to_vec(), client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), fallback); +} + +#[test] +pub fn cli_rename_active_pane_then_interactive_esc_restores() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut tab = create_new_tab(size, true); + let client_id = 1; + let pane_id = PaneId::Terminal(1); + // CLI rename + let _ = tab.rename_active_pane("spark".as_bytes().to_vec(), client_id); + // Enter interactive rename, type something + if let Some(pane) = tab.get_active_pane_or_floating_pane_mut(client_id) { + pane.store_pane_name(); + } + let _ = tab.update_active_pane_name(vec![b'!'], client_id); + // Esc — should restore "spark" + let _ = tab.undo_active_rename_pane(client_id); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!(pane.current_title(), "spark"); +} diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 07ec8623..cf6c7e3a 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -8080,3 +8080,94 @@ pub fn send_cli_new_pane_action_with_tab_id_and_stacked() { pty_debug ); } + +#[test] +fn cli_rename_active_pane_via_screen_replaces_name() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut screen = create_new_screen(size, true, true); + let client_id = 1; + new_tab(&mut screen, 1, 0); + + // First give the pane a name + let pane_id = PaneId::Terminal(1); + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + } + + // Now rename via the active pane path (what CLI rename without --pane-id does) + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_active_pane("spark".as_bytes().to_vec(), client_id); + } + + let tab = screen.get_active_tab(client_id).unwrap(); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!( + pane.current_title(), + "spark", + "CLI rename should fully replace the name" + ); +} + +#[test] +fn cli_rename_active_pane_single_char_via_screen() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut screen = create_new_screen(size, true, true); + let client_id = 1; + new_tab(&mut screen, 1, 0); + + let pane_id = PaneId::Terminal(1); + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + } + + // Single char rename via active pane path + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_active_pane("x".as_bytes().to_vec(), client_id); + } + + let tab = screen.get_active_tab(client_id).unwrap(); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!( + pane.current_title(), + "x", + "Single-char CLI rename should replace, not append" + ); +} + +#[test] +fn cli_rename_focused_pane_single_char_via_rename_active_pane() { + // Tests that single-char CLI rename via RenameActivePane (the path used + // by `zellij action rename-pane "x"` without --pane-id) correctly + // replaces the existing name. + let size = Size { + cols: 121, + rows: 20, + }; + let mut screen = create_new_screen(size, true, true); + let client_id = 1; + new_tab(&mut screen, 1, 0); + + let pane_id = PaneId::Terminal(1); + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_pane_by_pane_id(pane_id, "flame".as_bytes().to_vec()); + } + + // CLI rename single char via RenameActivePane (full replacement) + if let Ok(tab) = screen.get_active_tab_mut(client_id) { + let _ = tab.rename_active_pane("x".as_bytes().to_vec(), client_id); + } + + let tab = screen.get_active_tab(client_id).unwrap(); + let pane = tab.get_pane_with_id(pane_id).unwrap(); + assert_eq!( + pane.current_title(), + "x", + "Single-char CLI rename should replace the name, not append" + ); +} diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 0df851c4..d44a348a 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -352,6 +352,7 @@ pub enum ScreenContext { UnsuppressOrExpandPane, FocusPaneWithId, RenamePane, + RenameActivePane, RenameTab, RequestPluginPermissions, BreakPane, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 0ce1c507..ab4c9ae3 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -609,7 +609,7 @@ pub enum Action { pane_id: PaneId, }, RenamePaneByPaneId { - pane_id: PaneId, + pane_id: Option<PaneId>, name: Vec<u8>, }, UndoRenamePaneByPaneId { @@ -1323,23 +1323,19 @@ impl Action { }, None => Ok(vec![Action::CloseFocus]), }, - CliAction::RenamePane { name, pane_id } => match pane_id { - Some(pane_id_str) => { - let pane_id = PaneId::from_str(&pane_id_str) - .map_err(|_| format!( + CliAction::RenamePane { name, pane_id } => { + let pane_id = match pane_id { + Some(pane_id_str) => Some( + PaneId::from_str(&pane_id_str).map_err(|_| format!( "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)" - ))?; - Ok(vec![Action::RenamePaneByPaneId { - pane_id, - name: name.as_bytes().to_vec(), - }]) - }, - None => Ok(vec![ - Action::UndoRenamePane, - Action::PaneNameInput { - input: name.as_bytes().to_vec(), - }, - ]), + ))?, + ), + None => None, + }; + Ok(vec![Action::RenamePaneByPaneId { + pane_id, + name: name.as_bytes().to_vec(), + }]) }, CliAction::UndoRenamePane { pane_id } => match pane_id { Some(pane_id_str) => { @@ -2899,7 +2895,7 @@ mod tests { assert_eq!(actions.len(), 1); match &actions[0] { Action::RenamePaneByPaneId { pane_id, name } => { - assert!(matches!(pane_id, PaneId::Terminal(19))); + assert!(matches!(pane_id, Some(PaneId::Terminal(19)))); assert_eq!(name, &"my-pane".as_bytes().to_vec()); }, _ => panic!("Expected RenamePaneByPaneId action"), @@ -2915,9 +2911,14 @@ mod tests { let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); assert!(result.is_ok()); let actions = result.unwrap(); - assert_eq!(actions.len(), 2); - assert!(matches!(actions[0], Action::UndoRenamePane)); - assert!(matches!(actions[1], Action::PaneNameInput { .. })); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::RenamePaneByPaneId { pane_id, name } => { + assert!(pane_id.is_none()); + assert_eq!(name, &"my-pane".as_bytes().to_vec()); + }, + _ => panic!("Expected RenamePaneByPaneId action"), + } } // 18. UndoRenamePane diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index 189c1370..656ef54c 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -1725,7 +1725,7 @@ impl From<crate::input::actions::Action> }, crate::input::actions::Action::RenamePaneByPaneId { pane_id, name } => { ActionType::RenamePaneByPaneId(RenamePaneByPaneIdAction { - pane_id: Some(pane_id.into()), + pane_id: pane_id.map(|id| id.into()), name, }) }, @@ -2641,10 +2641,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> }, ActionType::RenamePaneByPaneId(a) => { Ok(crate::input::actions::Action::RenamePaneByPaneId { - pane_id: a - .pane_id - .ok_or_else(|| anyhow!("RenamePaneByPaneId missing pane_id"))? - .try_into()?, + pane_id: a.pane_id.map(|p| p.try_into()).transpose()?, name: a.name, }) }, diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 7ef7ac89..23b8c4ac 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -1481,6 +1481,24 @@ fn test_client_messages() { is_cli_client: true, }); test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::RenamePaneByPaneId { + pane_id: None, + name: "spark".as_bytes().to_vec(), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::RenamePaneByPaneId { + pane_id: None, + name: "x".as_bytes().to_vec(), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { action: Action::NewTab { tiled_layout: None, floating_layouts: vec![], @@ -3449,7 +3467,7 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::RenamePaneByPaneId { - pane_id: PaneId::Terminal(1), + pane_id: Some(PaneId::Terminal(1)), name: "test-name".as_bytes().to_vec(), }, terminal_id: Some(1), @@ -3694,3 +3712,44 @@ fn set_pane_color_wire_roundtrip() { assert_eq!(original, roundtrip); } + +/// Tests that RenamePaneByPaneId with `pane_id: None` (the active-pane CLI +/// rename case) survives a full wire-level round-trip +/// (Rust → proto struct → bytes → proto struct → Rust). +/// +/// The struct-level round-trip in `test_client_roundtrip!` doesn't catch +/// issues with prost's `tags` attribute on oneof fields, because prost +/// only uses the tags list during byte decoding, not struct-to-struct +/// conversion. +#[test] +fn rename_active_pane_wire_roundtrip() { + use prost::Message; + + let original = ClientToServerMsg::Action { + action: Action::RenamePaneByPaneId { + pane_id: None, + name: "x".as_bytes().to_vec(), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }; + + // Rust → proto struct + let proto: crate::client_server_contract::client_server_contract::ClientToServerMsg = + original.clone().into(); + + // proto struct → bytes → proto struct (the actual wire path) + let mut buf = Vec::new(); + proto.encode(&mut buf).expect("Failed to encode protobuf"); + let decoded = + crate::client_server_contract::client_server_contract::ClientToServerMsg::decode(&buf[..]) + .expect("Failed to decode protobuf bytes"); + + // proto struct → Rust + let roundtrip: ClientToServerMsg = decoded + .try_into() + .expect("Failed to convert decoded protobuf back to Rust"); + + assert_eq!(original, roundtrip); +} |
