diff --git a/CHANGELOG.md b/CHANGELOG.md index 8290e4be1..6c364bcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] * fix: idle CPU + disk i/o, CommandChanged plugin Event, GetSessionList plugin command (https://github.com/zellij-org/zellij/pull/5063) +* fix: restore kitty keyboard mode on error, handle DECRPM 2026 state 3, clean up client terminal teardown sequences (https://github.com/zellij-org/zellij/pull/5058) ## [0.44.1] - 2026-04-07 * fix: don't display default ports as offline in `share` plugin (https://github.com/zellij-org/zellij/pull/4908) diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index fbebb18f4..de50cfb66 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -46,6 +46,15 @@ use crate::web_client::control_message::{ static ASYNC_RUNTIME: OnceLock = OnceLock::new(); use std::sync::OnceLock; +const ENTER_ALTERNATE_SCREEN: &str = "\u{1b}[?1049h"; +const EXIT_ALTERNATE_SCREEN: &str = "\u{1b}[?1049l"; +const ENABLE_BRACKETED_PASTE: &str = "\u{1b}[?2004h"; +const RESET_STYLE: &str = "\u{1b}[m"; +const SHOW_CURSOR: &str = "\u{1b}[?25h"; +const ENTER_KITTY_KEYBOARD_MODE: &str = "\u{1b}[>1u"; +const EXIT_KITTY_KEYBOARD_MODE: &str = "\u{1b}[<1u"; +const CLEAR_CLIENT_TERMINAL_ATTRIBUTES: &str = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; + /// Spawn an async runtime for this client instance. /// /// The number of workers can be configured to any nonzero value. Passing zero or `None` will spawn @@ -616,23 +625,15 @@ pub fn start_remote_client( )?; let reconnect_to_session = None; - let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; - let take_snapshot = "\u{1b}[?1049h"; - let bracketed_paste = "\u{1b}[?2004h"; - let enter_kitty_keyboard_mode = "\u{1b}[>1u"; os_input.unset_raw_mode().unwrap(); - let _ = os_input - .get_stdout_writer() - .write(take_snapshot.as_bytes()) + let mut stdout = os_input.get_stdout_writer(); + stdout.write_all(ENTER_ALTERNATE_SCREEN.as_bytes()).unwrap(); + stdout + .write_all(CLEAR_CLIENT_TERMINAL_ATTRIBUTES.as_bytes()) .unwrap(); - let _ = os_input - .get_stdout_writer() - .write(clear_client_terminal_attributes.as_bytes()) - .unwrap(); - let _ = os_input - .get_stdout_writer() - .write(enter_kitty_keyboard_mode.as_bytes()) + stdout + .write_all(ENTER_KITTY_KEYBOARD_MODE.as_bytes()) .unwrap(); envs::set_zellij("0".to_string()); @@ -640,10 +641,7 @@ pub fn start_remote_client( let full_screen_ws = os_input.get_terminal_size(); os_input.set_raw_mode(); - let _ = os_input - .get_stdout_writer() - .write(bracketed_paste.as_bytes()) - .unwrap(); + stdout.write_all(ENABLE_BRACKETED_PASTE.as_bytes()).unwrap(); std::panic::set_hook({ use zellij_utils::errors::handle_panic; @@ -661,25 +659,10 @@ pub fn start_remote_client( os_input.disable_mouse().non_fatal(); os_input.unset_raw_mode().unwrap(); os_input.restore_console_mode(); - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let restore_alternate_screen = "\u{1b}[?1049l"; - let exit_kitty_keyboard_mode = "\u{1b}[<1u"; - let reset_style = "\u{1b}[m"; - let show_cursor = "\u{1b}[?25h"; - let error = format!( - "{}{}{}{}\n{}{}\n", - reset_style, - show_cursor, - restore_alternate_screen, - exit_kitty_keyboard_mode, - goto_start_of_last_line, - e - ); - let _ = os_input - .get_stdout_writer() - .write(error.as_bytes()) - .unwrap(); - let _ = os_input.get_stdout_writer().flush().unwrap(); + let error = terminal_teardown_message(&e, full_screen_ws.rows, true); + let mut stdout = os_input.get_stdout_writer(); + stdout.write_all(error.as_bytes()).unwrap(); + stdout.flush().unwrap(); if exit_status == 0 { log::info!("{}", e); } else { @@ -701,7 +684,7 @@ pub fn start_remote_client( } else { let clear_screen = "\u{1b}[2J"; let mut stdout = os_input.get_stdout_writer(); - let _ = stdout.write(clear_screen.as_bytes()).unwrap(); + stdout.write_all(clear_screen.as_bytes()).unwrap(); stdout.flush().unwrap(); } @@ -731,28 +714,20 @@ pub fn start_client( .unwrap_or(false); let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false); let mut reconnect_to_session = None; - let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; - let take_snapshot = "\u{1b}[?1049h"; - let bracketed_paste = "\u{1b}[?2004h"; - let enter_kitty_keyboard_mode = "\u{1b}[>1u"; os_input.unset_raw_mode().unwrap(); if !is_a_reconnect { // we don't do this for a reconnect because our controlling terminal already has the // attributes we want from it, and some terminals don't treat these atomically (looking at // you Windows Terminal...) - let _ = os_input - .get_stdout_writer() - .write(take_snapshot.as_bytes()) - .unwrap(); - let _ = os_input - .get_stdout_writer() - .write(clear_client_terminal_attributes.as_bytes()) + let mut stdout = os_input.get_stdout_writer(); + stdout.write_all(ENTER_ALTERNATE_SCREEN.as_bytes()).unwrap(); + stdout + .write_all(CLEAR_CLIENT_TERMINAL_ATTRIBUTES.as_bytes()) .unwrap(); if !explicitly_disable_kitty_keyboard_protocol { - let _ = os_input - .get_stdout_writer() - .write(enter_kitty_keyboard_mode.as_bytes()) + stdout + .write_all(ENTER_KITTY_KEYBOARD_MODE.as_bytes()) .unwrap(); } } @@ -945,10 +920,8 @@ pub fn start_client( let mut command_is_executing = CommandIsExecuting::new(); os_input.set_raw_mode(); - let _ = os_input - .get_stdout_writer() - .write(bracketed_paste.as_bytes()) - .unwrap(); + let mut stdout = os_input.get_stdout_writer(); + stdout.write_all(ENABLE_BRACKETED_PASTE.as_bytes()).unwrap(); let (send_client_instructions, receive_client_instructions): ChannelWithContext< ClientInstruction, @@ -1089,17 +1062,14 @@ pub fn start_client( os_input.disable_mouse().non_fatal(); os_input.unset_raw_mode().unwrap(); os_input.restore_console_mode(); - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let restore_snapshot = "\u{1b}[?1049l"; - let error = format!( - "{}\n{}{}\n", - restore_snapshot, goto_start_of_last_line, backtrace + let error = terminal_teardown_message( + &backtrace, + full_screen_ws.rows, + !explicitly_disable_kitty_keyboard_protocol, ); - let _ = os_input - .get_stdout_writer() - .write(error.as_bytes()) - .unwrap(); - let _ = os_input.get_stdout_writer().flush().unwrap(); + let mut stdout = os_input.get_stdout_writer(); + stdout.write_all(error.as_bytes()).unwrap(); + stdout.flush().unwrap(); std::process::exit(1); }; @@ -1261,13 +1231,10 @@ pub fn start_client( router_thread.join().unwrap(); if reconnect_to_session.is_none() { - let reset_style = "\u{1b}[m"; - let show_cursor = "\u{1b}[?25h"; - let restore_snapshot = "\u{1b}[?1049l"; - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let goodbye_message = format!( - "{}\n{}{}{}{}\n", - goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg + let goodbye_message = terminal_teardown_message( + &exit_msg, + full_screen_ws.rows, + !explicitly_disable_kitty_keyboard_protocol, ); os_input.disable_mouse().non_fatal(); @@ -1275,17 +1242,12 @@ pub fn start_client( os_input.unset_raw_mode().unwrap(); os_input.restore_console_mode(); let mut stdout = os_input.get_stdout_writer(); - let exit_kitty_keyboard_mode = "\u{1b}[<1u"; - if !explicitly_disable_kitty_keyboard_protocol { - let _ = stdout.write(exit_kitty_keyboard_mode.as_bytes()).unwrap(); - stdout.flush().unwrap(); - } - let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); + stdout.write_all(goodbye_message.as_bytes()).unwrap(); stdout.flush().unwrap(); } else { let clear_screen = "\u{1b}[2J"; let mut stdout = os_input.get_stdout_writer(); - let _ = stdout.write(clear_screen.as_bytes()).unwrap(); + stdout.write_all(clear_screen.as_bytes()).unwrap(); stdout.flush().unwrap(); } @@ -1421,5 +1383,23 @@ pub fn start_server_detached( os_input.send_to_server(first_msg); } +fn terminal_teardown_message(message: &str, rows: usize, include_kitty_exit: bool) -> String { + let goto_start_of_last_line = format!("\u{1b}[{};{}H", rows, 1); + let kitty_exit = if include_kitty_exit { + EXIT_KITTY_KEYBOARD_MODE + } else { + "" + }; + format!( + "{}{}{}{}{}{}\n", + kitty_exit, + EXIT_ALTERNATE_SCREEN, + RESET_STYLE, + SHOW_CURSOR, + goto_start_of_last_line, + message + ) +} + #[cfg(test)] mod unit; diff --git a/zellij-client/src/stdin_ansi_parser.rs b/zellij-client/src/stdin_ansi_parser.rs index 310360e36..f0e2df278 100644 --- a/zellij-client/src/stdin_ansi_parser.rs +++ b/zellij-client/src/stdin_ansi_parser.rs @@ -275,11 +275,10 @@ impl AnsiStdinInstruction { let key_string = String::from_utf8_lossy(bytes); if let Some(captures) = RE.captures_iter(&key_string).next() { match captures[1].parse::().ok()? { - 1 | 2 => Some(AnsiStdinInstruction::SynchronizedOutput(Some( + 1 | 2 | 3 => Some(AnsiStdinInstruction::SynchronizedOutput(Some( SyncOutput::CSI, ))), - 0 | 4 => Some(AnsiStdinInstruction::SynchronizedOutput(None)), - _ => None, + _ => Some(AnsiStdinInstruction::SynchronizedOutput(None)), } } else { None