fix(ux): allow pasting and inputing wide-chars in search + tab rename + pane rename (#4303)

* fix(ux): allow pasting to search/tab-rename/pane-rename

* fix(ux): allow setting wide chars (eg. emoji) in tab/pane names

* style(fmt): rustfmt

* docs(changelog): add PR
This commit is contained in:
Aram Drevekenin 2025-07-18 17:35:47 +02:00 committed by GitHub
parent 44a3c1dae9
commit 48ecb0e34f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 33 deletions

View file

@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: reap processes when using an external clipboard tool (https://github.com/zellij-org/zellij/pull/4298) * fix: reap processes when using an external clipboard tool (https://github.com/zellij-org/zellij/pull/4298)
* fix: out of bounds mouse release events (https://github.com/zellij-org/zellij/pull/4300) * fix: out of bounds mouse release events (https://github.com/zellij-org/zellij/pull/4300)
* fix: account for emoji/widechars when double/triple-clicking to mark words (https://github.com/zellij-org/zellij/pull/4302) * fix: account for emoji/widechars when double/triple-clicking to mark words (https://github.com/zellij-org/zellij/pull/4302)
* fix: allow pasting and emojis in tab/pane names and pasting in search (https://github.com/zellij-org/zellij/pull/4303)
## [0.42.2] - 2025-04-15 ## [0.42.2] - 2025-04-15
* refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082) * refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082)

View file

@ -125,7 +125,8 @@ impl InputHandler {
config: Config, config: Config,
options: Options, options: Options,
send_client_instructions: SenderWithContext<ClientInstruction>, send_client_instructions: SenderWithContext<ClientInstruction>,
mode: InputMode, mode: InputMode, // TODO: we can probably get rid of this now that we're tracking it on the
// server instead
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
) -> Self { ) -> Self {
InputHandler { InputHandler {

View file

@ -10,7 +10,7 @@ mod active_panes;
pub mod floating_panes; pub mod floating_panes;
mod plugin_pane; mod plugin_pane;
mod search; mod search;
mod terminal_pane; pub mod terminal_pane;
mod tiled_panes; mod tiled_panes;
pub use active_panes::*; pub use active_panes::*;

View file

@ -20,6 +20,7 @@ use zellij_utils::input::keybinds::Keybinds;
use zellij_utils::input::mouse::MouseEvent; use zellij_utils::input::mouse::MouseEvent;
use zellij_utils::input::options::Clipboard; use zellij_utils::input::options::Clipboard;
use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::shared::clean_string_from_control_and_linebreak;
use zellij_utils::{ use zellij_utils::{
consts::{session_info_folder_for_session, ZELLIJ_SOCK_DIR}, consts::{session_info_folder_for_session, ZELLIJ_SOCK_DIR},
envs::set_session_name, envs::set_session_name,
@ -36,6 +37,7 @@ use crate::os_input_output::ResizeCache;
use crate::pane_groups::PaneGroups; use crate::pane_groups::PaneGroups;
use crate::panes::alacritty_functions::xparse_color; use crate::panes::alacritty_functions::xparse_color;
use crate::panes::terminal_character::AnsiCode; use crate::panes::terminal_character::AnsiCode;
use crate::panes::terminal_pane::{BRACKETED_PASTE_BEGIN, BRACKETED_PASTE_END};
use crate::session_layout_metadata::{PaneLayoutMetadata, SessionLayoutMetadata}; use crate::session_layout_metadata::{PaneLayoutMetadata, SessionLayoutMetadata};
use crate::{ use crate::{
@ -1355,6 +1357,12 @@ impl Screen {
} }
} }
pub fn get_client_input_mode(&self, client_id: ClientId) -> Option<InputMode> {
self.get_active_tab(client_id)
.ok()
.and_then(|tab| tab.get_client_input_mode(client_id))
}
pub fn get_first_client_id(&self) -> Option<ClientId> { pub fn get_first_client_id(&self) -> Option<ClientId> {
self.active_tab_indices.keys().next().copied() self.active_tab_indices.keys().next().copied()
} }
@ -1849,10 +1857,9 @@ impl Screen {
active_tab.name.pop(); active_tab.name.pop();
}, },
c => { c => {
// It only allows printable unicode active_tab
if buf.iter().all(|u| matches!(u, 0x20..=0x7E | 0xA0..=0xFF)) { .name
active_tab.name.push_str(c); .push_str(&clean_string_from_control_and_linebreak(c));
}
}, },
} }
self.log_and_report_session_state() self.log_and_report_session_state()
@ -3547,24 +3554,73 @@ pub(crate) fn screen_thread_main(
} }
} }
let mut state_changed = false; let mut state_changed = false;
let client_input_mode = screen.get_client_input_mode(client_id);
match client_input_mode {
Some(InputMode::RenameTab) => {
if !(raw_bytes == BRACKETED_PASTE_BEGIN || raw_bytes == BRACKETED_PASTE_END)
{
screen.update_active_tab_name(raw_bytes, client_id)?;
state_changed = true;
}
},
_ => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
screen, screen,
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| { |tab: &mut Tab, client_id: ClientId| {
match client_input_mode {
Some(InputMode::EnterSearch) => {
if !(raw_bytes == BRACKETED_PASTE_BEGIN
|| raw_bytes == BRACKETED_PASTE_END)
{
if let Err(e) =
tab.update_search_term(raw_bytes, client_id)
{
log::error!("{}", e);
}
}
state_changed = true;
},
Some(InputMode::RenamePane) => {
if !(raw_bytes == BRACKETED_PASTE_BEGIN
|| raw_bytes == BRACKETED_PASTE_END)
{
if let Err(e) =
tab.update_active_pane_name(raw_bytes, client_id)
{
log::error!("{}", e);
}
state_changed = true;
}
},
_ => {
let write_result = match tab.is_sync_panes_active() { let write_result = match tab.is_sync_panes_active() {
true => tab.write_to_terminals_on_current_tab(&key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id), true => tab.write_to_terminals_on_current_tab(
false => tab.write_to_active_terminal(&key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id), &key_with_modifier,
raw_bytes,
is_kitty_keyboard_protocol,
client_id,
),
false => tab.write_to_active_terminal(
&key_with_modifier,
raw_bytes,
is_kitty_keyboard_protocol,
client_id,
),
}; };
if let Ok(true) = write_result { if let Ok(true) = write_result {
state_changed = true; state_changed = true;
} }
write_result
}, },
? }
}
); );
},
};
if state_changed { if state_changed {
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
} }
screen.unblock_input()?;
}, },
ScreenInstruction::Resize(client_id, strategy) => { ScreenInstruction::Resize(client_id, strategy) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(

View file

@ -21,6 +21,7 @@ use zellij_utils::input::command::RunCommand;
use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
use zellij_utils::position::Position; use zellij_utils::position::Position;
use zellij_utils::position::{Column, Line}; use zellij_utils::position::{Column, Line};
use zellij_utils::shared::clean_string_from_control_and_linebreak;
use crate::background_jobs::BackgroundJob; use crate::background_jobs::BackgroundJob;
use crate::pane_groups::PaneGroups; use crate::pane_groups::PaneGroups;
@ -4618,21 +4619,12 @@ impl Tab {
pub fn update_active_pane_name(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> { pub fn update_active_pane_name(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> {
let err_context = let err_context =
|| format!("failed to update name of active pane to '{buf:?}' for client {client_id}"); || format!("failed to update name of active pane to '{buf:?}' for client {client_id}");
// Only allow printable unicode, delete and backspace keys.
let is_updatable = buf
.iter()
.all(|u| matches!(u, 0x20..=0x7E | 0xA0..=0xFF | 0x08 | 0x7F));
if is_updatable {
let s = str::from_utf8(&buf).with_context(err_context)?; let s = str::from_utf8(&buf).with_context(err_context)?;
self.get_active_pane_mut(client_id) self.get_active_pane_mut(client_id)
.with_context(|| format!("no active pane found for client {client_id}")) .with_context(|| format!("no active pane found for client {client_id}"))
.map(|active_pane| { .map(|active_pane| {
active_pane.update_name(s); active_pane.update_name(&clean_string_from_control_and_linebreak(s));
})?; })?;
} else {
log::error!("Failed to update pane name due to unprintable characters");
}
Ok(()) Ok(())
} }
@ -4704,6 +4696,9 @@ impl Tab {
pub fn update_search_term(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> { pub fn update_search_term(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> {
if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) {
// It only allows terminating char(\0), printable unicode, delete and backspace keys. // It only allows terminating char(\0), printable unicode, delete and backspace keys.
// TODO: we should really remove this limitation to allow searching for emojis and
// other wide chars - currently the search mechanism itself ignores wide chars, so we
// should first fix that before removing this condition
let is_updatable = buf let is_updatable = buf
.iter() .iter()
.all(|u| matches!(u, 0x00 | 0x20..=0x7E | 0x08 | 0x7F)); .all(|u| matches!(u, 0x00 | 0x20..=0x7E | 0x08 | 0x7F));
@ -5360,6 +5355,9 @@ impl Tab {
pub fn get_display_area(&self) -> Size { pub fn get_display_area(&self) -> Size {
self.display_area.borrow().clone() self.display_area.borrow().clone()
} }
pub fn get_client_input_mode(&self, client_id: ClientId) -> Option<InputMode> {
self.mode_info.borrow().get(&client_id).map(|m| m.mode)
}
fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane { fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane {
let next_terminal_position = self.get_next_terminal_position(); let next_terminal_position = self.get_next_terminal_position();
let mut new_pane = TerminalPane::new( let mut new_pane = TerminalPane::new(

View file

@ -35,6 +35,19 @@ pub fn ansi_len(s: &str) -> usize {
from_utf8(&strip(s).unwrap()).unwrap().width() from_utf8(&strip(s).unwrap()).unwrap().width()
} }
pub fn clean_string_from_control_and_linebreak(input: &str) -> String {
input
.chars()
.filter(|c| {
!c.is_control() &&
*c != '\n' && // line feed
*c != '\r' && // carriage return
*c != '\u{2028}' && // line separator
*c != '\u{2029}' // paragraph separator
})
.collect()
}
pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String { pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String {
s.lines() s.lines()
.map(|l| { .map(|l| {