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}