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}