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 key::{Key, KeyCode, KeyModifiers, ParseKeyError};
87pub use mouse::{Mouse, MouseButton, mouse_pixel_to_cell};
88pub use source::{DEFAULT_ESC_TIMEOUT, DEFAULT_PASTE_IDLE_TIMEOUT, EventSource, Input, Waker};
89#[cfg(feature = "async")]
90pub use stream::EventStream;
91
92use crate::ansi::mode::{Mode, ModeSetting};
93use crate::color::Color;
94use crate::terminal::Winsize;
95
96/// Which system clipboard selection an OSC 52 event refers to.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum ClipboardSelection {
99    /// `c` — system clipboard.
100    System,
101    /// `p` — primary (X11 PRIMARY) selection.
102    Primary,
103    /// Any other / unknown selection character.
104    Other(char),
105}
106
107/// Decoded modifyOtherKeys mode (`CSI > 4 ; n m`).
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum ModifyOtherKeysMode {
110    /// Disabled.
111    Disabled,
112    /// Mode 1 — only modify keys that don't otherwise have an xterm sequence.
113    Mode1,
114    /// Mode 2 — modify all keys.
115    Mode2,
116}
117
118impl ModifyOtherKeysMode {
119    /// Convert a report value into a modifyOtherKeys mode.
120    pub fn from_value(v: u8) -> Self {
121        match v {
122            1 => Self::Mode1,
123            2 => Self::Mode2,
124            _ => Self::Disabled,
125        }
126    }
127}
128
129/// Reported terminal color scheme (DEC mode 2031).
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub enum ColorScheme {
132    /// Dark mode (`CSI ? 997 ; 1 n`).
133    Dark,
134    /// Light mode (`CSI ? 997 ; 2 n`).
135    Light,
136}
137
138impl std::fmt::Display for ColorScheme {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.write_str(match self {
141            ColorScheme::Dark => "dark",
142            ColorScheme::Light => "light",
143        })
144    }
145}
146
147/// Reported terminal visibility (DEC mode 2033).
148///
149/// This is an advisory, deliberately conservative hint used to skip expensive
150/// rendering that nobody can see. [`Hidden`](Visibility::Hidden) is precise:
151/// the terminal has positive knowledge that the view is not observable.
152/// [`Visible`](Visibility::Visible) only means it *may* be observable.
153///
154/// Only `1` and `2` decode to a report; any other value is left as
155/// [`Event::UnknownCsi`]. Treat a terminal that reports nothing, or reports
156/// something unrecognized, as visible.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158pub enum Visibility {
159    /// Potentially visible (`CSI ? 999 ; 1 n`). The view may be observable.
160    ///
161    /// This does not promise that any cell is onscreen: the terminal reports
162    /// it whenever visibility is unknown or any view may be observed.
163    Visible,
164    /// Not visible (`CSI ? 999 ; 2 n`). The terminal knows the view is not
165    /// ordinarily observable, so expensive visual updates can be paused.
166    ///
167    /// Never assume this lasts for any minimum duration.
168    Hidden,
169}
170
171impl Visibility {
172    /// Whether the terminal view may be observable, so visual work is worth
173    /// doing. `true` for [`Visible`](Visibility::Visible).
174    pub fn is_visible(self) -> bool {
175        matches!(self, Visibility::Visible)
176    }
177}
178
179impl std::fmt::Display for Visibility {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.write_str(match self {
182            Visibility::Visible => "visible",
183            Visibility::Hidden => "hidden",
184        })
185    }
186}
187
188/// A terminal event.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum Event {
191    // -- Input ---------------------------------------------------------------
192    /// Key was pressed.
193    KeyPress(Key),
194    /// Key auto-repeated (Kitty Keyboard Protocol).
195    KeyRepeat(Key),
196    /// Key was released (Kitty Keyboard Protocol).
197    KeyRelease(Key),
198    /// Mouse button was pressed.
199    MouseClick(Mouse),
200    /// Mouse button was released.
201    MouseRelease(Mouse),
202    /// Mouse wheel scrolled.
203    MouseWheel(Mouse),
204    /// Mouse moved (with or without a button held).
205    MouseMove(Mouse),
206
207    // -- Size / geometry -----------------------------------------------------
208    /// The terminal surface changed size. Emitted only for genuine
209    /// change notifications: kernel SIGWINCH (full `Winsize`),
210    /// `ReadConsoleInput` window-buffer-size events on Windows
211    /// (cells only), and in-band CSI 48 t reports under mode 2048
212    /// (full `Winsize`). Replies to explicit size queries are
213    /// delivered as `WindowCellSize` / `WindowPixelSize` /
214    /// `CellPixelSize` instead.
215    Resize(Winsize),
216    /// Reply to a `CSI 18 t` query — window size in cells.
217    WindowCellSize {
218        /// Width in terminal cells.
219        width: u16,
220        /// Height in terminal cells.
221        height: u16,
222    },
223    /// Reply to a `CSI 14 t` query — window size in pixels.
224    WindowPixelSize {
225        /// Width in pixels.
226        width: u16,
227        /// Height in pixels.
228        height: u16,
229    },
230    /// Reply to a `CSI 16 t` query — single-cell size in pixels.
231    CellPixelSize {
232        /// Cell width in pixels.
233        width: u16,
234        /// Cell height in pixels.
235        height: u16,
236    },
237
238    // -- Focus / paste -------------------------------------------------------
239    /// Focus gained.
240    FocusIn,
241    /// Focus lost.
242    FocusOut,
243    /// Bracketed paste started.
244    PasteStart,
245    /// Bracketed paste ended.
246    PasteEnd,
247    /// Streaming chunk of pasted bytes emitted between [`Event::PasteStart`]
248    /// and [`Event::PasteEnd`]. Pastes that exceed the source's read
249    /// buffer are split across multiple chunks; reassembly and any
250    /// text decoding are the caller's responsibility (terminals may
251    /// paste arbitrary binary content, not just valid UTF-8).
252    PasteChunk(Vec<u8>),
253
254    // -- Position / device attrs --------------------------------------------
255    /// Cursor position report (CPR). Coordinates are zero-based (the
256    /// 1-based wire form is normalized when parsed).
257    CursorPosition(crate::layout::Position),
258    /// Primary device attributes (DA1) — list of decoded numeric attributes.
259    PrimaryDeviceAttributes(Vec<Option<u32>>),
260    /// Secondary device attributes (DA2).
261    SecondaryDeviceAttributes(Vec<Option<u32>>),
262    /// Tertiary device attributes (DA3) — terminal ID string.
263    TertiaryDeviceAttributes(String),
264    /// Terminal name reply (XTVERSION). Carries the raw identifier string,
265    /// which typically combines a name and version (e.g. `"XTerm(380)"`).
266    TerminalName(String),
267
268    // -- Mode / capability reports ------------------------------------------
269    /// DECRPM / RM mode report.
270    ///
271    /// The `setting` distinguishes all five DECRPM states. A terminal can
272    /// report a mode as permanently set or permanently reset, meaning it
273    /// recognizes the mode but will not let the host toggle it. When deciding
274    /// whether a feature is usable, prefer
275    /// [`ModeSetting::is_available`](crate::ansi::mode::ModeSetting::is_available)
276    /// over [`is_recognized`](crate::ansi::mode::ModeSetting::is_recognized): a
277    /// permanently reset mode is recognized yet can never be enabled.
278    ModeReport {
279        /// Reported mode.
280        mode: Mode,
281        /// Current mode setting.
282        setting: ModeSetting,
283    },
284    /// modifyOtherKeys report.
285    ModifyOtherKeys(ModifyOtherKeysMode),
286    /// Kitty keyboard protocol active-enhancements report
287    /// (`CSI ? <flags> u`). The payload is the parsed
288    /// [`crate::ansi::kitty::KittyKeyboardFlags`] bitset.
289    KittyKeyboardEnhancements(crate::ansi::kitty::KittyKeyboardFlags),
290    /// XTWINOPS reply (window operation).
291    WindowOp {
292        /// Window operation number.
293        op: u32,
294        /// Window operation arguments.
295        args: Vec<Option<u32>>,
296    },
297    /// XTGETTCAP / termcap capability reply. `recognized` is `true` for a
298    /// successful reply (`DCS 1 + r`) and `false` for a failure
299    /// (`DCS 0 + r`); the entries are decoded the same way in both cases (a
300    /// failure echoes the requested, now known-unsupported, capability
301    /// names).
302    Termcap {
303        /// Whether the requested capabilities were recognized.
304        recognized: bool,
305        /// Decoded `(name, value)` pairs. The value is `None` when the entry
306        /// carried no `=`: either a boolean capability, reported as a bare
307        /// name, or a failure reply echoing a name it does not support.
308        /// `recognized` is what tells those apart.
309        ///
310        /// Kept as pairs because only the hex wire form is delimiter-safe:
311        /// decoded values commonly contain `;` and `=` (`kf13` is
312        /// `\E[1;2P`), so a joined string could not be split back apart.
313        entries: Vec<(String, Option<String>)>,
314    },
315    /// DECRPSS setting report (`DCS 1 $ r` on success, `DCS 0 $ r` on
316    /// failure), reporting a current setting such as the active SGR
317    /// attributes or cursor style. Sent in answer to
318    /// [`write_decrqss`](crate::ansi::status::write_decrqss).
319    SettingReport(SettingReport),
320
321    // -- Colors --------------------------------------------------------------
322    /// OSC 10 default foreground color reply.
323    ForegroundColor(Color),
324    /// OSC 11 default background color reply.
325    BackgroundColor(Color),
326    /// OSC 12 cursor color reply.
327    CursorColor(Color),
328    /// OSC 4 indexed palette color reply (`OSC 4 ; index ; color`).
329    PaletteColor {
330        /// Palette color index.
331        index: u8,
332        /// Reported palette color.
333        color: Color,
334    },
335    /// Color-scheme report (DEC mode 2031): whether the terminal is in its
336    /// dark or light scheme. Indicates only the dark/light preference, not
337    /// the actual colors.
338    ColorScheme(ColorScheme),
339
340    /// Terminal visibility report (DEC mode 2033): whether the terminal view
341    /// may be observed. Arrives unsolicited while
342    /// [`Program::enable_visibility_reports`] is active, and as the reply to
343    /// [`Program::request_visibility`].
344    ///
345    /// This is independent of focus: focus says which view receives keyboard
346    /// input, visibility says whether output can be seen.
347    ///
348    /// [`Program::enable_visibility_reports`]: crate::program::Program::enable_visibility_reports
349    /// [`Program::request_visibility`]: crate::program::Program::request_visibility
350    Visibility(Visibility),
351
352    // -- Clipboard / graphics ------------------------------------------------
353    /// OSC 52 clipboard content reply.
354    Clipboard {
355        /// Clipboard selection that was reported.
356        selection: ClipboardSelection,
357        /// Clipboard content.
358        content: String,
359    },
360    /// Kitty graphics response (APC `G ...` payload).
361    KittyGraphics {
362        /// Response options.
363        options: Vec<(String, String)>,
364        /// Response payload bytes.
365        payload: Vec<u8>,
366    },
367
368    // -- Group / unknown -----------------------------------------------------
369    /// Multiple events emitted by a single sequence.
370    Multi(Vec<Event>),
371    /// Unknown CSI sequence (parameters + intermediates + final byte).
372    UnknownCsi(Vec<u8>),
373    /// Unknown SS3 sequence.
374    UnknownSs3(Vec<u8>),
375    /// Unknown OSC sequence (payload bytes, no ESC/ST framing).
376    UnknownOsc(Vec<u8>),
377    /// Unknown DCS sequence (payload).
378    UnknownDcs(Vec<u8>),
379    /// Unknown SOS sequence (payload).
380    UnknownSos(Vec<u8>),
381    /// Unknown PM sequence (payload).
382    UnknownPm(Vec<u8>),
383    /// Unknown APC sequence (payload).
384    UnknownApc(Vec<u8>),
385    /// Catch-all for unrecognized byte sequences.
386    Unknown(Vec<u8>),
387}
388
389/// The payload of a DECRPSS reply, [`Event::SettingReport`].
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub enum SettingReport {
392    /// The terminal did not recognize the requested setting (`DCS 0 $ r ST`).
393    /// The reply carries no data at all, so only the request that provoked it
394    /// says which setting was turned down.
395    Unrecognized,
396    /// The setting as the terminal spelled it: the whole CSI sequence for the
397    /// control function without its introducer, so `0;1m` for SGR, `2 q` for
398    /// `DECSCUSR`, `>4;2m` for xterm's `XTQMODKEYS`.
399    Raw(String),
400}
401
402impl Event {
403    /// Borrow the [`Key`] payload if this is any key event
404    /// ([`Event::KeyPress`], [`Event::KeyRepeat`], or
405    /// [`Event::KeyRelease`]).
406    pub fn as_key(&self) -> Option<&Key> {
407        match self {
408            Event::KeyPress(k) | Event::KeyRepeat(k) | Event::KeyRelease(k) => Some(k),
409            _ => None,
410        }
411    }
412
413    /// Borrow the [`Mouse`] payload if this is any mouse event
414    /// ([`Event::MouseClick`], [`Event::MouseRelease`],
415    /// [`Event::MouseWheel`], or [`Event::MouseMove`]).
416    pub fn as_mouse(&self) -> Option<&Mouse> {
417        match self {
418            Event::MouseClick(m)
419            | Event::MouseRelease(m)
420            | Event::MouseWheel(m)
421            | Event::MouseMove(m) => Some(m),
422            _ => None,
423        }
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn color_scheme_display() {
433        assert_eq!(ColorScheme::Dark.to_string(), "dark");
434        assert_eq!(ColorScheme::Light.to_string(), "light");
435    }
436
437    #[test]
438    fn visibility_display_and_predicate() {
439        assert_eq!(Visibility::Visible.to_string(), "visible");
440        assert_eq!(Visibility::Hidden.to_string(), "hidden");
441        assert!(Visibility::Visible.is_visible());
442        assert!(!Visibility::Hidden.is_visible());
443    }
444}