aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-02 16:09:19 +0300
committerGitHub <noreply@github.com>2026-09-02 16:09:19 +0300
commitd08e44686d8b19bd9555cc99ec9068d9fde05f16 (patch)
treeca2f34d2117efbf770095c437f9d6402c24d9fb3 /src/client
parent8633a398e653eee47b375c963996c78a8a14aa48 (diff)
fix(client): detach when terminal geometry is unavailable (#3531)
refs #3519
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs15
-rw-r--r--src/client/terminal_geometry.rs77
-rw-r--r--src/client/tests/mod.rs30
3 files changed, 85 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),