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/// 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}