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