aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordivens <divens@users.noreply.github.com>2026-04-06 10:34:23 +0200
committerGitHub <noreply@github.com>2026-04-06 10:34:23 +0200
commit2b3ae1e69cbf6a84599415ad80de456970199961 (patch)
tree66d13c802e4a1fdbb5af26c1faa7ea15e167d2f2
parentdocs(changelog): update recent PRs (diff)
downloadsl0ppij-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.rs8
-rw-r--r--zellij-server/src/route.rs14
-rw-r--r--zellij-server/src/screen.rs13
-rw-r--r--zellij-server/src/tab/mod.rs15
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs392
-rw-r--r--zellij-server/src/unit/screen_tests.rs91
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs43
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs7
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs61
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);
+}