diff --git a/README.md b/README.md index fd57f2c..0319750 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,22 @@ This means there is no single answer for which protocol to use - it depends on y See feature comparison below. -| | VTE Mode | Legacy Console Mode | -| ------------------------------------------------------------ | --------------------------------------------------------- | ------------------- | -| **Kitty Keyboard Protocol** | ✓ | ✕ | -| **Extended key events** | terminal-dependent (must support Kitty Keyboard Protocol) | ✓ | -| **Bracketed paste** | ✓ | ✕ | -| **[AltGr](https://en.wikipedia.org/wiki/AltGr_key) Support** | ✓ | ✕ | +| | VTE Mode | Legacy Console Mode | +| --------------------------- | ------------------- | ------------------- | +| **Kitty Keyboard Protocol** | ✓ | ✕ | +| **Extended key events** | terminal-dependent* | ✓ | +| **Bracketed paste** | ✓ | ✕ | +| **[AltGr] Support** | ✓ | ✕ | + +* VTE mode depends on Kitty Keyboard Protocol support for extended key events. + +[AltGr]: https://en.wikipedia.org/wiki/AltGr_key The legacy input reader can be used by enabling the `windows-legacy` feature. See the `event-read` example for usage. ## Credit -Termina contains significant code sourced and/or modified from other projects, especially Crossterm and TermWiz. See "CREDIT" comments in the source for details on what was copied and what modifications were made. Since all copied code is licensed under MIT, Termina is offered under the MIT license as well at your option. +Termina contains significant code sourced and/or modified from other projects, especially Crossterm and TermWiz. Credits for various implementations are marked in implementation notes sections. Since all copied code is licensed under MIT, Termina is offered under the MIT license as well at your option.
Crossterm license... diff --git a/src/escape.rs b/src/escape.rs index 435e5e1..68db2e3 100644 --- a/src/escape.rs +++ b/src/escape.rs @@ -1,17 +1,70 @@ -//! ANSI escape sequences. - -// CREDIT: this tree of modules is mostly yanked from the equivalents in TermWiz with some -// stylistic edits and additions/subtractions of some escape sequences. +//! Typed ANSI escape-sequence helpers. +//! +//! Termina models Control Sequence Introducer (CSI), Device Control String (DCS), and Operating +//! System Command (OSC) sequences it knows how to emit so callers can compose terminal control +//! payloads through [`Display`] instead of hand-written byte strings. +//! +//! # Examples +//! +//! ``` +//! use termina::{ +//! escape::{ +//! csi::{Csi, Cursor}, +//! CSI, +//! }, +//! OneBased, +//! }; +//! +//! let cursor_home = Csi::Cursor(Cursor::Position { +//! line: OneBased::new(1).unwrap(), +//! col: OneBased::new(1).unwrap(), +//! }); +//! +//! assert_eq!(cursor_home.to_string(), format!("{CSI}1;1H")); +//! ``` +//! +//! # Implementation Notes +//! +//! This module tree is adapted from [termwiz escape helpers]. It was originally yanked from TermWiz +//! equivalents and then trimmed into the set of escape sequences Termina needs. Most differences +//! are stylistic edits plus additions and subtractions in the modeled sequence set. +//! +//! [termwiz escape helpers]: https://docs.rs/termwiz/latest/termwiz/escape/index.html +//! [`Display`]: std::fmt::Display pub mod csi; pub mod dcs; pub mod osc; -// Originally yanked from +/// Control Sequence Introducer (`ESC [`), the prefix for parameterized terminal control functions. +/// +/// CSI sequences carry numeric parameters and a final byte. Termina models the supported CSI +/// families in [`csi::Csi`]. pub const CSI: &str = "\x1b["; + +/// Operating System Command introducer (`ESC ]`), used for terminal integration commands. +/// +/// OSC sequences are commonly used for window titles, clipboard integration, and color queries. +/// Termina models the supported commands in [`osc::Osc`]. pub const OSC: &str = "\x1b]"; + +/// String Terminator (`ESC \`), used to end OSC and DCS string controls. +/// +/// Most modern terminal string controls may also be terminated by [`BEL`], but Termina emits the +/// explicit string terminator form for the sequences it formats. pub const ST: &str = "\x1b\\"; + +/// Single Shift 3 (`ESC O`), the prefix used by SS3 key sequences. +/// +/// Application-keypad and function-key encodings commonly use this prefix instead of [`CSI`]. pub const SS3: &str = "\x1bO"; + +/// Device Control String introducer (`ESC P`), used for structured terminal queries. +/// +/// Termina models the supported request and response forms in [`dcs::Dcs`]. pub const DCS: &str = "\x1bP"; +/// Bell control character (`BEL`, `0x07`). +/// +/// BEL can ring the terminal bell and is also accepted by many terminals as an OSC terminator. pub const BEL: &str = "\x07"; diff --git a/src/escape/csi.rs b/src/escape/csi.rs index 6db4f1a..ac5e1ec 100644 --- a/src/escape/csi.rs +++ b/src/escape/csi.rs @@ -1,9 +1,17 @@ -// CREDIT: This module was incrementally yanked from -// . -// I've made stylistic changes like eliminating some traits (FromPrimitive, ToPrimitive) and only -// copied parts - TermWiz has a more complete set of OSC escapes. I have added some new escapes -// though, for example Contour's theme mode extension in `Mode::QueryTheme` and friends and the -// `Sgr::Attributes` / `SgrAttributes` / `SgrModifiers` types. +//! Strongly typed Control Sequence Introducer (CSI) escape sequences. +//! +//! Each enum in this module represents a family of CSI sequences and implements [`Display`] where +//! Termina knows how to emit the sequence. This keeps terminal control code explicit while still +//! allowing applications to write the formatted value directly to any [`std::io::Write`] target. +//! +//! # Implementation Notes +//! +//! This module is adapted from [termwiz's CSI support], but it was incrementally pulled across +//! rather than copied wholesale. Termina keeps a curated subset, removes conversion traits, and +//! adds focused extensions such as Contour theme reporting and the [`SgrAttributes`] / +//! [`SgrModifiers`] grouping types. TermWiz has a more complete set of CSI escape sequences. +//! +//! [termwiz's CSI support]: https://docs.rs/termwiz/latest/termwiz/escape/enum.Csi.html use std::{ fmt::{self, Display}, @@ -16,17 +24,56 @@ use crate::{ OneBased, }; +/// A Control Sequence Introducer command. +/// +/// Formatting writes the `ESC [` introducer followed by the command family payload. CSI commands +/// are the main terminal protocol surface for cursor movement, text styling, mode changes, device +/// reports, mouse reports, and window operations. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Csi { - /// "Select Graphics Rendition" (SGR). - /// These sequences affect how the cell is rendered by the terminal. + /// Select Graphic Rendition commands described by [`Sgr`]. + /// + /// These `CSI ... m` commands change how subsequent cells are rendered. Sgr(Sgr), + + /// Cursor commands described by [`Cursor`]. + /// + /// This family covers cursor movement, cursor shape, margins, and position reports. Cursor(Cursor), + + /// Text and display editing commands described by [`Edit`]. + /// + /// This family covers insert, delete, erase, repeat, and scroll operations. Edit(Edit), + + /// Terminal mode commands described by [`Mode`]. + /// + /// This family covers setting, resetting, saving, restoring, querying, and reporting terminal + /// modes. Mode(Mode), + + /// Mouse input reports described by [`MouseReport`]. + /// + /// Modes such as [`DecPrivateModeCode::MouseTracking`], + /// [`DecPrivateModeCode::ButtonEventMouse`], and [`DecPrivateModeCode::AnyEventMouse`] + /// control when terminals send these reports. Mouse(MouseReport), + + /// Kitty keyboard protocol commands described by [`Keyboard`]. + /// + /// This family covers flag query, report, push, pop, and set commands. Keyboard(Keyboard), + + /// Device and status commands described by [`Device`]. + /// + /// This family covers device attributes, terminal status, terminal identity, and terminal + /// parameters. Device(Device), + + /// Window commands described by [`Window`]. + /// + /// This family covers window manipulation and reports, mostly from xterm-compatible + /// extensions. Window(Box), } @@ -47,24 +94,63 @@ impl Display for Csi { } } +/// A Select Graphic Rendition (`CSI ... m`) attribute update. +/// +/// SGR changes rendering state for text written after the sequence: color, intensity, underline, +/// blink, and related cell attributes. Terminals keep that state until another SGR command changes +/// it or [`Self::Reset`] clears it. The VT510 reference documents the standard [SGR] parameter +/// meanings that Termina models here. +/// +/// [SGR]: https://vt100.net/docs/vt510-rm/SGR.html #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Sgr { - /// Resets the graphics rendition to default. + /// SGR 0: reset all graphic rendition attributes to terminal defaults. Reset, + + /// Set text intensity described by [`Intensity`]. Intensity(Intensity), + + /// Set underline style described by [`Underline`]. + /// + /// This includes Kitty's styled underline extension when terminals support it. Underline(Underline), + + /// Set blink behavior described by [`Blink`]. Blink(Blink), + + /// Enable SGR 3 italic text or disable it with SGR 23. Italic(bool), + + /// Enable SGR 7 reverse video or disable it with SGR 27. Reverse(bool), + + /// Enable SGR 8 invisible text or disable it with SGR 28. Invisible(bool), + + /// Enable SGR 9 strikethrough text or disable it with SGR 29. StrikeThrough(bool), + + /// Enable SGR 53 overline text or disable it with SGR 55. Overline(bool), + + /// Select the active font described by [`Font`]. Font(Font), + + /// Set vertical alignment described by [`VerticalAlign`]. VerticalAlign(VerticalAlign), + + /// Set the foreground color described by [`ColorSpec`]. Foreground(ColorSpec), + + /// Set the background color described by [`ColorSpec`]. Background(ColorSpec), + + /// Set the underline color described by [`ColorSpec`]. + /// + /// This uses the SGR 58 underline-color extension. UnderlineColor(ColorSpec), - /// A helper type that can combine sequences into a single SGR update. + + /// Combine multiple SGR updates described by [`SgrAttributes`]. Attributes(SgrAttributes), } @@ -319,16 +405,16 @@ impl Display for Sgr { } } -/// A helper type which can contain multiple common SGR attributes. +/// A grouped SGR update. /// -/// This can be used to emit a single SGR escape sequence which updates multiple attributes, for -/// example setting the foreground and background color at the same time. This is useful to reduce -/// the total number of bytes needed to describe multiple SGR changes, giving the terminal less -/// work to do in terms of parsing. +/// [`Sgr`] accepts more than one parameter in a single `CSI ... m` sequence, so one escape can set +/// the foreground color, background color, underline color, and text modifiers together. Grouping +/// related changes reduces the number of bytes written and the number of CSI sequences the +/// terminal has to parse. /// -/// Note that if no attributes are set (`SgrAttributes::default`) the terminal will treat the -/// escape the same as `Sgr::Reset`. So if you are using this type you may wish to compare the -/// attributes you've built with `SgrAttributes::default()` to decide whether or not you want to +/// Note that if no attributes are set ([`SgrAttributes::default`]) the terminal will treat the +/// escape the same as [`Sgr::Reset`]. So if you are using this type you may wish to compare the +/// attributes you've built with [`SgrAttributes::default`] to decide whether or not you want to /// write it to the terminal. Otherwise the escape codes for this type do not reset SGR. The /// example below sets a green foreground and bold intensity but would not affect any other SGR /// settings like underline or background color. @@ -353,25 +439,26 @@ impl Display for Sgr { pub struct SgrAttributes { /// The foreground color used to paint text. pub foreground: Option, + /// The background color used to paint the cell. pub background: Option, + /// The color of the underline in the current cell. pub underline_color: Option, - /// Other modifiers like italic, bold, blinking, etc. - /// - /// See [SgrModifiers]. + + /// Bitflags for text modifiers such as intensity, underline, blink, italic, and reverse video. pub modifiers: SgrModifiers, + /// The number of parameters which are allowed in a chunk. /// /// The VT parsers used in many terminal emulators set limits on the number of parameters a /// CSI sequence can use. After the limit they typically ignore all other parameters. For - /// many terminal emulators this is a relatively high number like 256 but some terminal - /// emulators set their limit as low as 10. For maximum compatibility this is set to 10 by - /// default. + /// many terminals this is a relatively high number like 256 but some set their limit as low as + /// 10. For maximum compatibility this is set to 10 by default. /// - /// The number of parameters taken to describe a modifier varies by modifier. True-color - /// colors (foreground, background, underline color) take the most while simple modifiers like - /// `SgrModifiers::ITALIC` take just one. + /// The number of parameters taken to describe a modifier varies by modifier. True-color colors + /// (foreground, background, underline color) take the most while simple modifiers like + /// [`SgrModifiers::ITALIC`] take just one. pub parameter_chunk_size: NonZeroU16, } @@ -390,8 +477,8 @@ impl Default for SgrAttributes { impl SgrAttributes { /// Returns `true` if no attributes are set, `false` otherwise. /// - /// When empty attributes are displayed they produce the same escape sequence as `Sgr::Reset`. - /// If you are building attributes incrementally starting with `SgrAttributes::default()` then + /// When empty attributes are displayed they produce the same escape sequence as [`Sgr::Reset`]. + /// If you are building attributes incrementally starting with [`SgrAttributes::default`] then /// you may wish to check whether the attributes are empty to decide whether or not you should /// write them to the terminal. /// @@ -419,29 +506,76 @@ impl SgrAttributes { // We could represent `SgrAttributes` as a `Vec` but we can flatten the type out to have a // more compact representation with bitflags for each SGR instead: bitflags::bitflags! { + /// SGR modifier bits used by [`SgrAttributes`]. + /// + /// These flags mirror SGR attributes that can be represented without carrying additional + /// color or font data. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SgrModifiers: u32 { + /// No SGR modifiers. const NONE = 0; + + /// SGR 0: reset all attributes. const RESET = 1 << 1; + + /// SGR 22: normal intensity. const INTENSITY_NORMAL = 1 << 2; + + /// SGR 2: dim intensity. const INTENSITY_DIM = 1 << 3; + + /// SGR 1: bold intensity. const INTENSITY_BOLD = 1 << 4; + + /// SGR 24: no underline. const UNDERLINE_NONE = 1 << 5; + + /// SGR 4: single underline. const UNDERLINE_SINGLE = 1 << 6; + + /// SGR 21: double underline. const UNDERLINE_DOUBLE = 1 << 7; + + /// Kitty underline style 3: curly underline. const UNDERLINE_CURLY = 1 << 8; + + /// Kitty underline style 4: dotted underline. const UNDERLINE_DOTTED = 1 << 9; + + /// Kitty underline style 5: dashed underline. const UNDERLINE_DASHED = 1 << 10; + + /// SGR 25: no blink. const BLINK_NONE = 1 << 11; + + /// SGR 5: slow blink. const BLINK_SLOW = 1 << 12; + + /// SGR 6: rapid blink. const BLINK_RAPID = 1 << 13; + + /// SGR 3: italic. const ITALIC = 1 << 14; + + /// SGR 23: no italic. const NO_ITALIC = 1 << 15; + + /// SGR 7: reverse video. const REVERSE = 1 << 16; + + /// SGR 27: no reverse video. const NO_REVERSE = 1 << 17; + + /// SGR 8: invisible text. const INVISIBLE = 1 << 18; + + /// SGR 28: visible text. const NO_INVISIBLE = 1 << 19; + + /// SGR 9: strikethrough. const STRIKE_THROUGH = 1 << 20; + + /// SGR 29: no strikethrough. const NO_STRIKE_THROUGH = 1 << 21; // Support font and vertical align? They're not well supported in terminals so I think // it's fine to leave them out of this type. @@ -456,12 +590,16 @@ impl Default for SgrModifiers { // Cursor -/// The cursor shape for the kitty multi-cursor protocol. -/// This represents either a specific `CursorStyle` (protocol values 0-6) +/// The cursor shape for the Kitty multi-cursor protocol. +/// +/// This represents either a specific [`CursorStyle`] (protocol values 0-6) /// or the special "follow main cursor" value (protocol value 29). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MultiCursorShape { + /// Use a specific cursor style for secondary cursors. Style(CursorStyle), + + /// Use the main cursor's current shape for secondary cursors. FollowMainCursor, } @@ -507,6 +645,24 @@ impl TryFrom for MultiCursorCapability { } } +/// Cursor-related CSI commands. +/// +/// This includes cursor movement, tabulation, position reports, margins, cursor style, and +/// Kitty's multi-cursor extension. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Cursor}, +/// OneBased, +/// }; +/// +/// let position = Cursor::Position { +/// line: OneBased::new(3).unwrap(), +/// col: OneBased::new(10).unwrap(), +/// }; +/// assert_eq!(Csi::Cursor(position).to_string(), "\x1b[3;10H"); +/// assert_eq!(Csi::Cursor(Cursor::default_position()).to_string(), "\x1b[1;1H"); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Cursor { /// CBT Moves cursor to the Ps tabs backward. The default value of Ps is 1. @@ -536,7 +692,9 @@ pub enum Cursor { /// HVP Moves cursor to the Ps1-th line and to the Ps2-th column. The /// default value of Ps1 and Ps2 is 1. CharacterAndLinePosition { + /// The destination line. line: OneBased, + /// The destination column. col: OneBased, }, @@ -582,7 +740,9 @@ pub enum Cursor { /// Pn1 and m equals the value of Pn2. CPR may be solicited by a DEVICE /// STATUS REPORT (DSR) or be sent unsolicited . ActivePositionReport { + /// The reported line. line: OneBased, + /// The reported column. col: OneBased, }, @@ -593,6 +753,8 @@ pub enum Cursor { /// SCP - Save Cursor Position. /// Only works when DECLRMM is disabled SaveCursor, + + /// RCP - Restore Cursor Position. RestoreCursor, /// CTC - CURSOR TABULATION CONTROL @@ -619,7 +781,9 @@ pub enum Cursor { /// Moves cursor to the Ps1-th line and to the Ps2-th column. The default /// value of Ps1 and Ps2 is 1. Position { + /// The destination line. line: OneBased, + /// The destination column. col: OneBased, }, @@ -632,17 +796,26 @@ pub enum Cursor { /// DECSTBM - Set top and bottom margins. SetTopAndBottomMargins { + /// The top margin line. top: OneBased, + /// The bottom margin line. bottom: OneBased, }, - /// + /// [DECSLRM] - Set left and right margins. + /// + /// [DECSLRM]: https://vt100.net/docs/vt510-rm/DECSLRM.html SetLeftAndRightMargins { + /// The left margin column. left: OneBased, + /// The right margin column. right: OneBased, }, + /// Set the cursor style. CursorStyle(CursorStyle), + + /// Query the current cursor shape. QueryCursorShape, /// Capability query response (kitty multi-cursor protocol). @@ -651,15 +824,21 @@ pub enum Cursor { /// means the protocol is not supported. CursorShapeQueryResponse(Vec), + /// Set secondary cursor positions for the kitty multi-cursor protocol. SetMultipleCursors { + /// The shape used for the secondary cursors. shape: MultiCursorShape, + + /// The one-based `(line, column)` positions of the secondary cursors. positions: Vec<(OneBased, OneBased)>, }, + /// Clear all secondary cursors from the kitty multi-cursor protocol. ClearSecondaryCursors, } impl Cursor { + /// Returns the home cursor position, line 1 column 1. pub const fn default_position() -> Self { Self::Position { line: OneBased::from_zero_based(0), @@ -752,15 +931,29 @@ impl Display for Cursor { } } +/// Cursor tabulation control actions for CTC. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum CursorTabulationControl { + /// Set a character tab stop at the active position. #[default] SetCharacterTabStopAtActivePosition = 0, + + /// Set a line tab stop at the active line. SetLineTabStopAtActiveLine = 1, + + /// Clear the character tab stop at the active position. ClearCharacterTabStopAtActivePosition = 2, + + /// Clear the line tab stop at the active line. ClearLineTabstopAtActiveLine = 3, + + /// Clear all character tab stops on the active line. ClearAllCharacterTabStopsAtActiveLine = 4, + + /// Clear all character tab stops. ClearAllCharacterTabStops = 5, + + /// Clear all line tab stops. ClearAllLineTabStops = 6, } @@ -770,14 +963,26 @@ impl Display for CursorTabulationControl { } } +/// Tab-stop clearing actions for TBC. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum TabulationClear { + /// Clear the character tab stop at the active position. #[default] ClearCharacterTabStopAtActivePosition = 0, + + /// Clear the line tab stop at the active line. ClearLineTabStopAtActiveLine = 1, + + /// Clear all character tab stops on the active line. ClearCharacterTabStopsAtActiveLine = 2, + + /// Clear all character tab stops. ClearAllCharacterTabStops = 3, + + /// Clear all line tab stops. ClearAllLineTabStops = 4, + + /// Clear all character and line tab stops. ClearAllTabStops = 5, } @@ -789,6 +994,20 @@ impl Display for TabulationClear { // Edit +/// Text and display editing CSI commands. +/// +/// ``` +/// use termina::escape::csi::{Csi, Edit, EraseInDisplay, EraseInLine}; +/// +/// assert_eq!( +/// Csi::Edit(Edit::EraseInLine(EraseInLine::EraseToEndOfLine)).to_string(), +/// "\x1b[0K", +/// ); +/// assert_eq!( +/// Csi::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseDisplay)).to_string(), +/// "\x1b[2J", +/// ); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Edit { /// DCH - DELETE CHARACTER @@ -876,8 +1095,9 @@ pub enum Edit { /// appear to move down; where n equals the value of Pn. The active /// presentation position is not affected by this control function. /// - /// Also known as Pan Up in DEC: - /// + /// Also known as Pan Up in DEC; see [SD]. + /// + /// [SD]: https://vt100.net/docs/vt510-rm/SD.html ScrollDown(u32), /// SU - SCROLL UP @@ -920,14 +1140,21 @@ impl Display for Edit { } } +/// Erase-in-line modes for EL. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum EraseInLine { + /// Erase from the active position to the end of the line. #[default] EraseToEndOfLine = 0, + + /// Erase from the start of the line through the active position. EraseToStartOfLine = 1, + + /// Erase the entire active line. EraseLine = 2, } +/// Erase-in-display modes for ED. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum EraseInDisplay { /// the active presentation position and the character positions up to the @@ -946,28 +1173,73 @@ pub enum EraseInDisplay { // Mode +/// Terminal mode CSI commands. +/// +/// This enum covers Digital Equipment Corporation (DEC) private modes +/// (`CSI ? ... h/l/s/r/$p`), standard modes, xterm key modifier resources, and terminal theme +/// query/report extensions. +/// +/// ``` +/// use termina::escape::csi::{Csi, DecPrivateMode, DecPrivateModeCode, Mode}; +/// +/// let bracketed_paste = DecPrivateMode::Code(DecPrivateModeCode::BracketedPaste); +/// assert_eq!( +/// Csi::Mode(Mode::SetDecPrivateMode(bracketed_paste)).to_string(), +/// "\x1b[?2004h", +/// ); +/// assert_eq!( +/// Csi::Mode(Mode::ResetDecPrivateMode(bracketed_paste)).to_string(), +/// "\x1b[?2004l", +/// ); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mode { + /// Set a DEC private mode. SetDecPrivateMode(DecPrivateMode), + + /// Reset a DEC private mode. ResetDecPrivateMode(DecPrivateMode), + + /// Save a DEC private mode. SaveDecPrivateMode(DecPrivateMode), + + /// Restore a DEC private mode. RestoreDecPrivateMode(DecPrivateMode), - // + + /// Query a DEC private mode setting. QueryDecPrivateMode(DecPrivateMode), - // + + /// Report a DEC private mode setting. ReportDecPrivateMode { + /// The DEC private mode being reported. mode: DecPrivateMode, + + /// The current setting state for the mode. setting: DecModeSetting, }, + + /// Set a standard terminal mode. SetMode(TerminalMode), + + /// Reset a standard terminal mode. ResetMode(TerminalMode), + + /// Query a standard terminal mode. QueryMode(TerminalMode), + + /// Set or query an xterm key modifier resource. XtermKeyMode { + /// The xterm key modifier resource. resource: XtermKeyModifierResource, + + /// The resource value, or `None` to query it. value: Option, }, - // + + /// Query the current terminal theme. QueryTheme, + + /// Report the current terminal theme. ReportTheme(ThemeMode), } @@ -1000,9 +1272,16 @@ impl Display for Mode { } } +/// A Digital Equipment Corporation private mode value. +/// +/// DEC private modes are terminal-specific mode numbers encoded with `CSI ? ...` sequences. Many +/// modern terminal emulators still use this namespace for xterm-compatible extensions. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DecPrivateMode { + /// A known DEC private mode code. Code(DecPrivateModeCode), + + /// A DEC private mode code not modeled by [`DecPrivateModeCode`]. Unspecified(u16), } @@ -1016,104 +1295,261 @@ impl Display for DecPrivateMode { } } +/// Known Digital Equipment Corporation private mode numbers. +/// +/// The DEC private-mode namespace started with DEC terminals and now also carries common +/// xterm-compatible extensions such as mouse tracking, alternate screens, and bracketed paste. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DecPrivateModeCode { - /// + /// Mode 1: [DECCKM] - Application Cursor Keys. + /// /// This mode is only effective when the terminal is in keypad application mode (see DECKPAM) /// and the ANSI/VT52 mode (DECANM) is set (see DECANM). Under these conditions, if the cursor /// key mode is reset, the four cursor function keys will send ANSI cursor control commands. If /// cursor key mode is set, the four cursor function keys will send application functions. + /// + /// [DECCKM]: https://vt100.net/docs/vt510-rm/DECCKM.html ApplicationCursorKeys = 1, - /// - /// Behave like a vt52 + /// Mode 2: [DECANM] - behave like a VT52. + /// + /// This is a historical compatibility mode. Modern terminal applications usually leave the + /// terminal in ANSI mode and should not need to enable VT52 behavior. + /// + /// [DECANM]: https://vt100.net/docs/vt510-rm/DECANM.html DecAnsiMode = 2, - /// + /// Mode 3: [DECCOLM] - Select 132 columns. + /// + /// Setting this mode asks the terminal to switch from 80 columns to 132 columns. Many modern + /// terminals ignore or restrict this behavior. + /// + /// [DECCOLM]: https://vt100.net/docs/vt510-rm/DECCOLM.html Select132Columns = 3, - /// + + /// Mode 4: [DECSCLM] - Smooth scroll. + /// + /// This mode controls whether scrolling should be smooth or jump-scroll style on DEC + /// terminals. Modern emulators commonly ignore it. + /// + /// [DECSCLM]: https://vt100.net/docs/vt510-rm/DECSCLM.html SmoothScroll = 4, - /// + + /// Mode 5: [DECSCNM] - Reverse video. + /// + /// Setting this mode swaps the screen's foreground and background presentation. + /// + /// [DECSCNM]: https://vt100.net/docs/vt510-rm/DECSCNM.html ReverseVideo = 5, - /// - /// When OriginMode is enabled, cursor is constrained to the - /// scroll region and its position is relative to the scroll - /// region. + + /// Mode 6: [DECOM] - Origin Mode. + /// + /// When enabled, OriginMode constrains cursor to the scroll region and makes its position + /// relative to that region. + /// + /// [DECOM]: https://vt100.net/docs/vt510-rm/DECOM.html OriginMode = 6, - /// - /// When enabled, wrap to next line, Otherwise replace the last - /// character + + /// Mode 7: [DECAWM] - Auto Wrap. + /// + /// When enabled, wrap to next line. Otherwise replace the last character. + /// + /// [DECAWM]: https://vt100.net/docs/vt510-rm/DECAWM.html AutoWrap = 7, - /// + + /// Mode 8: [DECARM] - Auto Repeat. + /// + /// This controls whether held-down keys repeatedly generate input. + /// + /// [DECARM]: https://vt100.net/docs/vt510-rm/DECARM.html AutoRepeat = 8, + + /// Mode 12: start blinking the cursor. + /// + /// This is an xterm cursor-control extension, not a DEC private mode from the VT manuals. StartBlinkingCursor = 12, + + /// Mode 25: show the cursor. + /// + /// Applications commonly set this mode while running and reset it to hide the cursor during + /// full-screen drawing. ShowCursor = 25, + /// Mode 45: reverse-wrap from the left edge to the previous line. + /// + /// This xterm extension controls whether cursor-left from column 1 wraps to the previous line. ReverseWraparound = 45, - /// + /// Mode 69: [DECLRMM] - Left Right Margin Mode. + /// + /// [DECLRMM]: https://vt100.net/docs/vt510-rm/DECLRMM.html LeftRightMarginMode = 69, - /// DECSDM - + /// Mode 80: DECSDM - Sixel Display Mode. + /// + /// See the sixel mode discussion in [EK-VT38T-UG-001]. + /// + /// [EK-VT38T-UG-001]: https://vt100.net/dec/ek-vt38t-ug-001.pdf#page=132 SixelDisplayMode = 80, - /// Enable mouse button press/release reporting + + /// Mode 1000: enable mouse button press/release reporting. + /// + /// xterm mouse tracking defines the report encoding for this mode. Termina parses compatible + /// reports as [`crate::Event::Mouse`]. + /// + /// [xterm mouse tracking]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html MouseTracking = 1000, - /// Warning: this requires a cooperative and timely response from - /// the application otherwise the terminal can hang + + /// Mode 1001: enable highlight mouse tracking. + /// + /// Warning: this requires a cooperative and timely application response; otherwise the + /// terminal can hang. xterm mouse tracking defines the report encoding for this mode. + /// Termina parses compatible reports as [`crate::Event::Mouse`]. + /// + /// [xterm mouse tracking]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html HighlightMouseTracking = 1001, - /// Enable mouse button press/release and drag reporting + + /// Mode 1002: enable mouse button press/release and drag reporting. + /// + /// xterm mouse tracking defines the report encoding for this mode. Termina parses compatible + /// reports as [`crate::Event::Mouse`]. + /// + /// [xterm mouse tracking]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html ButtonEventMouse = 1002, - /// Enable mouse motion, button press/release and drag reporting + + /// Mode 1003: enable mouse motion, button press/release, and drag reporting. + /// + /// xterm mouse tracking defines the report encoding for this mode. Termina parses compatible + /// reports as [`crate::Event::Mouse`]. + /// + /// [xterm mouse tracking]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html AnyEventMouse = 1003, - /// Enable FocusIn/FocusOut events + + /// Mode 1004: enable FocusIn/FocusOut events. + /// + /// When enabled, compatible terminals send focus events as CSI `I` and CSI `O`. Termina parses + /// those reports as [`crate::Event::FocusIn`] and [`crate::Event::FocusOut`]. FocusTracking = 1004, + + /// Mode 1005: use the UTF-8 mouse coordinate encoding. + /// + /// This is an older extended-coordinate encoding. New applications generally prefer + /// [`Self::SGRMouse`] when supported. Utf8Mouse = 1005, - /// Use extended coordinate system in mouse reporting. Does not - /// enable mouse reporting itself, it just controls how reports - /// will be encoded. + + /// Mode 1006: use SGR extended coordinates in mouse reporting. + /// + /// This does not enable mouse reporting itself; it only controls how reports are encoded. + /// xterm extended coordinates define the `CSI < ... M/m` report shape parsed as + /// [`MouseReport::Sgr1006`]. + /// + /// [xterm extended coordinates]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html SGRMouse = 1006, + + /// Mode 1015: use RXVT mouse coordinate encoding. + /// + /// xterm extended coordinates document this as the older urxvt-style coordinate extension. + /// + /// [xterm extended coordinates]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html RXVTMouse = 1015, - /// Use pixels rather than text cells in mouse reporting. Does - /// not enable mouse reporting itself, it just controls how - /// reports will be encoded. + + /// Mode 1016: use pixels rather than text cells in mouse reporting. + /// + /// This does not enable mouse reporting itself; it only controls how reports are encoded. + /// xterm SGR pixels define the pixel-coordinate report shape parsed as + /// [`MouseReport::Sgr1016`]. + /// + /// [xterm SGR pixels]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-SGR-Pixels SGRPixelsMouse = 1016, + /// Mode 1036: make xterm send Escape before Meta-modified keys. + /// + /// This changes keyboard encoding so Meta-modified input is delivered as an Escape-prefixed + /// sequence. XTermMetaSendsEscape = 1036, + + /// Mode 1039: make xterm send Escape before Alt-modified keys. + /// + /// This changes keyboard encoding so Alt-modified input is delivered as an Escape-prefixed + /// sequence. XTermAltSendsEscape = 1039, - /// Save cursor as in DECSC + /// Mode 1048: save cursor as in DECSC. + /// + /// Full-screen applications often combine this with alternate-screen modes so they can restore + /// the user's cursor position on exit. SaveCursor = 1048, + + /// Mode 1049: clear and switch to the alternate screen. + /// + /// This is the common xterm private mode for full-screen applications. It switches away from + /// the scrollback-backed main screen and clears the alternate screen. ClearAndEnableAlternateScreen = 1049, + + /// Mode 47: switch to the alternate screen. + /// + /// This is an older alternate-screen mode. New applications usually use mode 1049. EnableAlternateScreen = 47, + + /// Mode 1047: switch to the alternate screen using xterm's optional alternate-screen mode. + /// + /// Unlike mode 1049, this does not imply saving/restoring the cursor. OptEnableAlternateScreen = 1047, + + /// Mode 2004: enable bracketed paste mode. + /// + /// When enabled, pasted text is wrapped in explicit start/end markers so applications can + /// distinguish paste from typed input. Termina parses the wrapped payload as + /// [`crate::Event::Paste`]. xterm documents this mode as [bracketed paste mode]. + /// + /// [bracketed paste mode]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode BracketedPaste = 2004, - /// - /// Grapheme clustering mode + /// Mode 2027: grapheme clustering mode from [Contour Unicode core]. + /// + /// [Contour Unicode core]: https://github.com/contour-terminal/terminal-unicode-core/ GraphemeClustering = 2027, - /// + /// Mode 2031: theme notification mode from [Contour color-palette notifications]. + /// + /// [Contour color-palette notifications]: https://github.com/contour-terminal/contour/ Theme = 2031, - /// Applies to sixel and regis modes + /// Mode 1070: use private color registers for each sixel or ReGIS graphic. + /// + /// This keeps image color registers local to a graphic instead of sharing them globally across + /// the terminal session. UsePrivateColorRegistersForEachGraphic = 1070, - /// + /// Mode 2026: [Synchronized output proposal] mode. + /// + /// [Synchronized output proposal]: https://gist.github.com/christianparpart/ SynchronizedOutput = 2026, + /// Mode 7727: enable MinTTY application Escape key mode. + /// + /// This MinTTY extension changes how the Escape key is encoded in application contexts. MinTTYApplicationEscapeKeyMode = 7727, - /// xterm: adjust cursor positioning after emitting sixel + /// Mode 8452: adjust cursor positioning after emitting sixel. + /// + /// This xterm mode controls whether sixel output advances the cursor as if the graphic + /// occupied cells. SixelScrollsRight = 8452, - /// Windows Terminal: win32-input-mode - /// + /// Mode 9001: Windows Terminal win32-input-mode from [Microsoft terminal keyboard handling]. + /// + /// [Microsoft terminal keyboard handling]: https://github.com/microsoft/terminal/ Win32InputMode = 9001, } +/// A standard terminal mode value. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TerminalMode { + /// A known standard terminal mode code. Code(TerminalModeCode), + + /// A standard terminal mode code not modeled by [`TerminalModeCode`]. Unspecified(u16), } @@ -1127,60 +1563,146 @@ impl Display for TerminalMode { } } +/// Known standard terminal mode numbers. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TerminalModeCode { - /// + /// Mode 2: [KAM] - Keyboard Action Mode. + /// + /// When set, keyboard input is disabled. Applications usually avoid this mode unless they are + /// intentionally locking local keyboard entry. + /// + /// [KAM]: https://vt100.net/docs/vt510-rm/KAM.html KeyboardAction = 2, - /// + + /// Mode 4: [IRM] - Insert Replace Mode. + /// + /// When set, printable characters insert at the cursor and shift existing content right. + /// When reset, printable characters replace existing cells. + /// + /// [IRM]: https://vt100.net/docs/vt510-rm/IRM.html Insert = 4, - /// <> + + /// Mode 8: bidirectional support mode. + /// + /// The terminal-wg bidi recommendation uses this mode to enable bidirectional text support in + /// terminals that implement the proposal. + /// + /// [Terminal WG bidi recommendation]: https://terminal-wg.pages.freedesktop.org/bidi/ BiDirectionalSupportMode = 8, - /// - /// But in the MS terminal this is cursor blinking. + + /// Mode 12: [SRM] - Send Receive Mode. + /// + /// ECMA-48 defines this as local echo control. Microsoft terminal implementations also use + /// mode 12 for cursor blinking, so callers should be aware of that compatibility difference. + /// + /// [SRM]: https://vt100.net/docs/vt510-rm/SRM.html SendReceive = 12, - /// + + /// Mode 20: [LNM] - Line Feed/New Line Mode. + /// + /// When set, line feed also performs carriage return. When reset, line feed only moves to the + /// next line. + /// + /// [LNM]: https://vt100.net/docs/vt510-rm/LNM.html AutomaticNewline = 20, - /// MS terminal cursor visibility + + /// Mode 25: Microsoft terminal cursor visibility. + /// + /// This mirrors the DEC private cursor-visibility mode in some Microsoft terminal contexts. ShowCursor = 25, } +/// xterm key modifier resources addressed by `CSI > ... m`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum XtermKeyModifierResource { + /// Resource 0: xterm keyboard modifier keys. + /// + /// This resource controls xterm's general modified-key behavior. Keyboard = 0, + + /// Resource 1: xterm cursor-key modifier keys. + /// + /// This resource controls modified arrow-key and navigation-key encodings. CursorKeys = 1, + + /// Resource 2: xterm function-key modifier keys. + /// + /// This resource controls modified function-key encodings. FunctionKeys = 2, + + /// Resource 4: xterm other-key modifier keys. + /// + /// This resource controls modified-key encodings for keys outside the cursor/function groups. OtherKeys = 4, } +/// Reported state for a DEC private mode query. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DecModeSetting { + /// Report value 0: the terminal does not recognize the requested mode. NotRecognized = 0, + + /// Report value 1: the mode is set. Set = 1, + + /// Report value 2: the mode is reset. Reset = 2, + + /// Report value 3: the mode is permanently set and cannot be changed. PermanentlySet = 3, + + /// Report value 4: the mode is permanently reset and cannot be changed. PermanentlyReset = 4, } +/// Terminal theme values reported by the Contour theme extension. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ThemeMode { + /// Report value 1: the terminal is using a dark theme. Dark = 1, + + /// Report value 2: the terminal is using a light theme. Light = 2, } // Mouse +/// Mouse reports emitted by terminal mouse tracking modes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseReport { + /// An SGR 1006 mouse report using text-cell coordinates. + /// + /// This is the report format enabled by [`DecPrivateModeCode::SGRMouse`]. Coordinates are + /// one-based terminal cell positions. Sgr1006 { + /// The one-based cell column. x: u16, + + /// The one-based cell row. y: u16, + + /// The reported mouse button action. button: MouseButton, + + /// The modifiers active during the mouse event. modifiers: Modifiers, }, + + /// An SGR 1016 mouse report using pixel coordinates. + /// + /// This is the report format enabled by [`DecPrivateModeCode::SGRPixelsMouse`]. Coordinates + /// are pixel positions instead of terminal cell positions. Sgr1016 { + /// The x coordinate in pixels. x_pixels: u16, + + /// The y coordinate in pixels. y_pixels: u16, + + /// The reported mouse button action. button: MouseButton, + + /// The modifiers active during the mouse event. modifiers: Modifiers, }, } @@ -1280,25 +1802,61 @@ impl Display for MouseReport { } } +/// Mouse button actions encoded in SGR mouse reports. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseButton { + /// Button 1 was pressed; encoded with button value 0 and trailer `M`. Button1Press, + + /// Button 2 was pressed; encoded with button value 1 and trailer `M`. Button2Press, + + /// Button 3 was pressed; encoded with button value 2 and trailer `M`. Button3Press, + + /// Button 4 was pressed; encoded with button value 64 and trailer `M`. Button4Press, + + /// Button 5 was pressed; encoded with button value 65 and trailer `M`. Button5Press, + + /// Button 6 was pressed; encoded with button value 66. Button6Press, + + /// Button 7 was pressed; encoded with button value 67. Button7Press, + + /// Button 1 was released; encoded with button value 0 and trailer `m`. Button1Release, + + /// Button 2 was released; encoded with button value 1 and trailer `m`. Button2Release, + + /// Button 3 was released; encoded with button value 2 and trailer `m`. Button3Release, + + /// Button 4 was released; encoded with button value 64 and trailer `m`. Button4Release, + + /// Button 5 was released; encoded with button value 65 and trailer `m`. Button5Release, + + /// Button 6 was released; encoded with button value 66. Button6Release, + + /// Button 7 was released; encoded with button value 67. Button7Release, + + /// Button 1 was dragged; encoded with button value 32 and trailer `M`. Button1Drag, + + /// Button 2 was dragged; encoded with button value 33 and trailer `M`. Button2Drag, + + /// Button 3 was dragged; encoded with button value 34 and trailer `M`. Button3Drag, + + /// No mouse button was involved; encoded with button value 35 and trailer `M`. None, } @@ -1307,13 +1865,25 @@ pub enum MouseButton { // . bitflags::bitflags! { + /// Feature flags for the Kitty keyboard protocol. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KittyKeyboardFlags: u8 { + /// No keyboard enhancement flags. const NONE = 0; + + /// Disambiguate escape codes for keys that otherwise share encodings. const DISAMBIGUATE_ESCAPE_CODES = 1; + + /// Report press, release, and repeat event types. const REPORT_EVENT_TYPES = 2; + + /// Report alternate key values. const REPORT_ALTERNATE_KEYS = 4; + + /// Report all keys as escape codes. const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8; + + /// Report associated text for key events. const REPORT_ASSOCIATED_TEXT = 16; } } @@ -1324,31 +1894,49 @@ impl Display for KittyKeyboardFlags { } } -/// CSI sequences for interacting with the [Kitty Keyboard -/// Protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). +/// CSI sequences for interacting with the [Kitty Keyboard Protocol]. +/// +/// [Kitty Keyboard Protocol]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ /// /// Note that the Kitty Keyboard Protocol requires terminals to maintain different stacks for the /// main and alternate screens. This means that applications which use alternate screens do not -/// necessarily need to pop flags (via `Self::PopFlags`) when exiting. By entering the main screen +/// necessarily need to pop flags (via [`Self::PopFlags`]) when exiting. By entering the main screen /// the flags must be automatically reset by the terminal. Any flags which were pushed, however, /// will remain active in the alternate screen, even if the alternate screen is entered by a -/// different application. So generally you should use `Self::PopFlags` when shutting down your -/// application. +/// different application. Pop flags during shutdown when the application pushed flags itself. +/// +/// ``` +/// use termina::escape::csi::{ +/// Csi, Keyboard, KittyKeyboardFlags, SetKeyboardFlagsMode, +/// }; +/// +/// let command = Keyboard::SetFlags { +/// flags: KittyKeyboardFlags::REPORT_EVENT_TYPES, +/// mode: SetKeyboardFlagsMode::SetSpecified, +/// }; +/// assert_eq!(Csi::Keyboard(command).to_string(), "\x1b[=2;2u"); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Keyboard { /// Query the current values of the flags. QueryFlags, + /// A report from the terminal declaring which flags are currently set. ReportFlags(KittyKeyboardFlags), + /// Pushes the given flags onto the terminal's stack. PushFlags(KittyKeyboardFlags), + /// Pops the given number of stack entries from the terminal's stack. PopFlags(u8), + /// Requests keyboard enhancement with the given flags according to the mode. /// /// Also see [SetKeyboardFlagsMode]. SetFlags { + /// The flags to assign, set, or clear. flags: KittyKeyboardFlags, + /// How the terminal should apply `flags`. mode: SetKeyboardFlagsMode, }, } @@ -1371,8 +1959,10 @@ impl Display for Keyboard { pub enum SetKeyboardFlagsMode { /// Request any of the given flags and reset any flags which are not given. AssignAll = 1, + /// Request the given flags and ignore any flags which are not given. SetSpecified = 2, + /// Clear the given flags and ignore any flags which are not given. ClearSpecified = 3, } @@ -1383,18 +1973,48 @@ impl Display for SetKeyboardFlagsMode { } } +/// Device and status CSI commands. +/// +/// ``` +/// use termina::escape::csi::{Csi, Device}; +/// +/// assert_eq!(Csi::Device(Device::StatusReport).to_string(), "\x1b[5n"); +/// assert_eq!( +/// Csi::Device(Device::RequestPrimaryDeviceAttributes).to_string(), +/// "\x1b[c", +/// ); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Device { + /// A device-attributes response. DeviceAttributes(()), - /// DECSTR - + + /// [DECSTR] - soft terminal reset. + /// + /// [DECSTR]: https://vt100.net/docs/vt510-rm/DECSTR.html SoftReset, + + /// Request primary device attributes. RequestPrimaryDeviceAttributes, + + /// Request secondary device attributes. RequestSecondaryDeviceAttributes, + + /// Request tertiary device attributes. RequestTertiaryDeviceAttributes, + + /// Request terminal status. StatusReport, - /// - /// + + /// Request the terminal name and version. + /// + /// Mintty and GNOME VTE discuss this query in [Mintty issue #881] and [GNOME VTE issue #235]. + /// + /// [Mintty issue #881]: https://github.com/mintty/mintty/issues/881 + /// [GNOME VTE issue #235]: https://gitlab.gnome.org/GNOME/vte/-/issues/235 RequestTerminalNameAndVersion, + + /// Request terminal parameters. RequestTerminalParameters(i64), } @@ -1415,60 +2035,167 @@ impl Display for Device { // Window +/// Window manipulation and window report CSI commands. +/// +/// ``` +/// use termina::escape::csi::{Csi, Window}; +/// +/// assert_eq!( +/// Csi::Window(Box::new(Window::ReportWindowTitle)).to_string(), +/// "\x1b[21t", +/// ); +/// assert_eq!( +/// Csi::Window(Box::new(Window::ResizeWindowCells { +/// width: Some(120), +/// height: Some(40), +/// })) +/// .to_string(), +/// "\x1b[8;40;120t", +/// ); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Window { + /// De-iconify the window. DeIconify, + + /// Iconify the window. Iconify, + + /// Move the window to a pixel position. MoveWindow { + /// The x coordinate in pixels. x: i64, + + /// The y coordinate in pixels. y: i64, }, + + /// Resize the window to a pixel size. ResizeWindowPixels { + /// The desired width in pixels. width: Option, + + /// The desired height in pixels. height: Option, }, + + /// Raise the window. RaiseWindow, + + /// Lower the window. LowerWindow, + + /// Refresh the window. RefreshWindow, + + /// Resize the window to a cell size. ResizeWindowCells { + /// The desired width in cells. width: Option, + + /// The desired height in cells. height: Option, }, + + /// Restore a maximized window. RestoreMaximizedWindow, + + /// Maximize the window. MaximizeWindow, + + /// Maximize the window vertically. MaximizeWindowVertically, + + /// Maximize the window horizontally. MaximizeWindowHorizontally, + + /// Leave fullscreen mode. UndoFullScreenMode, + + /// Enter fullscreen mode. ChangeToFullScreenMode, + + /// Toggle fullscreen mode. ToggleFullScreen, + + /// Request the window state. ReportWindowState, + + /// Request the window position. ReportWindowPosition, + + /// Request the text-area position. ReportTextAreaPosition, + + /// Request the text-area size in pixels. ReportTextAreaSizePixels, + + /// Request the window size in pixels. ReportWindowSizePixels, + + /// Request the screen size in pixels. ReportScreenSizePixels, + + /// Request the cell size in pixels. ReportCellSizePixels, + + /// Report the cell size in pixels. ReportCellSizePixelsResponse { + /// The reported cell width in pixels. width: Option, + + /// The reported cell height in pixels. height: Option, }, + + /// Request the text-area size in cells. ReportTextAreaSizeCells, + + /// Request the screen size in cells. ReportScreenSizeCells, + + /// Request the icon label. ReportIconLabel, + + /// Request the window title. ReportWindowTitle, + + /// Push the icon and window title onto the title stack. PushIconAndWindowTitle, + + /// Push the icon title onto the title stack. PushIconTitle, + + /// Push the window title onto the title stack. PushWindowTitle, + + /// Pop the icon and window title from the title stack. PopIconAndWindowTitle, + + /// Pop the icon title from the title stack. PopIconTitle, + + /// Pop the window title from the title stack. PopWindowTitle, + /// DECRQCRA; used by esctest ChecksumRectangularArea { + /// The checksum request identifier. request_id: i64, + + /// The page number to checksum. page_number: i64, + + /// The top row of the rectangular area. top: OneBased, + + /// The left column of the rectangular area. left: OneBased, + + /// The bottom row of the rectangular area. bottom: OneBased, + + /// The right column of the rectangular area. right: OneBased, }, } diff --git a/src/escape/dcs.rs b/src/escape/dcs.rs index 82d2da9..f4d02d5 100644 --- a/src/escape/dcs.rs +++ b/src/escape/dcs.rs @@ -1,14 +1,54 @@ +//! Device Control String (DCS) escape sequences. +//! +//! Device Control String sequences are framed by [`DCS`] and [`ST`]. Termina currently models the +//! [DECRQSS] request and [DECRPSS] response forms used for terminal state queries. +//! +//! # Examples +//! +//! ``` +//! use termina::escape::dcs::{Dcs, DcsRequest}; +//! +//! let request = Dcs::Request(DcsRequest::GraphicRendition); +//! assert_eq!(request.to_string(), "\x1bP$qm\x1b\\"); +//! ``` +//! +//! [`DCS`]: super::DCS +//! [DECRPSS]: https://vt100.net/docs/vt510-rm/DECRPSS.html +//! [DECRQSS]: https://vt100.net/docs/vt510-rm/DECRQSS.html +//! [`ST`]: super::ST + use std::fmt::{self, Display}; use crate::style::CursorStyle; +#[cfg(doc)] +use crate::escape::csi::Sgr; + +/// A Device Control String command. +/// +/// Termina uses DCS for terminal state queries that have structured request and response forms. +/// Formatting writes the DCS introducer, the request or response payload, and the string +/// terminator. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Dcs { - // DECRQSS: + /// Request a terminal setting with [DECRQSS] using a [`DcsRequest`] selector. + /// + /// For example, [`DcsRequest::GraphicRendition`] asks for the current [`Sgr`] state. + /// + /// [DECRQSS]: https://vt100.net/docs/vt510-rm/DECRQSS.html Request(DcsRequest), - // DECRPSS: + + /// Report a terminal setting with [DECRPSS] using a [`DcsResponse`] payload. + /// + /// Terminals use this response to answer a DECRQSS request. A valid response contains the + /// setting encoded as the terminal would send it in the corresponding control sequence. + /// + /// [DECRPSS]: https://vt100.net/docs/vt510-rm/DECRPSS.html Response { + /// Whether the terminal recognized the original request. is_request_valid: bool, + + /// The setting value returned by the terminal. value: DcsResponse, }, } @@ -31,36 +71,69 @@ impl Display for Dcs { } } +/// Request selectors for [DECRQSS]. +/// +/// Each variant names the setting being queried and shows the selector bytes sent after `DCS $ q`. +/// The selector bytes come from [DECRQSS]. +/// +/// [DECRQSS]: https://vt100.net/docs/vt510-rm/DECRQSS.html #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DcsRequest { + /// DECRQSS `$}`: request the active status display. ActiveStatusDisplay, + /// DECRQSS `*x`: request the attribute change extent. AttributeChangeExtent, + /// DECRQSS `"q`: request the character attribute. CharacterAttribute, + /// DECRQSS `"p`: request the conformance level. ConformanceLevel, + /// DECRQSS `$|`: request the number of columns per page. ColumnsPerPage, + /// DECRQSS `t`: request the number of lines per page. LinesPerPage, + /// DECRQSS `*|`: request the number of lines per screen. NumberOfLinesPerScreen, + /// DECRQSS `$~`: request the status line type. StatusLineType, + /// DECRQSS `s`: request the left and right margins. LeftAndRightMargins, + /// DECRQSS `r`: request the top and bottom margins. TopAndBottomMargins, - /// SGR + /// DECRQSS `m`: request [`Sgr`] state. GraphicRendition, + /// DECRQSS `p`: request the setup language. SetUpLanguage, + /// DECRQSS `$s`: request the printer type. PrinterType, + /// DECRQSS `"t`: request the refresh rate. RefreshRate, + /// DECRQSS `(p`: request the digital printed data type. DigitalPrintedDataType, + /// DECRQSS `*p`: request the ProPrinter character set. ProPrinterCharacterSet, + /// DECRQSS `*r`: request the communication speed. CommunicationSpeed, + /// DECRQSS `*u`: request the communication port. CommunicationPort, + /// DECRQSS ` p`: request the scroll speed. ScrollSpeed, + /// DECRQSS ` q`: request the cursor style. CursorStyle, + /// DECRQSS ` r`: request the key-click volume. KeyClickVolume, + /// DECRQSS ` t`: request the warning-bell volume. WarningBellVolume, + /// DECRQSS ` u`: request the margin-bell volume. MarginBellVolume, + /// DECRQSS ` v`: request the lock-key style. LockKeyStyle, + /// DECRQSS `*s`: request the flow-control type. FlowControlType, + /// DECRQSS `$q`: request the disconnect delay time. DisconnectDelayTime, + /// DECRQSS `"u`: request the transmit-rate limit. TransmitRateLimit, + /// DECRQSS `+w`: request the port parameter. PortParameter, } @@ -101,10 +174,26 @@ impl Display for DcsRequest { } } +/// Response payloads from [DECRPSS] parsed by Termina. +/// +/// [DECRPSS] is the response form terminals use for [DECRQSS] status-string queries. +/// +/// [DECRPSS]: https://vt100.net/docs/vt510-rm/DECRPSS.html +/// [DECRQSS]: https://vt100.net/docs/vt510-rm/DECRQSS.html #[derive(Debug, Clone, PartialEq, Eq)] pub enum DcsResponse { - /// SGR + /// A DECRPSS response containing [`Sgr`] attributes. + /// + /// [`DcsRequest::GraphicRendition`] produces this response. The payload carries the same + /// values that would appear in a [`Sgr`] sequence. + /// + /// [`Sgr`]: crate::escape::csi::Sgr GraphicRendition(Vec), + + /// A DECRPSS response containing the terminal's current cursor style. + /// + /// [`DcsRequest::CursorStyle`] produces this response. The payload corresponds to the + /// [`CursorStyle`] setting. CursorStyle(CursorStyle), // There are others but adding them would mean adding a lot of parsing code... } diff --git a/src/escape/osc.rs b/src/escape/osc.rs index 4ec0f33..1bf4f03 100644 --- a/src/escape/osc.rs +++ b/src/escape/osc.rs @@ -1,22 +1,87 @@ -// CREDIT: this is a quite shallow copy of . -// I've replaced some macros and the base64 implementation however, as well as make the commands -// borrow a `str` instead of own a `String`. +//! Operating System Command (OSC) escape sequences. +//! +//! OSC sequences carry string-style terminal integration commands such as window titles, clipboard +//! access, and dynamic color queries. Termina stores string payloads by borrowing where possible +//! so callers can format an OSC command without first allocating an owned [`String`]. +//! +//! # Examples +//! +//! ``` +//! use termina::escape::osc::Osc; +//! +//! assert_eq!(Osc::SetWindowTitle("demo").to_string(), "\x1b]2;demo\x1b\\"); +//! ``` +//! +//! OSC 52 selection payloads are base64-encoded when formatted: +//! +//! ``` +//! use termina::escape::osc::{Osc, Selection}; +//! +//! let command = Osc::SetSelection(Selection::CLIPBOARD, "copied text"); +//! assert_eq!(command.to_string(), "\x1b]52;c;Y29waWVkIHRleHQ=\x1b\\"); +//! ``` +//! +//! # Implementation Notes +//! +//! This is intentionally a shallow copy of [termwiz's OSC support]. Termina removes the +//! macro-heavy parts, replaces the base64 implementation, and changes command payloads to borrow +//! `str` values instead of owning [`String`] values. +//! +//! [termwiz's OSC support]: https://docs.rs/termwiz/latest/termwiz/escape/struct.Osc.html use std::fmt::{self, Display}; use crate::{base64, style::RgbColor}; +/// An Operating System Command string control. +/// +/// Formatting writes the OSC introducer, a command number or command letter, the command payload, +/// and the string terminator. The numbered variants use common xterm-compatible assignments: OSC +/// 2 sets the window title, OSC 52 manages selections, and OSC 10-19 manage dynamic colors. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Osc<'a> { + /// OSC 0: set both the icon name and window title. SetIconNameAndWindowTitle(&'a str), + + /// OSC 2: set the window title. SetWindowTitle(&'a str), + + /// Sun terminal form: set the window title with `OSC l`. SetWindowTitleSun(&'a str), + + /// OSC 1: set the icon name. SetIconName(&'a str), + + /// Sun terminal form: set the icon name with `OSC L`. SetIconNameSun(&'a str), + + /// OSC 52: clear one or more terminal selections described by [`Selection`]. + /// + /// Terminals use OSC 52 to expose clipboard-like selections. Clearing sends a selection target + /// without content. ClearSelection(Selection), + + /// OSC 52: query one or more terminal selections described by [`Selection`]. + /// + /// Querying sends `?` as the content payload and lets a cooperating terminal report the + /// selection contents. QuerySelection(Selection), + + /// OSC 52: set one or more terminal selections described by [`Selection`]. + /// + /// The string payload is base64-encoded when formatted, as required by OSC 52. SetSelection(Selection, &'a str), + + /// OSC 10-19: change or query dynamic terminal colors. + /// + /// Each [`DynamicColorNumber`] identifies the color slot. [`ColorOrQuery::Query`] formats as + /// `?`, asking the terminal to report its current value; [`ColorOrQuery::Color`] carries the + /// [`RgbColor`] to set. ChangeDynamicColors(DynamicColorNumber, Vec), + + /// OSC 110-119: reset a [`DynamicColorNumber`] slot to its default value. + /// + /// xterm defines reset commands by adding 100 to the dynamic color number. ResetDynamicColor(DynamicColorNumber), // TODO: I didn't copy many available commands yet... } @@ -50,21 +115,52 @@ impl Display for Osc<'_> { } bitflags::bitflags! { + /// OSC 52 selection targets. + /// + /// Multiple targets can be combined. Formatting concatenates the target letters/numbers in the + /// order expected by xterm-compatible terminals. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Selection : u16 { + /// No OSC 52 selection target. const NONE = 0; + + /// OSC 52 selection target `c`: the clipboard selection. const CLIPBOARD = 1<<1; + + /// OSC 52 selection target `p`: the primary selection. const PRIMARY=1<<2; + + /// OSC 52 selection target `s`: the select selection. const SELECT=1<<3; + + /// OSC 52 selection target `0`: cut buffer 0. const CUT0=1<<4; + + /// OSC 52 selection target `1`: cut buffer 1. const CUT1=1<<5; + + /// OSC 52 selection target `2`: cut buffer 2. const CUT2=1<<6; + + /// OSC 52 selection target `3`: cut buffer 3. const CUT3=1<<7; + + /// OSC 52 selection target `4`: cut buffer 4. const CUT4=1<<8; + + /// OSC 52 selection target `5`: cut buffer 5. const CUT5=1<<9; + + /// OSC 52 selection target `6`: cut buffer 6. const CUT6=1<<10; + + /// OSC 52 selection target `7`: cut buffer 7. const CUT7=1<<11; + + /// OSC 52 selection target `8`: cut buffer 8. const CUT8=1<<12; + + /// OSC 52 selection target `9`: cut buffer 9. const CUT9=1<<13; } } @@ -114,18 +210,29 @@ impl Display for Selection { } } +/// Dynamic color slots addressed by OSC 10-19. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum DynamicColorNumber { + /// OSC 10: the default text foreground color used for normal cells. TextForegroundColor = 10, + /// OSC 11: the default text background color used for normal cells. TextBackgroundColor = 11, + /// OSC 12: the text cursor color. TextCursorColor = 12, + /// OSC 13: the pointer foreground color used by xterm-compatible terminals. MouseForegroundColor = 13, + /// OSC 14: the pointer background color used by xterm-compatible terminals. MouseBackgroundColor = 14, + /// OSC 15: the Tektronix foreground color. TektronixForegroundColor = 15, + /// OSC 16: the Tektronix background color. TektronixBackgroundColor = 16, + /// OSC 17: the selection highlight background color. HighlightBackgroundColor = 17, + /// OSC 18: the Tektronix cursor color. TektronixCursorColor = 18, + /// OSC 19: the selection highlight foreground color. HighlightForegroundColor = 19, } @@ -147,9 +254,39 @@ impl DynamicColorNumber { } } +/// A dynamic color value or query marker for OSC color commands. +/// +/// Use [`Self::Query`] to ask the terminal for a slot's current value, or [`Self::Color`] to set a +/// new RGB value: +/// +/// ``` +/// use termina::{ +/// escape::osc::{ColorOrQuery, DynamicColorNumber, Osc}, +/// style::RgbColor, +/// }; +/// +/// let query = Osc::ChangeDynamicColors( +/// DynamicColorNumber::TextBackgroundColor, +/// vec![ColorOrQuery::Query], +/// ); +/// assert_eq!(query.to_string(), "\x1b]11;?\x1b\\"); +/// +/// let set = Osc::ChangeDynamicColors( +/// DynamicColorNumber::TextForegroundColor, +/// vec![RgbColor::new(40, 40, 40).into()], +/// ); +/// assert_eq!(set.to_string(), "\x1b]10;rgb:2828/2828/2828\x1b\\"); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColorOrQuery { + /// Set the dynamic color to an RGB value. + /// + /// Formatting emits the `rgb:RRRR/GGGG/BBBB` form used by xterm OSC color controls. Color(RgbColor), + + /// Query the current dynamic color value. + /// + /// Formatting emits `?`, which asks the terminal to report the current value for that slot. Query, } diff --git a/src/event.rs b/src/event.rs index 20ba105..042ca2b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,56 +1,174 @@ -// CREDIT: Most event code is adapted from crossterm. The main difference is that I include escape -// sequences like CSI and DCS in the `Event` struct and do not make a distinction between -// `InternalEvent` and `Event`. Otherwise all `KeyEvent` code is nearly identical to crossterm. +//! Terminal input events. +//! +//! [`Event`] is the typed output of [`Parser`], [`EventReader`], and the optional `EventStream`. +//! Termina reports ordinary input events such as keys, mouse input, focus changes, resize events, +//! and bracketed paste. It also keeps terminal responses such as CSI, OSC, and DCS in the same +//! public enum so callers can issue a query and read the response without a second internal event +//! model. +//! +//! Use [`EventReader::read`] or [`Terminal::read`] when reading from a process terminal. Use +//! [`Parser::pop`] when parsing bytes from a PTY, terminal multiplexer, or other caller-owned +//! input source. [`EventReader`] is the main place to look for event-reading examples and filter +//! behavior. +//! +//! # Implementation Notes +//! +//! Most event code is adapted from [crossterm events]. The main difference is intentional: Termina +//! includes escape sequences like [`Csi`] and [`Dcs`] in [`Event`] and does not split the model +//! into separate internal and public events. The key event types otherwise stay close to +//! crossterm's shape. +//! +//! [crossterm events]: https://docs.rs/crossterm/latest/crossterm/event/index.html +//! [`Csi`]: crate::escape::csi::Csi +//! [`Dcs`]: crate::escape::dcs::Dcs +//! [`EventReader`]: crate::EventReader +//! [`EventReader::read`]: crate::EventReader::read +//! [`Parser`]: crate::Parser +//! [`Parser::pop`]: crate::Parser::pop +//! [`Terminal::read`]: crate::Terminal::read use crate::{ escape::{csi::Csi, dcs::Dcs, osc::Osc}, WindowSize, }; +#[cfg(doc)] +use crate::escape::csi::{DecPrivateModeCode, KittyKeyboardFlags}; +#[cfg(doc)] +use crate::{EventReader, Parser, Terminal}; + pub(crate) mod reader; pub(crate) mod source; #[cfg(feature = "event-stream")] pub(crate) mod stream; +/// A parsed terminal input event or terminal protocol response. +/// +/// Values of this type are returned by [`EventReader::read`], [`Terminal::read`], and +/// [`Parser::pop`]. See [`EventReader`] for the normal terminal-reading flow, including how +/// filters skip events without losing them. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { + /// A keyboard event described by [`KeyEvent`]. + /// + /// Check [`KeyEvent::kind`] when a binding should run once per physical key press. Some input + /// sources report both press and release for the same key, so handling every `Event::Key` + /// without checking for [`KeyEventKind::Press`] can run the binding twice. Key(KeyEvent), + + /// A mouse event described by [`MouseEvent`]. + /// + /// Terminals produce these after an application enables mouse tracking with modes such as + /// [`DecPrivateModeCode::MouseTracking`], [`DecPrivateModeCode::ButtonEventMouse`], or + /// [`DecPrivateModeCode::AnyEventMouse`]. Mouse(MouseEvent), - /// The window was resized to the given dimensions. + + /// The terminal window was resized to the given [`WindowSize`]. WindowResized(WindowSize), + + /// Terminal focus entered the application window. + /// + /// Terminals send this only after [`DecPrivateModeCode::FocusTracking`] has enabled focus + /// tracking. FocusIn, + + /// Terminal focus left the application window. + /// + /// Terminals send this only after [`DecPrivateModeCode::FocusTracking`] has enabled focus + /// tracking. FocusOut, + /// A "bracketed" paste. /// /// Normally pasting into a terminal with Ctrl+v (or Super+v) enters the pasted text as if - /// you had typed the keys individually. Terminals commonly support ["bracketed - /// paste"](https://en.wikipedia.org/wiki/Bracketed-paste) now however, which uses an escape - /// sequence to deliver the entire pasted content. + /// you had typed the keys individually. [`DecPrivateModeCode::BracketedPaste`] asks compatible + /// terminals to wrap pasted text in explicit start/end markers so Termina can deliver the + /// entire pasted content as one event. xterm documents this as [bracketed paste mode]. + /// + /// [bracketed paste mode]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode Paste(String), - /// A parsed escape sequence starting with CSI (control sequence introducer). + + /// A parsed CSI response or report described by [`Csi`]. + /// + /// Applications see this when the terminal sends a Control Sequence Introducer response, such + /// as a cursor position report, device attributes, mode report, or [`Csi::Keyboard`] protocol + /// report. Csi(Csi), - /// A parsed escape sequence starting with OSC (operating system command). + + /// A parsed OSC response described by [`Osc`]. + /// + /// Applications see this when the terminal answers an Operating System Command query, such as a + /// dynamic color query. Osc(Osc<'static>), + + /// A parsed DCS response described by [`Dcs`]. + /// + /// Applications see this when the terminal answers a Device Control String query, such as + /// DECRQSS. Dcs(Dcs), } impl Event { + /// Returns `true` for CSI, OSC, and DCS protocol responses. #[inline] pub fn is_escape(&self) -> bool { matches!(self, Self::Csi(_) | Self::Dcs(_) | Self::Osc(_)) } } -// CREDIT: +/// A key event plus modifiers and protocol state. +/// +/// `KeyEvent` appears inside [`Event::Key`], which is normally returned by [`EventReader::read`] +/// or [`Terminal::read`]. See [`EventReader`] for examples of filtering key events while leaving +/// other terminal events buffered. +/// +/// `code` identifies the key, `kind` distinguishes press/release/repeat when the terminal reports +/// that detail, `modifiers` carries held modifier keys, and `state` carries protocol state such as +/// keypad-originated input. +/// +/// Code that handles shortcuts should usually check `kind == KeyEventKind::Press` before acting. +/// Unix-style terminal input commonly reports only presses unless a keyboard enhancement protocol +/// requests event types, but the Windows legacy console API reports press and release records for +/// many keys. Ignoring `kind` can make a shortcut run twice on backends that expose releases. +/// +/// Some key combinations also cannot be represented by some terminals at all. Crossterm's +/// [missing key combinations] issue is a useful catalogue of those terminal-level limitations; +/// report Termina bugs in Termina, not on that upstream issue. +/// +/// # Implementation Notes +/// +/// This mirrors the layout used by [crossterm key events]. +/// +/// [crossterm key events]: https://docs.rs/crossterm/latest/crossterm/event/struct.KeyEvent.html +/// [missing key combinations]: https://github.com/crossterm-rs/crossterm/issues/685 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeyEvent { + /// The key identity. pub code: KeyCode, + + /// Whether this event is a press, release, or repeat. + /// + /// Check this before triggering a shortcut or command. If the command should run once per + /// physical key press, require [`KeyEventKind::Press`] and ignore release events. pub kind: KeyEventKind, + + /// Modifier keys active for this key event. + /// + /// This is the modifier state Termina could infer from the terminal protocol or platform + /// backend. Plain terminal input does not report every modifier independently; for example, + /// Shift may be inferred from an uppercase decoded character, while lock-key and keypad state + /// require an enhanced keyboard protocol or platform backend that reports them. pub modifiers: Modifiers, + + /// Extra key state reported by the terminal protocol. + /// + /// This is empty unless the input source reports state outside the ordinary modifier mask, + /// such as keypad-originated input, Caps Lock, or Num Lock. pub state: KeyEventState, } impl KeyEvent { + /// Creates a key-press event with the given key code and modifiers. pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self { Self { code, @@ -72,70 +190,193 @@ impl From for KeyEvent { } } +/// Whether a key was pressed, released, or repeated. +/// +/// This controls whether a key event should trigger an action. Unix-style terminal input commonly +/// produces [`Self::Press`] only, while Windows legacy console input and enhanced keyboard +/// protocols can also produce [`Self::Release`] and [`Self::Repeat`]. Code that treats every +/// [`Event::Key`] as an action can therefore run twice for one physical key press. Shortcuts and +/// commands should usually act only on [`Self::Press`]. Some key combinations are also limited by +/// what the terminal can encode; Crossterm's [missing key combinations] issue is useful background +/// for those limitations, but Termina bugs should be reported to Termina. +/// +/// [missing key combinations]: https://github.com/crossterm-rs/crossterm/issues/685 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyEventKind { + /// A key was pressed. Press, + + /// A key was released. + /// + /// Terminals report releases when a keyboard protocol flag such as + /// [`KittyKeyboardFlags::REPORT_EVENT_TYPES`] requests event types, or when the platform + /// backend exposes releases directly. Release, + + /// A key press was repeated while held. Repeat, } bitflags::bitflags! { + /// Modifier keys active during a key or mouse event. + /// + /// Terminals vary in which modifiers they report. Treat these flags as the state Termina + /// observed, not as proof that every unlisted physical modifier was inactive. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Modifiers: u8 { + /// No modifier keys were active. const NONE = 0; + + /// Shift was active. const SHIFT = 1; + + /// Alt or Option was active. const ALT = 1 << 1; + + /// Control was active. const CONTROL = 1 << 2; - /// Windows/Linux key or Command key on Macs. + + /// Super key. + /// + /// This is Command on macOS, the Windows key on Windows, and commonly the Super key on + /// Linux and other Unix-like systems. const SUPER = 1 << 3; + /// Hyper key. + /// + /// Hyper is an additional modifier from older keyboard layouts and terminal protocols. It + /// is uncommon on modern Mac and Windows keyboards, but some terminals can still report it. const HYPER = 1 << 4; + + /// Meta was active. const META = 1 << 5; + + /// Caps Lock was active. const CAPS_LOCK = 1 << 6; + + /// Num Lock was active. const NUM_LOCK = 1 << 7; } } bitflags::bitflags! { + /// Extra key state reported by the terminal or platform backend. + /// + /// These flags are present only when the input source reports them. Ordinary terminal input + /// often cannot distinguish keypad-originated keys or lock-key state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeyEventState: u8 { + /// No extra key state was reported. const NONE = 0; + + /// The key came from the keypad. const KEYPAD = 1 << 1; + + /// Caps Lock was active for this key event. const CAPS_LOCK = 1 << 2; + + /// Num Lock was active for this key event. const NUM_LOCK = 1 << 3; } } +/// The key identity reported by the terminal. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyCode { + /// A Unicode character key after terminal decoding. + /// + /// Termina stores the character it can identify from the input source rather than normalizing + /// every alphabetic key to lowercase plus [`Modifiers::SHIFT`]. For ordinary UTF-8 terminal + /// input, `A` is reported as `Char('A')` and Shift is also recorded in `modifiers` when Termina + /// can infer it from the decoded character. Control bytes such as Ctrl+A are reported as + /// `Char('a')` with [`Modifiers::CONTROL`]. Enhanced keyboard protocols and the Windows + /// backend may use layout-aware or protocol-supplied characters, so callers should compare + /// against the character they want to handle instead of assuming another terminal library's + /// capitalization model. Char(char), + + /// The Enter or Return key. Enter, + + /// The Backspace key. Backspace, + + /// The Tab key. Tab, + + /// The Escape key. Escape, + + /// The left arrow key. Left, + + /// The right arrow key. Right, + + /// The up arrow key. Up, + + /// The down arrow key. Down, + + /// The Home key. Home, + + /// The End key. End, + + /// Shift+Tab or another backwards-tab key sequence. BackTab, + + /// The Page Up key. PageUp, + + /// The Page Down key. PageDown, + + /// The Insert key. Insert, + + /// The Delete key. Delete, + + /// The keypad begin key. KeypadBegin, + + /// The Caps Lock key. CapsLock, + + /// The Scroll Lock key. ScrollLock, + + /// The Num Lock key. NumLock, + + /// The Print Screen key. PrintScreen, + + /// The Pause or Break key. Pause, + + /// The Menu or Application key. Menu, + + /// A null key code. + /// + /// This can appear when a platform backend reports a key event without a printable or named + /// key identity. Null, - /// F1-F35 "function" keys + + /// F1-F35 function keys. Function(u8), + + /// A modifier key such as Shift, Control, Alt, Super, Hyper, or Meta. Modifier(ModifierKeyCode), + + /// A media control key. Media(MediaKeyCode), } + +/// Physical modifier keys reported as key events. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModifierKeyCode { /// Left Shift key. @@ -144,9 +385,9 @@ pub enum ModifierKeyCode { LeftControl, /// Left Alt key. (Option on macOS, Alt on other platforms) LeftAlt, - /// Left Super key. (Command on macOS, Windows on Windows, Super on other platforms) + /// Left Super key. (Command on macOS, Windows key on Windows, Super on Linux) LeftSuper, - /// Left Hyper key. + /// Left Hyper key. (An extended modifier used by some keyboards and terminal protocols) LeftHyper, /// Left Meta key. LeftMeta, @@ -156,9 +397,9 @@ pub enum ModifierKeyCode { RightControl, /// Right Alt key. (Option on macOS, Alt on other platforms) RightAlt, - /// Right Super key. (Command on macOS, Windows on Windows, Super on other platforms) + /// Right Super key. (Command on macOS, Windows key on Windows, Super on Linux) RightSuper, - /// Right Hyper key. + /// Right Hyper key. (An extended modifier used by some keyboards and terminal protocols) RightHyper, /// Right Meta key. RightMeta, @@ -168,6 +409,7 @@ pub enum ModifierKeyCode { IsoLevel5Shift, } +/// Media keys reported as key events. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MediaKeyCode { /// Play media key. @@ -198,38 +440,56 @@ pub enum MediaKeyCode { MuteVolume, } +/// Mouse input event with zero-based terminal cell coordinates. +/// +/// Terminal mouse protocols encode cell positions as one-based coordinates, but Termina converts +/// them to zero-based `column` and `row` values for consistency with Rust indexing and the parser's +/// existing event model. SGR pixel mouse reports are represented separately as +/// [`crate::escape::csi::MouseReport::Sgr1016`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MouseEvent { - /// The kind of mouse event that was caused. + /// The mouse action. pub kind: MouseEventKind, - /// The column that the event occurred on. + + /// The zero-based terminal column where the event occurred. pub column: u16, - /// The row that the event occurred on. + + /// The zero-based terminal row where the event occurred. pub row: u16, + /// The key modifiers active when the event occurred. pub modifiers: Modifiers, } +/// The mouse action reported by the terminal. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseEventKind { - /// Pressed mouse button. Contains the button that was pressed. + /// A mouse button was pressed. Down(MouseButton), - /// Released mouse button. Contains the button that was released. + + /// A mouse button was released. Up(MouseButton), - /// Moved the mouse cursor while pressing the contained mouse button. + + /// The pointer moved while a mouse button was pressed. Drag(MouseButton), - /// Moved the mouse cursor while not pressing a mouse button. + + /// The pointer moved without a pressed mouse button. Moved, - /// Scrolled mouse wheel downwards (towards the user). + + /// The wheel scrolled down, usually toward the user. ScrollDown, - /// Scrolled mouse wheel upwards (away from the user). + + /// The wheel scrolled up, usually away from the user. ScrollUp, - /// Scrolled mouse wheel left (mostly on a laptop touchpad). + + /// The wheel or touchpad scrolled left. ScrollLeft, - /// Scrolled mouse wheel right (mostly on a laptop touchpad). + + /// The wheel or touchpad scrolled right. ScrollRight, } +/// Mouse buttons reported by terminal mouse tracking. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseButton { /// Left mouse button. diff --git a/src/event/reader.rs b/src/event/reader.rs index e8d2958..8943199 100644 --- a/src/event/reader.rs +++ b/src/event/reader.rs @@ -1,8 +1,19 @@ -// CREDIT: -// This module provides an `Arc>` wrapper around a type which is basically the crossterm -// `InternalEventReader`. This allows it to live on the Terminal and an EventStream rather than -// statically. -// Instead of crossterm's `Filter` trait I have opted for a `Fn(&Event) -> bool` for simplicity. +//! Thread-safe terminal event reader. +//! +//! This module provides an `Arc>` wrapper around the platform event source. That lets a +//! reader live on a terminal handle and also be shared with the optional async stream, rather than +//! being stored globally. +//! +//! # Implementation Notes +//! +//! This is adapted from [crossterm's event reader]. The shared reader is mostly an +//! `Arc>` wrapper around the same shape as crossterm's internal event reader. This lets +//! it live on a [`Terminal`] and on an `EventStream` instead of being stored globally. Termina uses +//! `Fn(&Event) -> bool` filters instead of a dedicated filter trait so callers can pass ordinary +//! closures. +//! +//! [crossterm's event reader]: https://docs.rs/crossterm/latest/crossterm/event/index.html +//! [`Terminal`]: crate::Terminal use std::{collections::VecDeque, io, sync::Arc, time::Duration}; @@ -17,6 +28,63 @@ use super::{ /// /// Note that this type wraps an `Arc` and is cheap to clone. If the `event-stream` feature is /// enabled then this value should be passed to `EventStream::new`. +/// +/// [`Self::read`] and [`Self::poll`] both take filters. Events rejected by a filter remain buffered +/// so a caller can wait for a key press without discarding protocol responses, mouse events, or +/// other input that another part of the application may read later. Filtering preserves rejected +/// events for later reads, but callers should not rely on rejected events being re-buffered in exact +/// stream order across multiple filtered reads. +/// +/// # Examples +/// +/// Read every event and branch on the event kind: +/// +/// ```no_run +/// use std::io; +/// +/// use termina::{ +/// event::{Event, KeyCode, KeyEventKind}, +/// PlatformTerminal, Terminal, +/// }; +/// +/// fn main() -> io::Result<()> { +/// let reader = PlatformTerminal::new()?.event_reader(); +/// loop { +/// let event = reader.read(|_| true)?; +/// match event { +/// Event::Key(key) +/// if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') => +/// { +/// break +/// } +/// Event::Mouse(mouse) => eprintln!("mouse at {},{}", mouse.column, mouse.row), +/// Event::Csi(csi) => eprintln!("CSI response: {csi:?}"), +/// _ => {} +/// } +/// } +/// Ok(()) +/// } +/// ``` +/// +/// Use a filter when a call should wait for a specific class of event: +/// +/// ```no_run +/// use std::io; +/// +/// use termina::{ +/// event::{Event, KeyEventKind}, +/// PlatformTerminal, Terminal, +/// }; +/// +/// fn main() -> io::Result<()> { +/// let reader = PlatformTerminal::new()?.event_reader(); +/// let event = reader.read(|event| { +/// matches!(event, Event::Key(key) if key.kind == KeyEventKind::Press) +/// })?; +/// println!("received {event:?}"); +/// Ok(()) +/// } +/// ``` #[derive(Debug, Clone)] pub struct EventReader { shared: Arc>, @@ -34,11 +102,17 @@ impl EventReader { } } + /// Returns a platform-specific waker that can unblock [`poll`](Self::poll) calls. pub fn waker(&self) -> PlatformWaker { let reader = self.shared.lock(); reader.source.waker() } + /// Polls for availability of an event matching `filter`. + /// + /// When `timeout` is `None`, this call blocks indefinitely. Events rejected by `filter` are + /// retained so a later call can still return them. Use the same filter with [`Self::read`] if + /// the follow-up read should consume the event that made this method return `true`. pub fn poll(&self, timeout: Option, filter: F) -> io::Result where F: FnMut(&Event) -> bool, @@ -56,6 +130,11 @@ impl EventReader { reader.poll(timeout, filter) } + /// Blocks until an event matching `filter` is available. + /// + /// Events rejected by `filter` are retained for later reads. For keyboard shortcuts, filter on + /// `Event::Key(key) if key.kind == KeyEventKind::Press` unless the application intentionally + /// handles release or repeat events. pub fn read(&self, filter: F) -> io::Result where F: FnMut(&Event) -> bool, diff --git a/src/event/stream.rs b/src/event/stream.rs index 247bf42..375706e 100644 --- a/src/event/stream.rs +++ b/src/event/stream.rs @@ -1,6 +1,16 @@ -// CREDIT: This is basically all crossterm. -// -// I added the dummy stream for integration testing in Helix. +//! Futures [`Stream`] adapter for terminal events. +//! +//! This module is available with the `event-stream` feature. It adapts the blocking +//! [`EventReader`] API into a stream by parking a helper thread on the platform event source and +//! waking the async task when matching input arrives. +//! +//! # Implementation Notes +//! +//! This is intentionally close to [crossterm's event stream]. The Termina-specific part is the +//! shared [`EventReader`], which was added so the same reader model could support downstream +//! integration tests as well as normal terminal event streams. +//! +//! [crossterm's event stream]: https://docs.rs/crossterm/latest/crossterm/event/ use std::{ io, @@ -19,12 +29,32 @@ use futures_core::Stream; use super::{reader::EventReader, source::PlatformWaker, Event}; -/// A stream of `termina::Event`s received from the terminal. +/// A stream of [`Event`] values received from the terminal. /// /// This type is only available if the `event-stream` feature is enabled. /// -/// Create an event stream for a terminal by passing the reader [crate::Terminal::event_reader] -/// into [EventStream::new] with a filter. +/// Create an event stream for a terminal by passing the reader from +/// [`crate::Terminal::event_reader`] into [`EventStream::new`] with a filter. +/// +/// # Examples +/// +/// Requires the `event-stream` feature and an async runtime. +/// +/// ```ignore +/// use futures_lite::StreamExt as _; +/// use termina::{Event, EventStream, PlatformTerminal, Terminal}; +/// +/// # async fn demo() -> std::io::Result<()> { +/// let reader = PlatformTerminal::new()?.event_reader(); +/// let mut stream = EventStream::new(reader, |_| true); +/// while let Some(Ok(event)) = stream.next().await { +/// if matches!(event, Event::FocusOut) { +/// break; +/// } +/// } +/// # Ok(()) +/// # } +/// ``` pub struct EventStream { waker: PlatformWaker, filter: Arc bool>, @@ -34,6 +64,7 @@ pub struct EventStream { task_sender: SyncSender, } +/// Internal task handed to the helper thread managing the blocking poll. #[derive(Debug)] struct Task { stream_waker: std::task::Waker, @@ -42,6 +73,7 @@ struct Task { } impl EventStream { + /// Creates a stream backed by `reader` that only yields events accepted by `filter`. pub fn new(reader: EventReader, filter: F) -> Self where F: Fn(&Event) -> bool + Send + Sync + 'static, diff --git a/src/lib.rs b/src/lib.rs index 861e931..61405c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,54 @@ +//! Terminal I/O, escape-sequence types, styling, and input parsing. +//! +//! Termina keeps the terminal protocol visible. Applications write typed CSI, OSC, and DCS values +//! from [`escape`] instead of assembling byte strings, and read typed [`Event`] values instead of +//! decoding terminal input by hand. [`PlatformTerminal`] opens the current process terminal, +//! switches raw/cooked mode, writes bytes, and creates an [`EventReader`] for synchronous input. +//! +//! Code that already has terminal bytes can use [`Parser`] directly. That is useful for PTY tests, +//! terminal multiplexers, or callers that own the input source and only need Termina's parser. +//! +//! # Examples +//! +//! ```no_run +//! use std::io::{self, Write}; +//! +//! use termina::{ +//! event::{KeyCode, KeyEventKind}, +//! Event, PlatformTerminal, Terminal, +//! }; +//! +//! fn main() -> io::Result<()> { +//! let mut terminal = PlatformTerminal::new()?; +//! terminal.enter_raw_mode()?; +//! writeln!(terminal, "Press q to exit.")?; +//! +//! let reader = terminal.event_reader(); +//! loop { +//! let event = reader.read(|_| true)?; +//! if matches!( +//! event, +//! Event::Key(key) +//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') +//! ) { +//! break; +//! } +//! } +//! +//! terminal.enter_cooked_mode() +//! } +//! ``` +//! +//! Parsing PTY bytes directly does not require opening a terminal handle: +//! +//! ``` +//! use termina::{Event, Parser}; +//! +//! let mut parser = Parser::default(); +//! parser.parse(b"\x1b[5~", false); +//! assert!(matches!(parser.pop(), Some(Event::Key(_)))); +//! ``` + pub(crate) mod base64; pub mod escape; pub mod event; @@ -17,15 +68,38 @@ pub use terminal::{PlatformHandle, PlatformTerminal, Terminal}; #[cfg(feature = "event-stream")] pub use event::stream::EventStream; -/// A helper type which avoids tripping over Unix terminal's one-indexed conventions. +/// A one-based terminal coordinate or dimension. +/// +/// Terminal protocols generally count rows and columns from 1, while Rust collections and many +/// application models count from 0. `OneBased` stores the protocol value and rejects zero. Use +/// [`Self::from_zero_based`] when converting from an application index and [`Self::get_zero_based`] +/// when converting back. +/// +/// # Examples +/// +/// ``` +/// use termina::OneBased; +/// +/// let column = OneBased::from_zero_based(4); +/// assert_eq!(column.get(), 5); +/// assert_eq!(column.get_zero_based(), 4); +/// assert!(OneBased::new(0).is_none()); +/// ``` /// -/// Coordinates and terminal dimensions are one-based in Termina on both Unix and Windows. +/// # Implementation Notes +/// +/// This reimplements the coordinate helper from [termwiz escape helpers] on top of +/// [`NonZeroU16`]. +/// +/// [termwiz escape helpers]: https://docs.rs/termwiz/latest/termwiz/escape/index.html #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// CREDIT: . -// This can be seen as a reimplementation on top of NonZeroU16. pub struct OneBased(NonZeroU16); impl OneBased { + /// Creates a one-based value from an already one-based integer. + /// + /// Returns `None` for zero because zero is not a valid terminal row, column, or dimension in + /// the escape sequences modeled by Termina. pub const fn new(n: u16) -> Option { match NonZeroU16::new(n) { Some(n) => Some(Self(n)), @@ -33,15 +107,21 @@ impl OneBased { } } + /// Converts a zero-based application index into a one-based terminal value. + /// + /// This panics when `n` is [`u16::MAX`], because adding one would overflow the stored + /// [`NonZeroU16`]. pub const fn from_zero_based(n: u16) -> Self { assert!(n < u16::MAX); Self(unsafe { NonZeroU16::new_unchecked(n + 1) }) } + /// Returns the stored one-based value. pub const fn get(self) -> u16 { self.0.get() } + /// Converts the stored terminal value back to a zero-based application index. pub const fn get_zero_based(self) -> u16 { self.get() - 1 } @@ -65,20 +145,25 @@ impl From for OneBased { } } -/// The dimensions of a terminal screen. +/// The dimensions of a terminal window. /// -/// For both Unix and Windows, Termina returns the rows and columns. -/// Pixel width and height are not supported on Windows. +/// `cols` and `rows` describe the terminal window in character cells, which is the size used by +/// cursor positioning and layout code. Pixel dimensions are available when the platform reports +/// them. On Unix, Termina reads those optional pixel fields from the `TIOCGWINSZ` window-size +/// query when the terminal fills them in. Windows currently reports `None` for both pixel fields. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WindowSize { - /// The width - the number of columns. + /// The width in terminal cells. #[doc(alias = "width")] pub cols: u16, - /// The height - the number of rows. + + /// The height in terminal cells. #[doc(alias = "height")] pub rows: u16, - /// The height of the window in pixels. + + /// The width of the window in pixels, if the platform reports it. pub pixel_width: Option, - /// The width of the window in pixels. + + /// The height of the window in pixels, if the platform reports it. pub pixel_height: Option, } diff --git a/src/parse.rs b/src/parse.rs index cfa7921..5f36b20 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,14 +1,23 @@ -// CREDIT: This is nearly all crossterm (with modifications and additions). -// -// See a below credit comment about the `decode_input_records` function however. -// I have extended the parsing functions from -// -// Crossterm comments say that the parser is a bit scary and probably in need of a refactor. I -// like this approach though since it's quite easy to read and test. I'm unsure of the performance -// though because of the loop in `process_bytes`: we consider the bytes as an increasing slice of -// the buffer until it becomes valid or invalid. WezTerm and Alacritty have more formal parsers -// (`vtparse` and `vte`, respectively) but I'm unsure of using a terminal program's parser since -// it may be larger or more complex than an application needs. +//! Streaming parser for terminal input bytes. +//! +//! [`Parser`] turns bytes from a terminal or PTY into [`Event`] values. It accepts partial input: +//! callers append bytes with [`Parser::parse`], then drain completed events with [`Parser::pop`]. +//! This is the same parser used behind [`EventReader`], exposed for code that owns its input source +//! directly. +//! +//! # Implementation Notes +//! +//! The parser is adapted from [crossterm's Unix event parser], with additions for Termina-specific +//! escape sequences and Windows input modes. Crossterm comments call this style of parser a bit +//! scary and probably in need of a refactor. I like the approach though, because it is quite easy +//! to read and test. The main uncertainty is performance: `process_bytes` considers the bytes as an +//! increasing slice of the buffer until that slice becomes valid or invalid. WezTerm and Alacritty +//! use more formal parsers, [`vtparse`] and [`vte`] respectively, but those terminal-emulator +//! parsers may be larger or more complex than an application needs. +//! +//! [crossterm's Unix event parser]: https://docs.rs/crossterm/latest/crossterm/event/index.html +//! [`vtparse`]: https://docs.rs/vtparse/latest/vtparse/ +//! [`vte`]: https://docs.rs/vte/latest/vte/ #[cfg(windows)] pub mod windows; @@ -20,6 +29,9 @@ use windows::InputReaderMode; use std::{collections::VecDeque, num::NonZeroU16, str}; +#[cfg(doc)] +use crate::EventReader; + use crate::{ escape::{ self, @@ -33,11 +45,25 @@ use crate::{ style, Event, }; -/// A parser for ANSI escape sequences. +/// An incremental parser for terminal input. +/// +/// The parser keeps incomplete escape sequences in an internal buffer. Pass `maybe_more = true` +/// when the current byte slice may end in the middle of a sequence. Completed events are queued +/// until [`Self::pop`] removes them. +/// +/// # Examples +/// +/// ``` +/// use termina::{Event, Parser}; +/// +/// let mut parser = Parser::default(); +/// parser.parse(b"\x1b[5~", false); +/// assert!(matches!(parser.pop(), Some(Event::Key(_)))); +/// ``` #[derive(Debug)] pub struct Parser { buffer: Vec, - /// Events which have been parsed. Pop out with `Self::pop`. + /// Events which have been parsed. Pop out with [`Self::pop`]. events: VecDeque, #[cfg(windows)] mode: InputReaderMode, @@ -73,15 +99,16 @@ impl Parser { } } - /// Reads and removes a parsed event from the parser. + /// Removes and returns the oldest completed event. pub fn pop(&mut self) -> Option { self.events.pop_front() } - /// Parses additional data into the buffer. - /// Parsed events can be retrieved using [Parser::pop]. + /// Adds bytes to the parser and queues any completed events. /// - /// `maybe_more` should be set to true if the input might be a partial sequence. + /// Set `maybe_more` to `true` when the input source may provide more bytes for the same + /// escape sequence later. Set it to `false` when the buffer should be treated as complete for + /// now; malformed or incomplete sequences can then be discarded instead of held indefinitely. pub fn parse(&mut self, bytes: &[u8], maybe_more: bool) { self.buffer.extend_from_slice(bytes); self.process_bytes(maybe_more); diff --git a/src/parse/windows.rs b/src/parse/windows.rs index 1fa5f43..129e00b 100644 --- a/src/parse/windows.rs +++ b/src/parse/windows.rs @@ -15,18 +15,28 @@ use windows_sys::Win32::System::Console; #[cfg(feature = "windows-legacy")] pub use legacy::cursor_position; -/// Mode to use for reading input events. -/// See the [crate-level info](https://crates.io/crates/termina) for details on the differences between the modes. -/// -/// - [InputReaderMode::Vte] will enable reading input events using ANSI escape sequences. -/// - [InputReaderMode::Legacy] will enable reading input events using legacy console events. -/// -/// The `windows-legacy` feature must be enabled to use the legacy input reader. -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum InputReaderMode { - Vte, - Legacy, -} +/// Mode to use for reading Windows input events. +/// +/// VTE mode asks the Windows console to emit virtual-terminal input and then parses those bytes +/// with [`crate::Parser`]. Legacy mode reads `INPUT_RECORD` values from the classic console API and +/// translates them directly into [`crate::Event`] values. +/// +/// [`crate::PlatformTerminal`] uses [`Self::Vte`] by default. The `windows-legacy` feature must be +/// enabled to construct a terminal with a custom input reader mode. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum InputReaderMode { + /// Read input as virtual-terminal escape sequences. + /// + /// This is the default mode. It matches Unix terminal input more closely and supports terminal + /// protocol responses that arrive as escape sequences. + Vte, + + /// Read input through the classic Windows console API. + /// + /// This mode is available only with the `windows-legacy` feature. It can be useful in console + /// environments where virtual-terminal input is unavailable or unreliable. + Legacy, +} impl Parser { pub(crate) fn decode_input_records(&mut self, records: &[Console::INPUT_RECORD]) { @@ -181,7 +191,8 @@ pub(crate) mod legacy { /// RightmostButtonPressed = 0x0002, /// /// This button state is not recognized. /// Unknown = 0x0021, - /// /// The wheel was rotated backward, toward the user; this will only be activated for `MOUSE_WHEELED ` from `dwEventFlags` + /// /// The wheel was rotated backward, toward the user. + /// /// This is active only for `MOUSE_WHEELED` from `dwEventFlags`. /// Negative = 0x0020, /// # } /// ``` diff --git a/src/style.rs b/src/style.rs index 4f0ca94..56c1928 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,8 +1,28 @@ //! Types for styling terminal cells. - -// CREDIT: This is shared almost fairly between crossterm and termwiz. SGR properties like -// `Underline`, `CursorStyle` and `Intensity` are from termwiz. The `StyleExt` trait is similar -// to a crossterm trait. +//! +//! Terminal styling is controlled by [`Sgr`] commands, the `CSI ... m` escape sequences that set +//! foreground color, background color, intensity, underline, and related text attributes. This +//! module provides those low-level SGR attribute types and a small [`StyleExt`] convenience trait +//! for formatting styled text. +//! +//! # Examples +//! +//! ``` +//! use termina::style::StyleExt as _; +//! +//! # termina::style::Stylized::force_ansi_color(true); +//! let warning = "warning".red().bold(); +//! assert_eq!(warning.to_string(), "\x1b[0;31;1mwarning\x1b[m"); +//! ``` +//! +//! # Implementation Notes +//! +//! Styling support is shared almost fairly between crossterm and TermWiz: SGR property types like +//! [`Underline`], [`CursorStyle`], and [`Intensity`] are adapted from [termwiz styling], while +//! [`StyleExt`] follows the shape of [crossterm styling] helpers. +//! +//! [termwiz styling]: https://docs.rs/termwiz/latest/termwiz/ +//! [crossterm styling]: https://docs.rs/crossterm/latest/crossterm/style/index.html use std::{ borrow::Cow, @@ -16,34 +36,76 @@ use crate::escape::{ csi::{Csi, Sgr}, }; -/// Styling of a cell's underline. +/// Styling of a cell's underline according to the [Kitty underline extension]. +/// +/// Single and double underlines are widely understood SGR attributes. Curly, dotted, dashed, and +/// colored underlines are extensions and depend on terminal support. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::Underline, +/// }; +/// +/// assert_eq!(Csi::Sgr(Sgr::Underline(Underline::Curly)).to_string(), "\x1b[4:3m"); +/// ``` +/// +/// [kitty underline extension]: https://sw.kovidgoyal.net/kitty/underlines/ #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -// pub enum Underline { /// No underline #[default] None = 0, + /// Straight underline Single = 1, + /// Two underlines stacked on top of one another Double = 2, + /// Curly / "squiggly" / "wavy" underline Curly = 3, + /// Dotted underline Dotted = 4, + /// Dashed underline Dashed = 5, } +/// Cursor shape values for [DECSCUSR]. +/// +/// DECSCUSR is the DEC-style cursor shape setting used by many modern terminals. The numeric +/// values select the terminal default, block, underline, or vertical-bar cursor, with blinking and +/// steady variants where the protocol defines both. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Cursor}, +/// style::CursorStyle, +/// }; +/// +/// let cursor = Csi::Cursor(Cursor::CursorStyle(CursorStyle::SteadyBar)); +/// assert_eq!(cursor.to_string(), "\x1b[6 q"); +/// ``` +/// +/// [DECSCUSR]: https://vt100.net/docs/vt510-rm/DECSCUSR.html #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum CursorStyle { + /// DECSCUSR value 0: use the terminal's configured cursor style. #[default] Default = 0, + /// DECSCUSR value 1: a blinking block cursor. BlinkingBlock = 1, + /// DECSCUSR value 2: a steady block cursor. SteadyBlock = 2, + /// DECSCUSR value 3: a blinking underline cursor. BlinkingUnderline = 3, + /// DECSCUSR value 4: a steady underline cursor. SteadyUnderline = 4, + /// DECSCUSR value 5: a blinking vertical bar cursor. BlinkingBar = 5, + /// DECSCUSR value 6: a steady vertical bar cursor. SteadyBar = 6, } @@ -55,29 +117,65 @@ impl Display for CursorStyle { /// An 8-bit "256-color". /// -/// Colors 0-15 are the same as `AnsiColor`s (0-7 being normal colors and 8-15 being "bright"). -/// Colors 16-231 make up a 6x6x6 "color cube." The remaining 232-255 colors define a -/// dark-to-light grayscale in 24 steps. +/// Colors 0-15 are the same as [`AnsiColor`] values (0-7 being normal colors and 8-15 being +/// "bright"). +/// Colors 16-231 make up a 6x6x6 "color cube"; 232-255 define a dark-to-light grayscale. /// /// These are also known as "web-safe colors" or "X11 colors" historically, although the actual -/// colors varied somewhat between historical usages. +/// colors varied somewhat between historical usages; see the [ANSI 8-bit color] table. +/// +/// Convert a `WebColor` into [`ColorSpec`] when building SGR colors: +/// +/// ``` +/// use termina::style::{ColorSpec, WebColor}; +/// +/// let orange: ColorSpec = WebColor(208).into(); +/// assert_eq!(orange, ColorSpec::PaletteIndex(208)); +/// ``` +/// +/// [ANSI 8-bit color]: https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// pub struct WebColor(pub u8); +/// Red, green, and blue color with 8-bit channels. +/// +/// Use [`Self::new`] for byte channels, [`Self::new_f32`] for normalized floating-point channels, +/// or [`str::parse`] for xterm-style color strings: +/// +/// ``` +/// use termina::style::{ColorSpec, RgbColor}; +/// +/// let from_bytes = RgbColor::new(40, 80, 120); +/// let from_floats = RgbColor::new_f32(0.5, 0.0, 1.0); +/// let from_hex: RgbColor = "#285078".parse().unwrap(); +/// +/// assert_eq!(from_bytes, from_hex); +/// assert_eq!(from_floats, RgbColor::new(127, 0, 255)); +/// +/// let color_spec: ColorSpec = from_bytes.into(); +/// assert!(matches!(color_spec, ColorSpec::TrueColor(_))); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RgbColor { + /// Red channel. pub red: u8, + /// Green channel. pub green: u8, + /// Blue channel. pub blue: u8, } impl RgbColor { + /// Creates a new RGB color from 8-bit channel values. pub const fn new(red: u8, green: u8, blue: u8) -> Self { Self { red, green, blue } } - /// The floats are expected to be in the range `0.0..=1.0`. + /// Creates a new RGB color from floating-point channel values. + /// + /// Values are multiplied by 255 and cast to `u8`. Rust float-to-integer casts round toward + /// zero, so fractional values lose their fractional part after scaling. `NaN` and values below + /// `0.0` become `0`; values above `1.0` and positive infinity become `255`. pub fn new_f32(red: f32, green: f32, blue: f32) -> Self { let red = (red * 255.) as u8; let green = (green * 255.) as u8; @@ -101,6 +199,7 @@ impl RgbColor { } } +/// Error returned when parsing a red, green, and blue color string fails. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidFormatError; @@ -158,10 +257,27 @@ impl FromStr for RgbColor { } } +/// Red, green, blue, and alpha color with 8-bit channels. +/// +/// Alpha defaults to fully opaque when converting from [`RgbColor`]. Converting back to +/// [`RgbColor`] drops the alpha channel because standard terminal foreground/background colors are +/// RGB values. +/// +/// ``` +/// use termina::style::{RgbColor, RgbaColor}; +/// +/// let rgb = RgbColor::new(10, 20, 30); +/// let rgba = RgbaColor::from(rgb); +/// assert_eq!(rgba.alpha, 255); +/// assert_eq!(RgbColor::from(rgba), rgb); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RgbaColor { + /// Red channel. pub red: u8, + /// Green channel. pub green: u8, + /// Blue channel. pub blue: u8, /// Also known as "opacity" pub alpha: u8, @@ -188,53 +304,142 @@ impl From for RgbaColor { } } +/// Named ANSI colors used by standard 16-color palettes. +/// +/// The numeric SGR assignments are stable, but names are not fully consistent across terminal +/// libraries. In particular, SGR 37 may be called white, gray, or silver, while SGR 97 may be +/// called bright white, light white, or white. Termina names the base 37 color [`Self::White`] and +/// the bright 97 color [`Self::BrightWhite`]. Ratatui documents the same naming ambiguity in its +/// [ANSI color table]. +/// +/// `AnsiColor` converts to [`ColorSpec`] through the indexed palette path: +/// +/// ``` +/// use termina::style::{AnsiColor, ColorSpec}; +/// +/// let red: ColorSpec = AnsiColor::Red.into(); +/// assert_eq!(red, ColorSpec::RED); +/// ``` +/// +/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors +/// [Ratatui color docs]: https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// pub enum AnsiColor { + /// The standard black palette entry. Black = 0, + /// The standard red palette entry. Red, + /// The standard green palette entry. Green, + /// The standard yellow palette entry. Yellow, + /// The standard blue palette entry. Blue, + /// The standard magenta palette entry. Magenta, + /// The standard cyan palette entry. Cyan, + /// The standard white palette entry. + /// + /// This is SGR 37. Some terminal libraries call this gray or silver and reserve white for the + /// bright SGR 97 color. White, - /// "Bright" black (also known as "Gray") + /// Bright black. + /// + /// This is SGR 90. Some terminal libraries call this dark gray, gray, light black, or bright + /// black. BrightBlack, + /// The bright red palette entry. BrightRed, + /// The bright green palette entry. BrightGreen, + /// The bright yellow palette entry. BrightYellow, + /// The bright blue palette entry. BrightBlue, + /// The bright magenta palette entry. BrightMagenta, + /// The bright cyan palette entry. BrightCyan, + /// The bright white palette entry. + /// + /// This is SGR 97. Some terminal libraries call this white, bright white, or light white. BrightWhite, } +/// Index into the terminal's 256-color palette. pub type PaletteIndex = u8; +/// Terminal color specification for [`Sgr`] color commands. +/// +/// This is the common color input type for foreground, background, and underline colors. Named +/// ANSI colors and 256-color palette values use [`Self::PaletteIndex`]; true-color values use +/// [`Self::TrueColor`]; [`Self::Reset`] returns the terminal color to its default. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::{ColorSpec, RgbColor}, +/// }; +/// +/// assert_eq!(Csi::Sgr(Sgr::Foreground(ColorSpec::RED)).to_string(), "\x1b[31m"); +/// +/// let blue = ColorSpec::from(RgbColor::new(0, 0, 255)); +/// assert_eq!(Csi::Sgr(Sgr::Foreground(blue)).to_string(), "\x1b[38;2;0;0;255m"); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColorSpec { + /// Reset the color back to the terminal default. + /// + /// For SGR foreground and background colors this formats as the standard reset color codes + /// rather than as an indexed palette color. Reset, + + /// Use an indexed palette color. + /// + /// Values 0-15 address the standard ANSI palette. Values 16-255 address the 256-color + /// extension palette described by [`WebColor`]. PaletteIndex(PaletteIndex), + + /// Use a true-color red, green, blue, and alpha value. + /// + /// Red, green, and blue values format using SGR true-color parameters. Non-opaque alpha uses + /// the colon form from ITU T.416-style color notation. TrueColor(RgbaColor), } impl ColorSpec { + /// Standard black palette color. pub const BLACK: Self = Self::PaletteIndex(AnsiColor::Black as PaletteIndex); + /// Standard red palette color. pub const RED: Self = Self::PaletteIndex(AnsiColor::Red as PaletteIndex); + /// Standard green palette color. pub const GREEN: Self = Self::PaletteIndex(AnsiColor::Green as PaletteIndex); + /// Standard yellow palette color. pub const YELLOW: Self = Self::PaletteIndex(AnsiColor::Yellow as PaletteIndex); + /// Standard blue palette color. pub const BLUE: Self = Self::PaletteIndex(AnsiColor::Blue as PaletteIndex); + /// Standard magenta palette color. pub const MAGENTA: Self = Self::PaletteIndex(AnsiColor::Magenta as PaletteIndex); + /// Standard cyan palette color. pub const CYAN: Self = Self::PaletteIndex(AnsiColor::Cyan as PaletteIndex); + /// Standard white palette color. pub const WHITE: Self = Self::PaletteIndex(AnsiColor::White as PaletteIndex); + /// Bright black palette color. pub const BRIGHT_BLACK: Self = Self::PaletteIndex(AnsiColor::BrightBlack as PaletteIndex); + /// Bright red palette color. pub const BRIGHT_RED: Self = Self::PaletteIndex(AnsiColor::BrightRed as PaletteIndex); + /// Bright green palette color. pub const BRIGHT_GREEN: Self = Self::PaletteIndex(AnsiColor::BrightGreen as PaletteIndex); + /// Bright yellow palette color. pub const BRIGHT_YELLOW: Self = Self::PaletteIndex(AnsiColor::BrightYellow as PaletteIndex); + /// Bright blue palette color. pub const BRIGHT_BLUE: Self = Self::PaletteIndex(AnsiColor::BrightBlue as PaletteIndex); + /// Bright magenta palette color. pub const BRIGHT_MAGENTA: Self = Self::PaletteIndex(AnsiColor::BrightMagenta as PaletteIndex); + /// Bright cyan palette color. pub const BRIGHT_CYAN: Self = Self::PaletteIndex(AnsiColor::BrightCyan as PaletteIndex); + /// Bright white palette color. pub const BRIGHT_WHITE: Self = Self::PaletteIndex(AnsiColor::BrightWhite as PaletteIndex); } @@ -262,52 +467,128 @@ impl From for ColorSpec { } } +/// Text intensity for [`Sgr`]. +/// +/// Use this directly with [`Sgr::Intensity`] when building escape sequences, or through +/// [`StyleExt::bold`] for simple styled strings. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::{Intensity, StyleExt as _}, +/// }; +/// +/// assert_eq!(Csi::Sgr(Sgr::Intensity(Intensity::Bold)).to_string(), "\x1b[1m"); +/// +/// # termina::style::Stylized::force_ansi_color(true); +/// assert_eq!("warn".bold().to_string(), "\x1b[0;1mwarn\x1b[m"); +/// ``` #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Intensity { + /// SGR 22: normal text intensity. #[default] Normal, + /// SGR 1: bold text intensity. Bold, + /// SGR 2: dim text intensity. Dim, } +/// Text blink mode for [`Sgr`]. +/// +/// Blink support is terminal-dependent. Some terminals ignore blink entirely or let users disable +/// it in their settings. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::Blink, +/// }; +/// +/// assert_eq!(Csi::Sgr(Sgr::Blink(Blink::Slow)).to_string(), "\x1b[5m"); +/// ``` #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Blink { + /// SGR 25: disable blinking text. #[default] None, + /// SGR 5: slow blinking text. Slow, + /// SGR 6: rapid blinking text. Rapid, } +/// Font selection for SGR parameters 10-19. +/// +/// Alternate fonts are terminal-dependent and uncommon in modern terminal applications. The +/// variant value maps to SGR 11 through SGR 19; `Font::Alternate(1)` emits SGR 11. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::Font, +/// }; +/// +/// assert_eq!(Csi::Sgr(Sgr::Font(Font::Alternate(1))).to_string(), "\x1b[11m"); +/// ``` #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Font { + /// SGR 10: use the default font. #[default] Default, - /// An alternate font. Valid values are 1-9. + + /// SGR 11-19: select an alternate font. + /// + /// Valid values are 1-9, corresponding to SGR 11 through SGR 19. Alternate(u8), } +/// Vertical alignment for [`Sgr`]. +/// +/// Superscript and subscript support is terminal-dependent. +/// +/// ``` +/// use termina::{ +/// escape::csi::{Csi, Sgr}, +/// style::VerticalAlign, +/// }; +/// +/// let superscript = Csi::Sgr(Sgr::VerticalAlign(VerticalAlign::SuperScript)); +/// assert_eq!(superscript.to_string(), "\x1b[73m"); +/// ``` #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum VerticalAlign { + /// SGR 75: baseline text alignment. #[default] BaseLine = 0, + /// SGR 73: superscript text alignment. SuperScript = 1, + /// SGR 74: subscript text alignment. SubScript = 2, } -/// A helper type for conveniently rendering styled content to the terminal. +/// Styled text that renders by surrounding content with SGR escape sequences. /// -/// This is meant to be used instead of `PlatformTerminal` and proper CSI SGR codes when printing -/// basic text, for example a CLI's help string. +/// Use this for simple styled strings, for example a CLI help string. Code that already writes +/// structured terminal output can use [`crate::escape::csi::Sgr`] directly instead. /// -/// Instead of using this type directly, `use` the `StyleExt` trait and the helper functions +/// Instead of using this type directly, `use` the [`StyleExt`] trait and the helper functions /// attached to strings: /// /// ``` /// use termina::style::StyleExt as _; -/// assert_eq!("green".green().to_string(), "\x1b[0;32mgreen\x1b[m"); +/// +/// # termina::style::Stylized::force_ansi_color(true); +/// assert_eq!( +/// "warning".red().bold().underlined().to_string(), +/// "\x1b[0;31;1;4mwarning\x1b[m", +/// ); /// ``` +/// +/// [`PlatformTerminal`]: crate::PlatformTerminal #[derive(Debug, Clone, PartialEq, Eq)] pub struct Stylized<'a> { + /// The text rendered between the opening SGR sequence and reset sequence. pub content: Cow<'a, str>, styles: Vec, } @@ -316,13 +597,15 @@ static INITIALIZER: parking_lot::Once = parking_lot::Once::new(); static NO_COLOR: AtomicBool = AtomicBool::new(false); impl Stylized<'_> { - /// Checks whether ANSI color sequences where turned off in the environment. + /// Checks whether ANSI color sequences were turned off in the environment. + /// + /// This follows the guidance on [no-color.org][no-color]: if the `NO_COLOR` environment + /// variable is present and non-empty, color escape sequences will be omitted when rendering + /// this struct. This behavior can be overridden with [Self::force_ansi_color]. /// - /// See : if the `NO_COLOR` environment variable is present and - /// non-empty, color escape sequences will be omitted when rendering this struct. This - /// behavior can be overridden with [Self::force_ansi_color]. + /// [no-color]: https://no-color.org/ pub fn is_ansi_color_disabled() -> bool { - // + // Guidance on disabling colors comes from the no-color.org recommendations. INITIALIZER.call_once(|| { NO_COLOR.store( std::env::var("NO_COLOR").is_ok_and(|e| !e.is_empty()), @@ -371,28 +654,52 @@ impl Display for Stylized<'_> { } } +/// Convenience methods for building [`Stylized`] text. +/// +/// Methods that accept `impl Into` work with named ANSI colors, 256-color palette +/// values, and true-color RGB values: +/// +/// ``` +/// use termina::style::{AnsiColor, RgbColor, StyleExt as _, Stylized, WebColor}; +/// +/// Stylized::force_ansi_color(true); +/// +/// assert_eq!("red".foreground(AnsiColor::Red).to_string(), "\x1b[0;31mred\x1b[m"); +/// assert_eq!("orange".foreground(WebColor(208)).to_string(), "\x1b[0;38;5;208morange\x1b[m"); +/// assert_eq!( +/// "blue".foreground(RgbColor::new(0, 0, 255)).bold().to_string(), +/// "\x1b[0;38;2;0;0;255;1mblue\x1b[m", +/// ); +/// ``` pub trait StyleExt<'a>: Sized { + /// Wraps this value in [`Stylized`] without adding styles. fn stylized(self) -> Stylized<'a>; + /// Adds a foreground color. fn foreground(self, color: impl Into) -> Stylized<'a> { let mut this = self.stylized(); this.styles.push(Sgr::Foreground(color.into())); this } + /// Adds the standard red foreground color. fn red(self) -> Stylized<'a> { self.foreground(ColorSpec::RED) } + /// Adds the standard yellow foreground color. fn yellow(self) -> Stylized<'a> { self.foreground(ColorSpec::YELLOW) } + /// Adds the standard green foreground color. fn green(self) -> Stylized<'a> { self.foreground(ColorSpec::GREEN) } + /// Adds a single underline. fn underlined(self) -> Stylized<'a> { let mut this = self.stylized(); this.styles.push(Sgr::Underline(Underline::Single)); this } + /// Adds bold intensity. fn bold(self) -> Stylized<'a> { let mut this = self.stylized(); this.styles.push(Sgr::Intensity(Intensity::Bold)); diff --git a/src/terminal.rs b/src/terminal.rs index a090d3f..4fb8cd7 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,3 +1,40 @@ +//! Terminal handles and platform-neutral terminal operations. +//! +//! [`PlatformTerminal`] opens the process terminal for the current target. It implements +//! [`Terminal`], which combines byte output, raw/cooked mode switching, terminal dimensions, +//! synchronous event reads, polling, and panic-hook cleanup. Termina does not enter alternate +//! screen with [`DecPrivateModeCode::ClearAndEnableAlternateScreen`], enable +//! [`DecPrivateModeCode::BracketedPaste`], or enable mouse tracking modes such as +//! [`DecPrivateModeCode::MouseTracking`] for you. Those are protocol choices the application +//! writes explicitly with [`crate::escape`]. +//! +//! # Examples +//! +//! ```no_run +//! use std::{io, time::Duration}; +//! +//! use termina::{ +//! event::{Event, KeyEventKind}, +//! PlatformTerminal, Terminal, +//! }; +//! +//! fn main() -> io::Result<()> { +//! let mut terminal = PlatformTerminal::new()?; +//! terminal.enter_raw_mode()?; +//! +//! if terminal.poll(|_| true, Some(Duration::from_millis(250)))? { +//! let event = terminal.read(|event| { +//! matches!(event, Event::Key(key) if key.kind == KeyEventKind::Press) +//! })?; +//! if let Event::Key(key) = event { +//! println!("received {:?}", key.code); +//! } +//! } +//! +//! terminal.enter_cooked_mode() +//! } +//! ``` + #[cfg(unix)] mod unix; @@ -14,59 +51,90 @@ pub use windows::*; use crate::{Event, EventReader, WindowSize}; -/// An alias to the terminal available for the current platform. +#[cfg(doc)] +use crate::escape::csi::{DecPrivateModeCode, Keyboard}; + +/// The terminal implementation for the current platform. /// -/// On Windows this uses the `WindowsTerminal`, otherwise `UnixTerminal`. +/// On Unix this aliases `UnixTerminal`. On Windows this aliases `WindowsTerminal`. #[cfg(unix)] pub type PlatformTerminal = UnixTerminal; #[cfg(windows)] pub type PlatformTerminal = WindowsTerminal; +/// The output handle type passed to panic hooks on the current platform. +/// +/// The hook receives this lower-level handle instead of `PlatformTerminal` so cleanup code can +/// write terminal reset sequences even while the higher-level terminal value is unwinding. #[cfg(unix)] pub type PlatformHandle = FileDescriptor; #[cfg(windows)] pub type PlatformHandle = OutputHandle; -// CREDIT: This is heavily based on termwiz. -// -// This trait is simpler, however, and the terminals themselves do not have drop glue or try -// to enable features like bracketed paste: that is left to dependents of `termina`. The `poll` -// and `read` functions mirror . -// Also see `src/event/reader.rs`. - +/// Platform-agnostic terminal I/O surface. +/// +/// The trait is implemented by the Unix and Windows backends and also requires [`io::Write`], so a +/// terminal value is both an output sink and an input/event source. `enter_raw_mode` and +/// `enter_cooked_mode` only manage the platform terminal mode. Application-level terminal features +/// such as alternate screen, bracketed paste, focus tracking, mouse tracking, and keyboard +/// protocol flags are CSI/OSC writes and remain the caller's responsibility. See +/// [`DecPrivateModeCode::ClearAndEnableAlternateScreen`], +/// [`DecPrivateModeCode::BracketedPaste`], [`DecPrivateModeCode::FocusTracking`], +/// [`DecPrivateModeCode::MouseTracking`], and [`Keyboard`] for the typed escape values. +/// +/// `poll` and `read` use filters because the terminal stream may include responses that the caller +/// is not currently waiting for. Rejected events stay buffered in [`EventReader`] so later reads +/// can still observe them. +/// +/// # Implementation Notes +/// +/// This trait is based on [termwiz's terminal API], but Termina keeps feature setup outside the +/// terminal type and mirrors crossterm's synchronous event-reader shape for `poll` and `read`. +/// +/// [termwiz's terminal API]: https://docs.rs/termwiz/latest/termwiz/terminal/index.html pub trait Terminal: io::Write { - /// Enters the "raw" terminal mode. + /// Enters raw mode for the platform terminal. /// - /// While in "raw" mode a terminal will not attempt to do any helpful interpretation of input - /// such as waiting for Enter key presses to pass input. This is essentially the opposite of - /// "cooked" mode. To exit raw mode, use `Self::enter_cooked_mode`. + /// Raw mode disables line buffering and other terminal-driver processing, so key presses and + /// escape sequences can reach the application without waiting for Enter. Use + /// [`Self::enter_cooked_mode`] before returning control to a normal shell. fn enter_raw_mode(&mut self) -> io::Result<()>; - /// Enters the "cooked" terminal mode. - /// - /// This is considered the normal mode for a terminal device. + + /// Enters cooked mode for the platform terminal. /// - /// While in "cooked" mode a terminal will interpret the incoming data in ways that are useful - /// such as waiting for an Enter key press to pass input to the application. + /// Cooked mode is the normal shell-facing mode for a terminal device. The terminal driver + /// handles echo, line editing, and Enter-delimited input before passing data to the + /// application. On Unix, this restores the termios state captured when the terminal was opened. + /// On Windows, this switches the console input flags back to cooked behavior, but leaves other + /// captured state, such as code pages and virtual-terminal flags, for drop-time cleanup. fn enter_cooked_mode(&mut self) -> io::Result<()>; + + /// Reads the current terminal window dimensions. fn get_dimensions(&self) -> io::Result; + + /// Returns a cloneable event reader backed by the terminal input handle. fn event_reader(&self) -> EventReader; - /// Checks if there is an `Event` available. + + /// Checks if there is an [`Event`] available. /// - /// Returns `Ok(true)` if an `Event` is available or `Ok(false)` if one is not available. - /// If `timeout` is `None` then `poll` will block indefinitely. + /// Returns `Ok(true)` if an [`Event`] is available or `Ok(false)` if one is not available. + /// If `timeout` is `None`, this blocks until a matching event is available. fn poll bool>(&self, filter: F, timeout: Option) -> io::Result; - /// Reads a single `Event` from the terminal. + + /// Reads a single [`Event`] from the terminal. /// - /// This function blocks until an `Event` is available. Use `poll` first to guarantee that the - /// read won't block. + /// This function blocks until an [`Event`] is available. Use [`Self::poll`] first to guarantee + /// that the read won't block. fn read bool>(&self, filter: F) -> io::Result; - /// Sets a hook function to run. + /// Installs a panic hook that can write terminal cleanup sequences. + /// + /// Depending on how your application handles panics, you may want to eagerly reset + /// application-level terminal state before the process exits. Use this hook for cleanup such as + /// disabling [`DecPrivateModeCode::BracketedPaste`] or returning from + /// [`DecPrivateModeCode::ClearAndEnableAlternateScreen`] to the main screen. /// - /// Depending on how your application handles panics you may wish to set a panic hook which - /// eagerly resets the terminal (such as by disabling bracketed paste and entering the main - /// screen). The parameter for this hook is a platform handle to `std::io::stdout` or - /// equivalent which implements `std::io::Write`. When the hook function is finished running - /// the handle's modes will be reset (same as `enter_cooked_mode`). + /// The hook receives a [`PlatformHandle`] for stdout or the platform console output. After the + /// hook runs, Termina restores the platform mode as if [`Self::enter_cooked_mode`] had run. fn set_panic_hook(&mut self, f: impl Fn(&mut PlatformHandle) + Send + Sync + 'static); } diff --git a/src/terminal/unix.rs b/src/terminal/unix.rs index a777b14..44cc0f9 100644 --- a/src/terminal/unix.rs +++ b/src/terminal/unix.rs @@ -17,7 +17,10 @@ const BUF_SIZE: usize = 4096; #[derive(Debug)] pub enum FileDescriptor { + /// A file descriptor owned by Termina. Owned(OwnedFd), + + /// A borrowed process-global file descriptor such as stdin or stdout. Borrowed(BorrowedFd<'static>), } @@ -31,7 +34,10 @@ impl AsFd for FileDescriptor { } impl FileDescriptor { + /// The process stdin file descriptor. pub const STDIN: Self = Self::Borrowed(rustix::stdio::stdin()); + + /// The process stdout file descriptor. pub const STDOUT: Self = Self::Borrowed(rustix::stdio::stdout()); fn try_clone(&self) -> io::Result { @@ -99,15 +105,21 @@ impl From for WindowSize { } } -// CREDIT: -// Some discussion, though: Termwiz's terminals combine the terminal interaction (reading -// dimensions, reading events, writing bytes, etc.) all in one type. Crossterm splits these -// concerns and I prefer that interface. As such this type is very much based on Termwiz' -// `UnixTerminal` but the responsibilities are split between this file and -// `src/event/source/unix.rs` - the latter being more inspired by crossterm. -// Ultimately this terminal doesn't look much like Termwiz' due to the use of `rustix` and -// differences in the trait and `Drop` behavior (see `super`'s CREDIT comment). - +/// Unix terminal handle. +/// +/// `UnixTerminal` writes to stdout or `/dev/tty`, reads events from stdin or `/dev/tty`, and +/// restores the captured termios state when dropped. +/// +/// # Implementation Notes +/// +/// This type is based on [termwiz's Unix terminal], but Termina splits responsibilities +/// differently. TermWiz combines terminal dimensions, event reading, byte output, and terminal +/// state in one terminal type. Termina keeps byte output and terminal mode management here, while +/// event-source details live in `src/event/source/unix.rs`, closer to crossterm's event-source +/// shape. The implementation also uses `rustix` and has different `Drop` behavior, so it no longer +/// looks much like the original TermWiz code. +/// +/// [termwiz's Unix terminal]: https://docs.rs/termwiz/latest/termwiz/terminal/index.html #[derive(Debug)] pub struct UnixTerminal { /// Shared wrapper around the reader (stdin or `/dev/tty`) @@ -120,6 +132,10 @@ pub struct UnixTerminal { } impl UnixTerminal { + /// Opens the Unix terminal for input and output. + /// + /// If stdin or stdout is not a terminal, Termina opens `/dev/tty` for that side. The original + /// termios state is captured so [`Terminal::enter_cooked_mode`] and `Drop` can restore it. pub fn new() -> io::Result { let (read, write) = open_pty()?; let source = UnixEventSource::new(read, write.try_clone()?)?; diff --git a/src/terminal/windows.rs b/src/terminal/windows.rs index e93b2ae..bbca3c1 100644 --- a/src/terminal/windows.rs +++ b/src/terminal/windows.rs @@ -51,7 +51,10 @@ const CP_UTF8: CodePageID = 65001; #[derive(Debug)] pub enum Handle { + /// A Windows handle owned by Termina. Owned(OwnedHandle), + + /// A borrowed process-global handle such as stdin or stdout. Borrowed(BorrowedHandle<'static>), } @@ -68,11 +71,13 @@ impl Handle { // NOTE: stdin/stdout satisfy the `'static` lifetime requirements of `BorrowedHandle`. // > must remain open for the duration of the returned `BorrowedHandle` // This is true since stdin/stdout have process-global lifetimes. + /// Returns a borrowed handle for process stdin. pub fn stdin() -> Self { let stdin = io::stdin().as_raw_handle(); Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdin) }) } + /// Returns a borrowed handle for process stdout. pub fn stdout() -> Self { let stdout = io::stdout().as_raw_handle(); Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdout) }) @@ -231,6 +236,10 @@ impl AsRawHandle for InputHandle { } } +/// Windows console output handle. +/// +/// `OutputHandle` writes bytes with `WriteFile` after `WindowsTerminal` enables virtual-terminal +/// output processing. Panic hooks receive this type so they can write cleanup escape sequences. #[derive(Debug)] pub struct OutputHandle { handle: Handle, @@ -356,6 +365,11 @@ fn open_file(path: &str) -> io::Result { // // Also, the legacy Console API is not implemented. +/// Windows terminal handle. +/// +/// `WindowsTerminal` opens `CONIN$` or stdin for input and `CONOUT$` or stdout for output, enables +/// virtual-terminal output processing, and captures console modes/code pages so they can be +/// restored on drop. #[derive(Debug)] pub struct WindowsTerminal { input: InputHandle, @@ -370,12 +384,18 @@ pub struct WindowsTerminal { } impl WindowsTerminal { - /// Creates a new terminal that reads using [VTE mode][InputReaderMode::Vte]. + /// Opens the Windows terminal in [VTE input mode][InputReaderMode::Vte]. + /// + /// This mode enables virtual-terminal input and sets the input/output code pages to UTF-8 + /// while the terminal is active. pub fn new() -> io::Result { Self::with_mode_internal(InputReaderMode::Vte) } - /// Creates a new terminal using the specified [InputReaderMode]. + /// Opens the Windows terminal using the specified [`InputReaderMode`]. + /// + /// This is available only with the `windows-legacy` feature because legacy mode needs the + /// classic console-event parser compiled in. #[cfg(feature = "windows-legacy")] pub fn with_mode(mode: InputReaderMode) -> io::Result { // This is only exposed publicly when `windows-legacy` is enabled