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}