Skip to main content

uncurses/event/
mod.rs

1//! Terminal events and event-stream decoding.
2//!
3//! This module owns the core [`Event`] enum together with the
4//! internal decoder that parses raw terminal bytes into events, the
5//! platform-specific [`EventSource`] that drives the decoder from a
6//! tty, and the key/mouse types that events carry.
7//!
8//! ## The decode pipeline
9//!
10//! Input arrives as a raw byte stream. The source reads bytes (waking on a
11//! self-pipe so another thread can interrupt a blocking read), feeds them to
12//! the decoder, and hands back fully-formed [`Event`] values. Escape
13//! sequences may straddle reads, so the decoder buffers partial input and a
14//! short timeout disambiguates a lone `Esc` key from the start of a CSI/SS3
15//! sequence.
16//!
17//! ```text
18//!   tty input        EventSource              Decoder            caller
19//!   ─────────        ───────────              ───────            ──────
20//!   bytes  ───────▶  read + buffer  ───────▶  scan sequences ─▶  Event
21//!     │                   ▲                        │
22//!     │                   └── Esc-timeout ◀────────┘ (Esc key vs CSI/SS3?)
23//!     └── self-pipe wake ─┘  (interrupt a blocking read from another thread)
24//! ```
25//!
26//! Build an [`EventSource`] over a terminal's input half and read typed
27//! events in a loop. Keys parse from strings and compare by canonical
28//! chord, so matching a shortcut is plain equality.
29//!
30//! ```no_run
31//! use uncurses::event::{Event, EventSource, Key};
32//! use uncurses::terminal::Terminal;
33//!
34//! # fn main() -> std::io::Result<()> {
35//! let mut term = Terminal::stdio();
36//! term.make_raw()?;
37//! let mut events = EventSource::new(term.input())?;
38//!
39//! let quit: Key = "ctrl+c".parse().unwrap();
40//! loop {
41//!     match events.read()? {
42//!         Event::KeyPress(ref k) if *k == quit => break,
43//!         Event::KeyPress(k) => { let _ = k.code; }
44//!         Event::Resize(ws) => { let _ = (ws.col, ws.row); }
45//!         _ => {}
46//!     }
47//! }
48//! term.restore()
49//! # }
50//! ```
51//!
52//! ## Queries
53//!
54//! To ask the terminal a question (its background color, cell size,
55//! device attributes, and so on), write the request bytes from the
56//! [`ansi`](crate::ansi) module to the output and read the matching reply
57//! event back through the same source. The
58//! [`Screen`](crate::screen::Screen) facade wraps this in `request_*` methods
59//! whose replies surface as ordinary events, never swallowing the user's
60//! keystrokes in between.
61//!
62//! ## Async
63//!
64//! With the `async` feature, `EventStream` reads the same events through a
65//! [`futures_core::Stream`], so the loop becomes `while let Some(ev) =
66//! stream.next().await`.
67//!
68//! [`futures_core::Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
69
70pub(crate) mod decode;
71#[cfg(test)]
72mod decode_safety_tests;
73mod key;
74mod mouse;
75mod pending;
76pub(crate) mod poll;
77mod sigwinch;
78mod source;
79#[cfg(unix)]
80mod source_unix;
81#[cfg(windows)]
82mod source_windows;
83#[cfg(feature = "async")]
84mod stream;
85
86pub use decode::DecoderFlags;
87pub use key::{Key, KeyCode, KeyModifiers, ParseKeyError};
88pub use mouse::{Mouse, MouseButton, mouse_pixel_to_cell};
89pub use source::{DEFAULT_ESC_TIMEOUT, DEFAULT_PASTE_IDLE_TIMEOUT, EventSource, Input, Waker};
90#[cfg(feature = "async")]
91pub use stream::EventStream;
92
93use crate::ansi::mode::{Mode, ModeSetting};
94use crate::color::Color;
95use crate::terminal::Winsize;
96
97/// Which system clipboard selection an OSC 52 event refers to.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum ClipboardSelection {
100    /// `c` — system clipboard.
101    System,
102    /// `p` — primary (X11 PRIMARY) selection.
103    Primary,
104    /// Any other / unknown selection character.
105    Other(char),
106}
107
108/// Decoded modifyOtherKeys mode (`CSI > 4 ; n m`).
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum ModifyOtherKeysMode {
111    /// Disabled.
112    Disabled,
113    /// Mode 1 — only modify keys that don't otherwise have an xterm sequence.
114    Mode1,
115    /// Mode 2 — modify all keys.
116    Mode2,
117}
118
119impl ModifyOtherKeysMode {
120    /// Convert a report value into a modifyOtherKeys mode.
121    pub fn from_value(v: u8) -> Self {
122        match v {
123            1 => Self::Mode1,
124            2 => Self::Mode2,
125            _ => Self::Disabled,
126        }
127    }
128}
129
130/// Reported terminal color scheme (DEC mode 2031).
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132pub enum ColorScheme {
133    /// Dark mode (`CSI ? 997 ; 1 n`).
134    Dark,
135    /// Light mode (`CSI ? 997 ; 2 n`).
136    Light,
137}
138
139impl std::fmt::Display for ColorScheme {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.write_str(match self {
142            ColorScheme::Dark => "dark",
143            ColorScheme::Light => "light",
144        })
145    }
146}
147
148/// Reported terminal visibility (DEC mode 2033).
149///
150/// This is an advisory, deliberately conservative hint used to skip expensive
151/// rendering that nobody can see. [`Hidden`](Visibility::Hidden) is precise:
152/// the terminal has positive knowledge that the view is not observable.
153/// [`Visible`](Visibility::Visible) only means it *may* be observable.
154///
155/// Only `1` and `2` decode to a report; any other value is left as
156/// [`Event::UnknownCsi`]. Treat a terminal that reports nothing, or reports
157/// something unrecognized, as visible.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub enum Visibility {
160    /// Potentially visible (`CSI ? 999 ; 1 n`). The view may be observable.
161    ///
162    /// This does not promise that any cell is onscreen: the terminal reports
163    /// it whenever visibility is unknown or any view may be observed.
164    Visible,
165    /// Not visible (`CSI ? 999 ; 2 n`). The terminal knows the view is not
166    /// ordinarily observable, so expensive visual updates can be paused.
167    ///
168    /// Never assume this lasts for any minimum duration.
169    Hidden,
170}
171
172impl Visibility {
173    /// Whether the terminal view may be observable, so visual work is worth
174    /// doing. `true` for [`Visible`](Visibility::Visible).
175    pub fn is_visible(self) -> bool {
176        matches!(self, Visibility::Visible)
177    }
178}
179
180impl std::fmt::Display for Visibility {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.write_str(match self {
183            Visibility::Visible => "visible",
184            Visibility::Hidden => "hidden",
185        })
186    }
187}
188
189/// Accepted program status names from an OSC 7501 query reply.
190///
191/// The decoder supplies fixed defaults for absent fields and preserves
192/// explicit empty lists and names from protocol extensions.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ProgramStatusCapabilities {
195    /// Accepted state names. An absent field means `idle`, `working`,
196    /// `done`, `blocked`, and `error`. The list excludes `clear`.
197    pub states: Vec<String>,
198    /// Accepted kind names. An absent field means `permission`,
199    /// `question`, and `auth`.
200    pub kinds: Vec<String>,
201}
202
203/// A terminal event.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum Event {
206    // -- Input ---------------------------------------------------------------
207    /// Key was pressed.
208    KeyPress(Key),
209    /// Key auto-repeated (Kitty Keyboard Protocol).
210    KeyRepeat(Key),
211    /// Key was released (Kitty Keyboard Protocol).
212    KeyRelease(Key),
213    /// Mouse button was pressed.
214    MouseClick(Mouse),
215    /// Mouse button was released.
216    MouseRelease(Mouse),
217    /// Mouse wheel scrolled.
218    MouseWheel(Mouse),
219    /// Mouse moved (with or without a button held).
220    MouseMove(Mouse),
221
222    // -- Size / geometry -----------------------------------------------------
223    /// The terminal surface changed size. Emitted only for genuine
224    /// change notifications: kernel SIGWINCH (full `Winsize`),
225    /// `ReadConsoleInput` window-buffer-size events on Windows
226    /// (cells only), and in-band CSI 48 t reports under mode 2048
227    /// (full `Winsize`). Replies to explicit size queries are
228    /// delivered as `WindowCellSize` / `WindowPixelSize` /
229    /// `CellPixelSize` instead.
230    Resize(Winsize),
231    /// Reply to a `CSI 18 t` query — window size in cells.
232    WindowCellSize {
233        /// Width in terminal cells.
234        width: u16,
235        /// Height in terminal cells.
236        height: u16,
237    },
238    /// Reply to a `CSI 14 t` query — window size in pixels.
239    WindowPixelSize {
240        /// Width in pixels.
241        width: u16,
242        /// Height in pixels.
243        height: u16,
244    },
245    /// Reply to a `CSI 16 t` query — single-cell size in pixels.
246    CellPixelSize {
247        /// Cell width in pixels.
248        width: u16,
249        /// Cell height in pixels.
250        height: u16,
251    },
252
253    // -- Focus / paste -------------------------------------------------------
254    /// Focus gained.
255    FocusIn,
256    /// Focus lost.
257    FocusOut,
258    /// Bracketed paste started.
259    PasteStart,
260    /// Bracketed paste ended.
261    PasteEnd,
262    /// Streaming chunk of pasted bytes emitted between [`Event::PasteStart`]
263    /// and [`Event::PasteEnd`]. Pastes that exceed the source's read
264    /// buffer are split across multiple chunks; reassembly and any
265    /// text decoding are the caller's responsibility (terminals may
266    /// paste arbitrary binary content, not just valid UTF-8).
267    PasteChunk(Vec<u8>),
268
269    // -- Position / device attrs --------------------------------------------
270    /// Cursor position report (CPR or DECXCPR). Coordinates are zero-based (the
271    /// 1-based wire form is normalized when parsed).
272    ///
273    /// DECXCPR replies use `CSI ? row ; column R` or
274    /// `CSI ? row ; column ; page R`. The decoder retains the page number
275    /// and ignores additional parameters after it. These private replies
276    /// always produce a single cursor event, even at positions that make
277    /// standard CPR ambiguous with modified F3.
278    CursorPosition {
279        /// Zero-based cell coordinates.
280        position: crate::layout::Position,
281        /// One-based page number, or zero when absent.
282        page: u32,
283    },
284    /// Primary device attributes (DA1) — list of decoded numeric attributes.
285    PrimaryDeviceAttributes(Vec<Option<u32>>),
286    /// Secondary device attributes (DA2).
287    SecondaryDeviceAttributes(Vec<Option<u32>>),
288    /// Tertiary device attributes (DA3) — terminal ID string.
289    TertiaryDeviceAttributes(String),
290    /// Terminal name reply (XTVERSION). Carries the raw identifier string,
291    /// which typically combines a name and version (e.g. `"XTerm(380)"`).
292    TerminalName(String),
293
294    // -- Mode / capability reports ------------------------------------------
295    /// DECRPM / RM mode report.
296    ///
297    /// The `setting` distinguishes all five DECRPM states. A terminal can
298    /// report a mode as permanently set or permanently reset, meaning it
299    /// recognizes the mode but will not let the host toggle it. When deciding
300    /// whether a feature is usable, prefer
301    /// [`ModeSetting::is_available`](crate::ansi::mode::ModeSetting::is_available)
302    /// over [`is_recognized`](crate::ansi::mode::ModeSetting::is_recognized): a
303    /// permanently reset mode is recognized yet can never be enabled.
304    ModeReport {
305        /// Reported mode.
306        mode: Mode,
307        /// Current mode setting.
308        setting: ModeSetting,
309    },
310    /// modifyOtherKeys report.
311    ModifyOtherKeys(ModifyOtherKeysMode),
312    /// Kitty keyboard protocol active-enhancements report
313    /// (`CSI ? <flags> u`). The payload is the parsed
314    /// [`crate::ansi::kitty::KittyKeyboardFlags`] bitset.
315    KittyKeyboardEnhancements(crate::ansi::kitty::KittyKeyboardFlags),
316    /// XTWINOPS reply (window operation).
317    WindowOp {
318        /// Window operation number.
319        op: u32,
320        /// Window operation arguments.
321        args: Vec<Option<u32>>,
322    },
323    /// XTGETTCAP / termcap capability reply. `recognized` is `true` for a
324    /// successful reply (`DCS 1 + r`) and `false` for a failure
325    /// (`DCS 0 + r`); the entries are decoded the same way in both cases (a
326    /// failure echoes the requested, now known-unsupported, capability
327    /// names).
328    Termcap {
329        /// Whether the requested capabilities were recognized.
330        recognized: bool,
331        /// Decoded `(name, value)` pairs. The value is `None` when the entry
332        /// carried no `=`: either a boolean capability, reported as a bare
333        /// name, or a failure reply echoing a name it does not support.
334        /// `recognized` is what tells those apart.
335        ///
336        /// Kept as pairs because only the hex wire form is delimiter-safe:
337        /// decoded values commonly contain `;` and `=` (`kf13` is
338        /// `\E[1;2P`), so a joined string could not be split back apart.
339        entries: Vec<(String, Option<String>)>,
340    },
341    /// DECRPSS setting report (`DCS 1 $ r` on success, `DCS 0 $ r` on
342    /// failure), reporting a current setting such as the active SGR
343    /// attributes or cursor style. Sent in answer to
344    /// [`write_decrqss`](crate::ansi::status::write_decrqss).
345    SettingReport(SettingReport),
346
347    // -- Colors --------------------------------------------------------------
348    /// OSC 10 default foreground color reply.
349    ForegroundColor(Color),
350    /// OSC 11 default background color reply.
351    BackgroundColor(Color),
352    /// OSC 12 cursor color reply.
353    CursorColor(Color),
354    /// OSC 4 indexed palette color reply (`OSC 4 ; index ; color`).
355    PaletteColor {
356        /// Palette color index.
357        index: u8,
358        /// Reported palette color.
359        color: Color,
360    },
361    /// Color-scheme report (DEC mode 2031): whether the terminal is in its
362    /// dark or light scheme. Indicates only the dark/light preference, not
363    /// the actual colors.
364    ColorScheme(ColorScheme),
365
366    /// Terminal visibility report (DEC mode 2033): whether the terminal view
367    /// may be observed. Arrives unsolicited while
368    /// [`Program::enable_visibility_reports`] is active, and as the reply to
369    /// [`Program::request_visibility`].
370    ///
371    /// This is independent of focus: focus says which view receives keyboard
372    /// input, visibility says whether output can be seen.
373    ///
374    /// [`Program::enable_visibility_reports`]: crate::program::Program::enable_visibility_reports
375    /// [`Program::request_visibility`]: crate::program::Program::request_visibility
376    Visibility(Visibility),
377
378    /// Supported program status states and kinds (OSC 7501).
379    ///
380    /// Sent in reply to [`Program::request_program_status`]. Absent lists
381    /// use the protocol's fixed defaults. Names from future revisions are
382    /// preserved; unknown pairs are ignored.
383    ///
384    /// [`Program::request_program_status`]: crate::program::Program::request_program_status
385    ProgramStatus(ProgramStatusCapabilities),
386
387    // -- Clipboard / graphics ------------------------------------------------
388    /// OSC 52 clipboard content reply.
389    Clipboard {
390        /// Clipboard selection that was reported.
391        selection: ClipboardSelection,
392        /// Clipboard content.
393        content: String,
394    },
395    /// Kitty graphics response (APC `G ...` payload).
396    KittyGraphics {
397        /// Response options.
398        options: Vec<(String, String)>,
399        /// Response payload bytes.
400        payload: Vec<u8>,
401    },
402
403    // -- Group / unknown -----------------------------------------------------
404    /// Multiple events emitted by a single sequence.
405    Multi(Vec<Event>),
406    /// Unknown CSI sequence (parameters + intermediates + final byte).
407    UnknownCsi(Vec<u8>),
408    /// Unknown SS3 sequence.
409    UnknownSs3(Vec<u8>),
410    /// Unknown OSC sequence (payload bytes, no ESC/ST framing).
411    UnknownOsc(Vec<u8>),
412    /// Unknown DCS sequence (payload).
413    UnknownDcs(Vec<u8>),
414    /// Unknown SOS sequence (payload).
415    UnknownSos(Vec<u8>),
416    /// Unknown PM sequence (payload).
417    UnknownPm(Vec<u8>),
418    /// Unknown APC sequence (payload).
419    UnknownApc(Vec<u8>),
420    /// Catch-all for unrecognized byte sequences.
421    Unknown(Vec<u8>),
422}
423
424/// The payload of a DECRPSS reply, [`Event::SettingReport`].
425#[derive(Debug, Clone, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum SettingReport {
428    /// The terminal did not recognize the requested setting (`DCS 0 $ r ST`).
429    /// The reply carries no data at all, so only the request that provoked it
430    /// says which setting was turned down.
431    Unrecognized,
432    /// The cursor style the terminal is using, from a DECRPSS reply
433    /// reporting its `DECSCUSR` setting. Ask for it with
434    /// [`Program::request_cursor_style`](crate::program::Program::request_cursor_style).
435    CursorStyle(crate::ansi::cursor::CursorStyle),
436    /// The setting as the terminal spelled it, for a control function this
437    /// library does not decode: the whole CSI sequence without its
438    /// introducer, so `0;1m` for SGR or `>4;2m` for xterm's `XTQMODKEYS`.
439    Raw(String),
440}
441
442impl Event {
443    /// Borrow the [`Key`] payload if this is any key event
444    /// ([`Event::KeyPress`], [`Event::KeyRepeat`], or
445    /// [`Event::KeyRelease`]).
446    pub fn as_key(&self) -> Option<&Key> {
447        match self {
448            Event::KeyPress(k) | Event::KeyRepeat(k) | Event::KeyRelease(k) => Some(k),
449            _ => None,
450        }
451    }
452
453    /// Borrow the [`Mouse`] payload if this is any mouse event
454    /// ([`Event::MouseClick`], [`Event::MouseRelease`],
455    /// [`Event::MouseWheel`], or [`Event::MouseMove`]).
456    pub fn as_mouse(&self) -> Option<&Mouse> {
457        match self {
458            Event::MouseClick(m)
459            | Event::MouseRelease(m)
460            | Event::MouseWheel(m)
461            | Event::MouseMove(m) => Some(m),
462            _ => None,
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn color_scheme_display() {
473        assert_eq!(ColorScheme::Dark.to_string(), "dark");
474        assert_eq!(ColorScheme::Light.to_string(), "light");
475    }
476
477    #[test]
478    fn visibility_display_and_predicate() {
479        assert_eq!(Visibility::Visible.to_string(), "visible");
480        assert_eq!(Visibility::Hidden.to_string(), "hidden");
481        assert!(Visibility::Visible.is_visible());
482        assert!(!Visibility::Hidden.is_visible());
483    }
484}