gpui_terminal/terminal.rs
1//! Terminal state management.
2//!
3//! This module provides [`TerminalState`], a thread-safe wrapper around alacritty's
4//! [`Term`] structure. It manages the terminal
5//! emulator state, including the character grid, cursor position, and VTE parser.
6//!
7//! # Architecture
8//!
9//! `TerminalState` wraps the alacritty terminal in `Arc<Mutex<>>` to allow safe
10//! concurrent access from:
11//!
12//! - The async reader task (writing bytes to the terminal)
13//! - The render thread (reading the grid for display)
14//! - The main thread (handling resize events)
15//!
16//! # VTE Parsing
17//!
18//! The terminal uses alacritty's VTE parser to process byte streams. When bytes
19//! arrive from the PTY, they are fed through the parser via [`process_bytes`],
20//! which calls handler methods on the `Term` to update the grid:
21//!
22//! ```text
23//! PTY bytes → VTE Parser → Term handlers → Grid updates
24//! ├─ print() (regular characters)
25//! ├─ execute() (control chars: BEL, BS, etc.)
26//! ├─ esc_dispatch() (escape sequences)
27//! └─ csi_dispatch() (CSI sequences: colors, cursor, etc.)
28//! ```
29//!
30//! # Example
31//!
32//! ```
33//! use std::sync::mpsc::channel;
34//! use gpui_terminal::event::GpuiEventProxy;
35//! use gpui_terminal::terminal::TerminalState;
36//!
37//! let (tx, rx) = channel();
38//! let event_proxy = GpuiEventProxy::new(tx);
39//! let mut terminal = TerminalState::new(80, 24, event_proxy);
40//!
41//! // Process some output (e.g., colored text)
42//! terminal.process_bytes(b"\x1b[31mRed text\x1b[0m");
43//! ```
44//!
45//! [`process_bytes`]: TerminalState::process_bytes
46
47use crate::event::GpuiEventProxy;
48use alacritty_terminal::grid::Dimensions;
49use alacritty_terminal::term::{Config, Term, TermMode};
50use alacritty_terminal::vte::ansi::Processor;
51use parking_lot::Mutex;
52use std::sync::Arc;
53
54/// Simple dimensions implementation for terminal initialization.
55struct TermDimensions {
56 columns: usize,
57 screen_lines: usize,
58}
59
60impl TermDimensions {
61 fn new(columns: usize, screen_lines: usize) -> Self {
62 Self {
63 columns,
64 screen_lines,
65 }
66 }
67}
68
69impl Dimensions for TermDimensions {
70 fn total_lines(&self) -> usize {
71 // For initial setup, total lines equals screen lines
72 // The scrollback buffer will be managed by the Term itself
73 self.screen_lines
74 }
75
76 fn screen_lines(&self) -> usize {
77 self.screen_lines
78 }
79
80 fn columns(&self) -> usize {
81 self.columns
82 }
83
84 fn last_column(&self) -> alacritty_terminal::index::Column {
85 alacritty_terminal::index::Column(self.columns.saturating_sub(1))
86 }
87}
88
89/// Thread-safe terminal state wrapper.
90///
91/// This struct wraps alacritty's [`Term`] in an
92/// `Arc<parking_lot::Mutex<>>` to allow safe concurrent access from multiple threads.
93/// It also manages the VTE parser for processing incoming bytes from the PTY.
94///
95/// # Thread Safety
96///
97/// The terminal state can be safely shared across threads:
98///
99/// - Use [`term_arc`](Self::term_arc) to get a cloned `Arc` for sharing
100/// - Use [`with_term`](Self::with_term) for read access to the grid
101/// - Use [`with_term_mut`](Self::with_term_mut) for write access
102///
103/// The mutex is held only for the duration of the closure, minimizing contention.
104///
105/// # Grid Access
106///
107/// The terminal grid is accessed through the `Term` structure:
108///
109/// ```ignore
110/// terminal_state.with_term(|term| {
111/// let grid = term.grid();
112/// let cursor = grid.cursor.point;
113/// let cell = &grid[cursor];
114/// // Read cell content, colors, flags, etc.
115/// });
116/// ```
117///
118/// # Performance Notes
119///
120/// - `parking_lot::Mutex` is used for faster locking than `std::sync::Mutex`
121/// - Lock contention is minimized by keeping critical sections short
122/// - The VTE parser state is kept outside the mutex (only accessed from one thread)
123pub struct TerminalState {
124 /// The underlying alacritty terminal emulator.
125 term: Arc<Mutex<Term<GpuiEventProxy>>>,
126
127 /// VTE parser for converting byte streams into terminal actions.
128 parser: Processor,
129
130 /// Number of columns in the terminal.
131 cols: usize,
132
133 /// Number of rows (lines) in the terminal.
134 rows: usize,
135}
136
137impl TerminalState {
138 /// Create a new terminal state with the given dimensions.
139 ///
140 /// # Arguments
141 ///
142 /// * `cols` - The number of columns (character width) of the terminal
143 /// * `rows` - The number of rows (lines) of the terminal
144 /// * `event_proxy` - The event proxy for forwarding terminal events to GPUI
145 ///
146 /// # Returns
147 ///
148 /// A new `TerminalState` instance.
149 ///
150 /// # Examples
151 ///
152 /// ```
153 /// use std::sync::mpsc::channel;
154 /// use gpui_terminal::event::GpuiEventProxy;
155 /// use gpui_terminal::terminal::TerminalState;
156 ///
157 /// let (tx, rx) = channel();
158 /// let event_proxy = GpuiEventProxy::new(tx);
159 /// let terminal = TerminalState::new(80, 24, event_proxy);
160 /// ```
161 pub fn new(cols: usize, rows: usize, event_proxy: GpuiEventProxy) -> Self {
162 // Create a default configuration
163 // The Config struct controls various terminal behaviors like scrolling history
164 let config = Config::default();
165
166 // Create dimensions for terminal initialization
167 let dimensions = TermDimensions::new(cols, rows);
168
169 // Create the terminal with the given configuration and dimensions
170 let term = Term::new(config, &dimensions, event_proxy);
171
172 // Create the VTE parser for processing incoming bytes
173 let parser = Processor::new();
174
175 Self {
176 term: Arc::new(Mutex::new(term)),
177 parser,
178 cols,
179 rows,
180 }
181 }
182
183 /// Process incoming bytes from the PTY.
184 ///
185 /// This method feeds the bytes through the VTE parser, which will call
186 /// the appropriate handler methods on the terminal to update its state.
187 ///
188 /// # Arguments
189 ///
190 /// * `bytes` - The bytes received from the PTY
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// # use std::sync::mpsc::channel;
196 /// # use gpui_terminal::event::GpuiEventProxy;
197 /// # use gpui_terminal::terminal::TerminalState;
198 /// # let (tx, rx) = channel();
199 /// # let event_proxy = GpuiEventProxy::new(tx);
200 /// # let mut terminal = TerminalState::new(80, 24, event_proxy);
201 /// // Process some output from the PTY
202 /// terminal.process_bytes(b"Hello, world!\r\n");
203 /// ```
204 pub fn process_bytes(&mut self, bytes: &[u8]) {
205 let mut term = self.term.lock();
206 // The parser.advance method calls handler methods on the Term
207 // The Term implements the Handler trait from the VTE crate
208 self.parser.advance(&mut *term, bytes);
209 }
210
211 /// Resize the terminal to new dimensions.
212 ///
213 /// This method updates the terminal's internal grid to match the new size.
214 /// It should be called when the terminal view is resized.
215 ///
216 /// # Arguments
217 ///
218 /// * `cols` - The new number of columns
219 /// * `rows` - The new number of rows
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// # use std::sync::mpsc::channel;
225 /// # use gpui_terminal::event::GpuiEventProxy;
226 /// # use gpui_terminal::terminal::TerminalState;
227 /// # let (tx, rx) = channel();
228 /// # let event_proxy = GpuiEventProxy::new(tx);
229 /// # let mut terminal = TerminalState::new(80, 24, event_proxy);
230 /// // Resize to 120x30
231 /// terminal.resize(120, 30);
232 /// ```
233 pub fn resize(&mut self, cols: usize, rows: usize) {
234 self.cols = cols;
235 self.rows = rows;
236
237 let mut term = self.term.lock();
238
239 // Create dimensions for the resize
240 let dimensions = TermDimensions::new(cols, rows);
241
242 // Resize the terminal
243 term.resize(dimensions);
244 }
245
246 /// Get the current terminal mode.
247 ///
248 /// The terminal mode affects how certain key sequences are interpreted,
249 /// particularly arrow keys in application cursor mode.
250 ///
251 /// # Returns
252 ///
253 /// The current `TermMode` flags.
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// # use std::sync::mpsc::channel;
259 /// # use gpui_terminal::event::GpuiEventProxy;
260 /// # use gpui_terminal::terminal::TerminalState;
261 /// # let (tx, rx) = channel();
262 /// # let event_proxy = GpuiEventProxy::new(tx);
263 /// # let terminal = TerminalState::new(80, 24, event_proxy);
264 /// use alacritty_terminal::term::TermMode;
265 ///
266 /// let mode = terminal.mode();
267 /// if mode.contains(TermMode::APP_CURSOR) {
268 /// println!("Application cursor mode is enabled");
269 /// }
270 /// ```
271 pub fn mode(&self) -> TermMode {
272 let term = self.term.lock();
273 *term.mode()
274 }
275
276 /// Execute a function with read access to the terminal.
277 ///
278 /// This method provides safe read access to the underlying `Term` structure.
279 /// The terminal is locked for the duration of the function call.
280 ///
281 /// # Arguments
282 ///
283 /// * `f` - A function that takes a reference to the `Term` and returns a value
284 ///
285 /// # Returns
286 ///
287 /// The value returned by the function `f`.
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// # use std::sync::mpsc::channel;
293 /// # use gpui_terminal::event::GpuiEventProxy;
294 /// # use gpui_terminal::terminal::TerminalState;
295 /// # let (tx, rx) = channel();
296 /// # let event_proxy = GpuiEventProxy::new(tx);
297 /// # let terminal = TerminalState::new(80, 24, event_proxy);
298 /// let cursor_pos = terminal.with_term(|term| {
299 /// term.grid().cursor.point
300 /// });
301 /// ```
302 pub fn with_term<F, R>(&self, f: F) -> R
303 where
304 F: FnOnce(&Term<GpuiEventProxy>) -> R,
305 {
306 let term = self.term.lock();
307 f(&term)
308 }
309
310 /// Execute a function with mutable access to the terminal.
311 ///
312 /// This method provides safe write access to the underlying `Term` structure.
313 /// The terminal is locked for the duration of the function call.
314 ///
315 /// # Arguments
316 ///
317 /// * `f` - A function that takes a mutable reference to the `Term` and returns a value
318 ///
319 /// # Returns
320 ///
321 /// The value returned by the function `f`.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// # use std::sync::mpsc::channel;
327 /// # use gpui_terminal::event::GpuiEventProxy;
328 /// # use gpui_terminal::terminal::TerminalState;
329 /// # let (tx, rx) = channel();
330 /// # let event_proxy = GpuiEventProxy::new(tx);
331 /// # let terminal = TerminalState::new(80, 24, event_proxy);
332 /// terminal.with_term_mut(|term| {
333 /// // Perform some mutation on the term
334 /// term.scroll_display(alacritty_terminal::grid::Scroll::Delta(5));
335 /// });
336 /// ```
337 pub fn with_term_mut<F, R>(&self, f: F) -> R
338 where
339 F: FnOnce(&mut Term<GpuiEventProxy>) -> R,
340 {
341 let mut term = self.term.lock();
342 f(&mut term)
343 }
344
345 /// Get the number of columns in the terminal.
346 ///
347 /// # Returns
348 ///
349 /// The current number of columns.
350 pub fn cols(&self) -> usize {
351 self.cols
352 }
353
354 /// Get the number of rows in the terminal.
355 ///
356 /// # Returns
357 ///
358 /// The current number of rows.
359 pub fn rows(&self) -> usize {
360 self.rows
361 }
362
363 /// Get a cloned reference to the underlying terminal Arc.
364 ///
365 /// This allows sharing the terminal state across multiple threads or components.
366 ///
367 /// # Returns
368 ///
369 /// A cloned `Arc<Mutex<Term<GpuiEventProxy>>>`.
370 pub fn term_arc(&self) -> Arc<Mutex<Term<GpuiEventProxy>>> {
371 Arc::clone(&self.term)
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use std::sync::mpsc::channel;
379
380 #[test]
381 fn test_terminal_creation() {
382 let (tx, _rx) = channel();
383 let event_proxy = GpuiEventProxy::new(tx);
384 let terminal = TerminalState::new(80, 24, event_proxy);
385
386 assert_eq!(terminal.cols(), 80);
387 assert_eq!(terminal.rows(), 24);
388 }
389
390 #[test]
391 fn test_process_bytes() {
392 let (tx, _rx) = channel();
393 let event_proxy = GpuiEventProxy::new(tx);
394 let mut terminal = TerminalState::new(80, 24, event_proxy);
395
396 // Process some text
397 terminal.process_bytes(b"Hello, world!");
398
399 // Verify the text was written to the grid
400 terminal.with_term(|term| {
401 let grid = term.grid();
402 // The text should be at the cursor position
403 // We can't easily test the exact content without more complex grid inspection
404 assert!(grid.columns() == 80);
405 });
406 }
407
408 #[test]
409 fn test_resize() {
410 let (tx, _rx) = channel();
411 let event_proxy = GpuiEventProxy::new(tx);
412 let mut terminal = TerminalState::new(80, 24, event_proxy);
413
414 terminal.resize(120, 30);
415
416 assert_eq!(terminal.cols(), 120);
417 assert_eq!(terminal.rows(), 30);
418
419 terminal.with_term(|term| {
420 let grid = term.grid();
421 assert_eq!(grid.columns(), 120);
422 assert_eq!(grid.screen_lines(), 30);
423 });
424 }
425
426 #[test]
427 fn test_mode() {
428 let (tx, _rx) = channel();
429 let event_proxy = GpuiEventProxy::new(tx);
430 let terminal = TerminalState::new(80, 24, event_proxy);
431
432 let mode = terminal.mode();
433 // Mode should be a valid TermMode value (just verify we can get it)
434 let _bits = mode.bits();
435 }
436
437 #[test]
438 fn test_with_term() {
439 let (tx, _rx) = channel();
440 let event_proxy = GpuiEventProxy::new(tx);
441 let terminal = TerminalState::new(80, 24, event_proxy);
442
443 let cols = terminal.with_term(|term| term.grid().columns());
444 assert_eq!(cols, 80);
445 }
446
447 #[test]
448 fn test_with_term_mut() {
449 let (tx, _rx) = channel();
450 let event_proxy = GpuiEventProxy::new(tx);
451 let terminal = TerminalState::new(80, 24, event_proxy);
452
453 terminal.with_term_mut(|term| {
454 // Just verify we can get mutable access
455 let _grid = term.grid_mut();
456 });
457 }
458
459 #[test]
460 fn test_term_arc() {
461 let (tx, _rx) = channel();
462 let event_proxy = GpuiEventProxy::new(tx);
463 let terminal = TerminalState::new(80, 24, event_proxy);
464
465 let arc1 = terminal.term_arc();
466 let arc2 = terminal.term_arc();
467
468 // Both Arcs should point to the same terminal
469 assert!(Arc::ptr_eq(&arc1, &arc2));
470 }
471}