Skip to main content

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}