Skip to main content

urushi_terminal/
lib.rs

1//! Backend-independent terminal contracts and target-specific inspection for
2//! the Urushi ecosystem.
3//!
4//! This crate owns vocabulary shared by terminal surfaces without depending on
5//! another Urushi workspace crate.
6//! It does not choose rendering policy, perform layout, or render a view.
7//!
8//! # Cargo features
9//!
10//! - `crossterm` enables the portable Crossterm backend. The native backend is
11//!   available without a feature on Unix only.
12
13use std::io::{self, IsTerminal};
14
15pub mod backend;
16mod command;
17mod event;
18mod query;
19mod session;
20mod style;
21mod terminal;
22
23pub use command::{
24    ClearRegion, Command, CommandWriter, ControlString, CursorAppearance, CursorMove,
25    HyperlinkParameter, InvalidControlString, InvalidTerminalText, TerminalHyperlink,
26    TerminalOutput, TerminalText,
27};
28pub use event::{
29    Event, EventSource, FocusChange, KeyCode, KeyEvent, KeyEventState, KeyKind,
30    KeyboardEnhancementFlags, MediaKeyCode, ModifierKeyCode, Modifiers, MouseButton, MouseEvent,
31    MouseKind,
32};
33pub use query::{
34    KeyboardEnhancementQuery, PixelSize, TerminalBackground, TerminalQuery, WindowSize,
35};
36pub use session::{RawModeControl, SessionError, SessionOptions, TerminalSession};
37pub use style::{
38    Color, TerminalStyle, TextAttribute, TextAttributeIter, TextAttributes, Underline,
39    UnderlineStyle,
40};
41pub use terminal::Position;
42
43/// A complete interactive terminal connection.
44///
45/// Implementations own the input and output paths, parser state, process-side
46/// modes, and terminal queries for one physical terminal connection. The
47/// smaller supertraits remain independently useful in tests and adapters.
48pub trait TerminalBackend:
49    CommandWriter + EventSource + RawModeControl + TerminalQuery + KeyboardEnhancementQuery
50{
51}
52
53impl<T> TerminalBackend for T where
54    T: CommandWriter + EventSource + RawModeControl + TerminalQuery + KeyboardEnhancementQuery
55{
56}
57
58/// The color fidelity a terminal can display.
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
60pub enum ColorLevel {
61    /// No terminal color sequences.
62    #[default]
63    None,
64    /// The sixteen ANSI colors.
65    Ansi16,
66    /// The xterm 256-color palette.
67    Ansi256,
68    /// 24-bit RGB colors.
69    TrueColor,
70}
71
72/// A set of underline shapes supported by a terminal.
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
74pub struct UnderlineStyles(u8);
75
76impl UnderlineStyles {
77    pub const SINGLE: Self = Self(1 << 0);
78    pub const DOUBLE: Self = Self(1 << 1);
79    pub const CURLY: Self = Self(1 << 2);
80    pub const DOTTED: Self = Self(1 << 3);
81    pub const DASHED: Self = Self(1 << 4);
82
83    const ALL_BITS: u8 =
84        Self::SINGLE.0 | Self::DOUBLE.0 | Self::CURLY.0 | Self::DOTTED.0 | Self::DASHED.0;
85
86    pub const fn empty() -> Self {
87        Self(0)
88    }
89
90    pub const fn all() -> Self {
91        Self(Self::ALL_BITS)
92    }
93
94    pub const fn union(self, other: Self) -> Self {
95        Self(self.0 | other.0)
96    }
97
98    pub const fn contains(self, style: UnderlineStyle) -> bool {
99        let selected = match style {
100            UnderlineStyle::Single => Self::SINGLE,
101            UnderlineStyle::Double => Self::DOUBLE,
102            UnderlineStyle::Curly => Self::CURLY,
103            UnderlineStyle::Dotted => Self::DOTTED,
104            UnderlineStyle::Dashed => Self::DASHED,
105        };
106        self.0 & selected.0 != 0
107    }
108}
109
110impl std::ops::BitOr for UnderlineStyles {
111    type Output = Self;
112
113    fn bitor(self, rhs: Self) -> Self::Output {
114        self.union(rhs)
115    }
116}
117
118/// A terminal graphics protocol that can display raster images.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub enum TerminalGraphicsProtocol {
121    Kitty,
122    Sixel,
123}
124
125/// A set of terminal graphics protocols supported by a terminal.
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
127pub struct TerminalGraphicsProtocols(u8);
128
129impl TerminalGraphicsProtocols {
130    pub const KITTY: Self = Self(1 << 0);
131    pub const SIXEL: Self = Self(1 << 1);
132
133    const ALL_BITS: u8 = Self::KITTY.0 | Self::SIXEL.0;
134
135    pub const fn empty() -> Self {
136        Self(0)
137    }
138
139    pub const fn all() -> Self {
140        Self(Self::ALL_BITS)
141    }
142
143    pub const fn union(self, other: Self) -> Self {
144        Self(self.0 | other.0)
145    }
146
147    pub const fn contains(self, protocol: TerminalGraphicsProtocol) -> bool {
148        let selected = match protocol {
149            TerminalGraphicsProtocol::Kitty => Self::KITTY,
150            TerminalGraphicsProtocol::Sixel => Self::SIXEL,
151        };
152        self.0 & selected.0 != 0
153    }
154}
155
156impl std::ops::BitOr for TerminalGraphicsProtocols {
157    type Output = Self;
158
159    fn bitor(self, rhs: Self) -> Self::Output {
160        self.union(rhs)
161    }
162}
163
164/// Rendering features positively confirmed by a terminal query or supplied by
165/// an explicitly configured backend.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub struct TerminalCapabilities {
168    color_level: ColorLevel,
169    attributes: TextAttributes,
170    underline_styles: UnderlineStyles,
171    underline_colors: bool,
172    hyperlinks: bool,
173    graphics_protocols: TerminalGraphicsProtocols,
174}
175
176impl TerminalCapabilities {
177    /// Returns capabilities with every output feature disabled.
178    pub const fn none() -> Self {
179        Self {
180            color_level: ColorLevel::None,
181            attributes: TextAttributes::empty(),
182            underline_styles: UnderlineStyles::empty(),
183            underline_colors: false,
184            hyperlinks: false,
185            graphics_protocols: TerminalGraphicsProtocols::empty(),
186        }
187    }
188
189    pub const fn color_level(self) -> ColorLevel {
190        self.color_level
191    }
192
193    pub const fn attributes(self) -> TextAttributes {
194        self.attributes
195    }
196
197    pub const fn underline_styles(self) -> UnderlineStyles {
198        self.underline_styles
199    }
200
201    pub const fn underline_colors(self) -> bool {
202        self.underline_colors
203    }
204
205    pub const fn hyperlinks(self) -> bool {
206        self.hyperlinks
207    }
208
209    pub const fn graphics_protocols(self) -> TerminalGraphicsProtocols {
210        self.graphics_protocols
211    }
212
213    pub const fn supports_graphics(self, protocol: TerminalGraphicsProtocol) -> bool {
214        self.graphics_protocols.contains(protocol)
215    }
216
217    pub const fn with_color_level(mut self, color_level: ColorLevel) -> Self {
218        self.color_level = color_level;
219        self
220    }
221
222    pub const fn with_attributes(mut self, attributes: TextAttributes) -> Self {
223        self.attributes = attributes;
224        self
225    }
226
227    pub const fn with_underline_styles(mut self, styles: UnderlineStyles) -> Self {
228        self.underline_styles = styles;
229        self
230    }
231
232    pub const fn with_underline_colors(mut self, enabled: bool) -> Self {
233        self.underline_colors = enabled;
234        self
235    }
236
237    pub const fn with_hyperlinks(mut self, enabled: bool) -> Self {
238        self.hyperlinks = enabled;
239        self
240    }
241
242    pub const fn with_graphics_protocols(mut self, protocols: TerminalGraphicsProtocols) -> Self {
243        self.graphics_protocols = protocols;
244        self
245    }
246}
247
248/// The visible dimensions of a terminal in character cells.
249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
250pub struct TerminalSize {
251    columns: usize,
252    rows: usize,
253}
254
255impl TerminalSize {
256    /// The empty terminal surface.
257    pub const ZERO: Self = Self::new(0, 0);
258
259    pub const fn new(columns: usize, rows: usize) -> Self {
260        Self { columns, rows }
261    }
262
263    pub const fn columns(self) -> usize {
264        self.columns
265    }
266
267    pub const fn rows(self) -> usize {
268        self.rows
269    }
270}
271
272/// Information observed from one terminal output handle.
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274pub struct TerminalInfo {
275    size: TerminalSize,
276    capabilities: TerminalCapabilities,
277}
278
279impl TerminalInfo {
280    pub const fn new(size: TerminalSize, capabilities: TerminalCapabilities) -> Self {
281        Self { size, capabilities }
282    }
283
284    pub const fn size(self) -> TerminalSize {
285        self.size
286    }
287
288    pub const fn capabilities(self) -> TerminalCapabilities {
289        self.capabilities
290    }
291}
292
293/// Whether one output handle is attached to a terminal.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
295pub enum TerminalDetection {
296    Terminal(TerminalInfo),
297    NonTerminal,
298}
299
300#[cfg(unix)]
301pub fn detect(output: &(impl IsTerminal + std::os::fd::AsFd)) -> io::Result<TerminalDetection> {
302    detect_with(output, |output| terminal_size::terminal_size_of(output))
303}
304
305#[cfg(windows)]
306pub fn detect(
307    output: &(impl IsTerminal + std::os::windows::io::AsHandle),
308) -> io::Result<TerminalDetection> {
309    detect_with(output, |output| terminal_size::terminal_size_of(output))
310}
311
312#[cfg(not(any(unix, windows)))]
313pub fn detect(output: &impl IsTerminal) -> io::Result<TerminalDetection> {
314    if output.is_terminal() {
315        Err(io::Error::new(
316            io::ErrorKind::Unsupported,
317            "terminal size detection is unavailable on this platform",
318        ))
319    } else {
320        Ok(TerminalDetection::NonTerminal)
321    }
322}
323
324#[cfg(any(unix, windows))]
325fn detect_with<T, F>(output: &T, terminal_size: F) -> io::Result<TerminalDetection>
326where
327    T: IsTerminal,
328    F: FnOnce(&T) -> Option<(terminal_size::Width, terminal_size::Height)>,
329{
330    if !output.is_terminal() {
331        return Ok(TerminalDetection::NonTerminal);
332    }
333    let (terminal_size::Width(columns), terminal_size::Height(rows)) = terminal_size(output)
334        .ok_or_else(|| io::Error::other("failed to query the terminal size"))?;
335    Ok(TerminalDetection::Terminal(TerminalInfo::new(
336        TerminalSize::new(usize::from(columns), usize::from(rows)),
337        TerminalCapabilities::none(),
338    )))
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn graphics_protocol_capabilities_are_independent() {
347        let both = TerminalCapabilities::none().with_graphics_protocols(
348            TerminalGraphicsProtocols::KITTY | TerminalGraphicsProtocols::SIXEL,
349        );
350        assert_eq!(both.graphics_protocols(), TerminalGraphicsProtocols::all());
351        assert!(both.supports_graphics(TerminalGraphicsProtocol::Kitty));
352        assert!(both.supports_graphics(TerminalGraphicsProtocol::Sixel));
353    }
354
355    #[test]
356    fn non_terminal_output_is_not_a_terminal_with_missing_capabilities() {
357        let file = std::fs::File::open("Cargo.toml").unwrap();
358        assert_eq!(detect(&file).unwrap(), TerminalDetection::NonTerminal);
359    }
360}