pulldown_cmark_mdcat/terminal/
size.rs

1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Terminal size.
8
9use std::cmp::Ordering;
10
11/// The size of a terminal window in pixels.
12///
13/// This type is partially ordered; a value is smaller than another if all fields
14/// are smaller, and greater if all fields are greater.
15///
16/// If either field is greater and the other smaller values aren't orderable.
17#[derive(Debug, Copy, Clone)]
18pub struct PixelSize {
19    /// The width of the window, in pixels.
20    pub x: u32,
21    // The height of the window, in pixels.
22    pub y: u32,
23}
24
25impl PixelSize {
26    /// Create a pixel size for a `(x, y)` pair.
27    pub fn from_xy((x, y): (u32, u32)) -> Self {
28        Self { x, y }
29    }
30}
31
32impl PartialEq for PixelSize {
33    fn eq(&self, other: &Self) -> bool {
34        matches!(self.partial_cmp(other), Some(Ordering::Equal))
35    }
36}
37
38impl PartialOrd for PixelSize {
39    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
40        if self.x == other.x && self.y == other.y {
41            Some(Ordering::Equal)
42        } else if self.x < other.x && self.y < other.y {
43            Some(Ordering::Less)
44        } else if self.x > other.x && self.y > other.y {
45            Some(Ordering::Greater)
46        } else {
47            None
48        }
49    }
50}
51
52/// The size of a terminal.
53#[derive(Debug, Copy, Clone, PartialEq)]
54pub struct TerminalSize {
55    /// The width of the terminal, in characters aka columns.
56    pub columns: u16,
57    /// The height of the terminal, in lines.
58    pub rows: u16,
59    /// The size in pixels, if available.
60    pub pixels: Option<PixelSize>,
61    /// The size of once cell, if available.
62    pub cell: Option<PixelSize>,
63}
64
65impl Default for TerminalSize {
66    fn default() -> Self {
67        TerminalSize {
68            columns: 80,
69            rows: 24,
70            pixels: None,
71            cell: None,
72        }
73    }
74}
75
76#[cfg(unix)]
77mod implementation {
78    use rustix::termios::{tcgetwinsize, Winsize};
79    use tracing::{event, Level};
80
81    use crate::TerminalSize;
82    use std::fs::File;
83    use std::io::Result;
84    use std::path::Path;
85
86    use super::PixelSize;
87
88    /// Get the ID of the controlling terminal.
89    ///
90    /// This implementation currently just returns `/dev/tty`, which refers to the current TTY on
91    /// Linux and macOS at least.
92    fn ctermid() -> &'static Path {
93        Path::new("/dev/tty")
94    }
95
96    fn from_cterm() -> Result<Winsize> {
97        let tty = File::open(ctermid())?;
98        tcgetwinsize(&tty).map_err(Into::into)
99    }
100
101    /// Query terminal size on Unix.
102    ///
103    /// Open the underlying controlling terminal via ctermid and open, and issue a
104    /// TIOCGWINSZ ioctl to the device.
105    ///
106    /// We do this manually because terminal_size currently doesn't support pixel
107    /// size see <https://github.com/eminence/terminal-size/issues/22>.
108    pub fn from_terminal() -> Option<TerminalSize> {
109        let winsize = from_cterm()
110            .map_err(|error| {
111                event!(
112                    Level::ERROR,
113                    "Failed to read terminal size from controlling terminal: {}",
114                    error
115                );
116                error
117            })
118            .ok()?;
119        if winsize.ws_row == 0 || winsize.ws_col == 0 {
120            event!(
121                Level::WARN,
122                "Invalid terminal size returned, columns or rows were 0: {:?}",
123                winsize
124            );
125            None
126        } else {
127            let mut terminal_size = TerminalSize {
128                columns: winsize.ws_col,
129                rows: winsize.ws_row,
130                pixels: None,
131                cell: None,
132            };
133            if winsize.ws_xpixel != 0 && winsize.ws_ypixel != 0 {
134                let pixels = PixelSize {
135                    x: winsize.ws_xpixel as u32,
136                    y: winsize.ws_ypixel as u32,
137                };
138                terminal_size.pixels = Some(pixels);
139                terminal_size.cell = Some(PixelSize {
140                    x: pixels.x / terminal_size.columns as u32,
141                    y: pixels.y / terminal_size.rows as u32,
142                });
143            };
144            Some(terminal_size)
145        }
146    }
147}
148
149#[cfg(windows)]
150mod implementation {
151    use terminal_size::{terminal_size, Height, Width};
152
153    use super::TerminalSize;
154
155    pub fn from_terminal() -> Option<TerminalSize> {
156        terminal_size().map(|(Width(columns), Height(rows))| TerminalSize {
157            rows,
158            columns,
159            pixels: None,
160            cell: None,
161        })
162    }
163}
164
165impl TerminalSize {
166    /// Get terminal size from `$COLUMNS` and `$LINES`.
167    ///
168    /// Do not assume any knowledge about window size.
169    pub fn from_env() -> Option<Self> {
170        let columns = std::env::var("COLUMNS")
171            .ok()
172            .and_then(|value| value.parse::<u16>().ok());
173        let rows = std::env::var("LINES")
174            .ok()
175            .and_then(|value| value.parse::<u16>().ok());
176
177        match (columns, rows) {
178            (Some(columns), Some(rows)) => Some(Self {
179                columns,
180                rows,
181                pixels: None,
182                cell: None,
183            }),
184            _ => None,
185        }
186    }
187
188    /// Detect the terminal size by querying the underlying terminal.
189    ///
190    /// On unix this issues a ioctl to the controlling terminal.
191    ///
192    /// On Windows this uses the [terminal_size] crate which does some magic windows API calls.
193    ///
194    /// [terminal_size]: https://docs.rs/terminal_size/
195    pub fn from_terminal() -> Option<Self> {
196        implementation::from_terminal()
197    }
198
199    /// Detect the terminal size.
200    ///
201    /// Get the terminal size from the underlying TTY, and fallback to
202    /// `$COLUMNS` and `$LINES`.
203    pub fn detect() -> Option<Self> {
204        Self::from_terminal().or_else(Self::from_env)
205    }
206
207    /// Shrink the terminal size to the given amount of maximum columns.
208    ///
209    /// Also shrinks the pixel size accordingly.
210    pub fn with_max_columns(&self, max_columns: u16) -> Self {
211        let pixels = match (self.pixels, self.cell) {
212            (Some(pixels), Some(cell)) => Some(PixelSize {
213                x: cell.x * max_columns as u32,
214                y: pixels.y,
215            }),
216            _ => None,
217        };
218        Self {
219            columns: max_columns,
220            rows: self.rows,
221            pixels,
222            cell: self.cell,
223        }
224    }
225}