Skip to main content

urushi_terminal/
query.rs

1//! Queries for state owned or reported by a terminal backend.
2
3use std::io;
4
5use crate::{Position, TerminalCapabilities, TerminalSize};
6
7/// An RGB background color reported by a terminal.
8///
9/// Channels retain the sixteen-bit precision available from an OSC 11 reply.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct TerminalBackground {
12    red: u16,
13    green: u16,
14    blue: u16,
15}
16
17impl TerminalBackground {
18    pub const fn new(red: u16, green: u16, blue: u16) -> Self {
19        Self { red, green, blue }
20    }
21
22    pub const fn red(self) -> u16 {
23        self.red
24    }
25
26    pub const fn green(self) -> u16 {
27        self.green
28    }
29
30    pub const fn blue(self) -> u16 {
31        self.blue
32    }
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
36pub struct PixelSize {
37    width: usize,
38    height: usize,
39}
40
41impl PixelSize {
42    pub const fn new(width: usize, height: usize) -> Self {
43        Self { width, height }
44    }
45
46    pub const fn width(self) -> usize {
47        self.width
48    }
49
50    pub const fn height(self) -> usize {
51        self.height
52    }
53}
54
55/// Character-cell dimensions and optional pixel geometry for a terminal window.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
57pub struct WindowSize {
58    cells: TerminalSize,
59    pixels: Option<PixelSize>,
60}
61
62impl WindowSize {
63    pub const fn new(cells: TerminalSize, pixels: Option<PixelSize>) -> Self {
64        Self { cells, pixels }
65    }
66
67    pub const fn cells(self) -> TerminalSize {
68        self.cells
69    }
70
71    /// Returns pixel geometry when the platform reports meaningful values.
72    pub const fn pixels(self) -> Option<PixelSize> {
73        self.pixels
74    }
75
76    /// Returns the pixel dimensions of one character cell when the reported
77    /// window geometry divides into a uniform non-empty cell grid.
78    pub const fn cell_pixels(self) -> Option<PixelSize> {
79        let Some(pixels) = self.pixels else {
80            return None;
81        };
82        if self.cells.columns() == 0
83            || self.cells.rows() == 0
84            || pixels.width() % self.cells.columns() != 0
85            || pixels.height() % self.cells.rows() != 0
86        {
87            return None;
88        }
89        let width = pixels.width() / self.cells.columns();
90        let height = pixels.height() / self.cells.rows();
91        if width == 0 || height == 0 {
92            None
93        } else {
94            Some(PixelSize::new(width, height))
95        }
96    }
97}
98
99/// Synchronous inspection of terminal state.
100///
101/// Queries take mutable access because some terminals answer only after the
102/// backend writes a protocol request and consumes its response from the input
103/// stream. This also prevents an event reader from racing that exchange.
104pub trait TerminalQuery {
105    /// Queries the current visible size in character cells.
106    fn terminal_size(&mut self) -> io::Result<TerminalSize>;
107
108    /// Queries the current zero-based cursor position.
109    fn cursor_position(&mut self) -> io::Result<Position>;
110
111    /// Queries both cell and pixel dimensions when the platform exposes them.
112    fn window_size(&mut self) -> io::Result<WindowSize>;
113
114    /// Reports whether process terminal input is currently in raw mode.
115    fn raw_mode_enabled(&mut self) -> io::Result<bool>;
116
117    /// Queries rendering capabilities confirmed by the terminal.
118    ///
119    /// Backends that cannot exchange capability queries return an empty set
120    /// rather than inferring support from environment variables or terminal
121    /// names.
122    fn terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
123        Ok(TerminalCapabilities::none())
124    }
125
126    /// Queries the terminal's current background color.
127    ///
128    /// Backends that cannot exchange an OSC 11 query return `Ok(None)`.
129    /// A missing, timed-out, or malformed reply also produces `Ok(None)`;
130    /// failures of the owned terminal connection remain errors.
131    fn terminal_background(&mut self) -> io::Result<Option<TerminalBackground>> {
132        Ok(None)
133    }
134}
135
136impl<T: TerminalQuery + ?Sized> TerminalQuery for &mut T {
137    fn terminal_size(&mut self) -> io::Result<TerminalSize> {
138        T::terminal_size(self)
139    }
140
141    fn cursor_position(&mut self) -> io::Result<Position> {
142        T::cursor_position(self)
143    }
144
145    fn window_size(&mut self) -> io::Result<WindowSize> {
146        T::window_size(self)
147    }
148
149    fn raw_mode_enabled(&mut self) -> io::Result<bool> {
150        T::raw_mode_enabled(self)
151    }
152
153    fn terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
154        T::terminal_capabilities(self)
155    }
156
157    fn terminal_background(&mut self) -> io::Result<Option<TerminalBackground>> {
158        T::terminal_background(self)
159    }
160}
161
162/// Detects whether enhanced keyboard protocol negotiation is available.
163///
164/// This is separate from general inspection because session acquisition needs
165/// only this capability. Backends may answer from platform knowledge or by
166/// exchanging a protocol query with the terminal.
167pub trait KeyboardEnhancementQuery {
168    fn supports_keyboard_enhancement(&mut self) -> io::Result<bool>;
169}
170
171impl<T: KeyboardEnhancementQuery + ?Sized> KeyboardEnhancementQuery for &mut T {
172    fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
173        T::supports_keyboard_enhancement(self)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    struct UnsupportedQuery;
182
183    impl TerminalQuery for UnsupportedQuery {
184        fn terminal_size(&mut self) -> io::Result<TerminalSize> {
185            Ok(TerminalSize::new(80, 24))
186        }
187
188        fn cursor_position(&mut self) -> io::Result<Position> {
189            Ok(Position::new(0, 0))
190        }
191
192        fn window_size(&mut self) -> io::Result<WindowSize> {
193            Ok(WindowSize::new(TerminalSize::new(80, 24), None))
194        }
195
196        fn raw_mode_enabled(&mut self) -> io::Result<bool> {
197            Ok(false)
198        }
199    }
200
201    #[test]
202    fn cell_pixels_require_exact_uniform_geometry() {
203        assert_eq!(
204            WindowSize::new(TerminalSize::new(80, 24), Some(PixelSize::new(800, 480)),)
205                .cell_pixels(),
206            Some(PixelSize::new(10, 20))
207        );
208        assert_eq!(
209            WindowSize::new(TerminalSize::new(80, 24), Some(PixelSize::new(801, 480)),)
210                .cell_pixels(),
211            None
212        );
213        assert_eq!(WindowSize::default().cell_pixels(), None);
214    }
215
216    #[test]
217    fn unsupported_background_queries_return_none() {
218        let mut query = UnsupportedQuery;
219        assert_eq!(query.terminal_background().unwrap(), None);
220        let mut borrowed = &mut query;
221        assert_eq!(
222            TerminalQuery::terminal_background(&mut borrowed).unwrap(),
223            None
224        );
225    }
226}