terminal_size/
unix.rs

1use super::{Height, Width};
2use rustix::fd::{BorrowedFd, AsRawFd};
3use std::os::unix::io::RawFd;
4
5/// Returns the size of the terminal.
6///
7/// This function checks the stdout, stderr, and stdin streams (in that order).
8/// The size of the first stream that is a TTY will be returned.  If nothing
9/// is a TTY, then `None` is returned.
10pub fn terminal_size() -> Option<(Width, Height)> {
11    if let Some(size) = terminal_size_using_fd(std::io::stdout().as_raw_fd()) {
12        Some(size)
13    } else if let Some(size) = terminal_size_using_fd(std::io::stderr().as_raw_fd()) {
14        Some(size)
15    } else if let Some(size) = terminal_size_using_fd(std::io::stdin().as_raw_fd()) {
16        Some(size)
17    } else {
18        None
19    }
20}
21
22/// Returns the size of the terminal using the given file descriptor, if available.
23///
24/// If the given file descriptor is not a tty, returns `None`
25pub fn terminal_size_using_fd(fd: RawFd) -> Option<(Width, Height)> {
26    use rustix::termios::{isatty, tcgetwinsize};
27
28    // TODO: Once I/O safety is stabilized, the enlosing function here should
29    // be unsafe due to taking a `RawFd`. We should then move the main
30    // logic here into a new function which takes a `BorrowedFd` and is safe.
31    let fd = unsafe { BorrowedFd::borrow_raw(fd) };
32
33    if !isatty(fd) {
34        return None;
35    }
36
37    let winsize = tcgetwinsize(fd).ok()?;
38
39    let rows = winsize.ws_row;
40    let cols = winsize.ws_col;
41
42    if rows > 0 && cols > 0 {
43        Some((Width(cols), Height(rows)))
44    } else {
45        None
46    }
47}
48
49#[test]
50/// Compare with the output of `stty size`
51fn compare_with_stty() {
52    use std::process::Command;
53    use std::process::Stdio;
54
55    let (rows, cols) = if cfg!(target_os = "illumos") {
56        // illumos stty(1) does not accept a device argument, instead using
57        // stdin unconditionally:
58        let output = Command::new("stty")
59            .stdin(Stdio::inherit())
60            .output()
61            .unwrap();
62        assert!(output.status.success());
63
64        // stdout includes the row and columns thus: "rows = 80; columns = 24;"
65        let vals = String::from_utf8(output.stdout)
66            .unwrap()
67            .lines()
68            .map(|line| {
69                // Split each line on semicolons to get "k = v" strings:
70                line.split(';')
71                    .map(str::trim)
72                    .map(str::to_string)
73                    .collect::<Vec<_>>()
74            })
75            .flatten()
76            .filter_map(|term| {
77                // split each "k = v" string and look for rows/columns:
78                match term.splitn(2, " = ").collect::<Vec<_>>().as_slice() {
79                    ["rows", n] | ["columns", n] => Some(n.parse().unwrap()),
80                    _ => None,
81                }
82            })
83            .collect::<Vec<_>>();
84        (vals[0], vals[1])
85    } else {
86        let output = if cfg!(target_os = "linux") {
87            Command::new("stty")
88                .arg("size")
89                .arg("-F")
90                .arg("/dev/stderr")
91                .stderr(Stdio::inherit())
92                .output()
93                .unwrap()
94        } else {
95            Command::new("stty")
96                .arg("-f")
97                .arg("/dev/stderr")
98                .arg("size")
99                .stderr(Stdio::inherit())
100                .output()
101                .unwrap()
102        };
103
104        assert!(output.status.success());
105        let stdout = String::from_utf8(output.stdout).unwrap();
106        // stdout is "rows cols"
107        let mut data = stdout.split_whitespace();
108        println!("{}", stdout);
109        let rows = u16::from_str_radix(data.next().unwrap(), 10).unwrap();
110        let cols = u16::from_str_radix(data.next().unwrap(), 10).unwrap();
111        (rows, cols)
112    };
113    println!("{} {}", rows, cols);
114
115    if let Some((Width(w), Height(h))) = terminal_size() {
116        assert_eq!(rows, h);
117        assert_eq!(cols, w);
118    } else {
119        panic!("terminal_size() return None");
120    }
121}