Skip to main content

usage_argv/
tty.rs

1//! How wide the terminal is, asked of the terminal itself.
2//!
3//! A help page that wraps at 80 columns on a 200-column terminal is the most visible thing a
4//! CLI can get wrong about its own output, and `COLUMNS` does not answer the question: no
5//! POSIX shell exports it, so it is unset in almost every process that is not an interactive
6//! shell's own. The only answer is to ask the kernel — `TIOCGWINSZ` on unix, the console
7//! screen buffer on Windows — which is two dozen lines of FFI vendored here rather than a
8//! dependency, because this crate runs on every invocation of every CLI built on it and has
9//! none.
10//!
11//! This is the one place in the runtime that is allowed `unsafe`, which is why it is a module
12//! of its own: the crate is `deny(unsafe_code)` and only this file opts out. Nothing here
13//! dereferences a pointer the caller supplied or hands one out; each call fills a struct
14//! this module owns and reads two integers back out of it.
15//!
16//! A page is rendered by whichever implementation the CLI was built with, and all of them have
17//! to reach the same width on the same terminal. usage-lib calls this module rather than
18//! carrying a second copy of the FFI; Go's `argv.terminalColumns` is the one twin, because it
19//! cannot call this one.
20#![allow(unsafe_code)]
21
22/// The terminal's width in columns, or `None` when output is not a terminal.
23///
24/// Standard output is asked first and standard error second, so that a page a CLI prints to
25/// stderr — the short page that accompanies a usage error — is still laid out for the
26/// terminal the user is looking at when stdout has been redirected to a file.
27pub fn columns() -> Option<usize> {
28    const STDOUT: i32 = 1;
29    const STDERR: i32 = 2;
30    columns_of(STDOUT).or_else(|| columns_of(STDERR))
31}
32
33#[cfg(unix)]
34fn columns_of(fd: i32) -> Option<usize> {
35    use std::ffi::{c_int, c_ulong};
36
37    /// `struct winsize`, which every unix agrees on even where the request number differs.
38    #[repr(C)]
39    struct Winsize {
40        rows: u16,
41        columns: u16,
42        width_pixels: u16,
43        height_pixels: u16,
44    }
45
46    // `TIOCGWINSZ` is 0x5413 on Linux's asm-generic ioctl numbering and 0x40087468 in the
47    // BSD-derived numbering — which is what macOS and the BSDs use, and what the four Linux
48    // architectures that kept their historical ABI use as well.
49    #[cfg(all(
50        target_os = "linux",
51        not(any(
52            target_arch = "mips",
53            target_arch = "mips32r6",
54            target_arch = "mips64",
55            target_arch = "mips64r6",
56            target_arch = "powerpc",
57            target_arch = "powerpc64",
58            target_arch = "sparc",
59            target_arch = "sparc64",
60        ))
61    ))]
62    const TIOCGWINSZ: c_ulong = 0x5413;
63    #[cfg(target_os = "android")]
64    const TIOCGWINSZ: c_ulong = 0x5413;
65    #[cfg(not(any(
66        target_os = "android",
67        all(
68            target_os = "linux",
69            not(any(
70                target_arch = "mips",
71                target_arch = "mips32r6",
72                target_arch = "mips64",
73                target_arch = "mips64r6",
74                target_arch = "powerpc",
75                target_arch = "powerpc64",
76                target_arch = "sparc",
77                target_arch = "sparc64",
78            ))
79        )
80    )))]
81    const TIOCGWINSZ: c_ulong = 0x4008_7468;
82
83    unsafe extern "C" {
84        fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
85    }
86
87    let mut size = Winsize {
88        rows: 0,
89        columns: 0,
90        width_pixels: 0,
91        height_pixels: 0,
92    };
93    // SAFETY: `ioctl` is handed a descriptor number and a pointer to a `winsize` this frame
94    // owns and keeps alive across the call. `TIOCGWINSZ` writes that struct and nothing else,
95    // and a failure — a redirected descriptor, a platform whose request number this is not —
96    // returns non-zero without having written anything, which is the branch taken below.
97    let result = unsafe { ioctl(fd as c_int, TIOCGWINSZ, &raw mut size) };
98    if result != 0 {
99        return None;
100    }
101    // A terminal that reports no width — some CI pseudo-terminals do — is no answer, and
102    // falling through to the caller's default is better than laying a page out at zero.
103    (size.columns > 0).then_some(size.columns as usize)
104}
105
106#[cfg(windows)]
107fn columns_of(fd: i32) -> Option<usize> {
108    use std::ffi::c_void;
109
110    #[repr(C)]
111    #[derive(Default)]
112    struct Coord {
113        x: i16,
114        y: i16,
115    }
116
117    #[repr(C)]
118    #[derive(Default)]
119    struct SmallRect {
120        left: i16,
121        top: i16,
122        right: i16,
123        bottom: i16,
124    }
125
126    #[repr(C)]
127    #[derive(Default)]
128    struct ScreenBufferInfo {
129        size: Coord,
130        cursor_position: Coord,
131        attributes: u16,
132        window: SmallRect,
133        maximum_window_size: Coord,
134    }
135
136    // The console API names its streams with negative constants rather than descriptors.
137    const STD_OUTPUT_HANDLE: u32 = -11_i32 as u32;
138    const STD_ERROR_HANDLE: u32 = -12_i32 as u32;
139    const INVALID_HANDLE_VALUE: *mut c_void = -1_isize as *mut c_void;
140
141    #[link(name = "kernel32")]
142    unsafe extern "system" {
143        fn GetStdHandle(which: u32) -> *mut c_void;
144        fn GetConsoleScreenBufferInfo(handle: *mut c_void, info: *mut ScreenBufferInfo) -> i32;
145    }
146
147    let which = match fd {
148        2 => STD_ERROR_HANDLE,
149        _ => STD_OUTPUT_HANDLE,
150    };
151    // SAFETY: `GetStdHandle` takes an integer and returns a handle the process already owns;
152    // `GetConsoleScreenBufferInfo` writes the struct below, which this frame owns and keeps
153    // alive across the call. A redirected stream returns `INVALID_HANDLE_VALUE` or reports
154    // failure without writing, and both are handled here.
155    let columns = unsafe {
156        let handle = GetStdHandle(which);
157        if handle.is_null() || handle == INVALID_HANDLE_VALUE {
158            return None;
159        }
160        let mut info = ScreenBufferInfo::default();
161        if GetConsoleScreenBufferInfo(handle, &raw mut info) == 0 {
162            return None;
163        }
164        // The buffer is often taller and wider than the window; what a line has room for is
165        // the visible window, inclusive of both edges.
166        i32::from(info.window.right) - i32::from(info.window.left) + 1
167    };
168    (columns > 0).then_some(columns as usize)
169}
170
171#[cfg(not(any(unix, windows)))]
172fn columns_of(_fd: i32) -> Option<usize> {
173    None
174}
175
176#[cfg(test)]
177mod tests {
178    /// Under `cargo test` standard output is the developer's terminal or CI's pipe, so the
179    /// only thing this can assert is that asking is safe and answers something sane.
180    #[test]
181    fn asking_the_terminal_never_answers_zero() {
182        assert_ne!(super::columns(), Some(0));
183    }
184}