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: (Width, Height)) = terminal_size_using_fd(std::io::stdout().as_raw_fd()) {
12 Some(size)
13 } else if let Some(size: (Width, Height)) = terminal_size_using_fd(std::io::stderr().as_raw_fd()) {
14 Some(size)
15 } else if let Some(size: (Width, Height)) = 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: BorrowedFd<'_> = unsafe { BorrowedFd::borrow_raw(fd) };
32
33 if !isatty(fd) {
34 return None;
35 }
36
37 let winsize: winsize = tcgetwinsize(fd).ok()?;
38
39 let rows: u16 = winsize.ws_row;
40 let cols: u16 = 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}
122