1use 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
43pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
60pub enum ColorLevel {
61 #[default]
63 None,
64 Ansi16,
66 Ansi256,
68 TrueColor,
70}
71
72#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub enum TerminalGraphicsProtocol {
121 Kitty,
122 Sixel,
123}
124
125#[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#[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 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
250pub struct TerminalSize {
251 columns: usize,
252 rows: usize,
253}
254
255impl TerminalSize {
256 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#[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#[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}