diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/mod.rs | 15 | ||||
| -rw-r--r-- | src/client/terminal_geometry.rs | 77 | ||||
| -rw-r--r-- | src/client/tests/mod.rs | 30 | ||||
| -rw-r--r-- | src/platform/fallback.rs | 5 | ||||
| -rw-r--r-- | src/platform/mod.rs | 17 | ||||
| -rw-r--r-- | src/platform/unix_common.rs | 4 | ||||
| -rw-r--r-- | src/platform/windows.rs | 4 |
7 files changed, 115 insertions, 37 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs index b7fdcabe..b20a6e85 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -36,9 +36,9 @@ pub use terminal_sessions::{run_terminal_session_control, run_terminal_session_o use terminal_geometry::query_host_terminal_appearance; #[cfg(test)] use terminal_geometry::{ - cell_size_fallback, ioctl_cell_size, pack_cell_size, resize_report_required, - should_query_host_cell_size, write_host_cell_size_query, write_host_terminal_appearance_query, - write_host_terminal_theme_query, + cell_size_fallback, current_terminal_geometry_with, ioctl_cell_size, pack_cell_size, + resize_report_required, should_query_host_cell_size, write_host_cell_size_query, + write_host_terminal_appearance_query, write_host_terminal_theme_query, }; use terminal_geometry::{ host_cell_size_query_required, initial_terminal_geometry, query_host_cell_size, @@ -302,6 +302,8 @@ enum ClientLoopEvent { StdinEvents(Vec<crate::protocol::ClientInputEvent>), /// Terminal resize detected, including current exact-pixel eligibility. Resize(u16, u16, u32, u32, bool), + /// The client's host terminal can no longer report a valid grid. + TerminalUnavailable(io::Error), /// Server message received. ServerMessage(Box<ServerMessage>), /// Server reader thread exited (connection lost). @@ -400,7 +402,7 @@ fn run_client_with_mode( // Get the terminal geometry before handshake (before raw mode). let (cols, rows, cell_width_px, cell_height_px, exact_cell_size) = - initial_terminal_geometry(pixel_geometry_enabled, kitty_graphics_enabled); + initial_terminal_geometry(pixel_geometry_enabled, kitty_graphics_enabled)?; let shell_surface_size = loop_config .shell_config @@ -1216,6 +1218,11 @@ async fn run_client_loop( } // Direct terminal attach is Unix-only; every Windows client uses ClientShell. } + ClientLoopEvent::TerminalUnavailable(err) => { + info!(err = %err, "client terminal unavailable; detaching"); + let _ = write_to_server(&mut write_stream, &ClientMessage::Detach); + return Ok(()); + } ClientLoopEvent::Resize( new_cols, new_rows, diff --git a/src/client/terminal_geometry.rs b/src/client/terminal_geometry.rs index d1f305ab..aaf216df 100644 --- a/src/client/terminal_geometry.rs +++ b/src/client/terminal_geometry.rs @@ -58,26 +58,46 @@ fn unpack_cell_size(packed: u64) -> Option<(u32, u32)> { (width_px > 0 && height_px > 0).then_some((width_px, height_px)) } -fn current_terminal_geometry( +type TerminalGeometry = (u16, u16, u32, u32, bool); + +pub(super) fn current_terminal_geometry_with( pixel_geometry_enabled: bool, pixel_geometry_fallback: bool, reported_cell_size: &AtomicU64, last_cell_size: Option<(u32, u32)>, -) -> (u16, u16, u32, u32, bool) { + exact_geometry: Option<(u16, u16, u32, u32)>, + terminal_grid_size: impl FnOnce() -> io::Result<(u16, u16)>, +) -> io::Result<TerminalGeometry> { if !pixel_geometry_enabled { - let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24)); - return (cols, rows, 0, 0, false); + let (cols, rows) = terminal_grid_size()?; + return Ok((cols, rows, 0, 0, false)); } - if let Some((cols, rows, cell_width_px, cell_height_px)) = ioctl_terminal_geometry() { - return (cols, rows, cell_width_px, cell_height_px, true); + if let Some((cols, rows, cell_width_px, cell_height_px)) = exact_geometry { + return Ok((cols, rows, cell_width_px, cell_height_px, true)); } - let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24)); + let (cols, rows) = terminal_grid_size()?; if !pixel_geometry_fallback { - return (cols, rows, 0, 0, false); + return Ok((cols, rows, 0, 0, false)); } let (cell_width_px, cell_height_px) = cell_size_fallback(reported_cell_size.load(Ordering::Acquire), last_cell_size); - (cols, rows, cell_width_px, cell_height_px, false) + Ok((cols, rows, cell_width_px, cell_height_px, false)) +} + +fn current_terminal_geometry( + pixel_geometry_enabled: bool, + pixel_geometry_fallback: bool, + reported_cell_size: &AtomicU64, + last_cell_size: Option<(u32, u32)>, +) -> io::Result<TerminalGeometry> { + current_terminal_geometry_with( + pixel_geometry_enabled, + pixel_geometry_fallback, + reported_cell_size, + last_cell_size, + ioctl_terminal_geometry(), + crate::platform::terminal_grid_size, + ) } /// Reads terminal geometry before the handshake. Pixel input and direct graphics @@ -85,28 +105,13 @@ fn current_terminal_geometry( pub(super) fn initial_terminal_geometry( pixel_geometry_enabled: bool, pixel_geometry_fallback: bool, -) -> (u16, u16, u32, u32, bool) { - if !pixel_geometry_enabled { - let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24)); - return (cols, rows, 0, 0, false); - } - match ioctl_terminal_geometry() { - Some((cols, rows, width, height)) => (cols, rows, width, height, true), - None => { - let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24)); - if pixel_geometry_fallback { - ( - cols, - rows, - DEFAULT_CELL_WIDTH_PX, - DEFAULT_CELL_HEIGHT_PX, - false, - ) - } else { - (cols, rows, 0, 0, false) - } - } - } +) -> io::Result<TerminalGeometry> { + current_terminal_geometry( + pixel_geometry_enabled, + pixel_geometry_fallback, + &AtomicU64::new(0), + None, + ) } pub(super) fn resize_report_required( @@ -146,12 +151,18 @@ pub(super) fn resize_poll_loop( while !should_quit.load(Ordering::Acquire) { std::thread::sleep(Duration::from_millis(100)); let signalled = crate::platform::take_terminal_resize_signal(); - let new_size = current_terminal_geometry( + let new_size = match current_terminal_geometry( pixel_geometry_enabled, pixel_geometry_fallback, reported_cell_size, Some((last_size.2, last_size.3)), - ); + ) { + Ok(size) => size, + Err(err) => { + let _ = resize_tx.blocking_send(ClientLoopEvent::TerminalUnavailable(err)); + break; + } + }; if resize_report_required(signalled, new_size, last_size) { last_size = new_size; if resize_tx diff --git a/src/client/tests/mod.rs b/src/client/tests/mod.rs index d8f71a84..5d789444 100644 --- a/src/client/tests/mod.rs +++ b/src/client/tests/mod.rs @@ -18,6 +18,36 @@ fn resize_signal_reports_even_when_polled_size_is_unchanged() { } #[test] +fn unavailable_terminal_grid_is_not_fabricated() { + let reported_cell_size = AtomicU64::new(0); + let err = current_terminal_geometry_with(false, false, &reported_cell_size, None, None, || { + Err(io::Error::new( + io::ErrorKind::NotConnected, + "terminal is gone", + )) + }) + .expect_err("an unavailable terminal must not produce fallback geometry"); + + assert_eq!(err.kind(), io::ErrorKind::NotConnected); +} + +#[test] +fn missing_pixel_geometry_keeps_a_valid_terminal_grid() { + let reported_cell_size = AtomicU64::new(0); + let geometry = current_terminal_geometry_with( + true, + true, + &reported_cell_size, + Some((9, 18)), + None, + || Ok((80, 24)), + ) + .expect("grid geometry remains valid without pixel dimensions"); + + assert_eq!(geometry, (80, 24, 9, 18, false)); +} + +#[test] fn direct_graphics_profile_is_narrow_and_transport_safe() { for (program, term, kitty, expected) in [ ("ghostty", "", false, true), diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index 7e14dcd8..09d82c6c 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -13,6 +13,11 @@ pub(crate) fn set_default_plugin_pane_pwd( ) { } +#[cfg(not(unix))] +pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> { + crossterm::terminal::size() +} + pub(crate) fn remote_ssh_config_paths() -> super::RemoteSshConfigPaths { super::RemoteSshConfigPaths { user_config: std::env::var_os("HOME") diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fc80bc64..b831e120 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -69,6 +69,23 @@ pub(crate) const fn capabilities() -> PlatformCapabilities { } } +pub(crate) fn terminal_grid_size() -> std::io::Result<(u16, u16)> { + #[cfg(unix)] + let (cols, rows) = unix_common::read_terminal_grid_size()?; + #[cfg(windows)] + let (cols, rows) = windows::read_terminal_grid_size()?; + #[cfg(not(any(unix, windows)))] + let (cols, rows) = fallback::read_terminal_grid_size()?; + + if cols == 0 || rows == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "terminal reported a zero-sized grid", + )); + } + Ok((cols, rows)) +} + #[cfg(not(windows))] pub fn launch_server_daemon_command(command: &mut std::process::Command) -> std::io::Result<u32> { command.spawn().map(|child| child.id()) diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index ce5bba00..a7926c22 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -1,5 +1,9 @@ use std::path::{Path, PathBuf}; +pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> { + crossterm::terminal::window_size().map(|size| (size.columns, size.rows)) +} + fn set_sigpipe_disposition(handler: libc::sighandler_t) { let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; action.sa_sigaction = handler; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2fdba9c8..b05b9e6f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -15,6 +15,10 @@ use std::{ mod clipboard_image; +pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> { + crossterm::terminal::size() +} + pub(crate) fn replace_file( source: &std::path::Path, destination: &std::path::Path, |
