From 65aa939da4b42a328756e52e62c15271524bb178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:14:58 +0100 Subject: [PATCH 1/9] stty: Wrap parameters when using --all Do the same as GNU stty when it has to prints the parameter, doing proper text wrapping --- src/uu/stty/src/stty.rs | 166 ++++++++++++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 42 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index fdeee252df3..9862b76c644 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -492,8 +492,67 @@ fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> { Ok(()) } -fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { +struct WrappedPrinter { + width: usize, + current: usize, + first_in_line: bool, +} + +impl WrappedPrinter { + fn new(term_size: &Option) -> Self { + let columns = match term_size { + Some(term_size) => term_size.columns, + None => { + const DEFAULT_TERM_WIDTH: u16 = 80; + + std::env::var_os("COLUMNS") + .and_then(|s| s.to_str()?.parse().ok()) + .filter(|&c| c > 0) + .unwrap_or(DEFAULT_TERM_WIDTH) + } + }; + + Self { + width: columns.max(1) as usize, + current: 0, + first_in_line: true, + } + } + + fn print(&mut self, token: &str) { + let token_len = self.prefix().chars().count() + token.chars().count(); + if self.current > 0 && self.current + token_len > self.width { + println!(); + self.current = 0; + self.first_in_line = true; + } + + print!("{}{}", self.prefix(), token); + self.current += token_len; + self.first_in_line = false; + } + + fn prefix(&self) -> &str { + if self.first_in_line { "" } else { " " } + } + + fn flush(&mut self) { + if self.current > 0 { + println!(); + self.current = 0; + self.first_in_line = false; + } + } +} + +fn print_terminal_size( + termios: &Termios, + opts: &Options, + window_size: &Option, + term_size: &Option, +) -> nix::Result<()> { let speed = cfgetospeed(termios); + let mut printer = WrappedPrinter::new(window_size); // BSDs use a u32 for the baud rate, so we can simply print it. #[cfg(any( @@ -518,17 +577,15 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { )))] for (text, baud_rate) in BAUD_RATES { if *baud_rate == speed { - print!("{} ", translate!("stty-output-speed", "speed" => (*text))); + printer.print(&translate!("stty-output-speed", "speed" => (*text))); break; } } if opts.all { - let mut size = TermSize::default(); - unsafe { tiocgwinsz(opts.file.as_raw_fd(), &raw mut size)? }; - print!( - "{} ", - translate!("stty-output-rows-columns", "rows" => size.rows, "columns" => size.columns) + let term_size = term_size.as_ref().expect("terminal size should be set"); + printer.print( + &translate!("stty-output-rows-columns", "rows" => term_size.rows, "columns" => term_size.columns), ); } @@ -538,10 +595,9 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { // so we get the underlying libc::termios struct to get that information. let libc_termios: nix::libc::termios = termios.clone().into(); let line = libc_termios.c_line; - print!("{}", translate!("stty-output-line", "line" => line)); + printer.print(&translate!("stty-output-line", "line" => line)); } - - println!(); + printer.flush(); Ok(()) } @@ -647,39 +703,41 @@ fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result { Ok(format!("{meta_prefix}{ctrl_prefix}{character}")) } -fn print_control_chars(termios: &Termios, opts: &Options) -> nix::Result<()> { +fn print_control_chars( + termios: &Termios, + opts: &Options, + term_size: &Option, +) -> nix::Result<()> { if !opts.all { // Print only control chars that differ from sane defaults - let mut printed = false; + let mut printer = WrappedPrinter::new(term_size); for (text, cc_index) in CONTROL_CHARS { let current_val = termios.control_chars[*cc_index as usize]; let sane_val = get_sane_control_char(*cc_index); if current_val != sane_val { - print!("{text} = {}; ", control_char_to_string(current_val)?); - printed = true; + printer.print(&format!( + "{text} = {};", + control_char_to_string(current_val)? + )); } } - - if printed { - println!(); - } + printer.flush(); return Ok(()); } + let mut printer = WrappedPrinter::new(term_size); for (text, cc_index) in CONTROL_CHARS { - print!( - "{text} = {}; ", + printer.print(&format!( + "{text} = {};", control_char_to_string(termios.control_chars[*cc_index as usize])? - ); + )); } - println!( - "{}", - translate!("stty-output-min-time", + printer.print(&translate!("stty-output-min-time", "min" => termios.control_chars[S::VMIN as usize], "time" => termios.control_chars[S::VTIME as usize] - ) - ); + )); + printer.flush(); Ok(()) } @@ -701,18 +759,45 @@ fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { if opts.save { print_in_save_format(termios); } else { - print_terminal_size(termios, opts)?; - print_control_chars(termios, opts)?; - print_flags(termios, opts, CONTROL_FLAGS); - print_flags(termios, opts, INPUT_FLAGS); - print_flags(termios, opts, OUTPUT_FLAGS); - print_flags(termios, opts, LOCAL_FLAGS); + let device_fd = opts.file.as_raw_fd(); + let term_size = { + let mut term_size = TermSize::default(); + let term_size = + unsafe { tiocgwinsz(device_fd, &raw mut term_size) }.map(|_| term_size); + if opts.all { + Some(term_size?) + } else { + term_size.ok() + } + }; + + let stdout_fd = stdout().as_raw_fd(); + let window_size = if device_fd != stdout_fd { + let mut term_size = TermSize::default(); + &unsafe { tiocgwinsz(stdout_fd, &raw mut term_size) } + .map(|_| term_size) + .ok() + } else { + &term_size + }; + + print_terminal_size(termios, opts, &window_size, &term_size)?; + print_control_chars(termios, opts, &window_size)?; + print_flags(termios, opts, CONTROL_FLAGS, &window_size); + print_flags(termios, opts, INPUT_FLAGS, &window_size); + print_flags(termios, opts, OUTPUT_FLAGS, &window_size); + print_flags(termios, opts, LOCAL_FLAGS, &window_size); } Ok(()) } -fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag]) { - let mut printed = false; +fn print_flags( + termios: &Termios, + opts: &Options, + flags: &[Flag], + term_size: &Option, +) { + let mut printer = WrappedPrinter::new(term_size); for &Flag { name, flag, @@ -727,20 +812,17 @@ fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag< let val = flag.is_in(termios, group); if group.is_some() { if val && (!sane || opts.all) { - print!("{name} "); - printed = true; + printer.print(name); } } else if opts.all || val != sane { if !val { - print!("-"); + printer.print(&format!("-{name}")); + continue; } - print!("{name} "); - printed = true; + printer.print(name); } } - if printed { - println!(); - } + printer.flush(); } /// Apply a single setting From c8f02fc555a518e51d00965a9d0f89ebf8e3c551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:17:16 +0100 Subject: [PATCH 2/9] stty/flags: Do not show tandem option It is just an alias for ixoff, and it's marked with `OMIT` in GNU version --- src/uu/stty/src/flags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index c10e7c04b39..8199b8e2e7b 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -82,8 +82,8 @@ pub const INPUT_FLAGS: &[Flag] = &[ Flag::new("igncr", I::IGNCR), Flag::new("icrnl", I::ICRNL).sane(), Flag::new("ixoff", I::IXOFF), - Flag::new("tandem", I::IXOFF), Flag::new("ixon", I::IXON), + Flag::new("tandem", I::IXOFF).hidden(), // not supported by nix // Flag::new("iuclc", I::IUCLC), Flag::new("ixany", I::IXANY), From d4b886e2fcc0306eab452b2888d7349153f83297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:19:22 +0100 Subject: [PATCH 3/9] stty/flags: Use same order of GNU in printing ixoff/ixon Now the order is respected --- src/uu/stty/src/flags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 8199b8e2e7b..182d4f3bbab 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -81,8 +81,8 @@ pub const INPUT_FLAGS: &[Flag] = &[ Flag::new("inlcr", I::INLCR), Flag::new("igncr", I::IGNCR), Flag::new("icrnl", I::ICRNL).sane(), - Flag::new("ixoff", I::IXOFF), Flag::new("ixon", I::IXON), + Flag::new("ixoff", I::IXOFF), Flag::new("tandem", I::IXOFF).hidden(), // not supported by nix // Flag::new("iuclc", I::IUCLC), From fcbe06294d09de53fc6d9d26bc6ca918a0530c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:20:17 +0100 Subject: [PATCH 4/9] stty/flags: Add missing tabs combination setting --- src/uu/stty/src/flags.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 182d4f3bbab..101f79b321d 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -385,4 +385,5 @@ pub const COMBINATION_SETTINGS: &[(&str, bool)] = &[ ("pass8", true), ("raw", true), ("sane", false), + ("tabs", true), ]; From e91bd05a59d027b328c0d363af67405a9143b960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:22:13 +0100 Subject: [PATCH 5/9] stty/flags: Add delayed suspend (dsusp) support It's only supported by BSD, aix and solaris, but still available in nix --- src/uu/stty/src/flags.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 101f79b321d..b3cd27f6b31 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -7,7 +7,7 @@ // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf // spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc -// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase +// spell-checker:ignore lnext rprnt susp dsusp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase VDSUSP // spell-checker:ignore sigquit sigtstp // spell-checker:ignore cbreak decctlq evenp litout oddp @@ -357,6 +357,18 @@ pub const CONTROL_CHARS: &[(&str, S)] = &[ ("stop", S::VSTOP), // Sends a suspend signal (SIGTSTP). ("susp", S::VSUSP), + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "aix", + target_os = "solaris" + ))] + // Sends a delayed suspend signal (SIGTSTP). + ("dsusp", S::VDSUSP), // Reprints the current line. ("rprnt", S::VREPRINT), // Deletes the last word typed. From 7cb5a1848a9ff02f732a6b69b2583f6f0da962cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 21 Nov 2025 04:23:52 +0100 Subject: [PATCH 6/9] stty/flags: List all missing flags that GNU version supports List the remaining flags that GNU stty support, they cannot be be listed yet since we have no nix support for them, but it's better to keep the lists in sync so that it's easier to enable what will be supported in future --- src/uu/stty/src/flags.rs | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index b3cd27f6b31..e6ab6a703e4 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -10,6 +10,7 @@ // spell-checker:ignore lnext rprnt susp dsusp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase VDSUSP // spell-checker:ignore sigquit sigtstp // spell-checker:ignore cbreak decctlq evenp litout oddp +// spell-checker:ignore cdtrdsr CDTRDSR ofill OFILL dsusp VDSUSP VFLUSHO VSTATUS noncanonical VMIN deciseconds noncanonical VTIME use crate::Flag; @@ -65,10 +66,14 @@ pub const CONTROL_FLAGS: &[Flag] = &[ Flag::new_grouped("cs7", C::CS7, C::CSIZE), Flag::new_grouped("cs8", C::CS8, C::CSIZE).sane(), Flag::new("hupcl", C::HUPCL), + // Not supported by nix and libc. + // Flag::new("hup", C::HUP).hidden(), Flag::new("cstopb", C::CSTOPB), Flag::new("cread", C::CREAD).sane(), Flag::new("clocal", C::CLOCAL), Flag::new("crtscts", C::CRTSCTS), + // Not supported by nix and libc. + // Flag::new("cdtrdsr", C::CDTRDSR), ]; pub const INPUT_FLAGS: &[Flag] = &[ @@ -112,6 +117,16 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ target_os = "linux", target_os = "macos" ))] + // Not supported by nix. + // See: https://github.com/nix-rust/nix/pull/2701 + // #[cfg(any( + // target_os = "android", + // target_os = "haiku", + // target_os = "ios", + // target_os = "linux", + // target_os = "macos" + // ))] + // Flag::new("ofill", O::OFILL), Flag::new("ofdel", O::OFDEL), #[cfg(any( target_os = "android", @@ -253,7 +268,15 @@ pub const LOCAL_FLAGS: &[Flag] = &[ Flag::new("echok", L::ECHOK).sane(), Flag::new("echonl", L::ECHONL), Flag::new("noflsh", L::NOFLSH), - // Not supported by nix + // Not supported by nix and libc: + // - https://github.com/rust-lang/libc/pull/4847 + // - https://github.com/nix-rust/nix/pull/2703 + // #[cfg(any( + // target_os = "aix", + // target_os = "android", + // target_os = "haiku", + // target_os = "linux", + // ))] // Flag::new("xcase", L::XCASE), Flag::new("tostop", L::TOSTOP), Flag::new("echoprt", L::ECHOPRT), @@ -377,6 +400,25 @@ pub const CONTROL_CHARS: &[(&str, S)] = &[ ("lnext", S::VLNEXT), // Discards the current line. ("discard", S::VDISCARD), + // deprecated compat option. + // Not supported by nix and libc. + // ("flush", S::VFLUSHO), + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + ))] + // Status character + ("status", S::VSTATUS), + // Minimum number of characters for noncanonical read. + // We handle this manually. + // ("min", S::VMIN), + // Timeout in deciseconds for noncanonical read. + // We handle this manually. + // ("time", S::VTIME), ]; /// This constant lists all possible combination settings, using a bool to represent if the setting is negatable From 149fa34309b2fe22c4217f53ab7f7bedb4dc5212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sat, 22 Nov 2025 00:55:35 +0100 Subject: [PATCH 7/9] stty/flags: Add references to support to IUCLC This is going to be supported via those PRs, so let's track them to make it easier to support it in future --- src/uu/stty/src/flags.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index e6ab6a703e4..c1b8994aca5 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -89,7 +89,15 @@ pub const INPUT_FLAGS: &[Flag] = &[ Flag::new("ixon", I::IXON), Flag::new("ixoff", I::IXOFF), Flag::new("tandem", I::IXOFF).hidden(), - // not supported by nix + // not supported by nix and libc: + // - https://github.com/rust-lang/libc/pull/4846 + // - https://github.com/nix-rust/nix/pull/2702 + // #[cfg(any( + // target_os = "aix", + // target_os = "android", + // target_os = "haiku", + // target_os = "linux", + // ))] // Flag::new("iuclc", I::IUCLC), Flag::new("ixany", I::IXANY), Flag::new("imaxbel", I::IMAXBEL).sane(), From 70a7cd31b35feeefa5d4485b5875f67c5a80c8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sat, 22 Nov 2025 04:59:48 +0100 Subject: [PATCH 8/9] stty: Use libc definition for OFILL Until nix won't include it we can safely just rely on the libc definition --- src/uu/stty/src/flags.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index c1b8994aca5..c8717bfd313 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -125,16 +125,17 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ target_os = "linux", target_os = "macos" ))] + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] // Not supported by nix. // See: https://github.com/nix-rust/nix/pull/2701 - // #[cfg(any( - // target_os = "android", - // target_os = "haiku", - // target_os = "ios", - // target_os = "linux", - // target_os = "macos" - // ))] - // Flag::new("ofill", O::OFILL), + // FIXME: Flag::new("ofill", O::OFILL), + Flag::new("ofill", O::from_bits_retain(nix::libc::OFILL)), Flag::new("ofdel", O::OFDEL), #[cfg(any( target_os = "android", From 61ce79b36cf11ecc4dc6d608d75a01ff45c5fafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sat, 22 Nov 2025 05:15:15 +0100 Subject: [PATCH 9/9] stty: Use c_line (as line_discipline) from nix Nix now supports the line discipline parameter in various platforms, so use upstream definition instead of going through libc See: https://github.com/nix-rust/nix/issues/1802 --- src/uu/stty/src/stty.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 9862b76c644..3977d6af68d 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -589,12 +589,9 @@ fn print_terminal_size( ); } - #[cfg(any(target_os = "linux", target_os = "redox"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "haiku"))] { - // For some reason the normal nix Termios struct does not expose the line, - // so we get the underlying libc::termios struct to get that information. - let libc_termios: nix::libc::termios = termios.clone().into(); - let line = libc_termios.c_line; + let line = termios.line_discipline; printer.print(&translate!("stty-output-line", "line" => line)); } printer.flush(); @@ -888,7 +885,7 @@ fn apply_special_setting( SpecialSetting::Cols(n) => size.columns = *n, SpecialSetting::Line(_n) => { // nix only defines Termios's `line_discipline` field on these platforms - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "haiku"))] { _termios.line_discipline = *_n; }