use std::cell::Cell;
use std::ffi::OsString;
use std::{env, fmt, io};
use crossterm::cursor::Show;
use crossterm::event::{
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
supports_keyboard_enhancement,
};
use crossterm::{Command, execute};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use crate::runtime::TuiTerminal;
#[cfg_attr(test, mockall::automock)]
trait TerminalOperation {
fn enable_raw_mode(&self) -> io::Result<()>;
fn disable_raw_mode(&self) -> io::Result<()>;
fn supports_keyboard_enhancement(&self) -> io::Result<bool>;
fn is_ssh_session(&self) -> bool;
fn is_tmux_session(&self) -> bool;
fn enter_alternate_screen(
&self,
stdout: &mut io::Stdout,
enhancement: TerminalEnhancement,
) -> io::Result<()>;
fn leave_alternate_screen(
&self,
stdout: &mut io::Stdout,
enhancement: TerminalEnhancement,
) -> io::Result<()>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct TerminalEnhancement {
keyboard_enhancement_enabled: bool,
xterm_modified_keys_enabled: bool,
}
struct CrosstermTerminalOperation;
impl TerminalOperation for CrosstermTerminalOperation {
fn enable_raw_mode(&self) -> io::Result<()> {
enable_raw_mode()
}
fn disable_raw_mode(&self) -> io::Result<()> {
disable_raw_mode()
}
fn supports_keyboard_enhancement(&self) -> io::Result<bool> {
supports_keyboard_enhancement()
}
fn is_ssh_session(&self) -> bool {
has_ssh_environment(|name| env::var_os(name))
}
fn is_tmux_session(&self) -> bool {
crate::infra::tmux::is_tmux_session()
}
fn enter_alternate_screen(
&self,
stdout: &mut io::Stdout,
enhancement: TerminalEnhancement,
) -> io::Result<()> {
if enhancement.keyboard_enhancement_enabled {
execute!(
stdout,
EnableXtermCsiUModifiedKeys(enhancement),
PushKeyboardEnhancementFlags(keyboard_enhancement_flags()),
EnterAlternateScreen,
EnableBracketedPaste
)
} else {
execute!(stdout, EnterAlternateScreen, EnableBracketedPaste)
}
}
fn leave_alternate_screen(
&self,
stdout: &mut io::Stdout,
enhancement: TerminalEnhancement,
) -> io::Result<()> {
if enhancement.keyboard_enhancement_enabled {
execute!(
stdout,
PopKeyboardEnhancementFlags,
DisableXtermCsiUModifiedKeys(enhancement),
DisableBracketedPaste,
LeaveAlternateScreen,
Show
)
} else {
execute!(stdout, DisableBracketedPaste, LeaveAlternateScreen, Show)
}
}
}
static CROSSTERM_TERMINAL_OPERATION: CrosstermTerminalOperation = CrosstermTerminalOperation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct EnableXtermCsiUModifiedKeys(TerminalEnhancement);
impl Command for EnableXtermCsiUModifiedKeys {
fn write_ansi(&self, buffer: &mut impl fmt::Write) -> fmt::Result {
if self.0.xterm_modified_keys_enabled {
buffer.write_str("\x1B[>4;1f\x1B[>4;2m")?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DisableXtermCsiUModifiedKeys(TerminalEnhancement);
impl Command for DisableXtermCsiUModifiedKeys {
fn write_ansi(&self, buffer: &mut impl fmt::Write) -> fmt::Result {
if self.0.xterm_modified_keys_enabled {
buffer.write_str("\x1B[>4f\x1B[>4m")?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Ok(())
}
}
const fn keyboard_enhancement_flags() -> KeyboardEnhancementFlags {
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
.union(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS)
}
const _: () = {
assert!(
keyboard_enhancement_flags().contains(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES),
);
assert!(keyboard_enhancement_flags().contains(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS),);
assert!(
!keyboard_enhancement_flags()
.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES),
);
};
pub(crate) struct TerminalGuard {
enhancement: Cell<TerminalEnhancement>,
}
impl TerminalGuard {
pub(crate) fn new() -> Self {
Self {
enhancement: Cell::new(TerminalEnhancement {
keyboard_enhancement_enabled: false,
xterm_modified_keys_enabled: false,
}),
}
}
fn set_enhancement(&self, enhancement: TerminalEnhancement) {
self.enhancement.set(enhancement);
}
fn enhancement(&self) -> TerminalEnhancement {
self.enhancement.get()
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
restore_terminal_state(&CROSSTERM_TERMINAL_OPERATION, self.enhancement());
}
}
pub(crate) fn setup_terminal(guard: &TerminalGuard) -> io::Result<TuiTerminal> {
let stdout = prepare_terminal_stdout_with_operation(&CROSSTERM_TERMINAL_OPERATION, guard)?;
let backend = CrosstermBackend::new(stdout);
Terminal::new(backend)
}
fn prepare_terminal_stdout_with_operation(
operation: &dyn TerminalOperation,
guard: &TerminalGuard,
) -> io::Result<io::Stdout> {
operation.enable_raw_mode()?;
let enhancement = terminal_enhancement(operation);
guard.set_enhancement(enhancement);
let mut stdout = io::stdout();
operation.enter_alternate_screen(&mut stdout, enhancement)?;
Ok(stdout)
}
fn terminal_enhancement(operation: &dyn TerminalOperation) -> TerminalEnhancement {
let is_tmux_session = operation.is_tmux_session();
let keyboard_enhancement_enabled =
should_enable_keyboard_enhancement(operation, is_tmux_session);
TerminalEnhancement {
keyboard_enhancement_enabled,
xterm_modified_keys_enabled: keyboard_enhancement_enabled && is_tmux_session,
}
}
fn should_enable_keyboard_enhancement(
operation: &dyn TerminalOperation,
is_tmux_session: bool,
) -> bool {
match operation.supports_keyboard_enhancement() {
Ok(true) => true,
Ok(false) | Err(_) => operation.is_ssh_session() || is_tmux_session,
}
}
fn has_ssh_environment(mut get_var: impl FnMut(&str) -> Option<OsString>) -> bool {
["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"]
.iter()
.any(|name| get_var(name).is_some())
}
fn restore_terminal_state(operation: &dyn TerminalOperation, enhancement: TerminalEnhancement) {
let mut stdout = io::stdout();
let _ = operation.disable_raw_mode();
let _ = operation.leave_alternate_screen(&mut stdout, enhancement);
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
fn enhancement_fixture(keyboard: bool, xterm: bool) -> TerminalEnhancement {
TerminalEnhancement {
keyboard_enhancement_enabled: keyboard,
xterm_modified_keys_enabled: xterm,
}
}
#[test]
fn setup_terminal_returns_error_when_enable_raw_mode_fails() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Err(io::Error::other("enable failed")));
operation.expect_enter_alternate_screen().times(0);
operation.expect_supports_keyboard_enhancement().times(0);
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let error = result.expect_err("setup should fail when raw mode fails");
assert_eq!(error.to_string(), "enable failed");
}
#[test]
fn setup_terminal_returns_error_when_enter_alternate_screen_fails() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Ok(false));
operation.expect_is_ssh_session().once().returning(|| false);
operation
.expect_is_tmux_session()
.once()
.returning(|| false);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(false, false))
.returning(|_, _| Err(io::Error::other("enter failed")));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let error = result.expect_err("setup should fail when alternate screen fails");
assert_eq!(error.to_string(), "enter failed");
}
#[test]
fn setup_terminal_omits_xterm_modified_keys_when_supported_directly() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
let mut xterm_startup_sequence = String::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Ok(true));
operation
.expect_is_tmux_session()
.once()
.returning(|| false);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, false))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
EnableXtermCsiUModifiedKeys(guard.enhancement())
.write_ansi(&mut xterm_startup_sequence)
.expect("xterm startup sequence should render");
let _stdout = result.expect("setup should use Kitty enhancement directly");
assert_eq!(guard.enhancement(), enhancement_fixture(true, false));
assert!(xterm_startup_sequence.is_empty());
}
#[test]
fn setup_terminal_ignores_keyboard_enhancement_query_failures() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Err(io::Error::other("unsupported")));
operation.expect_is_ssh_session().once().returning(|| false);
operation
.expect_is_tmux_session()
.once()
.returning(|| false);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(false, false))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let _stdout = result.expect("setup should fall back when support query fails");
assert_eq!(guard.enhancement(), enhancement_fixture(false, false));
}
#[test]
fn setup_terminal_enables_keyboard_enhancement_for_tmux_query_failure() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Err(io::Error::other("timeout")));
operation.expect_is_ssh_session().once().returning(|| false);
operation.expect_is_tmux_session().once().returning(|| true);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, true))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let _stdout = result.expect("setup should enable keyboard enhancement inside tmux");
assert_eq!(guard.enhancement(), enhancement_fixture(true, true));
}
#[test]
fn setup_terminal_enables_keyboard_enhancement_for_tmux_unsupported_query() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Ok(false));
operation.expect_is_ssh_session().once().returning(|| false);
operation.expect_is_tmux_session().once().returning(|| true);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, true))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let _stdout = result.expect("setup should enable keyboard enhancement inside tmux");
assert_eq!(guard.enhancement(), enhancement_fixture(true, true));
}
#[test]
fn setup_terminal_enables_keyboard_enhancement_for_ssh_query_failure() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Err(io::Error::other("timeout")));
operation
.expect_is_tmux_session()
.once()
.returning(|| false);
operation.expect_is_ssh_session().once().returning(|| true);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, false))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let _stdout = result.expect("setup should enable keyboard enhancement over SSH");
assert_eq!(guard.enhancement(), enhancement_fixture(true, false));
}
#[test]
fn setup_terminal_enables_keyboard_enhancement_for_ssh_unsupported_query() {
let mut operation = MockTerminalOperation::new();
let guard = TerminalGuard::new();
operation
.expect_enable_raw_mode()
.once()
.returning(|| Ok(()));
operation
.expect_supports_keyboard_enhancement()
.once()
.returning(|| Ok(false));
operation
.expect_is_tmux_session()
.once()
.returning(|| false);
operation.expect_is_ssh_session().once().returning(|| true);
operation
.expect_enter_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, false))
.returning(|_, _| Ok(()));
let result = prepare_terminal_stdout_with_operation(&operation, &guard);
let _stdout = result.expect("setup should enable keyboard enhancement over SSH");
assert_eq!(guard.enhancement(), enhancement_fixture(true, false));
}
#[test]
fn ssh_environment_detects_common_ssh_variables() {
let variables = [
("SSH_CONNECTION", Some("client server")),
("SSH_CLIENT", None),
("SSH_TTY", None),
];
let is_ssh_session = has_ssh_environment(|name| {
variables
.iter()
.find(|(variable_name, _)| *variable_name == name)
.and_then(|(_, value)| value.map(OsString::from))
});
assert!(is_ssh_session);
}
#[test]
fn ssh_environment_rejects_local_terminal_without_ssh_variables() {
let is_ssh_session = has_ssh_environment(|_| None);
assert!(!is_ssh_session);
}
#[test]
fn xterm_modified_key_commands_request_and_reset_csi_u_reporting() {
let mut disabled_enable_sequence = String::new();
let mut disabled_disable_sequence = String::new();
let mut enable_sequence = String::new();
let mut disable_sequence = String::new();
EnableXtermCsiUModifiedKeys(enhancement_fixture(true, false))
.write_ansi(&mut disabled_enable_sequence)
.expect("disabled enable sequence should render");
DisableXtermCsiUModifiedKeys(enhancement_fixture(true, false))
.write_ansi(&mut disabled_disable_sequence)
.expect("disabled disable sequence should render");
EnableXtermCsiUModifiedKeys(enhancement_fixture(true, true))
.write_ansi(&mut enable_sequence)
.expect("enable sequence should render");
DisableXtermCsiUModifiedKeys(enhancement_fixture(true, true))
.write_ansi(&mut disable_sequence)
.expect("disable sequence should render");
assert!(disabled_enable_sequence.is_empty());
assert!(disabled_disable_sequence.is_empty());
assert_eq!(enable_sequence, "\x1B[>4;1f\x1B[>4;2m");
assert_eq!(disable_sequence, "\x1B[>4f\x1B[>4m");
}
#[test]
fn restore_terminal_state_attempts_leave_even_when_disable_fails() {
let mut operation = MockTerminalOperation::new();
let leave_calls = Arc::new(AtomicUsize::new(0));
let leave_calls_for_expectation = leave_calls.clone();
operation
.expect_disable_raw_mode()
.once()
.returning(|| Err(io::Error::other("disable failed")));
operation
.expect_leave_alternate_screen()
.once()
.withf(|_, enhancement| *enhancement == enhancement_fixture(true, true))
.returning(move |_, _| {
leave_calls_for_expectation.fetch_add(1, Ordering::Relaxed);
Ok(())
});
restore_terminal_state(&operation, enhancement_fixture(true, true));
assert_eq!(leave_calls.load(Ordering::Relaxed), 1);
}
}