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/// A terminal event.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum Event {
150 // -- Input ---------------------------------------------------------------
151 /// Key was pressed.
152 KeyPress(Key),
153 /// Key auto-repeated (Kitty Keyboard Protocol).
154 KeyRepeat(Key),
155 /// Key was released (Kitty Keyboard Protocol).
156 KeyRelease(Key),
157 /// Mouse button was pressed.
158 MouseClick(Mouse),
159 /// Mouse button was released.
160 MouseRelease(Mouse),
161 /// Mouse wheel scrolled.
162 MouseWheel(Mouse),
163 /// Mouse moved (with or without a button held).
164 MouseMove(Mouse),
165
166 // -- Size / geometry -----------------------------------------------------
167 /// The terminal surface changed size. Emitted only for genuine
168 /// change notifications: kernel SIGWINCH (full `Winsize`),
169 /// `ReadConsoleInput` window-buffer-size events on Windows
170 /// (cells only), and in-band CSI 48 t reports under mode 2048
171 /// (full `Winsize`). Replies to explicit size queries are
172 /// delivered as `WindowCellSize` / `WindowPixelSize` /
173 /// `CellPixelSize` instead.
174 Resize(Winsize),
175 /// Reply to a `CSI 18 t` query — window size in cells.
176 WindowCellSize {
177 /// Width in terminal cells.
178 width: u16,
179 /// Height in terminal cells.
180 height: u16,
181 },
182 /// Reply to a `CSI 14 t` query — window size in pixels.
183 WindowPixelSize {
184 /// Width in pixels.
185 width: u16,
186 /// Height in pixels.
187 height: u16,
188 },
189 /// Reply to a `CSI 16 t` query — single-cell size in pixels.
190 CellPixelSize {
191 /// Cell width in pixels.
192 width: u16,
193 /// Cell height in pixels.
194 height: u16,
195 },
196
197 // -- Focus / paste -------------------------------------------------------
198 /// Focus gained.
199 FocusIn,
200 /// Focus lost.
201 FocusOut,
202 /// Bracketed paste started.
203 PasteStart,
204 /// Bracketed paste ended.
205 PasteEnd,
206 /// Streaming chunk of pasted bytes emitted between [`Event::PasteStart`]
207 /// and [`Event::PasteEnd`]. Pastes that exceed the source's read
208 /// buffer are split across multiple chunks; reassembly and any
209 /// text decoding are the caller's responsibility (terminals may
210 /// paste arbitrary binary content, not just valid UTF-8).
211 PasteChunk(Vec<u8>),
212
213 // -- Position / device attrs --------------------------------------------
214 /// Cursor position report (CPR). Coordinates are zero-based (the
215 /// 1-based wire form is normalized when parsed).
216 CursorPosition(crate::layout::Position),
217 /// Primary device attributes (DA1) — list of decoded numeric attributes.
218 PrimaryDeviceAttributes(Vec<Option<u32>>),
219 /// Secondary device attributes (DA2).
220 SecondaryDeviceAttributes(Vec<Option<u32>>),
221 /// Tertiary device attributes (DA3) — terminal ID string.
222 TertiaryDeviceAttributes(String),
223 /// Terminal name reply (XTVERSION). Carries the raw identifier string,
224 /// which typically combines a name and version (e.g. `"XTerm(380)"`).
225 TerminalName(String),
226
227 // -- Mode / capability reports ------------------------------------------
228 /// DECRPM / RM mode report.
229 ///
230 /// The `setting` distinguishes all five DECRPM states. A terminal can
231 /// report a mode as permanently set or permanently reset, meaning it
232 /// recognizes the mode but will not let the host toggle it. When deciding
233 /// whether a feature is usable, prefer
234 /// [`ModeSetting::is_available`](crate::ansi::mode::ModeSetting::is_available)
235 /// over [`is_recognized`](crate::ansi::mode::ModeSetting::is_recognized): a
236 /// permanently reset mode is recognized yet can never be enabled.
237 ModeReport {
238 /// Reported mode.
239 mode: Mode,
240 /// Current mode setting.
241 setting: ModeSetting,
242 },
243 /// modifyOtherKeys report.
244 ModifyOtherKeys(ModifyOtherKeysMode),
245 /// Kitty keyboard protocol active-enhancements report
246 /// (`CSI ? <flags> u`). The payload is the parsed
247 /// [`crate::ansi::kitty::KittyKeyboardFlags`] bitset.
248 KittyKeyboardEnhancements(crate::ansi::kitty::KittyKeyboardFlags),
249 /// XTWINOPS reply (window operation).
250 WindowOp {
251 /// Window operation number.
252 op: u32,
253 /// Window operation arguments.
254 args: Vec<Option<u32>>,
255 },
256 /// XTGETTCAP / termcap capability reply. `recognized` is `true` for a
257 /// successful reply (`DCS 1 + r`) and `false` for a failure
258 /// (`DCS 0 + r`); `payload` is the decoded `;`-joined `cap[=value]`
259 /// string, decoded the same way in both cases (a failure echoes the
260 /// requested, now known-unsupported, capability names).
261 Termcap {
262 /// Whether the requested capability was recognized.
263 recognized: bool,
264 /// Decoded capability payload.
265 payload: String,
266 },
267
268 // -- Colors --------------------------------------------------------------
269 /// OSC 10 default foreground color reply.
270 ForegroundColor(Color),
271 /// OSC 11 default background color reply.
272 BackgroundColor(Color),
273 /// OSC 12 cursor color reply.
274 CursorColor(Color),
275 /// OSC 4 indexed palette color reply (`OSC 4 ; index ; color`).
276 PaletteColor {
277 /// Palette color index.
278 index: u8,
279 /// Reported palette color.
280 color: Color,
281 },
282 /// Color-scheme report (DEC mode 2031): whether the terminal is in its
283 /// dark or light scheme. Indicates only the dark/light preference, not
284 /// the actual colors.
285 ColorScheme(ColorScheme),
286
287 // -- Clipboard / graphics ------------------------------------------------
288 /// OSC 52 clipboard content reply.
289 Clipboard {
290 /// Clipboard selection that was reported.
291 selection: ClipboardSelection,
292 /// Clipboard content.
293 content: String,
294 },
295 /// Kitty graphics response (APC `G ...` payload).
296 KittyGraphics {
297 /// Response options.
298 options: Vec<(String, String)>,
299 /// Response payload bytes.
300 payload: Vec<u8>,
301 },
302
303 // -- Group / unknown -----------------------------------------------------
304 /// Multiple events emitted by a single sequence.
305 Multi(Vec<Event>),
306 /// Unknown CSI sequence (parameters + intermediates + final byte).
307 UnknownCsi(Vec<u8>),
308 /// Unknown SS3 sequence.
309 UnknownSs3(Vec<u8>),
310 /// Unknown OSC sequence (payload bytes, no ESC/ST framing).
311 UnknownOsc(Vec<u8>),
312 /// Unknown DCS sequence (payload).
313 UnknownDcs(Vec<u8>),
314 /// Unknown SOS sequence (payload).
315 UnknownSos(Vec<u8>),
316 /// Unknown PM sequence (payload).
317 UnknownPm(Vec<u8>),
318 /// Unknown APC sequence (payload).
319 UnknownApc(Vec<u8>),
320 /// Catch-all for unrecognized byte sequences.
321 Unknown(Vec<u8>),
322}
323
324impl Event {
325 /// Borrow the [`Key`] payload if this is any key event
326 /// ([`Event::KeyPress`], [`Event::KeyRepeat`], or
327 /// [`Event::KeyRelease`]).
328 pub fn as_key(&self) -> Option<&Key> {
329 match self {
330 Event::KeyPress(k) | Event::KeyRepeat(k) | Event::KeyRelease(k) => Some(k),
331 _ => None,
332 }
333 }
334
335 /// Borrow the [`Mouse`] payload if this is any mouse event
336 /// ([`Event::MouseClick`], [`Event::MouseRelease`],
337 /// [`Event::MouseWheel`], or [`Event::MouseMove`]).
338 pub fn as_mouse(&self) -> Option<&Mouse> {
339 match self {
340 Event::MouseClick(m)
341 | Event::MouseRelease(m)
342 | Event::MouseWheel(m)
343 | Event::MouseMove(m) => Some(m),
344 _ => None,
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn color_scheme_display() {
355 assert_eq!(ColorScheme::Dark.to_string(), "dark");
356 assert_eq!(ColorScheme::Light.to_string(), "light");
357 }
358}