Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 128 additions & 43 deletions src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,71 @@ fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> {
Ok(())
}

fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
/// Handles line wrapping for stty output to fit within terminal width
struct WrappedPrinter {
width: usize,
current: usize,
first_in_line: bool,
}

impl WrappedPrinter {
/// Creates a new printer with the specified terminal width.
/// If term_size is None (typically when output is piped), falls back to
/// the COLUMNS environment variable or a default width of 80 columns.
fn new(term_size: Option<&TermSize>) -> 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<&TermSize>,
term_size: Option<&TermSize>,
) -> 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(
Expand All @@ -551,7 +614,7 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
target_os = "netbsd",
target_os = "openbsd"
))]
print!("{} ", translate!("stty-output-speed", "speed" => speed));
printer.print(&translate!("stty-output-speed", "speed" => speed));

// Other platforms need to use the baud rate enum, so printing the right value
// becomes slightly more complicated.
Expand All @@ -565,17 +628,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),
);
}

Expand All @@ -585,10 +646,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(())
}

Expand Down Expand Up @@ -756,39 +816,41 @@ fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result<String> {
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<&TermSize>,
) -> 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(())
}

Expand All @@ -806,22 +868,48 @@ fn print_in_save_format(termios: &Termios) {
println!();
}

/// Gets terminal size using the tiocgwinsz ioctl system call.
/// This queries the kernel for the current terminal window dimensions.
fn get_terminal_size(fd: RawFd) -> nix::Result<TermSize> {
let mut term_size = TermSize::default();
unsafe { tiocgwinsz(fd, &raw mut term_size) }.map(|_| term_size)
}

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 = if opts.all {
Some(get_terminal_size(device_fd)?)
} else {
get_terminal_size(device_fd).ok()
};

let stdout_fd = stdout().as_raw_fd();
let window_size = if device_fd == stdout_fd {
&term_size
} else {
&get_terminal_size(stdout_fd).ok()
};

print_terminal_size(termios, opts, window_size.as_ref(), term_size.as_ref())?;
print_control_chars(termios, opts, window_size.as_ref())?;
print_flags(termios, opts, CONTROL_FLAGS, window_size.as_ref());
print_flags(termios, opts, INPUT_FLAGS, window_size.as_ref());
print_flags(termios, opts, OUTPUT_FLAGS, window_size.as_ref());
print_flags(termios, opts, LOCAL_FLAGS, window_size.as_ref());
}
Ok(())
}

fn print_flags<T: TermiosFlag>(termios: &Termios, opts: &Options, flags: &[Flag<T>]) {
let mut printed = false;
fn print_flags<T: TermiosFlag>(
termios: &Termios,
opts: &Options,
flags: &[Flag<T>],
term_size: Option<&TermSize>,
) {
let mut printer = WrappedPrinter::new(term_size);
for &Flag {
name,
flag,
Expand All @@ -836,20 +924,17 @@ fn print_flags<T: TermiosFlag>(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
Expand Down
57 changes: 57 additions & 0 deletions tests/by-util/test_stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,60 @@ fn test_saved_state_with_control_chars() {
.stderr_is(exp_result.stderr_str())
.code_is(exp_result.code());
}

#[test]
#[cfg(unix)]
fn test_columns_env_wrapping() {
use std::process::Stdio;
let (path, _controller, _replica) = pty_path();

// Must pipe output so stty uses COLUMNS env instead of actual terminal size
for (columns, max_len) in [(20, 20), (40, 40), (50, 50)] {
let result = new_ucmd!()
.args(&["--all", "--file", &path])
.env("COLUMNS", columns.to_string())
.set_stdout(Stdio::piped())
.succeeds();

for line in result.stdout_str().lines() {
assert!(
line.len() <= max_len,
"Line exceeds COLUMNS={columns}: '{line}'"
);
}
}

// Wide columns should allow longer lines
let result = new_ucmd!()
.args(&["--all", "--file", &path])
.env("COLUMNS", "200")
.set_stdout(Stdio::piped())
.succeeds();
let has_long_line = result.stdout_str().lines().any(|line| line.len() > 80);
assert!(
has_long_line,
"Expected at least one line longer than 80 chars with COLUMNS=200"
);

// Invalid values should fall back to default
for invalid in ["invalid", "0", "-10"] {
new_ucmd!()
.args(&["--all", "--file", &path])
.env("COLUMNS", invalid)
.set_stdout(Stdio::piped())
.succeeds();
}

// Without --all flag
let result = new_ucmd!()
.args(&["--file", &path])
.env("COLUMNS", "30")
.set_stdout(Stdio::piped())
.succeeds();
for line in result.stdout_str().lines() {
assert!(
line.len() <= 30,
"Line exceeds COLUMNS=30 without --all: '{line}'"
);
}
}
Loading