From f7e580c407ca23b32ab92e65df0467422c9e4040 Mon Sep 17 00:00:00 2001 From: coderbants Date: Fri, 28 Aug 2026 19:58:49 +0100 Subject: [PATCH 1/2] fix:isolate-unix-terminal-code-on-windows --- UPSTREAM_MAPPING.md | 7 ++-- src/tty.rs | 88 +++++++++++++++++--------------------------- src/tty_unix.rs | 77 +++++++++++++++++++++++++++++++++++++- src/tty_windows.rs | 18 ++++++++- tests/windows_tty.rs | 22 +++++++++++ 5 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 tests/windows_tty.rs diff --git a/UPSTREAM_MAPPING.md b/UPSTREAM_MAPPING.md index 7847e52..78532ba 100644 --- a/UPSTREAM_MAPPING.md +++ b/UPSTREAM_MAPPING.md @@ -48,9 +48,9 @@ upstream tag `v2.0.8`, checked out locally in `upstream-go/` (gitignored). | `termios_other.go` | `src/termios_other.rs` | Non-POSIX fallback | | `termios_unix.go` | `src/termios_unix.rs` | POSIX termios helper | | `termios_windows.go` | `src/termios_windows.rs` | Windows console mode helper | -| `tty.go` | `src/tty.rs` | `init_terminal` / `restore_terminal` | -| `tty_unix.go` | `src/tty_unix.rs` | Unix TTY reader + raw mode | -| `tty_windows.go` | `src/tty_windows.rs` | Windows VT console helper | +| `tty.go` | `src/tty.rs` | Public terminal facade; dispatches raw mode and restore operations to the target platform while sharing window-size queries | +| `tty_unix.go` | `src/tty_unix.rs` | Unix-only TTY state, termios, and raw-file-descriptor implementation | +| `tty_windows.go` | `src/tty_windows.rs` | Windows-only safe crossterm console-mode implementation | | `xterm.go` | `src/xterm.rs` | XTVERSION query, `TerminalVersionMsg` | ## Test Files (`*_test.go` -> `tests/`) @@ -66,6 +66,7 @@ upstream tag `v2.0.8`, checked out locally in `upstream-go/` (gitignored). | `mouse_test.go` | `tests/mouse_test.rs` | Mouse event suite | | `options_test.go` | `tests/commands_test.rs` | Program option tests | | `screen_test.go` | `tests/commands_test.rs` | Screen buffer tests | +| Rust Windows regression | `tests/windows_tty.rs` | Native Windows compile and public terminal-surface regression for the platform split | Golden files under `testdata/` are accounted for by the corresponding Rust test assertions (values verified against upstream output): `testdata/TestClearMsg/*.golden` diff --git a/src/tty.rs b/src/tty.rs index 178b4a5..d8a8b4e 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -4,79 +4,57 @@ //! //! # TTY Terminal Management //! -//! TTY initialization, input stream setup, raw mode toggles, and window dimension queries for Bubble Tea v2.0.8. +//! TTY initialization, raw mode toggles, and window dimension queries for +//! Bubble Tea v2.0.8. Unix targets use the upstream-compatible termios and +//! raw-file-descriptor implementation. Windows targets use crossterm's safe +//! console-mode API for enable, disable, initialize, and restore operations. //! use crossterm::terminal::size as term_size; -use std::sync::{Mutex, OnceLock}; -/// Saved terminal state for the raw mode toggle, mirroring the upstream -/// `p.previousTtyInputState` (x/term `MakeRaw`/`Restore`). -static SAVED_TERMIOS: OnceLock>> = OnceLock::new(); +#[cfg(unix)] +use crate::tty_unix as platform; +#[cfg(windows)] +use crate::tty_windows as platform; +#[cfg(not(any(unix, windows)))] +use unsupported as platform; -/// Initializes terminal raw mode, mirroring the upstream `initInput` -> -/// `term.MakeRaw` path (`tty_unix.go`). Unlike a fully-zeroed `cfmakeraw`, -/// only `OPOST` is cleared from the output flags, so `TABDLY` (and thus the -/// hard-tab cursor optimization) behaves exactly as it does upstream. -pub fn enable_raw_mode() -> std::io::Result<()> { - use std::os::fd::AsRawFd; - let fd = std::io::stdin().as_raw_fd(); - let mut t: libc::termios = unsafe { std::mem::zeroed() }; - if unsafe { libc::tcgetattr(fd, &mut t) } != 0 { - return Err(std::io::Error::last_os_error()); +#[cfg(not(any(unix, windows)))] +mod unsupported { + /// Reports that raw mode is unavailable on an unsupported target. + pub(super) fn enable_raw_mode() -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "raw terminal mode is unsupported on this target", + )) } - *SAVED_TERMIOS - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap() = Some(t); - // This attempts to replicate the behaviour documented for cfmakeraw in - // the termios(3) manpage, as x/term's `makeRaw` does. - t.c_iflag &= !(libc::IGNBRK - | libc::BRKINT - | libc::PARMRK - | libc::ISTRIP - | libc::INLCR - | libc::IGNCR - | libc::ICRNL - | libc::IXON); - t.c_oflag &= !libc::OPOST; - t.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); - t.c_cflag &= !(libc::CSIZE | libc::PARENB); - t.c_cflag |= libc::CS8; - t.c_cc[libc::VMIN] = 1; - t.c_cc[libc::VTIME] = 0; - if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &t) } != 0 { - return Err(std::io::Error::last_os_error()); + /// Reports that raw mode is unavailable on an unsupported target. + pub(super) fn disable_raw_mode() -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "raw terminal mode is unsupported on this target", + )) } - Ok(()) } -/// Restores the terminal state saved by [`enable_raw_mode`], mirroring the -/// upstream `term.Restore` path. +/// Enables terminal raw mode through the target platform implementation. +pub fn enable_raw_mode() -> std::io::Result<()> { + platform::enable_raw_mode() +} + +/// Disables terminal raw mode through the target platform implementation. pub fn disable_raw_mode() -> std::io::Result<()> { - use std::os::fd::AsRawFd; - let saved = SAVED_TERMIOS - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap() - .take(); - if let Some(t) = saved { - let fd = std::io::stdin().as_raw_fd(); - if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &t) } != 0 { - return Err(std::io::Error::last_os_error()); - } - } - Ok(()) + platform::disable_raw_mode() } -/// Initializes terminal raw mode. +/// Initializes terminal raw mode through the target platform implementation. pub fn init_terminal() -> Result<(), Box> { enable_raw_mode()?; Ok(()) } -/// Restores terminal raw mode. +/// Restores terminal raw mode through the target platform implementation. pub fn restore_terminal() -> Result<(), Box> { disable_raw_mode()?; Ok(()) diff --git a/src/tty_unix.rs b/src/tty_unix.rs index 29c5e47..96bdae0 100644 --- a/src/tty_unix.rs +++ b/src/tty_unix.rs @@ -4,10 +4,83 @@ //! //! # TTY (Unix) //! -//! POSIX Unix TTY handle and raw mode initialization. +//! POSIX Unix TTY handle and raw mode initialization. The implementation is +//! compiled only for Unix targets so Windows never type-checks its termios or +//! raw-file-descriptor operations. //! /// Unix TTY initialization check. pub fn is_unix_tty() -> bool { - !cfg!(target_os = "windows") + cfg!(unix) } + +#[cfg(unix)] +mod unix { + use std::io; + use std::sync::{Mutex, OnceLock}; + + /// Saved terminal state for the raw mode toggle, mirroring the upstream + /// `p.previousTtyInputState` (`term.MakeRaw`/`term.Restore`). + static SAVED_TERMIOS: OnceLock>> = OnceLock::new(); + + fn saved_termios() -> &'static Mutex> { + SAVED_TERMIOS.get_or_init(|| Mutex::new(None)) + } + + /// Enables POSIX raw mode while preserving the upstream output behavior. + pub(super) fn enable_raw_mode() -> io::Result<()> { + use std::os::fd::AsRawFd; + + let fd = io::stdin().as_raw_fd(); + let mut termios: libc::termios = unsafe { std::mem::zeroed() }; + if unsafe { libc::tcgetattr(fd, &mut termios) } != 0 { + return Err(io::Error::last_os_error()); + } + saved_termios() + .lock() + .map_err(|_| io::Error::other("saved terminal state lock poisoned"))? + .replace(termios); + + // This replicates the behavior documented for cfmakeraw in the + // termios(3) manpage, as x/term's makeRaw does. Only OPOST is cleared + // from output flags so TABDLY retains the upstream hard-tab behavior. + termios.c_iflag &= !(libc::IGNBRK + | libc::BRKINT + | libc::PARMRK + | libc::ISTRIP + | libc::INLCR + | libc::IGNCR + | libc::ICRNL + | libc::IXON); + termios.c_oflag &= !libc::OPOST; + termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); + termios.c_cflag &= !(libc::CSIZE | libc::PARENB); + termios.c_cflag |= libc::CS8; + termios.c_cc[libc::VMIN] = 1; + termios.c_cc[libc::VTIME] = 0; + if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + /// Restores the terminal state saved by [`enable_raw_mode`]. + pub(super) fn disable_raw_mode() -> io::Result<()> { + use std::os::fd::AsRawFd; + + let saved = saved_termios() + .lock() + .map_err(|_| io::Error::other("saved terminal state lock poisoned"))? + .take(); + if let Some(termios) = saved { + let fd = io::stdin().as_raw_fd(); + if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) } != 0 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) + } +} + +#[cfg(unix)] +pub(crate) use unix::{disable_raw_mode, enable_raw_mode}; diff --git a/src/tty_windows.rs b/src/tty_windows.rs index 490cfb9..8795b55 100644 --- a/src/tty_windows.rs +++ b/src/tty_windows.rs @@ -4,10 +4,24 @@ //! //! # TTY (Windows) //! -//! Windows VT console mode handle and raw mode initialization. +//! Windows VT console mode handle and raw mode initialization. Windows uses +//! crossterm's safe console-mode operations; no Unix termios or handwritten +//! raw-file-descriptor code is compiled for this target. //! /// Windows TTY initialization check. pub fn is_windows_tty() -> bool { - cfg!(target_os = "windows") + cfg!(windows) +} + +#[cfg(windows)] +/// Enables Windows console raw mode through crossterm's safe API. +pub(crate) fn enable_raw_mode() -> std::io::Result<()> { + crossterm::terminal::enable_raw_mode() +} + +#[cfg(windows)] +/// Disables Windows console raw mode through crossterm's safe API. +pub(crate) fn disable_raw_mode() -> std::io::Result<()> { + crossterm::terminal::disable_raw_mode() } diff --git a/tests/windows_tty.rs b/tests/windows_tty.rs new file mode 100644 index 0000000..d873d69 --- /dev/null +++ b/tests/windows_tty.rs @@ -0,0 +1,22 @@ +//! Native Windows regressions for the platform-separated terminal facade. + +#![cfg(windows)] + +use rusty_bubbletea::{termios_unix, termios_windows, tty, tty_unix, tty_windows}; + +#[test] +fn windows_selects_windows_terminal_modules() { + assert!(tty_windows::is_windows_tty()); + assert!(termios_windows::is_windows_termios()); + assert!(!tty_unix::is_unix_tty()); + assert!(!termios_unix::is_unix_termios()); +} + +#[test] +fn windows_terminal_surface_has_safe_platform_neutral_signatures() { + let _enable: fn() -> std::io::Result<()> = tty::enable_raw_mode; + let _disable: fn() -> std::io::Result<()> = tty::disable_raw_mode; + let _init: fn() -> Result<(), Box> = tty::init_terminal; + let _restore: fn() -> Result<(), Box> = tty::restore_terminal; + let _window_size: fn() -> Result<(u16, u16), Box> = tty::get_window_size; +} From 95f65cab4e55617da517c5c469b2642b0f2b73a4 Mon Sep 17 00:00:00 2001 From: coderbants Date: Sat, 29 Aug 2026 16:32:59 +0100 Subject: [PATCH 2/2] fix(build): unify sibling dependency sources --- .github/workflows/ci.yml | 4 ++++ Cargo.toml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16d0847..016d224 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,10 @@ jobs: mv siblings/rusty-testkit ../rusty-testkit mv siblings/rusty-ultraviolet ../rusty-ultraviolet mv siblings/rusty-x-ansi ../rusty-x-ansi + - name: Verify sibling dependency source identity + run: | + cargo tree --package rusty-bubbletea --invert rusty-colorprofile@0.4.3 + cargo tree --package rusty-bubbletea --invert rusty-x-ansi@0.11.7 - name: Format run: cargo fmt --all --check - name: Lint diff --git a/Cargo.toml b/Cargo.toml index ab15af6..2df8537 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,12 @@ rusty-bubbles = { version = "2.1.0", path = "../rusty-bubbles" } [target.'cfg(unix)'.dev-dependencies] rusty-testkit = { version = "0.1.1", path = "../rusty-testkit" } +[patch.crates-io] +# Root patches are required because Cargo ignores patch sections declared by +# dependencies such as rusty-ultraviolet. Keep every sibling edge on one source. +rusty-colorprofile = { path = "../rusty-colorprofile" } +rusty-x-ansi = { path = "../rusty-x-ansi" } + [lib] name = "rusty_bubbletea" path = "src/lib.rs"