diff -wpruN --no-dereference a~/src/builtins/fg.rs a/src/builtins/fg.rs --- a~/src/builtins/fg.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/builtins/fg.rs 2026-05-20 13:38:23.291989325 +0000 @@ -8,9 +8,7 @@ use crate::tokenizer::tok_command; use crate::{env::EnvMode, tty_handoff::TtyHandoff}; use crate::{err_fmt, err_str}; use fish_util::perror; -use libc::STDIN_FILENO; -use nix::sys::termios::{self, tcsetattr}; -use std::os::fd::BorrowedFd; +use libc::{STDIN_FILENO, TCSADRAIN}; use super::prelude::*; @@ -140,14 +138,9 @@ pub fn fg(parser: &Parser, streams: &mut } let tmodes = job_group.tmodes.borrow(); if job_group.wants_terminal() && tmodes.is_some() { - let tmodes = tmodes.as_ref().unwrap(); - if tcsetattr( - unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, - termios::SetArg::TCSADRAIN, - tmodes, - ) - .is_err() - { + let termios = tmodes.as_ref().unwrap(); + let res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSADRAIN, termios) }; + if res < 0 { perror("tcsetattr"); } } diff -wpruN --no-dereference a~/src/builtins/fish_key_reader.rs a/src/builtins/fish_key_reader.rs --- a~/src/builtins/fish_key_reader.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/builtins/fish_key_reader.rs 2026-05-20 13:38:23.292319209 +0000 @@ -49,7 +49,7 @@ fn should_exit( for evt in [VINTR, VEOF] { let modes = shell_modes(); - let cc = Key::from_single_byte(modes.control_chars[evt]); + let cc = Key::from_single_byte(modes.c_cc[evt]); if match_key_event_to_key(&key_evt, &cc).is_some() { if recent_keys @@ -63,7 +63,7 @@ fn should_exit( } streams.err.appendln(&wgettext_fmt!( "Press ctrl-%c again to exit", - char::from(modes.control_chars[evt] + 0x60) + char::from(modes.c_cc[evt] + 0x60) )); return false; } @@ -164,8 +164,8 @@ fn setup_and_process_keys( let modes = shell_modes(); streams.err.appendln(&wgettext_fmt!( "or press ctrl-%c or ctrl-%c twice in a row.", - char::from(modes.control_chars[VINTR] + 0x60), - char::from(modes.control_chars[VEOF] + 0x60) + char::from(modes.c_cc[VINTR] + 0x60), + char::from(modes.c_cc[VEOF] + 0x60) )); streams.err.appendln(L!("\n")); } diff -wpruN --no-dereference a~/src/common.rs a/src/common.rs --- a~/src/common.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/common.rs 2026-05-20 13:38:23.292733249 +0000 @@ -9,7 +9,6 @@ use crate::{ }; use fish_fallback::fish_wcwidth; use fish_widestring::subslice_position; -use nix::sys::termios::Termios; use std::{ env, sync::{MutexGuard, OnceLock, atomic::Ordering}, @@ -21,7 +20,7 @@ pub const BUILD_DIR: &str = env!("FISH_R pub const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); -pub fn shell_modes() -> MutexGuard<'static, Termios> { +pub fn shell_modes() -> MutexGuard<'static, libc::termios> { crate::reader::SHELL_MODES.lock().unwrap() } diff -wpruN --no-dereference a~/src/input_common.rs a/src/input_common.rs --- a~/src/input_common.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/input_common.rs 2026-05-20 13:38:23.293562243 +0000 @@ -877,7 +877,7 @@ pub trait InputEventQueuer { self.push_back(evt); } }); - let vintr = shell_modes().control_chars[libc::VINTR]; + let vintr = shell_modes().c_cc[libc::VINTR]; if vintr != 0 && key.is_some_and(|key| { match_key_event_to_key(&key, &Key::from_single_byte(vintr)) @@ -1618,7 +1618,7 @@ pub trait InputEventQueuer { fn select_interrupted(&mut self) {} fn enqueue_interrupt_key(&mut self) { - let vintr = shell_modes().control_chars[libc::VINTR]; + let vintr = shell_modes().c_cc[libc::VINTR]; if vintr != 0 { let interrupt_evt = CharEvent::from_key(KeyEvent::from_single_byte(vintr)); if stop_query(self.blocking_query()) { diff -wpruN --no-dereference a~/src/job_group.rs a/src/job_group.rs --- a~/src/job_group.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/job_group.rs 2026-05-20 13:38:23.293920516 +0000 @@ -2,7 +2,6 @@ use crate::global_safety::RelaxedAtomicB use crate::prelude::*; use crate::proc::{JobGroupRef, Pid}; use crate::signal::Signal; -use nix::sys::termios::Termios; use std::cell::RefCell; use std::num::NonZeroU32; use std::sync::atomic::{AtomicI32, Ordering}; @@ -61,7 +60,7 @@ impl<'a> fish_printf::ToArg<'a> for Mayb pub struct JobGroup { /// If set, the saved terminal modes of this job. This needs to be saved so that we can restore /// the terminal to the same state when resuming a stopped job. - pub tmodes: RefCell>, + pub tmodes: RefCell>, /// Whether job control is enabled in this `JobGroup` or not. /// /// If this is set, then the first process in the root job must be external, as it will become diff -wpruN --no-dereference a~/src/reader/reader.rs a/src/reader/reader.rs --- a~/src/reader/reader.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/reader/reader.rs 2026-05-20 13:38:35.221217180 +0000 @@ -125,15 +125,15 @@ use fish_wcstringutil::{ }; use fish_widestring::{ELLIPSIS_CHAR, UTF8_BOM_WCHAR, bytes2wcstring}; use libc::{ - _POSIX_VDISABLE, EIO, EISDIR, ENOTTY, ESRCH, O_NONBLOCK, O_RDONLY, SIGINT, STDERR_FILENO, - STDIN_FILENO, STDOUT_FILENO, VMIN, VQUIT, VSUSP, VTIME, c_char, + _POSIX_VDISABLE, ECHO, EINTR, EIO, EISDIR, ENOTTY, ESRCH, FLUSHO, ICANON, ICRNL, IEXTEN, + INLCR, IXOFF, IXON, O_NONBLOCK, O_RDONLY, ONLCR, OPOST, SIGINT, STDERR_FILENO, STDIN_FILENO, + STDOUT_FILENO, TCSANOW, VMIN, VQUIT, VSUSP, VTIME, c_char, }; use nix::{ fcntl::OFlag, sys::{ signal::{Signal, killpg}, stat::Mode, - termios::{self, SetArg, Termios, tcgetattr, tcsetattr}, }, unistd::{getpgrp, getpid, setpgid}, }; @@ -142,6 +142,7 @@ use std::{ cell::UnsafeCell, cmp, io::BufReader, + mem::MaybeUninit, num::NonZeroUsize, ops::{ControlFlow, Range}, os::fd::{AsRawFd as _, BorrowedFd, FromRawFd as _, OwnedFd, RawFd}, @@ -166,19 +167,15 @@ enum ExitState { static EXIT_STATE: AtomicU8 = AtomicU8::new(ExitState::None as u8); -fn zeroed_termios() -> Termios { - let termios: libc::termios = unsafe { std::mem::zeroed() }; - termios.into() -} - -pub static SHELL_MODES: LazyLock> = LazyLock::new(|| Mutex::new(zeroed_termios())); +pub static SHELL_MODES: LazyLock> = + LazyLock::new(|| Mutex::new(unsafe { std::mem::zeroed() })); /// The valid terminal modes on startup. static TERMINAL_MODE_ON_STARTUP: OnceLock = OnceLock::new(); /// Mode we use to execute programs. -static TTY_MODES_FOR_EXTERNAL_CMDS: LazyLock> = - LazyLock::new(|| Mutex::new(zeroed_termios())); +static TTY_MODES_FOR_EXTERNAL_CMDS: LazyLock> = + LazyLock::new(|| Mutex::new(unsafe { std::mem::zeroed() })); static RUN_COUNT: AtomicU64 = AtomicU64::new(0); @@ -222,11 +219,13 @@ fn redirect_tty_after_sighup() { }; let fd = devnull.as_raw_fd(); for stdfd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] { - if matches!( - tcgetattr(unsafe { BorrowedFd::borrow_raw(stdfd) }), - Err(nix::Error::EIO | nix::Error::ENOTTY) - ) { - unsafe { libc::dup2(fd, stdfd) }; + let mut t = std::mem::MaybeUninit::uninit(); + unsafe { + if libc::tcgetattr(stdfd, t.as_mut_ptr()) != 0 + && matches!(errno::errno().0, EIO | ENOTTY) + { + libc::dup2(fd, stdfd); + } } } } @@ -985,24 +984,19 @@ fn read_ni(parser: &Parser, fd: RawFd, i } } -const FLOW_CONTROL_FLAGS: termios::InputFlags = { - use termios::InputFlags; - InputFlags::IXON.union(InputFlags::IXOFF) -}; +const FLOW_CONTROL_FLAGS: libc::tcflag_t = IXON | IXOFF; /// Initialize the reader. pub fn reader_init(will_restore_foreground_pgroup: bool) { assert_is_main_thread(); - let terminal_mode_on_startup = match tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) - { - Ok(modes) => { // Save the initial terminal mode. + // Note this field is read by a signal handler, so do it atomically, with a leaked mode. + let mut terminal_mode_on_startup = unsafe { std::mem::zeroed::() }; + let ret = unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut terminal_mode_on_startup) }; // TODO: rationalize behavior if initial tcgetattr() fails. - TERMINAL_MODE_ON_STARTUP.get_or_init(|| libc::termios::from(modes.clone())); - modes + if ret == 0 { + TERMINAL_MODE_ON_STARTUP.get_or_init(|| terminal_mode_on_startup); } - Err(_) => zeroed_termios(), - }; AT_EXIT .set(Box::new(move || { @@ -1016,12 +1010,12 @@ pub fn reader_init(will_restore_foregrou term_fix_external_modes(&mut external_modes); // Disable flow control by default. - external_modes.input_flags &= !FLOW_CONTROL_FLAGS; + external_modes.c_iflag &= !FLOW_CONTROL_FLAGS; // Set the mode used for the terminal, initialized to the current mode. { let mut shell_modes = shell_modes(); - *shell_modes = external_modes.clone(); + *shell_modes = external_modes; term_fix_shell_modes(&mut shell_modes); } @@ -1050,7 +1044,7 @@ pub fn restore_term_mode() { return; } if let Some(modes) = get_terminal_mode_on_startup() { - unsafe { libc::tcsetattr(STDIN_FILENO, libc::TCSANOW, modes) }; + unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, modes) }; } } @@ -1221,7 +1215,7 @@ pub fn reader_reading_interrupted(data: /// commandline. pub fn reader_readline( parser: &Parser, - old_modes: Option, + old_modes: Option, nchars: Option, ) -> Option { let data = current_data().unwrap(); @@ -2558,7 +2552,7 @@ impl<'a> Reader<'a> { /// Return the command, or none if we were asked to cancel (e.g. SIGHUP). fn readline( &mut self, - old_modes: Option, + old_modes: Option, nchars: Option, ) -> Option { let mut tty = TtyHandoff::new(reader_save_screen_state); @@ -2656,14 +2650,10 @@ impl<'a> Reader<'a> { // The order of the two conditions below is important. Try to restore the mode // in all cases, but only complain if interactive. if let Some(old_modes) = old_modes { - if let Err(err) = tcsetattr( - unsafe { BorrowedFd::borrow_raw(self.conf.inputfd) }, - SetArg::TCSANOW, - &old_modes, - ) { - if is_interactive_session() { - perror_nix("tcsetattr", err); - } + if unsafe { libc::tcsetattr(self.conf.inputfd, TCSANOW, &old_modes) } == -1 + && is_interactive_session() + { + perror("tcsetattr"); } } Outputter::stdoutput().borrow_mut().reset_text_face(); @@ -4729,39 +4719,32 @@ impl ReaderData { // Turning off OPOST or ONLCR breaks output (staircase effect), we don't allow it. // See #7133. -fn term_fix_oflag(modes: &mut Termios) { - modes.output_flags |= { - use termios::OutputFlags; +fn term_fix_oflag(modes: &mut libc::termios) { + modes.c_oflag |= { // turn on "implementation-defined post processing" - this often changes how line breaks work. - OutputFlags::OPOST + OPOST // "translate newline to carriage return-newline" - without you see staircase output. - | OutputFlags::ONLCR + | ONLCR }; } /// Restore terminal settings we care about, to prevent a broken shell. -fn term_fix_shell_modes(modes: &mut Termios) { - modes.input_flags &= { - use termios::InputFlags; +fn term_fix_shell_modes(modes: &mut libc::termios) { + modes.c_iflag &= { // disable mapping CR (\cM) to NL (\cJ) - !InputFlags::ICRNL + !ICRNL // disable mapping NL (\cJ) to CR (\cM) - & !InputFlags::INLCR + & !INLCR }; - modes.local_flags &= { - use termios::LocalFlags; - let echo = LocalFlags::ECHO; - let flusho = LocalFlags::FLUSHO; - let icanon = LocalFlags::ICANON; - let iexten = LocalFlags::IEXTEN; - !echo - & !icanon - & !iexten // turn off handling of discard and lnext characters - & !flusho + modes.c_lflag &= { + !ECHO + & !ICANON + & !IEXTEN // turn off handling of discard and lnext characters + & !FLUSHO }; term_fix_oflag(modes); - let c_cc = &mut modes.control_chars; + let c_cc = &mut modes.c_cc; c_cc[VMIN] = 1; c_cc[VTIME] = 0; @@ -4775,95 +4758,81 @@ fn term_fix_shell_modes(modes: &mut Term c_cc[VQUIT] = disabling_char; } -fn term_fix_external_modes(modes: &mut Termios) { +fn term_fix_external_modes(modes: &mut libc::termios) { term_fix_oflag(modes); // These cause other ridiculous behaviors like input not being shown. - modes.local_flags = { - use termios::LocalFlags; - let echo = LocalFlags::ECHO; - let flusho = LocalFlags::FLUSHO; - let icanon = LocalFlags::ICANON; - let iexten = LocalFlags::IEXTEN; - (modes.local_flags | echo | icanon | iexten) & !flusho - }; - modes.input_flags = { - use termios::InputFlags; - let icrnl = InputFlags::ICRNL; - let inlcr = InputFlags::INLCR; - (modes.input_flags | icrnl) & !inlcr - }; + modes.c_lflag = (modes.c_lflag | ECHO | ICANON | IEXTEN) & !FLUSHO; + modes.c_iflag = (modes.c_iflag | ICRNL) & !INLCR; } /// Give up control of terminal. fn term_donate(quiet: bool /* = false */) { - loop { - match tcsetattr( - unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, - SetArg::TCSANOW, - &TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(), - ) { - Ok(_) => (), - Err(nix::Error::EINTR) => continue, - Err(err) => { + while unsafe { + libc::tcsetattr( + STDIN_FILENO, + TCSANOW, + &*TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(), + ) + } == -1 + { + if errno().0 != EINTR { if !quiet { flog!( warning, wgettext!("Could not set terminal mode for new job") ); - perror_nix("tcsetattr", err); + perror("tcsetattr"); } break; } } - break; - } } /// Copy the (potentially changed) terminal modes and use them from now on. pub fn term_copy_modes() { - let mut external_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) - .unwrap_or_else(|_| zeroed_termios()); + let mut modes = MaybeUninit::uninit(); + unsafe { libc::tcgetattr(STDIN_FILENO, modes.as_mut_ptr()) }; + let mut external_modes = unsafe { modes.assume_init() }; // We still want to fix most egregious breakage. // E.g. OPOST is *not* something that should be set globally, // and 99% triggered by a crashed program. term_fix_external_modes(&mut external_modes); - let external_flow_control = external_modes.input_flags & FLOW_CONTROL_FLAGS; *TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap() = external_modes; let mut shell_modes = shell_modes(); - shell_modes.input_flags = - (shell_modes.input_flags & !FLOW_CONTROL_FLAGS) | external_flow_control; + shell_modes.c_iflag = + (shell_modes.c_iflag & !FLOW_CONTROL_FLAGS) | (external_modes.c_iflag & FLOW_CONTROL_FLAGS); } pub fn set_shell_modes(fd: RawFd, whence: &str) -> bool { - loop { - match tcsetattr( - unsafe { BorrowedFd::borrow_raw(fd) }, - SetArg::TCSANOW, - &shell_modes(), - ) { - Ok(_) => return true, - Err(nix::Error::EINTR) => continue, - Err(err) => { - perror_nix("tcsetattr", err); + let ok = loop { + let ok = unsafe { libc::tcsetattr(fd, TCSANOW, &*shell_modes()) } != -1; + if ok || errno().0 != EINTR { + break ok; + } + }; + if !ok { + perror("tcsetattr"); flog!( warning, wgettext_fmt!("Failed to set terminal mode (%s)", whence) ); - return false; - } - } } + ok } -pub fn set_shell_modes_temporarily(inputfd: RawFd) -> Option { +pub fn set_shell_modes_temporarily(inputfd: RawFd) -> Option { // It may happen that a command we ran when job control was disabled nevertheless stole the tty // from us. In that case when we read from our fd, it will trigger SIGTTIN. So just // unconditionally reclaim the tty. See #9181. unsafe { libc::tcsetpgrp(inputfd, libc::getpgrp()) }; // Get the current terminal modes. These will be restored when the function returns. - let old_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(inputfd) }).ok(); + let old_modes = { + let mut old_modes = MaybeUninit::uninit(); + let ok = unsafe { libc::tcgetattr(inputfd, old_modes.as_mut_ptr()) } == 0; + ok.then(|| unsafe { old_modes.assume_init() }) + }; // Set the new modes. set_shell_modes(inputfd, "readline"); diff -wpruN --no-dereference a~/src/screen.rs a/src/screen.rs --- a~/src/screen.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/screen.rs 2026-05-20 13:38:23.298645182 +0000 @@ -28,8 +28,7 @@ use fish_common::write_loop; use fish_fallback::{fish_wcswidth_canonicalizing, fish_wcwidth}; use fish_wcstringutil::{fish_wcwidth_visible, string_prefixes_string}; use fish_widestring::{ELLIPSIS_CHAR, wcs2bytes}; -use libc::{STDERR_FILENO, STDOUT_FILENO}; -use nix::sys::termios; +use libc::{ONLCR, STDERR_FILENO, STDOUT_FILENO}; use std::cell::RefCell; use std::collections::LinkedList; use std::io::Write as _; @@ -889,10 +888,7 @@ impl Screen { let s = if y_steps < 0 { Some(CursorUp) } else if y_steps > 0 { - if shell_modes() - .output_flags - .contains(termios::OutputFlags::ONLCR) - { + if (shell_modes().c_oflag & ONLCR) != 0 { // See GitHub issue #4505. // Most consoles use a simple newline as the cursor down escape. // If ONLCR is enabled (which it normally is) this will of course diff -wpruN --no-dereference a~/src/tty_handoff.rs a/src/tty_handoff.rs --- a~/src/tty_handoff.rs 2026-05-07 16:02:14.000000000 +0000 +++ a/src/tty_handoff.rs 2026-05-20 13:38:23.299025193 +0000 @@ -16,13 +16,12 @@ use crate::terminal::TerminalCommand::{ KittyKeyboardProgressiveEnhancementsEnable, ModifyOtherKeysDisable, ModifyOtherKeysEnable, }; use crate::threads::assert_is_main_thread; -use crate::wutil::{perror_nix, wcstoi}; +use crate::wutil::wcstoi; use fish_common::write_loop; use fish_util::perror; use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG}; -use nix::sys::termios::tcgetattr; use nix::unistd::getpgrp; -use std::os::fd::BorrowedFd; +use std::mem::MaybeUninit; use std::sync::{ OnceLock, atomic::{AtomicPtr, Ordering}, @@ -402,17 +401,12 @@ impl TtyHandoff { /// Save the current tty modes into the owning job group, if we are transferred. pub fn save_tty_modes(&mut self) { - let Some(ref mut owner) = self.owner else { - return; - }; - match tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) { - Ok(modes) => { - owner.tmodes.replace(Some(modes)); - } - Err(err) => { - if err != nix::Error::ENOTTY { - perror_nix("tcgetattr", err); - } + if let Some(ref mut owner) = self.owner { + let mut tmodes = MaybeUninit::uninit(); + if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 { + owner.tmodes.replace(Some(unsafe { tmodes.assume_init() })); + } else if errno::errno().0 != ENOTTY { + perror("tcgetattr"); } } }