Skip to main content

rskit_process/pty/
size.rs

1//! Pseudoterminal window size and terminal detection.
2
3use std::os::unix::io::AsRawFd;
4
5/// Window size of a pseudoterminal, in character cells plus optional pixels.
6///
7/// A child process attached to a PTY reads this through `TIOCGWINSZ` to lay out its output (wrapping, progress bars, table widths).
8/// Mirroring the size of the controlling terminal keeps rendered output identical to running the command directly.
9#[derive(Debug, Clone, Copy, Eq, PartialEq)]
10pub struct PtySize {
11    /// Number of visible rows (character cells).
12    pub rows: u16,
13    /// Number of visible columns (character cells).
14    pub cols: u16,
15    /// Pixel width of the terminal, `0` when unknown.
16    pub pixel_width: u16,
17    /// Pixel height of the terminal, `0` when unknown.
18    pub pixel_height: u16,
19}
20
21impl Default for PtySize {
22    /// The conventional `80x24` fallback used when no terminal size is known.
23    fn default() -> Self {
24        Self {
25            rows: 24,
26            cols: 80,
27            pixel_width: 0,
28            pixel_height: 0,
29        }
30    }
31}
32
33impl PtySize {
34    /// Create a size from an explicit `rows` and `cols`, leaving pixels unset.
35    #[must_use]
36    pub const fn new(rows: u16, cols: u16) -> Self {
37        Self {
38            rows,
39            cols,
40            pixel_width: 0,
41            pixel_height: 0,
42        }
43    }
44
45    /// Convert to the libc `winsize` used by `openpty`/`TIOCSWINSZ`.
46    pub(crate) const fn to_winsize(self) -> libc::winsize {
47        libc::winsize {
48            ws_row: self.rows,
49            ws_col: self.cols,
50            ws_xpixel: self.pixel_width,
51            ws_ypixel: self.pixel_height,
52        }
53    }
54}
55
56/// Query the window size of the terminal backing `fd`.
57///
58/// Returns `None` when `fd` is not a terminal (for example a pipe or a file, as under CI or output redirection)
59/// or the `TIOCGWINSZ` query fails.
60/// Callers use a `None` result to decide that no PTY should be allocated.
61#[must_use]
62pub fn terminal_size(fd: &impl AsRawFd) -> Option<PtySize> {
63    // SAFETY: `winsize` is a plain C struct with no invariants,
64    // so an all-zero value is a valid initial state that `ioctl` fully overwrites.
65    let mut winsize: libc::winsize = unsafe { std::mem::zeroed() };
66    // SAFETY: `TIOCGWINSZ` writes a `winsize` through the provided pointer, which is a valid,
67    // uniquely-borrowed local. A non-zero return means the fd is not a terminal,
68    // which we surface as `None`.
69    let result = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCGWINSZ, &raw mut winsize) };
70    if result != 0 {
71        return None;
72    }
73    Some(PtySize {
74        rows: winsize.ws_row,
75        cols: winsize.ws_col,
76        pixel_width: winsize.ws_xpixel,
77        pixel_height: winsize.ws_ypixel,
78    })
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn default_is_eighty_by_twenty_four() {
87        let size = PtySize::default();
88        assert_eq!(size.cols, 80);
89        assert_eq!(size.rows, 24);
90        assert_eq!(size.pixel_width, 0);
91        assert_eq!(size.pixel_height, 0);
92    }
93
94    #[test]
95    fn new_sets_rows_and_cols_only() {
96        let size = PtySize::new(40, 120);
97        assert_eq!(size.rows, 40);
98        assert_eq!(size.cols, 120);
99        assert_eq!(size.pixel_width, 0);
100    }
101
102    #[test]
103    fn winsize_round_trips_dimensions() {
104        let winsize = PtySize {
105            rows: 10,
106            cols: 20,
107            pixel_width: 30,
108            pixel_height: 40,
109        }
110        .to_winsize();
111        assert_eq!(winsize.ws_row, 10);
112        assert_eq!(winsize.ws_col, 20);
113        assert_eq!(winsize.ws_xpixel, 30);
114        assert_eq!(winsize.ws_ypixel, 40);
115    }
116
117    #[test]
118    fn non_terminal_fd_has_no_size() {
119        // A pipe read end is never a terminal.
120        let (reader, _writer) = std::io::pipe().expect("pipe");
121        assert!(terminal_size(&reader).is_none());
122    }
123
124    #[test]
125    fn pty_slave_reports_configured_terminal_size() {
126        let pair = crate::pty::open_pty(PtySize::new(33, 111)).expect("openpty");
127        let size = terminal_size(&pair.slave).expect("pty slave has size");
128
129        assert_eq!(size.rows, 33);
130        assert_eq!(size.cols, 111);
131    }
132}