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: (Width, Height)) = terminal_size_of(fd:std::io::stdout()) {
11 Some(size)
12 } else if let Some(size: (Width, Height)) = terminal_size_of(fd:std::io::stderr()) {
13 Some(size)
14 } else if let Some(size: (Width, Height)) = terminal_size_of(fd: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: Winsize = tcgetwinsize(&fd).ok()?;
32
33 let rows: u16 = winsize.ws_row;
34 let cols: u16 = 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(fd: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}
131