termina/lib.rs
1//! Terminal I/O, escape-sequence types, styling, and input parsing.
2//!
3//! Termina keeps the terminal protocol visible. Applications write typed CSI, OSC, and DCS values
4//! from [`escape`] instead of assembling byte strings, and read typed [`Event`] values instead of
5//! decoding terminal input by hand. [`PlatformTerminal`] opens the current process terminal,
6//! switches raw/cooked mode, writes bytes, and creates an [`EventReader`] for synchronous input.
7//!
8//! Code that already has terminal bytes can use [`Parser`] directly. That is useful for PTY tests,
9//! terminal multiplexers, or callers that own the input source and only need Termina's parser.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! use std::io::{self, Write};
15//!
16//! use termina::{
17//! event::{KeyCode, KeyEventKind},
18//! Event, PlatformTerminal, Terminal,
19//! };
20//!
21//! fn main() -> io::Result<()> {
22//! let mut terminal = PlatformTerminal::new()?;
23//! terminal.enter_raw_mode()?;
24//! writeln!(terminal, "Press q to exit.")?;
25//!
26//! let reader = terminal.event_reader();
27//! loop {
28//! let event = reader.read(|_| true)?;
29//! if matches!(
30//! event,
31//! Event::Key(key)
32//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q')
33//! ) {
34//! break;
35//! }
36//! }
37//!
38//! terminal.enter_cooked_mode()
39//! }
40//! ```
41//!
42//! Parsing PTY bytes directly does not require opening a terminal handle:
43//!
44//! ```
45//! use termina::{Event, Parser};
46//!
47//! let mut parser = Parser::default();
48//! parser.parse(b"\x1b[5~", false);
49//! assert!(matches!(parser.pop(), Some(Event::Key(_))));
50//! ```
51
52pub(crate) mod base64;
53pub mod escape;
54pub mod event;
55pub(crate) mod parse;
56pub mod style;
57mod terminal;
58
59use std::{fmt, num::NonZeroU16};
60
61pub use event::{reader::EventReader, Event};
62#[cfg(windows)]
63pub use parse::windows;
64pub use parse::Parser;
65
66pub use terminal::{PlatformHandle, PlatformTerminal, Terminal};
67
68#[cfg(feature = "event-stream")]
69pub use event::stream::EventStream;
70
71/// A one-based terminal coordinate or dimension.
72///
73/// Terminal protocols generally count rows and columns from 1, while Rust collections and many
74/// application models count from 0. `OneBased` stores the protocol value and rejects zero. Use
75/// [`Self::from_zero_based`] when converting from an application index and [`Self::get_zero_based`]
76/// when converting back.
77///
78/// # Examples
79///
80/// ```
81/// use termina::OneBased;
82///
83/// let column = OneBased::from_zero_based(4);
84/// assert_eq!(column.get(), 5);
85/// assert_eq!(column.get_zero_based(), 4);
86/// assert!(OneBased::new(0).is_none());
87/// ```
88///
89/// # Implementation Notes
90///
91/// This reimplements the coordinate helper from [termwiz escape helpers] on top of
92/// [`NonZeroU16`].
93///
94/// [termwiz escape helpers]: https://docs.rs/termwiz/latest/termwiz/escape/index.html
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct OneBased(NonZeroU16);
97
98impl OneBased {
99 /// Creates a one-based value from an already one-based integer.
100 ///
101 /// Returns `None` for zero because zero is not a valid terminal row, column, or dimension in
102 /// the escape sequences modeled by Termina.
103 pub const fn new(n: u16) -> Option<Self> {
104 match NonZeroU16::new(n) {
105 Some(n) => Some(Self(n)),
106 None => None,
107 }
108 }
109
110 /// Converts a zero-based application index into a one-based terminal value.
111 ///
112 /// This panics when `n` is [`u16::MAX`], because adding one would overflow the stored
113 /// [`NonZeroU16`].
114 pub const fn from_zero_based(n: u16) -> Self {
115 assert!(n < u16::MAX);
116 Self(unsafe { NonZeroU16::new_unchecked(n + 1) })
117 }
118
119 /// Returns the stored one-based value.
120 pub const fn get(self) -> u16 {
121 self.0.get()
122 }
123
124 /// Converts the stored terminal value back to a zero-based application index.
125 pub const fn get_zero_based(self) -> u16 {
126 self.get() - 1
127 }
128}
129
130impl Default for OneBased {
131 fn default() -> Self {
132 Self(unsafe { NonZeroU16::new_unchecked(1) })
133 }
134}
135
136impl fmt::Display for OneBased {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 self.0.fmt(f)
139 }
140}
141
142impl From<NonZeroU16> for OneBased {
143 fn from(n: NonZeroU16) -> Self {
144 Self(n)
145 }
146}
147
148/// The dimensions of a terminal window.
149///
150/// `cols` and `rows` describe the terminal window in character cells, which is the size used by
151/// cursor positioning and layout code. Pixel dimensions are available when the platform reports
152/// them. On Unix, Termina reads those optional pixel fields from the `TIOCGWINSZ` window-size
153/// query when the terminal fills them in. Windows currently reports `None` for both pixel fields.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct WindowSize {
156 /// The width in terminal cells.
157 #[doc(alias = "width")]
158 pub cols: u16,
159
160 /// The height in terminal cells.
161 #[doc(alias = "height")]
162 pub rows: u16,
163
164 /// The width of the window in pixels, if the platform reports it.
165 pub pixel_width: Option<u16>,
166
167 /// The height of the window in pixels, if the platform reports it.
168 pub pixel_height: Option<u16>,
169}