terminal_size/
unix.rs

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