diff -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/builtins/fg.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/builtins/fg.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/builtins/fg.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/builtins/fg.rs 2026-02-01 03:58:51.000000000 -0500 @@ -6,9 +6,7 @@ use crate::tokenizer::tok_command; use crate::wutil::perror; use crate::{env::EnvMode, tty_handoff::TtyHandoff}; -use libc::STDIN_FILENO; -use nix::sys::termios::{self, tcsetattr}; -use std::os::fd::BorrowedFd; +use libc::{STDIN_FILENO, TCSADRAIN}; use super::prelude::*; @@ -144,14 +142,9 @@ } 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 -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/builtins/fish_key_reader.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/builtins/fish_key_reader.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/builtins/fish_key_reader.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/builtins/fish_key_reader.rs 2026-02-01 03:58:51.000000000 -0500 @@ -47,7 +47,7 @@ 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 @@ -61,7 +61,7 @@ } 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; } @@ -162,8 +162,8 @@ 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 -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/common.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/common.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/common.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/common.rs 2026-02-01 03:58:51.000000000 -0500 @@ -19,7 +19,6 @@ use fish_widestring::{ ENCODE_DIRECT_END, decode_byte_from_char, encode_byte_to_char, subslice_position, }; -use nix::sys::termios::Termios; use std::env; use std::ffi::{CStr, CString, OsStr, OsString}; use std::os::unix::prelude::*; @@ -885,7 +884,7 @@ Some(in_pos) } -pub fn shell_modes() -> MutexGuard<'static, Termios> { +pub fn shell_modes() -> MutexGuard<'static, libc::termios> { crate::reader::SHELL_MODES.lock().unwrap() } diff -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/input_common.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/input_common.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/input_common.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/input_common.rs 2026-02-01 03:58:51.000000000 -0500 @@ -878,7 +878,7 @@ 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)) @@ -1619,7 +1619,7 @@ 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 -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/job_group.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/job_group.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/job_group.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/job_group.rs 2026-02-01 03:58:51.000000000 -0500 @@ -2,7 +2,6 @@ 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 @@ 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 -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/reader/reader.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/reader/reader.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/reader/reader.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/reader/reader.rs 2026-02-01 03:58:51.000000000 -0500 @@ -127,10 +127,10 @@ }; use fish_wcstringutil::{IsPrefix, is_prefix}; use libc::{ - _POSIX_VDISABLE, EIO, EISDIR, ENOTTY, EPERM, 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, EPERM, 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::sys::termios::{self, SetArg, Termios, tcgetattr, tcsetattr}; use nix::{ fcntl::OFlag, sys::{ @@ -144,6 +144,7 @@ cell::UnsafeCell, cmp, io::BufReader, + mem::MaybeUninit, num::NonZeroUsize, ops::{ControlFlow, Range}, os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}, @@ -168,20 +169,16 @@ 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. /// Warning: this is read from the SIGTERM handler! Hence the raw global. 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); @@ -226,11 +223,13 @@ }; 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); + } } } } @@ -987,24 +986,18 @@ } } -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) { - 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. - // TODO: rationalize behavior if initial tcgetattr() fails. - TERMINAL_MODE_ON_STARTUP.get_or_init(|| libc::termios::from(modes.clone())); - modes - } - Err(_) => zeroed_termios(), - }; + // 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. + if ret == 0 { + TERMINAL_MODE_ON_STARTUP.get_or_init(|| terminal_mode_on_startup); + } if !cfg!(test) { assert!(AT_EXIT.get().is_none()); @@ -1016,12 +1009,12 @@ 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); } @@ -1051,7 +1044,7 @@ return; } if let Some(modes) = safe_get_terminal_mode_on_startup() { - unsafe { libc::tcsetattr(STDIN_FILENO, libc::TCSANOW, modes) }; + unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, modes) }; } } @@ -1213,7 +1206,7 @@ /// commandline. pub fn reader_readline( parser: &Parser, - old_modes: Option, + old_modes: Option, nchars: Option, ) -> Option { let data = current_data().unwrap(); @@ -2542,7 +2535,7 @@ /// 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); @@ -2640,12 +2633,7 @@ // 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 tcsetattr( - unsafe { BorrowedFd::borrow_raw(self.conf.inputfd) }, - SetArg::TCSANOW, - &old_modes, - ) - .is_err() + if unsafe { libc::tcsetattr(self.conf.inputfd, TCSANOW, &old_modes) } == -1 && is_interactive_session() { perror("tcsetattr"); @@ -4714,39 +4702,32 @@ // 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; @@ -4760,76 +4741,57 @@ 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(_) => { - if !quiet { - flog!( - warning, - wgettext!("Could not set terminal mode for new job") - ); - perror("tcsetattr"); - } - break; + 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("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 { let ok = loop { - match tcsetattr( - unsafe { BorrowedFd::borrow_raw(fd) }, - SetArg::TCSANOW, - &shell_modes(), - ) { - Ok(_) => break true, - Err(nix::Error::EINTR) => continue, - Err(_) => break false, + let ok = unsafe { libc::tcsetattr(fd, TCSANOW, &*shell_modes()) } != -1; + if ok || errno().0 != EINTR { + break ok; } }; if !ok { @@ -4842,14 +4804,18 @@ 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 -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/screen.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/screen.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/screen.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/screen.rs 2026-02-01 03:58:51.000000000 -0500 @@ -19,8 +19,7 @@ use std::sync::atomic::AtomicU32; use std::time::SystemTime; -use libc::{STDERR_FILENO, STDOUT_FILENO}; -use nix::sys::termios; +use libc::{ONLCR, STDERR_FILENO, STDOUT_FILENO}; use crate::common::{ get_ellipsis_char, get_omitted_newline_str, has_working_tty_timestamps, shell_modes, wcs2bytes, @@ -891,9 +891,7 @@ 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. diff -Naur fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/tty_handoff.rs fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/tty_handoff.rs --- fish-shell-bb7dce941acf51b11ee49994f799ba90911872ac/src/tty_handoff.rs 2026-02-01 04:13:13.000000000 -0500 +++ fish-shell-55e925526480e9e22d659724660ae9e484bc77fd/src/tty_handoff.rs 2026-02-01 03:58:51.000000000 -0500 @@ -20,9 +20,8 @@ use crate::wutil::{perror, wcstoi}; use fish_widestring::ToWString; 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::{AtomicBool, AtomicPtr, Ordering}, @@ -409,17 +408,12 @@ /// 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("tcgetattr"); - } + 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"); } } }