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}