gpui_terminal/
event.rs

1//! Event handling for the terminal emulator.
2//!
3//! This module bridges alacritty's event system with GPUI by providing
4//! [`GpuiEventProxy`], which implements alacritty's [`EventListener`] trait
5//! and forwards relevant events through a channel.
6//!
7//! # Event Flow
8//!
9//! ```text
10//! alacritty Term → GpuiEventProxy → mpsc channel → TerminalView
11//!                        │
12//!                        └─ Translates Event enum to TerminalEvent
13//! ```
14//!
15//! # Supported Events
16//!
17//! | Alacritty Event | TerminalEvent | Description |
18//! |-----------------|---------------|-------------|
19//! | `Event::Wakeup` | `Wakeup` | Terminal has new content |
20//! | `Event::Bell` | `Bell` | BEL character received |
21//! | `Event::Title(_)` | `Title(String)` | Title escape sequence (OSC 0/2) |
22//! | `Event::ClipboardStore(_, _)` | `ClipboardStore(String)` | Copy request (OSC 52) |
23//! | `Event::ClipboardLoad(_, _)` | `ClipboardLoad` | Paste request |
24//! | `Event::Exit` | `Exit` | Terminal exited |
25//! | `Event::ChildExit(_)` | `Exit` | Child process exited |
26//! | `Event::ResetTitle` | `Title("")` | Reset to empty title |
27//!
28//! Events like `MouseCursorDirty`, `PtyWrite`, and `CursorBlinkingChange` are
29//! ignored as they're handled internally or not needed for GPUI integration.
30//!
31//! # Example
32//!
33//! ```
34//! use std::sync::mpsc::channel;
35//! use gpui_terminal::event::{GpuiEventProxy, TerminalEvent};
36//!
37//! let (tx, rx) = channel();
38//! let proxy = GpuiEventProxy::new(tx);
39//!
40//! // The proxy is passed to alacritty's Term and will forward events
41//! // Events can be received on the other end of the channel
42//! ```
43//!
44//! [`EventListener`]: alacritty_terminal::event::EventListener
45
46use alacritty_terminal::event::{Event, EventListener};
47use std::sync::mpsc::Sender;
48
49/// Events emitted by the terminal that the GPUI application cares about.
50///
51/// This enum represents a subset of alacritty's events that are relevant
52/// for the GPUI terminal emulator implementation.
53#[derive(Debug, Clone)]
54pub enum TerminalEvent {
55    /// The terminal has new content to display and needs a redraw.
56    Wakeup,
57
58    /// The terminal bell was triggered (visual or audible alert).
59    Bell,
60
61    /// The terminal title has changed.
62    Title(String),
63
64    /// The terminal wants to store data to the clipboard.
65    ClipboardStore(String),
66
67    /// The terminal wants to load data from the clipboard.
68    ClipboardLoad,
69
70    /// The terminal process has exited.
71    Exit,
72}
73
74/// An event proxy that implements alacritty's EventListener trait.
75///
76/// This struct forwards relevant terminal events to a channel that can be
77/// consumed by the GPUI application on the main thread.
78pub struct GpuiEventProxy {
79    /// Channel sender for forwarding events to the GPUI application.
80    tx: Sender<TerminalEvent>,
81}
82
83impl GpuiEventProxy {
84    /// Creates a new event proxy with the given channel sender.
85    ///
86    /// # Arguments
87    ///
88    /// * `tx` - The channel sender to forward events through
89    ///
90    /// # Returns
91    ///
92    /// A new GpuiEventProxy instance
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use std::sync::mpsc::channel;
98    /// use gpui_terminal::event::GpuiEventProxy;
99    ///
100    /// let (tx, rx) = channel();
101    /// let proxy = GpuiEventProxy::new(tx);
102    /// ```
103    pub fn new(tx: Sender<TerminalEvent>) -> Self {
104        Self { tx }
105    }
106
107    /// Sends a terminal event through the channel.
108    ///
109    /// If the channel is disconnected, this method will silently drop the event.
110    /// This can happen if the GPUI application has been shut down.
111    fn send(&self, event: TerminalEvent) {
112        // Ignore send errors - they just mean the receiver has been dropped
113        let _ = self.tx.send(event);
114    }
115}
116
117impl EventListener for GpuiEventProxy {
118    /// Handles events from the alacritty terminal.
119    ///
120    /// This method is called by alacritty when terminal events occur.
121    /// It translates alacritty's Event enum to our TerminalEvent enum
122    /// and forwards relevant events through the channel.
123    fn send_event(&self, event: Event) {
124        match event {
125            Event::Wakeup => {
126                self.send(TerminalEvent::Wakeup);
127            }
128            Event::Bell => {
129                self.send(TerminalEvent::Bell);
130            }
131            Event::Title(title) => {
132                self.send(TerminalEvent::Title(title));
133            }
134            Event::ClipboardStore(_clipboard_type, data) => {
135                // For simplicity, we ignore the clipboard type and just store the data
136                self.send(TerminalEvent::ClipboardStore(data));
137            }
138            Event::ClipboardLoad(_clipboard_type, _format) => {
139                // For simplicity, we ignore the clipboard type and format
140                self.send(TerminalEvent::ClipboardLoad);
141            }
142            Event::Exit => {
143                self.send(TerminalEvent::Exit);
144            }
145            // Ignore events we don't care about
146            Event::MouseCursorDirty => {}
147            Event::PtyWrite(ref _data) => {
148                // This is handled internally by alacritty
149            }
150            Event::ColorRequest(ref _index, ref _format) => {
151                // Color requests are not commonly used
152            }
153            Event::TextAreaSizeRequest(ref _format) => {
154                // Text area size requests are handled internally
155            }
156            Event::CursorBlinkingChange => {
157                // Cursor blinking changes could be handled if needed
158            }
159            Event::ResetTitle => {
160                // Reset title to default - we can treat this as an empty title
161                self.send(TerminalEvent::Title(String::new()));
162            }
163            Event::ChildExit(_exit_code) => {
164                // Child process exited - treat this as a terminal exit
165                self.send(TerminalEvent::Exit);
166            }
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use std::sync::mpsc::channel;
175
176    #[test]
177    fn test_event_proxy_creation() {
178        let (tx, _rx) = channel();
179        let _proxy = GpuiEventProxy::new(tx);
180    }
181
182    #[test]
183    fn test_wakeup_event() {
184        let (tx, rx) = channel();
185        let proxy = GpuiEventProxy::new(tx);
186
187        proxy.send_event(Event::Wakeup);
188
189        let event = rx.recv().unwrap();
190        assert!(matches!(event, TerminalEvent::Wakeup));
191    }
192
193    #[test]
194    fn test_bell_event() {
195        let (tx, rx) = channel();
196        let proxy = GpuiEventProxy::new(tx);
197
198        proxy.send_event(Event::Bell);
199
200        let event = rx.recv().unwrap();
201        assert!(matches!(event, TerminalEvent::Bell));
202    }
203
204    #[test]
205    fn test_title_event() {
206        let (tx, rx) = channel();
207        let proxy = GpuiEventProxy::new(tx);
208
209        proxy.send_event(Event::Title("Test Title".to_string()));
210
211        let event = rx.recv().unwrap();
212        match event {
213            TerminalEvent::Title(title) => assert_eq!(title, "Test Title"),
214            _ => panic!("Expected Title event"),
215        }
216    }
217
218    #[test]
219    fn test_clipboard_store_event() {
220        use alacritty_terminal::term::ClipboardType;
221
222        let (tx, rx) = channel();
223        let proxy = GpuiEventProxy::new(tx);
224
225        proxy.send_event(Event::ClipboardStore(
226            ClipboardType::Clipboard,
227            "clipboard data".to_string(),
228        ));
229
230        let event = rx.recv().unwrap();
231        match event {
232            TerminalEvent::ClipboardStore(data) => assert_eq!(data, "clipboard data"),
233            _ => panic!("Expected ClipboardStore event"),
234        }
235    }
236
237    #[test]
238    fn test_clipboard_load_event() {
239        use alacritty_terminal::term::ClipboardType;
240        use std::sync::Arc;
241
242        let (tx, rx) = channel();
243        let proxy = GpuiEventProxy::new(tx);
244
245        // ClipboardLoad requires a callback function
246        let callback = Arc::new(|s: &str| s.to_string());
247        proxy.send_event(Event::ClipboardLoad(ClipboardType::Clipboard, callback));
248
249        let event = rx.recv().unwrap();
250        assert!(matches!(event, TerminalEvent::ClipboardLoad));
251    }
252
253    #[test]
254    fn test_exit_event() {
255        let (tx, rx) = channel();
256        let proxy = GpuiEventProxy::new(tx);
257
258        proxy.send_event(Event::Exit);
259
260        let event = rx.recv().unwrap();
261        assert!(matches!(event, TerminalEvent::Exit));
262    }
263
264    #[test]
265    fn test_reset_title_event() {
266        let (tx, rx) = channel();
267        let proxy = GpuiEventProxy::new(tx);
268
269        proxy.send_event(Event::ResetTitle);
270
271        let event = rx.recv().unwrap();
272        match event {
273            TerminalEvent::Title(title) => assert!(title.is_empty()),
274            _ => panic!("Expected Title event"),
275        }
276    }
277
278    #[test]
279    fn test_ignored_events() {
280        let (tx, rx) = channel();
281        let proxy = GpuiEventProxy::new(tx);
282
283        // These events should be ignored and not sent through the channel
284        proxy.send_event(Event::MouseCursorDirty);
285        proxy.send_event(Event::CursorBlinkingChange);
286
287        // The channel should be empty
288        assert!(rx.try_recv().is_err());
289    }
290
291    #[test]
292    fn test_disconnected_channel() {
293        let (tx, rx) = channel();
294        let proxy = GpuiEventProxy::new(tx);
295
296        // Drop the receiver to disconnect the channel
297        drop(rx);
298
299        // Sending should not panic even though the channel is disconnected
300        proxy.send_event(Event::Wakeup);
301    }
302}