argp/term_size/
unix.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// SPDX-FileCopyrightText: 2023 Jakub Jirutka <jakub@jirutka.cz>
3
4// It's based on https://github.com/clap-rs/term_size-rs/blob/master/src/platform/unix.rs.
5
6use std::mem::zeroed;
7use std::os::raw::{c_int, c_ulong, c_ushort};
8
9static STDOUT_FILENO: c_int = 1;
10
11// Unfortunately the actual command is not standardised...
12#[cfg(any(target_os = "linux", target_os = "android"))]
13static TIOCGWINSZ: c_ulong = 0x5413;
14
15#[cfg(any(
16    target_os = "macos",
17    target_os = "ios",
18    target_os = "dragonfly",
19    target_os = "freebsd",
20    target_os = "netbsd",
21    target_os = "openbsd"
22))]
23static TIOCGWINSZ: c_ulong = 0x40087468;
24
25#[cfg(target_os = "solaris")]
26static TIOCGWINSZ: c_ulong = 0x5468;
27
28// This has been copied from the libc crate.
29#[repr(C)]
30struct winsize {
31    ws_row: c_ushort,
32    ws_col: c_ushort,
33    ws_xpixel: c_ushort,
34    ws_ypixel: c_ushort,
35}
36
37extern "C" {
38    fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
39}
40
41/// Runs the ioctl command. Returns (0, 0) if the output is not to a terminal,
42/// or 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() -> winsize {
45    let mut window: winsize = zeroed();
46    let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window);
47
48    if result != -1 {
49        return window;
50    }
51    zeroed()
52}
53
54/// Returns width of the terminal based on the current processes' stdout, if the
55/// stdout stream is actually a tty. If it is not a tty, returns `None`.
56pub fn term_cols() -> Option<usize> {
57    let winsize { ws_col, .. } = unsafe { get_dimensions() };
58
59    if ws_col == 0 {
60        None
61    } else {
62        Some(ws_col as usize)
63    }
64}
65
66#[cfg(test)]
67mod test {
68    // Compare with the output of `stty size`
69    // This has been copied from https://github.com/eminence/terminal-size/blob/master/src/unix.rs.
70    #[test]
71    fn compare_with_stty() {
72        use std::process::{Command, Stdio};
73
74        let output = if cfg!(target_os = "linux") {
75            Command::new("stty")
76                .arg("size")
77                .arg("-F")
78                .arg("/dev/stderr")
79                .stderr(Stdio::inherit())
80                .output()
81                .unwrap()
82        } else {
83            Command::new("stty")
84                .arg("-f")
85                .arg("/dev/stderr")
86                .arg("size")
87                .stderr(Stdio::inherit())
88                .output()
89                .unwrap()
90        };
91        assert!(output.status.success());
92
93        let stdout = String::from_utf8(output.stdout).unwrap();
94        println!("stty: {}", stdout);
95
96        // stdout is "rows cols"
97        let mut data = stdout.split_whitespace();
98        let expected: usize = str::parse(data.nth(1).unwrap()).unwrap();
99        println!("cols: {}", expected);
100
101        if let Some(actual) = super::term_cols() {
102            assert_eq!(actual, expected);
103        // This may happen e.g. on CI.
104        } else if expected == 0 {
105            eprintln!("WARN: stty reports cols 0, skipping test");
106        } else {
107            panic!("term_cols() return None");
108        }
109    }
110}