Skip to main content

supercode_frontend_tui/terminal/
event_stream.rs

1// Derived from OpenAI Codex: codex-rs/tui/src/tui/event_stream.rs
2// Pinned source: 8604689ec5e3437eb79802d8d72249b7722fbf5b
3// Copyright 2025 OpenAI
4// Licensed under the Apache License, Version 2.0.
5// Modified by the Supercode contributors; see docs/legal/codex-frontend-extraction.toml.
6
7//! Runtime-neutral mapping of Crossterm input, focus, and resize events.
8
9use crossterm::event::Event;
10use crossterm::event::KeyEvent;
11
12/// Terminal input the frontend may translate into SDK actions.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum TerminalEvent {
15    Key(KeyEvent),
16    Paste(String),
17    Resize { width: u16, height: u16 },
18    FocusChanged(bool),
19}
20
21/// Map one Crossterm event without introducing app/session behavior.
22pub fn map_crossterm_event(event: Event) -> Option<TerminalEvent> {
23    match event {
24        Event::Key(key) => Some(TerminalEvent::Key(key)),
25        Event::Paste(text) => Some(TerminalEvent::Paste(text)),
26        Event::Resize(width, height) => Some(TerminalEvent::Resize { width, height }),
27        Event::FocusGained => Some(TerminalEvent::FocusChanged(true)),
28        Event::FocusLost => Some(TerminalEvent::FocusChanged(false)),
29        Event::Mouse(_) => None,
30    }
31}
32
33/// Focus and viewport state used by render scheduling and notifications.
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct TerminalState {
36    pub width: u16,
37    pub height: u16,
38    pub focused: bool,
39}
40
41impl TerminalState {
42    pub const fn new(width: u16, height: u16) -> Self {
43        Self {
44            width,
45            height,
46            focused: true,
47        }
48    }
49
50    /// Apply focus/resize state and report whether a redraw is required.
51    pub fn apply(&mut self, event: &TerminalEvent) -> bool {
52        match event {
53            TerminalEvent::Resize { width, height } => {
54                let changed = self.width != *width || self.height != *height;
55                self.width = *width;
56                self.height = *height;
57                changed
58            }
59            TerminalEvent::FocusChanged(focused) => {
60                let changed = self.focused != *focused;
61                self.focused = *focused;
62                changed
63            }
64            TerminalEvent::Key(_) | TerminalEvent::Paste(_) => false,
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use crossterm::event::KeyCode;
72    use crossterm::event::KeyEvent;
73    use crossterm::event::KeyModifiers;
74
75    use super::*;
76
77    #[test]
78    fn maps_focus_resize_paste_and_key_without_app_events() {
79        assert_eq!(
80            map_crossterm_event(Event::FocusLost),
81            Some(TerminalEvent::FocusChanged(false))
82        );
83        assert_eq!(
84            map_crossterm_event(Event::FocusGained),
85            Some(TerminalEvent::FocusChanged(true))
86        );
87        assert_eq!(
88            map_crossterm_event(Event::Resize(80, 24)),
89            Some(TerminalEvent::Resize {
90                width: 80,
91                height: 24
92            })
93        );
94        assert_eq!(
95            map_crossterm_event(Event::Paste("hello".into())),
96            Some(TerminalEvent::Paste("hello".into()))
97        );
98        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
99        assert_eq!(
100            map_crossterm_event(Event::Key(key)),
101            Some(TerminalEvent::Key(key))
102        );
103    }
104
105    #[test]
106    fn focus_and_resize_change_state_and_require_redraw() {
107        let mut state = TerminalState::new(80, 24);
108        assert!(state.apply(&TerminalEvent::FocusChanged(false)));
109        assert!(!state.focused);
110        assert!(!state.apply(&TerminalEvent::FocusChanged(false)));
111        assert!(state.apply(&TerminalEvent::Resize {
112            width: 160,
113            height: 48,
114        }));
115        assert_eq!((state.width, state.height), (160, 48));
116        assert!(!state.apply(&TerminalEvent::Resize {
117            width: 160,
118            height: 48,
119        }));
120    }
121}