aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordivens <divens@users.noreply.github.com>2026-02-18 15:07:19 +0100
committerGitHub <noreply@github.com>2026-02-18 15:07:19 +0100
commit36fa696c1a71b236e27d62bac26e4f0ee136cde4 (patch)
treed46e371641be07247c5f825a698e90b48d23c64e
parentrefactor: replace nix::Error with std::io::Error in public APIs (#4720) (diff)
downloadsl0ppij-36fa696c1a71b236e27d62bac26e4f0ee136cde4.tar.gz
sl0ppij-36fa696c1a71b236e27d62bac26e4f0ee136cde4.tar.xz
sl0ppij-36fa696c1a71b236e27d62bac26e4f0ee136cde4.zip
[Windows port PR2] Replace client-side termios/ioctl with crossterm for raw mode and terminal size (#4721)
* refactor: replace nix::Error with std::io::Error in public APIs * refactor: replace client-side termios/ioctl with crossterm --------- Co-authored-by: divens <divens.dev@gmail.com>
-rw-r--r--Cargo.lock12
-rw-r--r--Cargo.toml1
-rw-r--r--zellij-client/Cargo.toml1
-rw-r--r--zellij-client/src/lib.rs32
-rw-r--r--zellij-client/src/os_input_output.rs147
-rw-r--r--zellij-client/src/remote_attach/unit/remote_attach_tests.rs6
-rw-r--r--zellij-client/src/unit/terminal_loop_tests.rs7
-rw-r--r--zellij-client/src/web_client/server_listener.rs2
-rw-r--r--zellij-client/src/web_client/unit/web_client_tests.rs6
9 files changed, 103 insertions, 111 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fc37affc..f7ae2317 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -943,6 +943,17 @@ dependencies = [
]
[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags 2.5.0",
+ "parking_lot 0.12.1",
+ "rustix 0.38.44",
+]
+
+[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5326,6 +5337,7 @@ dependencies = [
"axum",
"axum-extra",
"axum-server",
+ "crossterm",
"daemonize",
"dialoguer",
"futures",
diff --git a/Cargo.toml b/Cargo.toml
index 652f5283..7fcb99b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,6 +75,7 @@ anyhow = { version = "1.0.70", default-features = false, features = ["backtrace"
async-std = { version = "1.3.0", default-features = false, features = ["attributes", "default", "std", "unstable"] }
async-trait = { version = "0.1", default-features = false }
clap = { version = "3.2.2", default-features = false, features = ["env", "derive", "color", "std", "suggestions"] }
+crossterm = { version = "0.28", default-features = false }
daemonize = { version = "0.5", default-features = false }
dialoguer = { version = "0.10.4", default-features = false, features = ["password", "completion"] }
humantime = { version = "2.1.0", default-features = false }
diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml
index 6f0b90e5..655f0f04 100644
--- a/zellij-client/Cargo.toml
+++ b/zellij-client/Cargo.toml
@@ -21,6 +21,7 @@ tower-http = { version = "0.6.4", features = ["cors"], optional = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
+crossterm = { workspace = true }
daemonize = { workspace = true }
dialoguer = { workspace = true }
interprocess = { workspace = true }
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index 4061b690..e7b6bc97 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -300,7 +300,7 @@ pub async fn run_remote_client_terminal_loop(
};
// send size on startup
- let new_size = os_input.get_terminal_size_using_fd(0);
+ let new_size = os_input.get_terminal_size();
if let Err(e) = connections
.control_ws
.send(create_resize_message(new_size))
@@ -335,7 +335,7 @@ pub async fn run_remote_client_terminal_loop(
Some(signal) = async_signals.recv() => {
match signal {
crate::os_input_output::SignalEvent::Resize => {
- let new_size = os_input.get_terminal_size_using_fd(0);
+ let new_size = os_input.get_terminal_size();
if let Err(e) = connections.control_ws.send(create_resize_message(new_size)).await {
log::error!("Failed to send resize message: {}", e);
break;
@@ -409,7 +409,7 @@ pub async fn run_remote_client_terminal_loop(
// no-op
}
Ok(WebServerToWebClientControlMessage::QueryTerminalSize) => {
- let new_size = os_input.get_terminal_size_using_fd(0);
+ let new_size = os_input.get_terminal_size();
if let Err(e) = connections.control_ws.send(create_resize_message(new_size)).await {
log::error!("Failed to send resize message: {}", e);
}
@@ -477,7 +477,7 @@ pub fn start_remote_client(
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(0).unwrap();
+ os_input.unset_raw_mode().unwrap();
let _ = os_input
.get_stdout_writer()
@@ -494,9 +494,9 @@ pub fn start_remote_client(
envs::set_zellij("0".to_string());
- let full_screen_ws = os_input.get_terminal_size_using_fd(0);
+ let full_screen_ws = os_input.get_terminal_size();
- os_input.set_raw_mode(0);
+ os_input.set_raw_mode();
let _ = os_input
.get_stdout_writer()
.write(bracketed_paste.as_bytes())
@@ -506,14 +506,14 @@ pub fn start_remote_client(
use zellij_utils::errors::handle_panic;
let os_input = os_input.clone();
Box::new(move |info| {
- if let Ok(()) = os_input.unset_raw_mode(0) {
+ if let Ok(()) = os_input.unset_raw_mode() {
handle_panic::<ClientInstruction>(info, None);
}
})
});
let reset_controlling_terminal_state = |e: String, exit_status: i32| {
- os_input.unset_raw_mode(0).unwrap();
+ os_input.unset_raw_mode().unwrap();
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";
@@ -589,7 +589,7 @@ pub fn start_client(
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(0).unwrap();
+ 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
@@ -613,7 +613,7 @@ pub fn start_client(
envs::set_zellij("0".to_string());
config.env.set_vars();
- let full_screen_ws = os_input.get_terminal_size_using_fd(0);
+ let full_screen_ws = os_input.get_terminal_size();
let web_server_ip = config_options
.web_server_ip
@@ -793,7 +793,7 @@ pub fn start_client(
let mut command_is_executing = CommandIsExecuting::new();
- os_input.set_raw_mode(0);
+ os_input.set_raw_mode();
let _ = os_input
.get_stdout_writer()
.write(bracketed_paste.as_bytes())
@@ -814,7 +814,7 @@ pub fn start_client(
let send_client_instructions = send_client_instructions.clone();
let os_input = os_input.clone();
Box::new(move |info| {
- if let Ok(()) = os_input.unset_raw_mode(0) {
+ if let Ok(()) = os_input.unset_raw_mode() {
handle_panic(info, Some(&send_client_instructions));
}
})
@@ -869,7 +869,7 @@ pub fn start_client(
let os_api = os_input.clone();
move || {
os_api.send_to_server(ClientToServerMsg::TerminalResize {
- new_size: os_api.get_terminal_size_using_fd(0),
+ new_size: os_api.get_terminal_size(),
});
}
}),
@@ -929,7 +929,7 @@ pub fn start_client(
.unwrap();
let handle_error = |backtrace: String| {
- os_input.unset_raw_mode(0).unwrap();
+ os_input.unset_raw_mode().unwrap();
let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
let restore_snapshot = "\u{1b}[?1049l";
os_input.disable_mouse().non_fatal();
@@ -1073,7 +1073,7 @@ pub fn start_client(
},
ClientInstruction::QueryTerminalSize => {
os_input.send_to_server(ClientToServerMsg::TerminalResize {
- new_size: os_input.get_terminal_size_using_fd(0),
+ new_size: os_input.get_terminal_size(),
});
},
ClientInstruction::StartWebServer => {
@@ -1114,7 +1114,7 @@ pub fn start_client(
os_input.disable_mouse().non_fatal();
info!("{}", exit_msg);
- os_input.unset_raw_mode(0).unwrap();
+ os_input.unset_raw_mode().unwrap();
let mut stdout = os_input.get_stdout_writer();
let exit_kitty_keyboard_mode = "\u{1b}[<1u";
if !explicitly_disable_kitty_keyboard_protocol {
diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs
index 57c351e5..b1f95fa4 100644
--- a/zellij-client/src/os_input_output.rs
+++ b/zellij-client/src/os_input_output.rs
@@ -1,19 +1,13 @@
use anyhow::{Context, Result};
use async_trait::async_trait;
use interprocess;
-use libc;
-use nix;
use signal_hook;
use zellij_utils::pane_size::Size;
use interprocess::local_socket::LocalSocketStream;
-use mio::{unix::SourceFd, Events, Interest, Poll, Token};
-use nix::pty::Winsize;
-use nix::sys::termios;
use signal_hook::{consts::signal::*, iterator::Signals};
use std::io::prelude::*;
use std::io::IsTerminal;
-use std::os::unix::io::RawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::{io, thread, time};
@@ -118,62 +112,20 @@ impl AsyncSignals for AsyncSignalListener {
}
}
-fn into_raw_mode(pid: RawFd) {
- let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute");
- termios::cfmakeraw(&mut tio);
- match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &tio) {
- Ok(_) => {},
- Err(e) => panic!("error {:?}", e),
- };
-}
-
-fn unset_raw_mode(pid: RawFd, orig_termios: termios::Termios) -> Result<(), std::io::Error> {
- Ok(termios::tcsetattr(
- pid,
- termios::SetArg::TCSANOW,
- &orig_termios,
- )?)
-}
-
-pub(crate) fn get_terminal_size_using_fd(fd: RawFd) -> Size {
- // TODO: do this with the nix ioctl
- use libc::ioctl;
- use libc::TIOCGWINSZ;
-
- let mut winsize = Winsize {
- ws_row: 0,
- ws_col: 0,
- ws_xpixel: 0,
- ws_ypixel: 0,
- };
-
- // TIOCGWINSZ is an u32, but the second argument to ioctl is u64 on
- // some platforms. When checked on Linux, clippy will complain about
- // useless conversion.
- #[allow(clippy::useless_conversion)]
- unsafe {
- ioctl(fd, TIOCGWINSZ.into(), &mut winsize)
- };
-
- // fallback to default values when rows/cols == 0: https://github.com/zellij-org/zellij/issues/1551
- let rows = if winsize.ws_row != 0 {
- winsize.ws_row as usize
- } else {
- 24
- };
-
- let cols = if winsize.ws_col != 0 {
- winsize.ws_col as usize
- } else {
- 80
- };
-
- Size { rows, cols }
+pub(crate) fn get_terminal_size() -> Size {
+ match crossterm::terminal::size() {
+ Ok((cols, rows)) => {
+ // fallback to default values when rows/cols == 0: https://github.com/zellij-org/zellij/issues/1551
+ let rows = if rows != 0 { rows as usize } else { 24 };
+ let cols = if cols != 0 { cols as usize } else { 80 };
+ Size { rows, cols }
+ },
+ Err(_) => Size { rows: 24, cols: 80 },
+ }
}
#[derive(Clone)]
pub struct ClientOsInputOutput {
- orig_termios: Option<Arc<Mutex<termios::Termios>>>,
send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>,
receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>,
reading_from_stdin: Arc<Mutex<Option<Vec<u8>>>>,
@@ -189,14 +141,14 @@ impl std::fmt::Debug for ClientOsInputOutput {
/// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that
/// Zellij client requires.
pub trait ClientOsApi: Send + Sync + std::fmt::Debug {
- /// Returns the size of the terminal associated to file descriptor `fd`.
- fn get_terminal_size_using_fd(&self, fd: RawFd) -> Size;
- /// Set the terminal associated to file descriptor `fd` to
+ /// Returns the size of the terminal.
+ fn get_terminal_size(&self) -> Size;
+ /// Set the terminal to
/// [raw mode](https://en.wikipedia.org/wiki/Terminal_mode).
- fn set_raw_mode(&mut self, fd: RawFd);
- /// Set the terminal associated to file descriptor `fd` to
+ fn set_raw_mode(&mut self);
+ /// Set the terminal to
/// [cooked mode](https://en.wikipedia.org/wiki/Terminal_mode).
- fn unset_raw_mode(&self, fd: RawFd) -> Result<(), std::io::Error>;
+ fn unset_raw_mode(&self) -> Result<(), std::io::Error>;
/// Returns the writer that allows writing to standard output.
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
/// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
@@ -237,23 +189,14 @@ pub trait ClientOsApi: Send + Sync + std::fmt::Debug {
}
impl ClientOsApi for ClientOsInputOutput {
- fn get_terminal_size_using_fd(&self, fd: RawFd) -> Size {
- get_terminal_size_using_fd(fd)
+ fn get_terminal_size(&self) -> Size {
+ get_terminal_size()
}
- fn set_raw_mode(&mut self, fd: RawFd) {
- into_raw_mode(fd);
+ fn set_raw_mode(&mut self) {
+ crossterm::terminal::enable_raw_mode().expect("could not enable raw mode");
}
- fn unset_raw_mode(&self, fd: RawFd) -> Result<(), std::io::Error> {
- match &self.orig_termios {
- Some(orig_termios) => {
- let orig_termios = orig_termios.lock().unwrap();
- unset_raw_mode(fd, orig_termios.clone())
- },
- None => {
- log::warn!("trying to unset raw mode for a non-terminal session");
- Ok(())
- },
- }
+ fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
+ crossterm::terminal::disable_raw_mode()
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
Box::new((*self).clone())
@@ -422,11 +365,8 @@ impl Clone for Box<dyn ClientOsApi> {
}
pub fn get_client_os_input() -> Result<ClientOsInputOutput, std::io::Error> {
- let current_termios = termios::tcgetattr(0).ok();
- let orig_termios = current_termios.map(|termios| Arc::new(Mutex::new(termios)));
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
- orig_termios,
send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
@@ -435,10 +375,8 @@ pub fn get_client_os_input() -> Result<ClientOsInputOutput, std::io::Error> {
}
pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, std::io::Error> {
- let orig_termios = None; // not a terminal
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
- orig_termios,
send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
@@ -447,3 +385,44 @@ pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, std::io::Error>
}
pub const DEFAULT_STDIN_POLL_TIMEOUT_MS: u64 = 10;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn get_terminal_size_returns_nonzero_or_fallback() {
+ let size = get_terminal_size();
+ // In CI or when not attached to a terminal, crossterm may return an error
+ // and we fall back to 80x24. Either way, size should be valid.
+ assert!(size.rows > 0, "rows should be positive");
+ assert!(size.cols > 0, "cols should be positive");
+ }
+
+ #[test]
+ fn get_terminal_size_fallback_values() {
+ // Verify the fallback constants are what we expect
+ let fallback = Size { rows: 24, cols: 80 };
+ // When crossterm::terminal::size() fails (no terminal), we should get 80x24
+ // This is implicitly tested by get_terminal_size_returns_nonzero_or_fallback
+ // but we verify the constants here
+ assert_eq!(fallback.rows, 24);
+ assert_eq!(fallback.cols, 80);
+ }
+
+ #[test]
+ fn client_os_input_output_can_be_constructed() {
+ let os_input = get_client_os_input().expect("should construct ClientOsInputOutput");
+ let size = os_input.get_terminal_size();
+ assert!(size.rows > 0, "rows should be positive");
+ assert!(size.cols > 0, "cols should be positive");
+ }
+
+ #[test]
+ fn cli_client_os_input_can_be_constructed() {
+ let os_input = get_cli_client_os_input().expect("should construct CLI ClientOsInputOutput");
+ let size = os_input.get_terminal_size();
+ assert!(size.rows > 0, "rows should be positive");
+ assert!(size.cols > 0, "cols should be positive");
+ }
+}
diff --git a/zellij-client/src/remote_attach/unit/remote_attach_tests.rs b/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
index 8cccbc67..4673abb4 100644
--- a/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
+++ b/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
@@ -229,13 +229,13 @@ fn cleanup_test_db(server_url: &str) {
struct MockClientOsApi;
impl crate::os_input_output::ClientOsApi for MockClientOsApi {
- fn get_terminal_size_using_fd(&self, _fd: i32) -> zellij_utils::pane_size::Size {
+ fn get_terminal_size(&self) -> zellij_utils::pane_size::Size {
zellij_utils::pane_size::Size { rows: 24, cols: 80 }
}
- fn set_raw_mode(&mut self, _fd: i32) {}
+ fn set_raw_mode(&mut self) {}
- fn unset_raw_mode(&self, _fd: i32) -> Result<(), std::io::Error> {
+ fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
Ok(())
}
diff --git a/zellij-client/src/unit/terminal_loop_tests.rs b/zellij-client/src/unit/terminal_loop_tests.rs
index 4d037cae..e43adb4b 100644
--- a/zellij-client/src/unit/terminal_loop_tests.rs
+++ b/zellij-client/src/unit/terminal_loop_tests.rs
@@ -9,7 +9,6 @@ use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use serial_test::serial;
use std::io::{self, Write};
-use std::os::unix::io::RawFd;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::mpsc;
@@ -77,13 +76,13 @@ impl std::fmt::Debug for TestClientOsApi {
}
impl ClientOsApi for TestClientOsApi {
- fn get_terminal_size_using_fd(&self, _fd: RawFd) -> Size {
+ fn get_terminal_size(&self) -> Size {
self.terminal_size
}
- fn set_raw_mode(&mut self, _fd: RawFd) {}
+ fn set_raw_mode(&mut self) {}
- fn unset_raw_mode(&self, _fd: RawFd) -> Result<(), std::io::Error> {
+ fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
Ok(())
}
diff --git a/zellij-client/src/web_client/server_listener.rs b/zellij-client/src/web_client/server_listener.rs
index daad1233..83a770d4 100644
--- a/zellij-client/src/web_client/server_listener.rs
+++ b/zellij-client/src/web_client/server_listener.rs
@@ -64,7 +64,7 @@ pub fn zellij_server_listener(
reload_config_from_disk(&mut config, &mut config_options, &config_file_path);
- let full_screen_ws = os_input.get_terminal_size_using_fd(0);
+ let full_screen_ws = os_input.get_terminal_size();
let mut sent_init_messages = false;
let palette = config
diff --git a/zellij-client/src/web_client/unit/web_client_tests.rs b/zellij-client/src/web_client/unit/web_client_tests.rs
index dde293cf..ccaf8e0b 100644
--- a/zellij-client/src/web_client/unit/web_client_tests.rs
+++ b/zellij-client/src/web_client/unit/web_client_tests.rs
@@ -1891,11 +1891,11 @@ impl MockClientOsApi {
}
impl ClientOsApi for MockClientOsApi {
- fn get_terminal_size_using_fd(&self, _fd: std::os::unix::io::RawFd) -> Size {
+ fn get_terminal_size(&self) -> Size {
self.terminal_size
}
- fn set_raw_mode(&mut self, _fd: std::os::unix::io::RawFd) {}
- fn unset_raw_mode(&self, _fd: std::os::unix::io::RawFd) -> Result<(), std::io::Error> {
+ fn set_raw_mode(&mut self) {}
+ fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
Ok(())
}
fn get_stdout_writer(&self) -> Box<dyn std::io::Write> {