1// Supress warnings for `TIOCGWINSZ.into()` since freebsd requires it.
2#![allow(clippy::identity_conversion)]
3
4use libc::{ioctl, winsize, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ};
5use std::mem::zeroed;
6
7/// Runs the ioctl command. Returns (0, 0) if all of the streams are not to a terminal, or
8/// there is an error. (0, 0) is an invalid size to have anyway, which is why
9/// it can be used as a nil value.
10unsafe fn get_dimensions_any() -> winsize {
11 let mut window: winsize = zeroed();
12 let mut result: i32 = ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut window);
13
14 if result == -1 {
15 window = zeroed();
16 result = ioctl(STDIN_FILENO, TIOCGWINSZ.into(), &mut window);
17 if result == -1 {
18 window = zeroed();
19 result = ioctl(STDERR_FILENO, TIOCGWINSZ.into(), &mut window);
20 if result == -1 {
21 return zeroed();
22 }
23 }
24 }
25 window
26}
27
28/// Runs the ioctl command. Returns (0, 0) if the output is not to a terminal, or
29/// there is an error. (0, 0) is an invalid size to have anyway, which is why
30/// it can be used as a nil value.
31unsafe fn get_dimensions_out() -> winsize {
32 let mut window: winsize = zeroed();
33 let result: i32 = ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut window);
34
35 if result != -1 {
36 return window;
37 }
38 zeroed()
39}
40
41/// Runs the ioctl command. Returns (0, 0) if the input is not to a terminal, or
42/// there is an error. (0, 0) is an invalid size to have anyway, which is why
43/// it can be used as a nil value.
44unsafe fn get_dimensions_in() -> winsize {
45 let mut window: winsize = zeroed();
46 let result: i32 = ioctl(STDIN_FILENO, TIOCGWINSZ.into(), &mut window);
47
48 if result != -1 {
49 return window;
50 }
51 zeroed()
52}
53
54/// Runs the ioctl command. Returns (0, 0) if the error is not to a terminal, or
55/// there is an error. (0, 0) is an invalid size to have anyway, which is why
56/// it can be used as a nil value.
57unsafe fn get_dimensions_err() -> winsize {
58 let mut window: winsize = zeroed();
59 let result: i32 = ioctl(STDERR_FILENO, TIOCGWINSZ.into(), &mut window);
60
61 if result != -1 {
62 return window;
63 }
64 zeroed()
65}
66
67/// Query the current processes's output (`stdout`), input (`stdin`), and error (`stderr`) in
68/// that order, in the attempt to determine terminal width. If one of those streams is actually
69/// a tty, this function returns its width and height as a number of characters.
70///
71/// # Errors
72///
73/// If *all* of the streams are not ttys or return any errors this function will return `None`.
74///
75/// # Example
76///
77/// To get the dimensions of your terminal window, simply use the following:
78///
79/// ```no_run
80/// if let Some((w, h)) = termize::dimensions() {
81/// println!("Width: {}\nHeight: {}", w, h);
82/// } else {
83/// println!("Unable to get term size :(");
84/// }
85/// ```
86pub fn dimensions() -> Option<(usize, usize)> {
87 let w: winsize = unsafe { get_dimensions_any() };
88
89 if w.ws_col == 0 || w.ws_row == 0 {
90 None
91 } else {
92 Some((w.ws_col as usize, w.ws_row as usize))
93 }
94}
95
96/// Query the current processes's output (`stdout`) *only*, in the attempt to determine
97/// terminal width. If that stream is actually a tty, this function returns its width
98/// and height as a number of characters.
99///
100/// # Errors
101///
102/// If the stream is not a tty or return any errors this function will return `None`.
103///
104/// # Example
105///
106/// To get the dimensions of your terminal window, simply use the following:
107///
108/// ```no_run
109/// if let Some((w, h)) = termize::dimensions_stdout() {
110/// println!("Width: {}\nHeight: {}", w, h);
111/// } else {
112/// println!("Unable to get term size :(");
113/// }
114/// ```
115pub fn dimensions_stdout() -> Option<(usize, usize)> {
116 let w: winsize = unsafe { get_dimensions_out() };
117
118 if w.ws_col == 0 || w.ws_row == 0 {
119 None
120 } else {
121 Some((w.ws_col as usize, w.ws_row as usize))
122 }
123}
124
125/// Query the current processes's input (`stdin`) *only*, in the attempt to determine
126/// terminal width. If that stream is actually a tty, this function returns its width
127/// and height as a number of characters.
128///
129/// # Errors
130///
131/// If the stream is not a tty or return any errors this function will return `None`.
132///
133/// # Example
134///
135/// To get the dimensions of your terminal window, simply use the following:
136///
137/// ```no_run
138/// if let Some((w, h)) = termize::dimensions_stdin() {
139/// println!("Width: {}\nHeight: {}", w, h);
140/// } else {
141/// println!("Unable to get term size :(");
142/// }
143/// ```
144pub fn dimensions_stdin() -> Option<(usize, usize)> {
145 let w: winsize = unsafe { get_dimensions_in() };
146
147 if w.ws_col == 0 || w.ws_row == 0 {
148 None
149 } else {
150 Some((w.ws_col as usize, w.ws_row as usize))
151 }
152}
153
154/// Query the current processes's error output (`stderr`) *only*, in the attempt to dtermine
155/// terminal width. If that stream is actually a tty, this function returns its width
156/// and height as a number of characters.
157///
158/// # Errors
159///
160/// If the stream is not a tty or return any errors this function will return `None`.
161///
162/// # Example
163///
164/// To get the dimensions of your terminal window, simply use the following:
165///
166/// ```no_run
167/// if let Some((w, h)) = termize::dimensions_stderr() {
168/// println!("Width: {}\nHeight: {}", w, h);
169/// } else {
170/// println!("Unable to get term size :(");
171/// }
172/// ```
173pub fn dimensions_stderr() -> Option<(usize, usize)> {
174 let w: winsize = unsafe { get_dimensions_err() };
175
176 if w.ws_col == 0 || w.ws_row == 0 {
177 None
178 } else {
179 Some((w.ws_col as usize, w.ws_row as usize))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::dimensions;
186 use std::process::{Command, Output, Stdio};
187
188 #[cfg(target_os = "macos")]
189 fn stty_size() -> Output {
190 Command::new("stty")
191 .arg("-f")
192 .arg("/dev/stderr")
193 .arg("size")
194 .stderr(Stdio::inherit())
195 .output()
196 .unwrap()
197 }
198
199 #[cfg(not(target_os = "macos"))]
200 fn stty_size() -> Output {
201 Command::new("stty")
202 .arg("-F")
203 .arg("/dev/stderr")
204 .arg("size")
205 .stderr(Stdio::inherit())
206 .output()
207 .expect("failed to run `stty_size()`")
208 }
209
210 #[test]
211 fn test_shell() {
212 let output = stty_size();
213 let stdout = String::from_utf8(output.stdout).expect("failed to turn into String");
214 let mut data = stdout.split_whitespace();
215 let rs = data
216 .next()
217 .unwrap_or("0")
218 .parse::<usize>()
219 .expect("failed to parse rows");
220 let cs = data
221 .next()
222 .unwrap_or("0")
223 .parse::<usize>()
224 .expect("failed to parse cols");
225 println!("stdout: {}", stdout);
226 println!("rows: {}\ncols: {}", rs, cs);
227 if let Some((w, h)) = dimensions() {
228 assert_eq!(rs, h);
229 assert_eq!(cs, w);
230 }
231 }
232}
233