Skip to main content

coreutils_rs/stty/
core.rs

1use std::io;
2
3/// Convert a baud rate constant to its numeric value.
4pub fn baud_to_num(speed: libc::speed_t) -> u32 {
5    match speed {
6        libc::B0 => 0,
7        libc::B50 => 50,
8        libc::B75 => 75,
9        libc::B110 => 110,
10        libc::B134 => 134,
11        libc::B150 => 150,
12        libc::B200 => 200,
13        libc::B300 => 300,
14        libc::B600 => 600,
15        libc::B1200 => 1200,
16        libc::B1800 => 1800,
17        libc::B2400 => 2400,
18        libc::B4800 => 4800,
19        libc::B9600 => 9600,
20        libc::B19200 => 19200,
21        libc::B38400 => 38400,
22        libc::B57600 => 57600,
23        libc::B115200 => 115200,
24        libc::B230400 => 230400,
25        _ => 0,
26    }
27}
28
29/// Convert a numeric baud value to the corresponding constant.
30pub fn num_to_baud(num: u32) -> Option<libc::speed_t> {
31    match num {
32        0 => Some(libc::B0),
33        50 => Some(libc::B50),
34        75 => Some(libc::B75),
35        110 => Some(libc::B110),
36        134 => Some(libc::B134),
37        150 => Some(libc::B150),
38        200 => Some(libc::B200),
39        300 => Some(libc::B300),
40        600 => Some(libc::B600),
41        1200 => Some(libc::B1200),
42        1800 => Some(libc::B1800),
43        2400 => Some(libc::B2400),
44        4800 => Some(libc::B4800),
45        9600 => Some(libc::B9600),
46        19200 => Some(libc::B19200),
47        38400 => Some(libc::B38400),
48        57600 => Some(libc::B57600),
49        115200 => Some(libc::B115200),
50        230400 => Some(libc::B230400),
51        _ => None,
52    }
53}
54
55/// Get the termios structure for a file descriptor.
56pub fn get_termios(fd: i32) -> io::Result<libc::termios> {
57    let mut termios: libc::termios = unsafe { std::mem::zeroed() };
58    if unsafe { libc::tcgetattr(fd, &mut termios) } != 0 {
59        return Err(io::Error::last_os_error());
60    }
61    Ok(termios)
62}
63
64/// Set the termios structure for a file descriptor.
65pub fn set_termios(fd: i32, termios: &libc::termios) -> io::Result<()> {
66    if unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, termios) } != 0 {
67        return Err(io::Error::last_os_error());
68    }
69    Ok(())
70}
71
72/// Get the window size for a file descriptor.
73pub fn get_winsize(fd: i32) -> io::Result<libc::winsize> {
74    let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
75    if unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) } != 0 {
76        return Err(io::Error::last_os_error());
77    }
78    Ok(ws)
79}
80
81/// Print terminal size as "rows cols".
82pub fn print_size(fd: i32) -> io::Result<()> {
83    let ws = get_winsize(fd)?;
84    println!("{} {}", ws.ws_row, ws.ws_col);
85    Ok(())
86}
87
88/// Print terminal speed.
89pub fn print_speed(termios: &libc::termios) {
90    let ispeed = unsafe { libc::cfgetispeed(termios) };
91    let ospeed = unsafe { libc::cfgetospeed(termios) };
92    if ispeed == ospeed {
93        println!("{}", baud_to_num(ospeed));
94    } else {
95        println!("{} {}", baud_to_num(ispeed), baud_to_num(ospeed));
96    }
97}
98
99/// Format a control character for display.
100pub fn format_cc(c: libc::cc_t) -> String {
101    if c == 0 {
102        "<undef>".to_string()
103    } else if c == 0x7f {
104        "^?".to_string()
105    } else if c < 0x20 {
106        format!("^{}", (c + 0x40) as char)
107    } else {
108        format!("{}", c as char)
109    }
110}
111
112/// Special character names and their termios indices (GNU order).
113#[cfg(target_os = "linux")]
114const SPECIAL_CHARS_ALL: &[(&str, usize)] = &[
115    ("intr", libc::VINTR as usize),
116    ("quit", libc::VQUIT as usize),
117    ("erase", libc::VERASE as usize),
118    ("kill", libc::VKILL as usize),
119    ("eof", libc::VEOF as usize),
120    ("eol", libc::VEOL as usize),
121    ("eol2", libc::VEOL2 as usize),
122    ("swtch", libc::VSWTC as usize),
123    ("start", libc::VSTART as usize),
124    ("stop", libc::VSTOP as usize),
125    ("susp", libc::VSUSP as usize),
126    ("rprnt", libc::VREPRINT as usize),
127    ("werase", libc::VWERASE as usize),
128    ("lnext", libc::VLNEXT as usize),
129    ("discard", libc::VDISCARD as usize),
130    ("min", libc::VMIN as usize),
131    ("time", libc::VTIME as usize),
132];
133
134#[cfg(not(target_os = "linux"))]
135const SPECIAL_CHARS_ALL: &[(&str, usize)] = &[
136    ("intr", libc::VINTR as usize),
137    ("quit", libc::VQUIT as usize),
138    ("erase", libc::VERASE as usize),
139    ("kill", libc::VKILL as usize),
140    ("eof", libc::VEOF as usize),
141    ("eol", libc::VEOL as usize),
142    ("eol2", libc::VEOL2 as usize),
143    ("start", libc::VSTART as usize),
144    ("stop", libc::VSTOP as usize),
145    ("susp", libc::VSUSP as usize),
146    ("rprnt", libc::VREPRINT as usize),
147    ("werase", libc::VWERASE as usize),
148    ("lnext", libc::VLNEXT as usize),
149    ("discard", libc::VDISCARD as usize),
150    ("min", libc::VMIN as usize),
151    ("time", libc::VTIME as usize),
152];
153
154/// Input flags and their names (GNU order).
155#[cfg(target_os = "linux")]
156const INPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
157    ("ignbrk", libc::IGNBRK),
158    ("brkint", libc::BRKINT),
159    ("ignpar", libc::IGNPAR),
160    ("parmrk", libc::PARMRK),
161    ("inpck", libc::INPCK),
162    ("istrip", libc::ISTRIP),
163    ("inlcr", libc::INLCR),
164    ("igncr", libc::IGNCR),
165    ("icrnl", libc::ICRNL),
166    ("ixon", libc::IXON),
167    ("ixoff", libc::IXOFF),
168    ("iuclc", libc::IUCLC),
169    ("ixany", libc::IXANY),
170    ("imaxbel", libc::IMAXBEL),
171    ("iutf8", libc::IUTF8),
172];
173
174#[cfg(not(target_os = "linux"))]
175const INPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
176    ("ignbrk", libc::IGNBRK),
177    ("brkint", libc::BRKINT),
178    ("ignpar", libc::IGNPAR),
179    ("parmrk", libc::PARMRK),
180    ("inpck", libc::INPCK),
181    ("istrip", libc::ISTRIP),
182    ("inlcr", libc::INLCR),
183    ("igncr", libc::IGNCR),
184    ("icrnl", libc::ICRNL),
185    ("ixon", libc::IXON),
186    ("ixany", libc::IXANY),
187    ("ixoff", libc::IXOFF),
188    ("imaxbel", libc::IMAXBEL),
189];
190
191/// Output flags and their names (GNU order).
192#[cfg(target_os = "linux")]
193const OUTPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
194    ("opost", libc::OPOST),
195    ("olcuc", libc::OLCUC),
196    ("ocrnl", libc::OCRNL),
197    ("onlcr", libc::ONLCR),
198    ("onocr", libc::ONOCR),
199    ("onlret", libc::ONLRET),
200    ("ofill", libc::OFILL),
201    ("ofdel", libc::OFDEL),
202];
203
204#[cfg(not(target_os = "linux"))]
205const OUTPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
206    ("opost", libc::OPOST),
207    ("onlcr", libc::ONLCR),
208    ("ocrnl", libc::OCRNL),
209    ("onocr", libc::ONOCR),
210    ("onlret", libc::ONLRET),
211    ("ofill", libc::OFILL),
212    ("ofdel", libc::OFDEL),
213];
214
215/// Output delay flags (displayed but not negatable).
216#[cfg(target_os = "linux")]
217const OUTPUT_DELAY_FLAGS: &[(&str, libc::tcflag_t, libc::tcflag_t)] = &[
218    ("nl0", libc::NL0, libc::NLDLY),
219    ("nl1", libc::NL1, libc::NLDLY),
220    ("cr0", libc::CR0, libc::CRDLY),
221    ("cr1", libc::CR1, libc::CRDLY),
222    ("cr2", libc::CR2, libc::CRDLY),
223    ("cr3", libc::CR3, libc::CRDLY),
224    ("tab0", libc::TAB0, libc::TABDLY),
225    ("tab1", libc::TAB1, libc::TABDLY),
226    ("tab2", libc::TAB2, libc::TABDLY),
227    ("tab3", libc::TAB3, libc::TABDLY),
228    ("bs0", libc::BS0, libc::BSDLY),
229    ("bs1", libc::BS1, libc::BSDLY),
230    ("vt0", libc::VT0, libc::VTDLY),
231    ("vt1", libc::VT1, libc::VTDLY),
232    ("ff0", libc::FF0, libc::FFDLY),
233    ("ff1", libc::FF1, libc::FFDLY),
234];
235
236#[cfg(not(target_os = "linux"))]
237const OUTPUT_DELAY_FLAGS: &[(&str, libc::tcflag_t, libc::tcflag_t)] = &[];
238
239/// Control flags and their names (GNU order for -a output).
240const CONTROL_FLAGS: &[(&str, libc::tcflag_t)] = &[
241    ("parenb", libc::PARENB),
242    ("parodd", libc::PARODD),
243    ("hupcl", libc::HUPCL),
244    ("cstopb", libc::CSTOPB),
245    ("cread", libc::CREAD),
246    ("clocal", libc::CLOCAL),
247];
248
249/// Linux-only control flags.
250#[cfg(target_os = "linux")]
251const CONTROL_FLAGS_LINUX: &[(&str, libc::tcflag_t)] =
252    &[("cmspar", libc::CMSPAR), ("crtscts", libc::CRTSCTS)];
253
254#[cfg(not(target_os = "linux"))]
255const CONTROL_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[];
256
257/// Local flags and their names (GNU order).
258#[cfg(target_os = "linux")]
259const LOCAL_FLAGS: &[(&str, libc::tcflag_t)] = &[
260    ("isig", libc::ISIG),
261    ("icanon", libc::ICANON),
262    ("iexten", libc::IEXTEN),
263    ("echo", libc::ECHO),
264    ("echoe", libc::ECHOE),
265    ("echok", libc::ECHOK),
266    ("echonl", libc::ECHONL),
267    ("noflsh", libc::NOFLSH),
268    ("xcase", libc::XCASE),
269    ("tostop", libc::TOSTOP),
270    ("echoprt", libc::ECHOPRT),
271    ("echoctl", libc::ECHOCTL),
272    ("echoke", libc::ECHOKE),
273    ("flusho", libc::FLUSHO),
274    ("extproc", libc::EXTPROC),
275];
276
277#[cfg(not(target_os = "linux"))]
278const LOCAL_FLAGS: &[(&str, libc::tcflag_t)] = &[
279    ("isig", libc::ISIG),
280    ("icanon", libc::ICANON),
281    ("iexten", libc::IEXTEN),
282    ("echo", libc::ECHO),
283    ("echoe", libc::ECHOE),
284    ("echok", libc::ECHOK),
285    ("echonl", libc::ECHONL),
286    ("noflsh", libc::NOFLSH),
287    ("tostop", libc::TOSTOP),
288    ("echoprt", libc::ECHOPRT),
289    ("echoctl", libc::ECHOCTL),
290    ("echoke", libc::ECHOKE),
291    ("flusho", libc::FLUSHO),
292];
293
294/// Character size names.
295fn csize_str(cflag: libc::tcflag_t) -> &'static str {
296    match cflag & libc::CSIZE {
297        libc::CS5 => "cs5",
298        libc::CS6 => "cs6",
299        libc::CS7 => "cs7",
300        libc::CS8 => "cs8",
301        _ => "cs8",
302    }
303}
304
305/// Helper: iterate all flag entries (portable + linux-specific).
306fn print_flags(parts: &mut Vec<String>, flags: libc::tcflag_t, entries: &[(&str, libc::tcflag_t)]) {
307    for &(name, flag) in entries {
308        if flags & flag != 0 {
309            parts.push(name.to_string());
310        } else {
311            parts.push(format!("-{}", name));
312        }
313    }
314}
315
316/// Wrap a list of items into lines at approximately `max_cols` columns,
317/// joining items with `sep` and ending each line.
318fn print_wrapped(items: &[String], sep: &str, max_cols: usize) {
319    let mut line = String::new();
320    for item in items {
321        let add_len = if line.is_empty() {
322            item.len()
323        } else {
324            sep.len() + item.len()
325        };
326        if !line.is_empty() && line.len() + add_len > max_cols {
327            println!("{}", line);
328            line.clear();
329        }
330        if line.is_empty() {
331            line.push_str(item);
332        } else {
333            line.push_str(sep);
334            line.push_str(item);
335        }
336    }
337    if !line.is_empty() {
338        println!("{}", line);
339    }
340}
341
342/// Print all terminal settings (stty -a), matching GNU output format.
343pub fn print_all(termios: &libc::termios, fd: i32) {
344    let ispeed = unsafe { libc::cfgetispeed(termios) };
345    let ospeed = unsafe { libc::cfgetospeed(termios) };
346
347    // Query terminal width for line wrapping (GNU uses stdout tty width, falls back to 80)
348    let wrap_cols = if unsafe { libc::isatty(1) } == 1 {
349        get_winsize(1).map(|ws| ws.ws_col as usize).unwrap_or(80)
350    } else {
351        80
352    };
353
354    // Line 1: speed, window size, and line discipline
355    let speed_str = if ispeed == ospeed {
356        format!("speed {} baud", baud_to_num(ospeed))
357    } else {
358        format!(
359            "speed {} baud; ispeed {} baud; ospeed {} baud",
360            baud_to_num(ospeed),
361            baud_to_num(ispeed),
362            baud_to_num(ospeed)
363        )
364    };
365    let ws_str = if let Ok(ws) = get_winsize(fd) {
366        format!("; rows {}; columns {}", ws.ws_row, ws.ws_col)
367    } else {
368        String::new()
369    };
370    // Line discipline (Linux only)
371    #[cfg(target_os = "linux")]
372    let line_str = format!("; line = {}", termios.c_line);
373    #[cfg(not(target_os = "linux"))]
374    let line_str = String::new();
375    println!("{}{}{};", speed_str, ws_str, line_str);
376
377    // Special characters (wrapped at ~80 columns)
378    let mut cc_parts: Vec<String> = Vec::new();
379    for &(name, idx) in SPECIAL_CHARS_ALL.iter() {
380        // min and time are numeric values, not control chars
381        let formatted = if name == "min" || name == "time" {
382            termios.c_cc[idx].to_string()
383        } else {
384            format_cc(termios.c_cc[idx])
385        };
386        cc_parts.push(format!("{} = {};", name, formatted));
387    }
388    print_wrapped(&cc_parts, " ", wrap_cols);
389
390    // GNU order: control, input, output, local
391
392    // Control flags (with csize before other flags, matching GNU)
393    let mut parts: Vec<String> = Vec::new();
394    // GNU order: -parenb -parodd [-cmspar] cs8 -hupcl -cstopb cread -clocal [-crtscts]
395    let mut control_items: Vec<String> = Vec::new();
396    // parenb, parodd first
397    for &(name, flag) in &[("parenb", libc::PARENB), ("parodd", libc::PARODD)] {
398        if termios.c_cflag & flag != 0 {
399            control_items.push(name.to_string());
400        } else {
401            control_items.push(format!("-{}", name));
402        }
403    }
404    // cmspar (Linux only) - between parodd and cs8
405    #[cfg(target_os = "linux")]
406    {
407        if termios.c_cflag & libc::CMSPAR != 0 {
408            control_items.push("cmspar".to_string());
409        } else {
410            control_items.push("-cmspar".to_string());
411        }
412    }
413    // cs5-cs8
414    control_items.push(csize_str(termios.c_cflag).to_string());
415    // hupcl, cstopb, cread, clocal
416    for &(name, flag) in &[
417        ("hupcl", libc::HUPCL),
418        ("cstopb", libc::CSTOPB),
419        ("cread", libc::CREAD),
420        ("clocal", libc::CLOCAL),
421    ] {
422        if termios.c_cflag & flag != 0 {
423            control_items.push(name.to_string());
424        } else {
425            control_items.push(format!("-{}", name));
426        }
427    }
428    // crtscts (Linux only) - at the end
429    #[cfg(target_os = "linux")]
430    {
431        if termios.c_cflag & libc::CRTSCTS != 0 {
432            control_items.push("crtscts".to_string());
433        } else {
434            control_items.push("-crtscts".to_string());
435        }
436    }
437    print_wrapped(&control_items, " ", wrap_cols);
438
439    // Input flags
440    parts.clear();
441    print_flags(&mut parts, termios.c_iflag, INPUT_FLAGS);
442    print_wrapped(&parts, " ", wrap_cols);
443
444    // Output flags (with delay flags on Linux)
445    parts.clear();
446    print_flags(&mut parts, termios.c_oflag, OUTPUT_FLAGS);
447    // Add delay flags (nl0, cr0, tab0, bs0, vt0, ff0)
448    #[cfg(target_os = "linux")]
449    {
450        // For each delay category, show the active delay value
451        let mut seen_masks: Vec<libc::tcflag_t> = Vec::new();
452        for &(name, val, mask) in OUTPUT_DELAY_FLAGS {
453            if !seen_masks.contains(&mask) && (termios.c_oflag & mask) == val {
454                parts.push(name.to_string());
455                seen_masks.push(mask);
456            }
457        }
458    }
459    print_wrapped(&parts, " ", wrap_cols);
460
461    // Local flags
462    parts.clear();
463    print_flags(&mut parts, termios.c_lflag, LOCAL_FLAGS);
464    print_wrapped(&parts, " ", wrap_cols);
465}
466
467/// Parse a control character specification like "^C", "^?", "^-", or a literal.
468pub fn parse_control_char(s: &str) -> Option<libc::cc_t> {
469    if s == "^-" || s == "undef" {
470        Some(0)
471    } else if s == "^?" {
472        Some(0x7f)
473    } else if s.len() == 2 && s.starts_with('^') {
474        let ch = s.as_bytes()[1];
475        if ch >= b'@' && ch <= b'_' {
476            Some(ch - b'@')
477        } else if ch >= b'a' && ch <= b'z' {
478            Some(ch - b'a' + 1)
479        } else {
480            None
481        }
482    } else if s.len() == 1 {
483        Some(s.as_bytes()[0])
484    } else {
485        // Try parsing as decimal
486        s.parse::<u8>().ok()
487    }
488}
489
490/// Apply "sane" settings to a termios structure.
491pub fn set_sane(termios: &mut libc::termios) {
492    // Input flags
493    termios.c_iflag = libc::BRKINT | libc::ICRNL | libc::IMAXBEL | libc::IXON;
494    #[cfg(target_os = "linux")]
495    {
496        termios.c_iflag |= libc::IUTF8;
497    }
498
499    // Output flags
500    termios.c_oflag = libc::OPOST | libc::ONLCR;
501
502    // Control flags: preserve baud rate, set cs8, cread, hupcl
503    #[cfg(target_os = "linux")]
504    {
505        termios.c_cflag = (termios.c_cflag & (libc::CBAUD | libc::CBAUDEX))
506            | libc::CS8
507            | libc::CREAD
508            | libc::HUPCL;
509    }
510    #[cfg(not(target_os = "linux"))]
511    {
512        // On macOS/BSD, there are no CBAUD/CBAUDEX constants.
513        // Preserve existing speed bits by using cfget/cfset speed functions.
514        let ispeed = unsafe { libc::cfgetispeed(termios) };
515        let ospeed = unsafe { libc::cfgetospeed(termios) };
516        termios.c_cflag = libc::CS8 | libc::CREAD | libc::HUPCL;
517        unsafe {
518            libc::cfsetispeed(termios, ispeed);
519            libc::cfsetospeed(termios, ospeed);
520        }
521    }
522
523    // Local flags
524    termios.c_lflag = libc::ISIG
525        | libc::ICANON
526        | libc::IEXTEN
527        | libc::ECHO
528        | libc::ECHOE
529        | libc::ECHOK
530        | libc::ECHOCTL
531        | libc::ECHOKE;
532
533    // Special characters
534    termios.c_cc[libc::VINTR] = 0x03; // ^C
535    termios.c_cc[libc::VQUIT] = 0x1c; // ^\
536    termios.c_cc[libc::VERASE] = 0x7f; // ^?
537    termios.c_cc[libc::VKILL] = 0x15; // ^U
538    termios.c_cc[libc::VEOF] = 0x04; // ^D
539    termios.c_cc[libc::VSTART] = 0x11; // ^Q
540    termios.c_cc[libc::VSTOP] = 0x13; // ^S
541    termios.c_cc[libc::VSUSP] = 0x1a; // ^Z
542    termios.c_cc[libc::VREPRINT] = 0x12; // ^R
543    termios.c_cc[libc::VWERASE] = 0x17; // ^W
544    termios.c_cc[libc::VLNEXT] = 0x16; // ^V
545    termios.c_cc[libc::VDISCARD] = 0x0f; // ^O
546    termios.c_cc[libc::VMIN] = 1;
547    termios.c_cc[libc::VTIME] = 0;
548}
549
550/// Set raw mode on a termios structure.
551pub fn set_raw(termios: &mut libc::termios) {
552    // Equivalent to cfmakeraw
553    termios.c_iflag &= !(libc::IGNBRK
554        | libc::BRKINT
555        | libc::PARMRK
556        | libc::ISTRIP
557        | libc::INLCR
558        | libc::IGNCR
559        | libc::ICRNL
560        | libc::IXON);
561    termios.c_oflag &= !libc::OPOST;
562    termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
563    termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
564    termios.c_cflag |= libc::CS8;
565    termios.c_cc[libc::VMIN] = 1;
566    termios.c_cc[libc::VTIME] = 0;
567}
568
569/// Set cooked mode (undo raw) on a termios structure.
570pub fn set_cooked(termios: &mut libc::termios) {
571    termios.c_iflag |= libc::BRKINT | libc::IGNPAR | libc::ICRNL | libc::IXON;
572    termios.c_oflag |= libc::OPOST;
573    termios.c_lflag |= libc::ISIG | libc::ICANON | libc::ECHO;
574}
575
576/// Open a device and return its file descriptor.
577pub fn open_device(path: &str) -> io::Result<i32> {
578    use std::ffi::CString;
579    let cpath = CString::new(path)
580        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid device path"))?;
581    let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NONBLOCK) };
582    if fd < 0 {
583        return Err(io::Error::last_os_error());
584    }
585    Ok(fd)
586}
587
588/// Look up a special character name and return its index.
589pub fn find_special_char(name: &str) -> Option<usize> {
590    for &(n, idx) in SPECIAL_CHARS_ALL.iter() {
591        if n == name {
592            return Some(idx);
593        }
594    }
595    None
596}
597
598/// Apply a single flag setting. Returns true if the argument was recognized.
599pub fn apply_flag(termios: &mut libc::termios, name: &str) -> bool {
600    let (negate, flag_name) = if let Some(stripped) = name.strip_prefix('-') {
601        (true, stripped)
602    } else {
603        (false, name)
604    };
605
606    // Check input flags
607    for &(n, flag) in INPUT_FLAGS.iter() {
608        if n == flag_name {
609            if negate {
610                termios.c_iflag &= !flag;
611            } else {
612                termios.c_iflag |= flag;
613            }
614            return true;
615        }
616    }
617
618    // Check output flags
619    for &(n, flag) in OUTPUT_FLAGS.iter() {
620        if n == flag_name {
621            if negate {
622                termios.c_oflag &= !flag;
623            } else {
624                termios.c_oflag |= flag;
625            }
626            return true;
627        }
628    }
629
630    // Check control flags
631    for &(n, flag) in CONTROL_FLAGS.iter().chain(CONTROL_FLAGS_LINUX.iter()) {
632        if n == flag_name {
633            if negate {
634                termios.c_cflag &= !flag;
635            } else {
636                termios.c_cflag |= flag;
637            }
638            return true;
639        }
640    }
641
642    // Check local flags
643    for &(n, flag) in LOCAL_FLAGS.iter() {
644        if n == flag_name {
645            if negate {
646                termios.c_lflag &= !flag;
647            } else {
648                termios.c_lflag |= flag;
649            }
650            return true;
651        }
652    }
653
654    // Check character size
655    match flag_name {
656        "cs5" => {
657            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS5;
658            return true;
659        }
660        "cs6" => {
661            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS6;
662            return true;
663        }
664        "cs7" => {
665            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS7;
666            return true;
667        }
668        "cs8" => {
669            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS8;
670            return true;
671        }
672        _ => {}
673    }
674
675    false
676}
677
678/// The result of parsing stty arguments.
679pub enum SttyAction {
680    PrintAll,
681    PrintSize,
682    PrintSpeed,
683    ApplySettings,
684}
685
686/// Parsed stty configuration.
687pub struct SttyConfig {
688    pub action: SttyAction,
689    pub device: Option<String>,
690    pub settings: Vec<String>,
691}
692
693/// Parse command-line arguments for stty.
694pub fn parse_args(args: &[String]) -> Result<SttyConfig, String> {
695    let mut action = SttyAction::ApplySettings;
696    let mut device: Option<String> = None;
697    let mut settings: Vec<String> = Vec::new();
698    let mut i = 0;
699    let mut has_explicit_action = false;
700
701    while i < args.len() {
702        match args[i].as_str() {
703            "-a" | "--all" => {
704                action = SttyAction::PrintAll;
705                has_explicit_action = true;
706            }
707            "-F" | "--file" => {
708                i += 1;
709                if i >= args.len() {
710                    return Err("option requires an argument -- 'F'".to_string());
711                }
712                device = Some(args[i].clone());
713            }
714            s if s.starts_with("--file=") => {
715                device = Some(s["--file=".len()..].to_string());
716            }
717            s if s.starts_with("-F") && s.len() > 2 => {
718                device = Some(s[2..].to_string());
719            }
720            "size" => {
721                action = SttyAction::PrintSize;
722                has_explicit_action = true;
723            }
724            "speed" => {
725                action = SttyAction::PrintSpeed;
726                has_explicit_action = true;
727            }
728            _ => {
729                settings.push(args[i].clone());
730            }
731        }
732        i += 1;
733    }
734
735    if !has_explicit_action && settings.is_empty() {
736        action = SttyAction::PrintAll;
737    }
738
739    Ok(SttyConfig {
740        action,
741        device,
742        settings,
743    })
744}
745
746/// Apply settings from the parsed arguments to a termios structure.
747/// Returns Ok(true) if any changes were made, Ok(false) otherwise.
748pub fn apply_settings(termios: &mut libc::termios, settings: &[String]) -> Result<bool, String> {
749    let mut changed = false;
750    let mut i = 0;
751
752    while i < settings.len() {
753        let arg = &settings[i];
754
755        match arg.as_str() {
756            "sane" => {
757                set_sane(termios);
758                changed = true;
759            }
760            "raw" => {
761                set_raw(termios);
762                changed = true;
763            }
764            "-raw" | "cooked" => {
765                set_cooked(termios);
766                changed = true;
767            }
768            "ispeed" => {
769                i += 1;
770                if i >= settings.len() {
771                    return Err("missing argument to 'ispeed'".to_string());
772                }
773                let n: u32 = settings[i]
774                    .parse()
775                    .map_err(|_| format!("invalid integer argument: '{}'", settings[i]))?;
776                let baud = num_to_baud(n).ok_or_else(|| format!("invalid speed: '{}'", n))?;
777                unsafe {
778                    libc::cfsetispeed(termios, baud);
779                }
780                changed = true;
781            }
782            "ospeed" => {
783                i += 1;
784                if i >= settings.len() {
785                    return Err("missing argument to 'ospeed'".to_string());
786                }
787                let n: u32 = settings[i]
788                    .parse()
789                    .map_err(|_| format!("invalid integer argument: '{}'", settings[i]))?;
790                let baud = num_to_baud(n).ok_or_else(|| format!("invalid speed: '{}'", n))?;
791                unsafe {
792                    libc::cfsetospeed(termios, baud);
793                }
794                changed = true;
795            }
796            _ => {
797                // Check if it is a bare baud rate (numeric)
798                if let Ok(n) = arg.parse::<u32>() {
799                    if let Some(baud) = num_to_baud(n) {
800                        unsafe {
801                            libc::cfsetispeed(termios, baud);
802                            libc::cfsetospeed(termios, baud);
803                        }
804                        changed = true;
805                        i += 1;
806                        continue;
807                    }
808                }
809
810                // Check if it is a special character setting (e.g., "intr ^C")
811                if let Some(idx) = find_special_char(arg) {
812                    i += 1;
813                    if i >= settings.len() {
814                        return Err(format!("missing argument to '{}'", arg));
815                    }
816                    let cc = parse_control_char(&settings[i])
817                        .ok_or_else(|| format!("invalid integer argument: '{}'", settings[i]))?;
818                    termios.c_cc[idx] = cc;
819                    changed = true;
820                    i += 1;
821                    continue;
822                }
823
824                // Try as a flag
825                if !apply_flag(termios, arg) {
826                    return Err(format!("invalid argument '{}'", arg));
827                }
828                changed = true;
829            }
830        }
831
832        i += 1;
833    }
834
835    Ok(changed)
836}