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:
parent
44a3c1dae9
commit
48ecb0e34f
6 changed files with 102 additions and 33 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue