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 (portable).
113const SPECIAL_CHARS: &[(&str, usize)] = &[
114    ("intr", libc::VINTR as usize),
115    ("quit", libc::VQUIT as usize),
116    ("erase", libc::VERASE as usize),
117    ("kill", libc::VKILL as usize),
118    ("eof", libc::VEOF as usize),
119    ("eol", libc::VEOL as usize),
120    ("eol2", libc::VEOL2 as usize),
121    ("start", libc::VSTART as usize),
122    ("stop", libc::VSTOP as usize),
123    ("susp", libc::VSUSP as usize),
124    ("rprnt", libc::VREPRINT as usize),
125    ("werase", libc::VWERASE as usize),
126    ("lnext", libc::VLNEXT as usize),
127    ("discard", libc::VDISCARD as usize),
128    ("min", libc::VMIN as usize),
129    ("time", libc::VTIME as usize),
130];
131
132/// Linux-only special characters.
133#[cfg(target_os = "linux")]
134const SPECIAL_CHARS_LINUX: &[(&str, usize)] = &[("swtch", libc::VSWTC as usize)];
135
136#[cfg(not(target_os = "linux"))]
137const SPECIAL_CHARS_LINUX: &[(&str, usize)] = &[];
138
139/// Input flags and their names (portable).
140const INPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
141    ("ignbrk", libc::IGNBRK),
142    ("brkint", libc::BRKINT),
143    ("ignpar", libc::IGNPAR),
144    ("parmrk", libc::PARMRK),
145    ("inpck", libc::INPCK),
146    ("istrip", libc::ISTRIP),
147    ("inlcr", libc::INLCR),
148    ("igncr", libc::IGNCR),
149    ("icrnl", libc::ICRNL),
150    ("ixon", libc::IXON),
151    ("ixany", libc::IXANY),
152    ("ixoff", libc::IXOFF),
153    ("imaxbel", libc::IMAXBEL),
154];
155
156/// Linux-only input flags.
157#[cfg(target_os = "linux")]
158const INPUT_FLAGS_LINUX: &[(&str, libc::tcflag_t)] =
159    &[("iuclc", libc::IUCLC), ("iutf8", libc::IUTF8)];
160
161#[cfg(not(target_os = "linux"))]
162const INPUT_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[];
163
164/// Output flags and their names (portable).
165const OUTPUT_FLAGS: &[(&str, libc::tcflag_t)] = &[
166    ("opost", libc::OPOST),
167    ("onlcr", libc::ONLCR),
168    ("ocrnl", libc::OCRNL),
169    ("onocr", libc::ONOCR),
170    ("onlret", libc::ONLRET),
171    ("ofill", libc::OFILL),
172    ("ofdel", libc::OFDEL),
173];
174
175/// Linux-only output flags.
176#[cfg(target_os = "linux")]
177const OUTPUT_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[("olcuc", libc::OLCUC)];
178
179#[cfg(not(target_os = "linux"))]
180const OUTPUT_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[];
181
182/// Control flags and their names.
183const CONTROL_FLAGS: &[(&str, libc::tcflag_t)] = &[
184    ("cread", libc::CREAD),
185    ("clocal", libc::CLOCAL),
186    ("hupcl", libc::HUPCL),
187    ("cstopb", libc::CSTOPB),
188    ("parenb", libc::PARENB),
189    ("parodd", libc::PARODD),
190];
191
192/// Local flags and their names (portable).
193const LOCAL_FLAGS: &[(&str, libc::tcflag_t)] = &[
194    ("isig", libc::ISIG),
195    ("icanon", libc::ICANON),
196    ("iexten", libc::IEXTEN),
197    ("echo", libc::ECHO),
198    ("echoe", libc::ECHOE),
199    ("echok", libc::ECHOK),
200    ("echonl", libc::ECHONL),
201    ("echoctl", libc::ECHOCTL),
202    ("echoprt", libc::ECHOPRT),
203    ("echoke", libc::ECHOKE),
204    ("noflsh", libc::NOFLSH),
205    ("tostop", libc::TOSTOP),
206];
207
208/// Linux-only local flags.
209#[cfg(target_os = "linux")]
210const LOCAL_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[("xcase", libc::XCASE)];
211
212#[cfg(not(target_os = "linux"))]
213const LOCAL_FLAGS_LINUX: &[(&str, libc::tcflag_t)] = &[];
214
215/// Character size names.
216fn csize_str(cflag: libc::tcflag_t) -> &'static str {
217    match cflag & libc::CSIZE {
218        libc::CS5 => "cs5",
219        libc::CS6 => "cs6",
220        libc::CS7 => "cs7",
221        libc::CS8 => "cs8",
222        _ => "cs8",
223    }
224}
225
226/// Helper: iterate all flag entries (portable + linux-specific).
227fn print_flags(
228    parts: &mut Vec<String>,
229    flags: libc::tcflag_t,
230    entries: &[(&str, libc::tcflag_t)],
231    extra: &[(&str, libc::tcflag_t)],
232) {
233    for &(name, flag) in entries.iter().chain(extra.iter()) {
234        if flags & flag != 0 {
235            parts.push(name.to_string());
236        } else {
237            parts.push(format!("-{}", name));
238        }
239    }
240}
241
242/// Print all terminal settings (stty -a).
243pub fn print_all(termios: &libc::termios, fd: i32) {
244    let ispeed = unsafe { libc::cfgetispeed(termios) };
245    let ospeed = unsafe { libc::cfgetospeed(termios) };
246
247    // Line 1: speed and window size
248    let speed_str = if ispeed == ospeed {
249        format!("speed {} baud", baud_to_num(ospeed))
250    } else {
251        format!(
252            "speed {} baud; ispeed {} baud; ospeed {} baud",
253            baud_to_num(ospeed),
254            baud_to_num(ispeed),
255            baud_to_num(ospeed)
256        )
257    };
258    if let Ok(ws) = get_winsize(fd) {
259        println!("{}; rows {}; columns {};", speed_str, ws.ws_row, ws.ws_col);
260    } else {
261        println!("{};", speed_str);
262    }
263
264    // Line 2: special characters
265    let mut cc_parts: Vec<String> = Vec::new();
266    for &(name, idx) in SPECIAL_CHARS.iter().chain(SPECIAL_CHARS_LINUX.iter()) {
267        cc_parts.push(format!("{} = {}", name, format_cc(termios.c_cc[idx])));
268    }
269    println!("{};", cc_parts.join("; "));
270
271    // Input flags
272    let mut parts: Vec<String> = Vec::new();
273    print_flags(&mut parts, termios.c_iflag, INPUT_FLAGS, INPUT_FLAGS_LINUX);
274    println!("{}", parts.join(" "));
275
276    // Output flags
277    parts.clear();
278    print_flags(
279        &mut parts,
280        termios.c_oflag,
281        OUTPUT_FLAGS,
282        OUTPUT_FLAGS_LINUX,
283    );
284    println!("{}", parts.join(" "));
285
286    // Control flags
287    parts.clear();
288    parts.push(csize_str(termios.c_cflag).to_string());
289    print_flags(&mut parts, termios.c_cflag, CONTROL_FLAGS, &[]);
290    println!("{}", parts.join(" "));
291
292    // Local flags
293    parts.clear();
294    print_flags(&mut parts, termios.c_lflag, LOCAL_FLAGS, LOCAL_FLAGS_LINUX);
295    println!("{}", parts.join(" "));
296}
297
298/// Parse a control character specification like "^C", "^?", "^-", or a literal.
299pub fn parse_control_char(s: &str) -> Option<libc::cc_t> {
300    if s == "^-" || s == "undef" {
301        Some(0)
302    } else if s == "^?" {
303        Some(0x7f)
304    } else if s.len() == 2 && s.starts_with('^') {
305        let ch = s.as_bytes()[1];
306        if ch >= b'@' && ch <= b'_' {
307            Some(ch - b'@')
308        } else if ch >= b'a' && ch <= b'z' {
309            Some(ch - b'a' + 1)
310        } else {
311            None
312        }
313    } else if s.len() == 1 {
314        Some(s.as_bytes()[0])
315    } else {
316        // Try parsing as decimal
317        s.parse::<u8>().ok()
318    }
319}
320
321/// Apply "sane" settings to a termios structure.
322pub fn set_sane(termios: &mut libc::termios) {
323    // Input flags
324    termios.c_iflag = libc::BRKINT | libc::ICRNL | libc::IMAXBEL | libc::IXON;
325    #[cfg(target_os = "linux")]
326    {
327        termios.c_iflag |= libc::IUTF8;
328    }
329
330    // Output flags
331    termios.c_oflag = libc::OPOST | libc::ONLCR;
332
333    // Control flags: preserve baud rate, set cs8, cread, hupcl
334    #[cfg(target_os = "linux")]
335    {
336        termios.c_cflag = (termios.c_cflag & (libc::CBAUD | libc::CBAUDEX))
337            | libc::CS8
338            | libc::CREAD
339            | libc::HUPCL;
340    }
341    #[cfg(not(target_os = "linux"))]
342    {
343        // On macOS/BSD, there are no CBAUD/CBAUDEX constants.
344        // Preserve existing speed bits by using cfget/cfset speed functions.
345        let ispeed = unsafe { libc::cfgetispeed(termios) };
346        let ospeed = unsafe { libc::cfgetospeed(termios) };
347        termios.c_cflag = libc::CS8 | libc::CREAD | libc::HUPCL;
348        unsafe {
349            libc::cfsetispeed(termios, ispeed);
350            libc::cfsetospeed(termios, ospeed);
351        }
352    }
353
354    // Local flags
355    termios.c_lflag = libc::ISIG
356        | libc::ICANON
357        | libc::IEXTEN
358        | libc::ECHO
359        | libc::ECHOE
360        | libc::ECHOK
361        | libc::ECHOCTL
362        | libc::ECHOKE;
363
364    // Special characters
365    termios.c_cc[libc::VINTR] = 0x03; // ^C
366    termios.c_cc[libc::VQUIT] = 0x1c; // ^\
367    termios.c_cc[libc::VERASE] = 0x7f; // ^?
368    termios.c_cc[libc::VKILL] = 0x15; // ^U
369    termios.c_cc[libc::VEOF] = 0x04; // ^D
370    termios.c_cc[libc::VSTART] = 0x11; // ^Q
371    termios.c_cc[libc::VSTOP] = 0x13; // ^S
372    termios.c_cc[libc::VSUSP] = 0x1a; // ^Z
373    termios.c_cc[libc::VREPRINT] = 0x12; // ^R
374    termios.c_cc[libc::VWERASE] = 0x17; // ^W
375    termios.c_cc[libc::VLNEXT] = 0x16; // ^V
376    termios.c_cc[libc::VDISCARD] = 0x0f; // ^O
377    termios.c_cc[libc::VMIN] = 1;
378    termios.c_cc[libc::VTIME] = 0;
379}
380
381/// Set raw mode on a termios structure.
382pub fn set_raw(termios: &mut libc::termios) {
383    // Equivalent to cfmakeraw
384    termios.c_iflag &= !(libc::IGNBRK
385        | libc::BRKINT
386        | libc::PARMRK
387        | libc::ISTRIP
388        | libc::INLCR
389        | libc::IGNCR
390        | libc::ICRNL
391        | libc::IXON);
392    termios.c_oflag &= !libc::OPOST;
393    termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
394    termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
395    termios.c_cflag |= libc::CS8;
396    termios.c_cc[libc::VMIN] = 1;
397    termios.c_cc[libc::VTIME] = 0;
398}
399
400/// Set cooked mode (undo raw) on a termios structure.
401pub fn set_cooked(termios: &mut libc::termios) {
402    termios.c_iflag |= libc::BRKINT | libc::IGNPAR | libc::ICRNL | libc::IXON;
403    termios.c_oflag |= libc::OPOST;
404    termios.c_lflag |= libc::ISIG | libc::ICANON | libc::ECHO;
405}
406
407/// Open a device and return its file descriptor.
408pub fn open_device(path: &str) -> io::Result<i32> {
409    use std::ffi::CString;
410    let cpath = CString::new(path)
411        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid device path"))?;
412    let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NONBLOCK) };
413    if fd < 0 {
414        return Err(io::Error::last_os_error());
415    }
416    Ok(fd)
417}
418
419/// Look up a special character name and return its index.
420pub fn find_special_char(name: &str) -> Option<usize> {
421    for &(n, idx) in SPECIAL_CHARS.iter().chain(SPECIAL_CHARS_LINUX.iter()) {
422        if n == name {
423            return Some(idx);
424        }
425    }
426    None
427}
428
429/// Apply a single flag setting. Returns true if the argument was recognized.
430pub fn apply_flag(termios: &mut libc::termios, name: &str) -> bool {
431    let (negate, flag_name) = if let Some(stripped) = name.strip_prefix('-') {
432        (true, stripped)
433    } else {
434        (false, name)
435    };
436
437    // Check input flags
438    for &(n, flag) in INPUT_FLAGS.iter().chain(INPUT_FLAGS_LINUX.iter()) {
439        if n == flag_name {
440            if negate {
441                termios.c_iflag &= !flag;
442            } else {
443                termios.c_iflag |= flag;
444            }
445            return true;
446        }
447    }
448
449    // Check output flags
450    for &(n, flag) in OUTPUT_FLAGS.iter().chain(OUTPUT_FLAGS_LINUX.iter()) {
451        if n == flag_name {
452            if negate {
453                termios.c_oflag &= !flag;
454            } else {
455                termios.c_oflag |= flag;
456            }
457            return true;
458        }
459    }
460
461    // Check control flags
462    for &(n, flag) in CONTROL_FLAGS {
463        if n == flag_name {
464            if negate {
465                termios.c_cflag &= !flag;
466            } else {
467                termios.c_cflag |= flag;
468            }
469            return true;
470        }
471    }
472
473    // Check local flags
474    for &(n, flag) in LOCAL_FLAGS.iter().chain(LOCAL_FLAGS_LINUX.iter()) {
475        if n == flag_name {
476            if negate {
477                termios.c_lflag &= !flag;
478            } else {
479                termios.c_lflag |= flag;
480            }
481            return true;
482        }
483    }
484
485    // Check character size
486    match flag_name {
487        "cs5" => {
488            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS5;
489            return true;
490        }
491        "cs6" => {
492            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS6;
493            return true;
494        }
495        "cs7" => {
496            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS7;
497            return true;
498        }
499        "cs8" => {
500            termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS8;
501            return true;
502        }
503        _ => {}
504    }
505
506    false
507}
508
509/// The result of parsing stty arguments.
510pub enum SttyAction {
511    PrintAll,
512    PrintSize,
513    PrintSpeed,
514    ApplySettings,
515}
516
517/// Parsed stty configuration.
518pub struct SttyConfig {
519    pub action: SttyAction,
520    pub device: Option<String>,
521    pub settings: Vec<String>,
522}
523
524/// Parse command-line arguments for stty.
525pub fn parse_args(args: &[String]) -> Result<SttyConfig, String> {
526    let mut action = SttyAction::ApplySettings;
527    let mut device: Option<String> = None;
528    let mut settings: Vec<String> = Vec::new();
529    let mut i = 0;
530    let mut has_explicit_action = false;
531
532    while i < args.len() {
533        match args[i].as_str() {
534            "-a" | "--all" => {
535                action = SttyAction::PrintAll;
536                has_explicit_action = true;
537            }
538            "-F" | "--file" => {
539                i += 1;
540                if i >= args.len() {
541                    return Err("option requires an argument -- 'F'".to_string());
542                }
543                device = Some(args[i].clone());
544            }
545            s if s.starts_with("--file=") => {
546                device = Some(s["--file=".len()..].to_string());
547            }
548            s if s.starts_with("-F") && s.len() > 2 => {
549                device = Some(s[2..].to_string());
550            }
551            "size" => {
552                action = SttyAction::PrintSize;
553                has_explicit_action = true;
554            }
555            "speed" => {
556                action = SttyAction::PrintSpeed;
557                has_explicit_action = true;
558            }
559            _ => {
560                settings.push(args[i].clone());
561            }
562        }
563        i += 1;
564    }
565
566    if !has_explicit_action && settings.is_empty() {
567        action = SttyAction::PrintAll;
568    }
569
570    Ok(SttyConfig {
571        action,
572        device,
573        settings,
574    })
575}
576
577/// Apply settings from the parsed arguments to a termios structure.
578/// Returns Ok(true) if any changes were made, Ok(false) otherwise.
579pub fn apply_settings(termios: &mut libc::termios, settings: &[String]) -> Result<bool, String> {
580    let mut changed = false;
581    let mut i = 0;
582
583    while i < settings.len() {
584        let arg = &settings[i];
585
586        match arg.as_str() {
587            "sane" => {
588                set_sane(termios);
589                changed = true;
590            }
591            "raw" => {
592                set_raw(termios);
593                changed = true;
594            }
595            "-raw" | "cooked" => {
596                set_cooked(termios);
597                changed = true;
598            }
599            "ispeed" => {
600                i += 1;
601                if i >= settings.len() {
602                    return Err("missing argument to 'ispeed'".to_string());
603                }
604                let n: u32 = settings[i]
605                    .parse()
606                    .map_err(|_| format!("invalid integer argument: '{}'", settings[i]))?;
607                let baud = num_to_baud(n).ok_or_else(|| format!("invalid speed: '{}'", n))?;
608                unsafe {
609                    libc::cfsetispeed(termios, baud);
610                }
611                changed = true;
612            }
613            "ospeed" => {
614                i += 1;
615                if i >= settings.len() {
616                    return Err("missing argument to 'ospeed'".to_string());
617                }
618                let n: u32 = settings[i]
619                    .parse()
620                    .map_err(|_| format!("invalid integer argument: '{}'", settings[i]))?;
621                let baud = num_to_baud(n).ok_or_else(|| format!("invalid speed: '{}'", n))?;
622                unsafe {
623                    libc::cfsetospeed(termios, baud);
624                }
625                changed = true;
626            }
627            _ => {
628                // Check if it is a bare baud rate (numeric)
629                if let Ok(n) = arg.parse::<u32>() {
630                    if let Some(baud) = num_to_baud(n) {
631                        unsafe {
632                            libc::cfsetispeed(termios, baud);
633                            libc::cfsetospeed(termios, baud);
634                        }
635                        changed = true;
636                        i += 1;
637                        continue;
638                    }
639                }
640
641                // Check if it is a special character setting (e.g., "intr ^C")
642                if let Some(idx) = find_special_char(arg) {
643                    i += 1;
644                    if i >= settings.len() {
645                        return Err(format!("missing argument to '{}'", arg));
646                    }
647                    let cc = parse_control_char(&settings[i])
648                        .ok_or_else(|| format!("invalid integer argument: '{}'", settings[i]))?;
649                    termios.c_cc[idx] = cc;
650                    changed = true;
651                    i += 1;
652                    continue;
653                }
654
655                // Try as a flag
656                if !apply_flag(termios, arg) {
657                    return Err(format!("invalid argument '{}'", arg));
658                }
659                changed = true;
660            }
661        }
662
663        i += 1;
664    }
665
666    Ok(changed)
667}