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}