gpui_terminal/
view.rs

1//! Main terminal view component for GPUI.
2//!
3//! This module provides [`TerminalView`], the primary component for embedding terminals
4//! in GPUI applications. It manages:
5//!
6//! - **I/O Streams**: Accepts arbitrary [`Read`]/[`Write`]
7//!   streams, allowing integration with any PTY implementation
8//! - **Event Handling**: Keyboard and mouse input, with configurable callbacks
9//! - **Rendering**: Efficient canvas-based rendering via [`TerminalRenderer`]
10//! - **Configuration**: Font, colors, dimensions, and padding via [`TerminalConfig`]
11//!
12//! # Architecture
13//!
14//! The terminal uses a push-based async I/O architecture:
15//!
16//! 1. A background thread reads bytes from the PTY stdout in 4KB chunks
17//! 2. Bytes are sent through a [flume](https://docs.rs/flume) channel to an async task
18//! 3. The async task processes bytes through the VTE parser and calls `cx.notify()`
19//! 4. GPUI repaints the terminal with the updated grid
20//!
21//! This approach ensures the terminal only wakes when data arrives, avoiding polling.
22//!
23//! # Thread Safety
24//!
25//! - [`TerminalView`] itself is not `Send` (it contains GPUI handles)
26//! - The stdin writer is wrapped in `Arc<parking_lot::Mutex<>>` for thread-safe writes
27//! - Callbacks ([`ResizeCallback`], [`KeyHandler`]) must be `Send + Sync`
28//!
29//! # Example
30//!
31//! ```ignore
32//! use gpui::{Context, Edges, px};
33//! use gpui_terminal::{ColorPalette, TerminalConfig, TerminalView};
34//!
35//! // In a GPUI window context:
36//! let terminal = cx.new(|cx| {
37//!     TerminalView::new(pty_writer, pty_reader, TerminalConfig::default(), cx)
38//!         .with_resize_callback(move |cols, rows| {
39//!             // Notify PTY of new dimensions
40//!         })
41//!         .with_exit_callback(|_, cx| {
42//!             cx.quit();
43//!         })
44//! });
45//!
46//! // Focus the terminal to receive keyboard input
47//! terminal.read(cx).focus_handle().focus(window);
48//! ```
49
50use crate::colors::ColorPalette;
51use crate::event::{GpuiEventProxy, TerminalEvent};
52use crate::input::keystroke_to_bytes;
53use crate::render::TerminalRenderer;
54use crate::terminal::TerminalState;
55use gpui::{Edges, *};
56use std::io::{Read, Write};
57use std::sync::Arc;
58use std::sync::mpsc;
59use std::thread;
60
61/// Configuration for terminal creation and runtime updates.
62///
63/// This struct defines the terminal's appearance and behavior, including
64/// grid dimensions, font settings, scrollback buffer, and color scheme.
65///
66/// # Default Values
67///
68/// | Field | Default |
69/// |-------|---------|
70/// | `cols` | 80 |
71/// | `rows` | 24 |
72/// | `font_family` | "monospace" |
73/// | `font_size` | 14px |
74/// | `scrollback` | 10000 |
75/// | `line_height_multiplier` | 1.2 |
76/// | `padding` | 0px all sides |
77/// | `colors` | Default palette |
78///
79/// # Example
80///
81/// ```ignore
82/// use gpui::{Edges, px};
83/// use gpui_terminal::{ColorPalette, TerminalConfig};
84///
85/// let config = TerminalConfig {
86///     cols: 120,
87///     rows: 40,
88///     font_family: "JetBrains Mono".into(),
89///     font_size: px(13.0),
90///     scrollback: 50000,
91///     line_height_multiplier: 1.1,
92///     padding: Edges::all(px(10.0)),
93///     colors: ColorPalette::builder()
94///         .background(0x1a, 0x1a, 0x1a)
95///         .foreground(0xe0, 0xe0, 0xe0)
96///         .build(),
97/// };
98/// ```
99///
100/// # Runtime Updates
101///
102/// Configuration can be updated at runtime via [`TerminalView::update_config`].
103/// This is useful for implementing features like dynamic font sizing:
104///
105/// ```ignore
106/// terminal.update(cx, |terminal, cx| {
107///     let mut config = terminal.config().clone();
108///     config.font_size += px(1.0);
109///     terminal.update_config(config, cx);
110/// });
111/// ```
112#[derive(Clone, Debug)]
113pub struct TerminalConfig {
114    /// Number of columns (character width) in the terminal
115    pub cols: usize,
116
117    /// Number of rows (lines) in the terminal
118    pub rows: usize,
119
120    /// Font family name (e.g., "Fira Code", "JetBrains Mono")
121    pub font_family: String,
122
123    /// Font size in pixels
124    pub font_size: Pixels,
125
126    /// Maximum number of scrollback lines to keep in history
127    pub scrollback: usize,
128
129    /// Multiplier for line height to accommodate tall glyphs (e.g., nerd fonts)
130    /// Default is 1.2 (20% extra height)
131    pub line_height_multiplier: f32,
132
133    /// Padding around the terminal content (top, right, bottom, left)
134    /// The padding area renders with the terminal's background color
135    pub padding: Edges<Pixels>,
136
137    /// Color palette for terminal colors (16 ANSI colors, 256 extended colors,
138    /// foreground, background, and cursor colors)
139    pub colors: ColorPalette,
140}
141
142impl Default for TerminalConfig {
143    fn default() -> Self {
144        Self {
145            cols: 80,
146            rows: 24,
147            font_family: "monospace".into(),
148            font_size: px(14.0),
149            scrollback: 10000,
150            line_height_multiplier: 1.2,
151            padding: Edges::all(px(0.0)),
152            colors: ColorPalette::default(),
153        }
154    }
155}
156
157/// Callback type for PTY resize notifications.
158///
159/// This callback is invoked when the terminal grid dimensions change,
160/// typically due to window resizing. The callback receives the new
161/// column and row counts.
162///
163/// # Arguments
164///
165/// * `cols` - New number of columns (characters wide)
166/// * `rows` - New number of rows (lines tall)
167///
168/// # Thread Safety
169///
170/// This callback must be `Send + Sync` as it may be called from the render thread.
171///
172/// # Example
173///
174/// ```ignore
175/// use portable_pty::PtySize;
176///
177/// let pty = Arc::new(Mutex::new(pty_master));
178/// let pty_clone = pty.clone();
179///
180/// terminal.with_resize_callback(move |cols, rows| {
181///     pty_clone.lock().resize(PtySize {
182///         cols: cols as u16,
183///         rows: rows as u16,
184///         pixel_width: 0,
185///         pixel_height: 0,
186///     }).ok();
187/// });
188/// ```
189pub type ResizeCallback = Box<dyn Fn(usize, usize) + Send + Sync>;
190
191/// Callback type for key event interception.
192///
193/// This callback is invoked before the terminal processes a key event,
194/// allowing you to intercept and handle specific key combinations.
195///
196/// # Arguments
197///
198/// * `event` - The key down event from GPUI
199///
200/// # Returns
201///
202/// * `true` - Consume the event (terminal will not process it)
203/// * `false` - Let the terminal handle the event normally
204///
205/// # Thread Safety
206///
207/// This callback must be `Send + Sync`.
208///
209/// # Example
210///
211/// ```ignore
212/// terminal.with_key_handler(|event| {
213///     let keystroke = &event.keystroke;
214///
215///     // Intercept Ctrl++ for font size increase
216///     if keystroke.modifiers.control && (keystroke.key == "+" || keystroke.key == "=") {
217///         // Handle font size increase
218///         return true; // Consume the event
219///     }
220///
221///     // Intercept Ctrl+- for font size decrease
222///     if keystroke.modifiers.control && keystroke.key == "-" {
223///         // Handle font size decrease
224///         return true;
225///     }
226///
227///     false // Let terminal handle all other keys
228/// });
229/// ```
230pub type KeyHandler = Box<dyn Fn(&KeyDownEvent) -> bool + Send + Sync>;
231
232/// Callback for terminal bell events.
233///
234/// This callback is invoked when the terminal bell is triggered (BEL character,
235/// ASCII 0x07), allowing you to play a sound or show a visual indicator.
236///
237/// # Arguments
238///
239/// * `window` - The GPUI window
240/// * `cx` - The context for the TerminalView
241///
242/// # Example
243///
244/// ```ignore
245/// terminal.with_bell_callback(|window, cx| {
246///     // Option 1: Visual bell (flash the window or show an indicator)
247///     // Option 2: Play a sound
248///     // Option 3: Notify the user via system notification
249/// });
250/// ```
251pub type BellCallback = Box<dyn Fn(&mut Window, &mut Context<TerminalView>)>;
252
253/// Callback for terminal title changes.
254///
255/// This callback is invoked when the terminal title changes via escape sequences
256/// (OSC 0, OSC 2), allowing you to update the window or tab title.
257///
258/// # Arguments
259///
260/// * `window` - The GPUI window
261/// * `cx` - The context for the TerminalView
262/// * `title` - The new title string
263///
264/// # Example
265///
266/// ```ignore
267/// terminal.with_title_callback(|window, cx, title| {
268///     // Update the window title
269///     // Or update a tab label in a tabbed interface
270///     println!("Terminal title changed to: {}", title);
271/// });
272/// ```
273pub type TitleCallback = Box<dyn Fn(&mut Window, &mut Context<TerminalView>, &str)>;
274
275/// Callback for clipboard store requests.
276///
277/// This callback is invoked when the terminal wants to store data to the clipboard
278/// via OSC 52 escape sequence. Applications like tmux and vim can use this to
279/// copy text to the system clipboard.
280///
281/// # Arguments
282///
283/// * `window` - The GPUI window
284/// * `cx` - The context for the TerminalView
285/// * `text` - The text to store in the clipboard
286///
287/// # Example
288///
289/// ```ignore
290/// use gpui_terminal::Clipboard;
291///
292/// terminal.with_clipboard_store_callback(|window, cx, text| {
293///     if let Ok(mut clipboard) = Clipboard::new() {
294///         clipboard.copy(text).ok();
295///     }
296/// });
297/// ```
298pub type ClipboardStoreCallback = Box<dyn Fn(&mut Window, &mut Context<TerminalView>, &str)>;
299
300/// Callback for terminal exit events.
301///
302/// This callback is invoked when the terminal process exits (e.g., shell exits,
303/// process terminates). This is detected when the PTY reader reaches EOF.
304///
305/// # Arguments
306///
307/// * `window` - The GPUI window
308/// * `cx` - The context for the TerminalView
309///
310/// # Example
311///
312/// ```ignore
313/// terminal.with_exit_callback(|window, cx| {
314///     // Option 1: Quit the application
315///     cx.quit();
316///
317///     // Option 2: Close this terminal tab/pane
318///     // terminal_manager.close_terminal(terminal_id);
319///
320///     // Option 3: Show an exit message
321///     // show_notification("Terminal exited");
322/// });
323/// ```
324pub type ExitCallback = Box<dyn Fn(&mut Window, &mut Context<TerminalView>)>;
325
326/// The main terminal view component for GPUI applications.
327///
328/// `TerminalView` is a GPUI entity that implements the [`Render`] trait,
329/// providing a complete terminal emulator that can be embedded in any GPUI application.
330///
331/// # Responsibilities
332///
333/// - **Terminal State**: Manages the grid, cursor, and colors via [`TerminalState`]
334/// - **I/O Streams**: Reads from PTY stdout and writes to PTY stdin
335/// - **Event Handling**: Processes keyboard, mouse, and resize events
336/// - **Rendering**: Paints text, backgrounds, and cursor via [`TerminalRenderer`]
337/// - **Callbacks**: Dispatches events to user-provided callbacks
338///
339/// # Creating a Terminal
340///
341/// Use [`TerminalView::new`] within a GPUI entity context:
342///
343/// ```ignore
344/// let terminal = cx.new(|cx| {
345///     TerminalView::new(writer, reader, config, cx)
346///         .with_resize_callback(resize_callback)
347///         .with_exit_callback(|_, cx| cx.quit())
348/// });
349/// ```
350///
351/// # Focus
352///
353/// The terminal must be focused to receive keyboard input:
354///
355/// ```ignore
356/// terminal.read(cx).focus_handle().focus(window);
357/// ```
358///
359/// # Callbacks
360///
361/// Configure behavior through builder methods:
362///
363/// - [`with_resize_callback`](Self::with_resize_callback) - PTY size changes
364/// - [`with_exit_callback`](Self::with_exit_callback) - Process exit
365/// - [`with_key_handler`](Self::with_key_handler) - Key event interception
366/// - [`with_bell_callback`](Self::with_bell_callback) - Terminal bell
367/// - [`with_title_callback`](Self::with_title_callback) - Title changes
368/// - [`with_clipboard_store_callback`](Self::with_clipboard_store_callback) - Clipboard writes
369///
370/// # Thread Safety
371///
372/// `TerminalView` is not `Send` as it contains GPUI handles. The stdin writer
373/// is internally wrapped in `Arc<parking_lot::Mutex<>>` for safe concurrent access.
374pub struct TerminalView {
375    /// The terminal state managing the grid and VTE parser
376    state: TerminalState,
377
378    /// The renderer for drawing terminal content
379    renderer: TerminalRenderer,
380
381    /// Focus handle for keyboard event handling
382    focus_handle: FocusHandle,
383
384    /// Writer for sending input to the terminal process
385    stdin_writer: Arc<parking_lot::Mutex<Box<dyn Write + Send>>>,
386
387    /// Receiver for terminal events from the event proxy
388    event_rx: mpsc::Receiver<TerminalEvent>,
389
390    /// Configuration used to create this terminal
391    config: TerminalConfig,
392
393    /// Async task that reads bytes and notifies the view (push-based)
394    #[allow(dead_code)]
395    _reader_task: Task<()>,
396
397    /// Callback to notify the PTY about size changes
398    resize_callback: Option<Arc<ResizeCallback>>,
399
400    /// Optional callback to intercept key events before terminal processing
401    key_handler: Option<Arc<KeyHandler>>,
402
403    /// Callback for terminal bell events
404    bell_callback: Option<BellCallback>,
405
406    /// Callback for terminal title changes
407    title_callback: Option<TitleCallback>,
408
409    /// Callback for clipboard store requests
410    clipboard_store_callback: Option<ClipboardStoreCallback>,
411
412    /// Callback for terminal exit events
413    exit_callback: Option<ExitCallback>,
414}
415
416impl TerminalView {
417    /// Create a new terminal with provided I/O streams.
418    ///
419    /// This method initializes a new terminal emulator with the given stdin writer
420    /// and stdout reader. It spawns a background task to read from stdout and
421    /// process incoming bytes through the VTE parser.
422    ///
423    /// # Arguments
424    ///
425    /// * `stdin_writer` - Writer for sending input bytes to the terminal process
426    /// * `stdout_reader` - Reader for receiving output bytes from the terminal process
427    /// * `config` - Terminal configuration (dimensions, font, etc.)
428    /// * `cx` - GPUI context for this view
429    ///
430    /// # Returns
431    ///
432    /// A new `TerminalView` instance ready to be rendered.
433    ///
434    /// # Examples
435    ///
436    /// ```ignore
437    /// // In a GPUI window context:
438    /// let terminal = cx.new(|cx| {
439    ///     TerminalView::new(stdin_writer, stdout_reader, TerminalConfig::default(), cx)
440    /// });
441    /// ```
442    pub fn new<W, R>(
443        stdin_writer: W,
444        stdout_reader: R,
445        config: TerminalConfig,
446        cx: &mut Context<Self>,
447    ) -> Self
448    where
449        W: Write + Send + 'static,
450        R: Read + Send + 'static,
451    {
452        // Create event channel for terminal events
453        let (event_tx, event_rx) = mpsc::channel();
454
455        // Clone event_tx for the reader task to send Exit event when PTY closes
456        let exit_event_tx = event_tx.clone();
457
458        // Create event proxy for alacritty
459        let event_proxy = GpuiEventProxy::new(event_tx);
460
461        // Create terminal state
462        let state = TerminalState::new(config.cols, config.rows, event_proxy);
463
464        // Create renderer with font settings and color palette
465        let renderer = TerminalRenderer::new(
466            config.font_family.clone(),
467            config.font_size,
468            config.line_height_multiplier,
469            config.colors.clone(),
470        );
471
472        // Create focus handle
473        let focus_handle = cx.focus_handle();
474
475        // Wrap stdin writer in Arc<Mutex> for thread-safe access
476        let stdin_writer = Arc::new(parking_lot::Mutex::new(
477            Box::new(stdin_writer) as Box<dyn Write + Send>
478        ));
479
480        // Create async channel for bytes (push-based notification)
481        // Using flume instead of smol::channel because flume is executor-agnostic
482        // and properly wakes GPUI's async executor when data arrives
483        let (bytes_tx, bytes_rx) = flume::unbounded::<Vec<u8>>();
484
485        // Spawn background thread to read from stdout
486        // This thread sends bytes through the async channel
487        thread::spawn(move || {
488            Self::read_stdout_blocking(stdout_reader, bytes_tx);
489        });
490
491        // Spawn async task that awaits on the channel and notifies the view
492        // This is push-based: the task blocks until bytes arrive, then immediately notifies
493        let reader_task = cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
494            loop {
495                // Wait for bytes from the background reader (blocks until data arrives)
496                match bytes_rx.recv_async().await {
497                    Ok(bytes) => {
498                        // Process bytes and notify the view
499                        let result = this.update(cx, |view: &mut Self, cx: &mut Context<Self>| {
500                            view.state.process_bytes(&bytes);
501                            cx.notify();
502                        });
503                        if result.is_err() {
504                            // View was dropped, exit
505                            break;
506                        }
507                    }
508                    Err(_) => {
509                        // Channel closed - PTY has finished, send Exit event
510                        let _ = exit_event_tx.send(TerminalEvent::Exit);
511                        // Notify view to process the Exit event
512                        let _ = this.update(cx, |_view, cx: &mut Context<Self>| {
513                            cx.notify();
514                        });
515                        break;
516                    }
517                }
518            }
519        });
520
521        Self {
522            state,
523            renderer,
524            focus_handle,
525            stdin_writer,
526            event_rx,
527            config,
528            _reader_task: reader_task,
529            resize_callback: None,
530            key_handler: None,
531            bell_callback: None,
532            title_callback: None,
533            clipboard_store_callback: None,
534            exit_callback: None,
535        }
536    }
537
538    /// Set a callback to be invoked when the terminal is resized.
539    ///
540    /// This callback should resize the underlying PTY to match the new dimensions.
541    /// The callback receives (cols, rows) as arguments.
542    ///
543    /// # Arguments
544    ///
545    /// * `callback` - A function that will be called with (cols, rows) on resize
546    pub fn with_resize_callback(
547        mut self,
548        callback: impl Fn(usize, usize) + Send + Sync + 'static,
549    ) -> Self {
550        self.resize_callback = Some(Arc::new(Box::new(callback)));
551        self
552    }
553
554    /// Set a callback to intercept key events before terminal processing.
555    ///
556    /// The callback receives the key event and should return `true` to consume
557    /// the event (prevent the terminal from processing it), or `false` to allow
558    /// normal terminal processing.
559    ///
560    /// # Arguments
561    ///
562    /// * `handler` - A function that receives key events and returns whether to consume them
563    ///
564    /// # Example
565    ///
566    /// ```ignore
567    /// terminal.with_key_handler(|event| {
568    ///     // Handle Ctrl++ to increase font size
569    ///     if event.keystroke.modifiers.control && event.keystroke.key == "+" {
570    ///         // Handle the event
571    ///         return true; // Consume the event
572    ///     }
573    ///     false // Let terminal handle it
574    /// })
575    /// ```
576    pub fn with_key_handler(
577        mut self,
578        handler: impl Fn(&KeyDownEvent) -> bool + Send + Sync + 'static,
579    ) -> Self {
580        self.key_handler = Some(Arc::new(Box::new(handler)));
581        self
582    }
583
584    /// Set a callback to be invoked when the terminal bell is triggered.
585    ///
586    /// The callback receives a mutable reference to the window and context,
587    /// allowing you to play a sound or show a visual indicator.
588    ///
589    /// # Arguments
590    ///
591    /// * `callback` - A function that will be called when the bell is triggered
592    ///
593    /// # Example
594    ///
595    /// ```ignore
596    /// terminal.with_bell_callback(|window, cx| {
597    ///     // Play a sound or flash the screen
598    /// })
599    /// ```
600    pub fn with_bell_callback(
601        mut self,
602        callback: impl Fn(&mut Window, &mut Context<TerminalView>) + 'static,
603    ) -> Self {
604        self.bell_callback = Some(Box::new(callback));
605        self
606    }
607
608    /// Set a callback to be invoked when the terminal title changes.
609    ///
610    /// The callback receives a mutable reference to the window and context,
611    /// along with the new title string.
612    ///
613    /// # Arguments
614    ///
615    /// * `callback` - A function that will be called with the new title
616    ///
617    /// # Example
618    ///
619    /// ```ignore
620    /// terminal.with_title_callback(|window, cx, title| {
621    ///     // Update window title or tab title
622    /// })
623    /// ```
624    pub fn with_title_callback(
625        mut self,
626        callback: impl Fn(&mut Window, &mut Context<TerminalView>, &str) + 'static,
627    ) -> Self {
628        self.title_callback = Some(Box::new(callback));
629        self
630    }
631
632    /// Set a callback to be invoked when the terminal wants to store data to the clipboard.
633    ///
634    /// The callback receives a mutable reference to the window and context,
635    /// along with the text to store. This is typically triggered by OSC 52 escape sequences.
636    ///
637    /// # Arguments
638    ///
639    /// * `callback` - A function that will be called with the text to store
640    ///
641    /// # Example
642    ///
643    /// ```ignore
644    /// terminal.with_clipboard_store_callback(|window, cx, text| {
645    ///     // Store text to system clipboard
646    /// })
647    /// ```
648    pub fn with_clipboard_store_callback(
649        mut self,
650        callback: impl Fn(&mut Window, &mut Context<TerminalView>, &str) + 'static,
651    ) -> Self {
652        self.clipboard_store_callback = Some(Box::new(callback));
653        self
654    }
655
656    /// Set a callback to be invoked when the terminal process exits.
657    ///
658    /// The callback receives a mutable reference to the window and context,
659    /// allowing you to close the terminal view or show an exit message.
660    ///
661    /// # Arguments
662    ///
663    /// * `callback` - A function that will be called when the process exits
664    ///
665    /// # Example
666    ///
667    /// ```ignore
668    /// terminal.with_exit_callback(|window, cx| {
669    ///     // Close the terminal tab or show exit message
670    /// })
671    /// ```
672    pub fn with_exit_callback(
673        mut self,
674        callback: impl Fn(&mut Window, &mut Context<TerminalView>) + 'static,
675    ) -> Self {
676        self.exit_callback = Some(Box::new(callback));
677        self
678    }
679
680    /// Background thread that reads from stdout.
681    ///
682    /// This function runs in a background thread, continuously reading bytes
683    /// from the stdout reader and sending them through the async channel.
684    /// The async channel allows the main async task to be woken up immediately
685    /// when data arrives (push-based).
686    fn read_stdout_blocking<R: Read + Send + 'static>(
687        mut stdout_reader: R,
688        bytes_tx: flume::Sender<Vec<u8>>,
689    ) {
690        let mut buffer = [0u8; 4096];
691
692        loop {
693            match stdout_reader.read(&mut buffer) {
694                Ok(0) => {
695                    // EOF - channel will be dropped, signaling completion
696                    break;
697                }
698                Ok(n) => {
699                    // Send bytes to the async task
700                    let bytes = buffer[..n].to_vec();
701                    if bytes_tx.send(bytes).is_err() {
702                        break; // Channel closed
703                    }
704                }
705                Err(_) => {
706                    // Read error
707                    break;
708                }
709            }
710        }
711    }
712
713    /// Handle keyboard input events.
714    ///
715    /// Converts GPUI keystrokes to terminal escape sequences and writes them
716    /// to the stdin writer. If a key handler is set and returns true, the event
717    /// is consumed and not sent to the terminal.
718    fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, _cx: &mut Context<Self>) {
719        // Check if key handler wants to consume this event
720        if let Some(ref handler) = self.key_handler
721            && handler(event)
722        {
723            return; // Event consumed by handler
724        }
725
726        if let Some(bytes) = keystroke_to_bytes(&event.keystroke, self.state.mode()) {
727            let mut writer = self.stdin_writer.lock();
728            let _ = writer.write_all(&bytes);
729            let _ = writer.flush();
730        }
731    }
732
733    /// Handle mouse down events.
734    ///
735    /// Currently a placeholder for future mouse selection and interaction support.
736    fn on_mouse_down(
737        &mut self,
738        _event: &MouseDownEvent,
739        window: &mut Window,
740        cx: &mut Context<Self>,
741    ) {
742        // Request focus when clicking the terminal
743        window.focus(&self.focus_handle);
744        cx.notify();
745
746        // TODO: Implement mouse selection
747        // - Convert pixel coordinates to cell coordinates
748        // - Start selection at clicked cell
749        // - Send mouse reports if mouse tracking is enabled
750    }
751
752    /// Handle mouse up events.
753    ///
754    /// Currently a placeholder for future mouse selection support.
755    fn on_mouse_up(
756        &mut self,
757        _event: &MouseUpEvent,
758        _window: &mut Window,
759        _cx: &mut Context<Self>,
760    ) {
761        // TODO: Implement mouse selection
762        // - End selection at released cell
763        // - Copy selection to clipboard if configured
764    }
765
766    /// Handle mouse move events.
767    ///
768    /// Currently a placeholder for future mouse selection support.
769    fn on_mouse_move(
770        &mut self,
771        _event: &MouseMoveEvent,
772        _window: &mut Window,
773        _cx: &mut Context<Self>,
774    ) {
775        // TODO: Implement mouse selection
776        // - Update selection range while dragging
777        // - Send mouse motion reports if mouse tracking is enabled
778    }
779
780    /// Handle scroll events.
781    ///
782    /// Currently a placeholder for future scrollback support.
783    fn on_scroll(
784        &mut self,
785        _event: &ScrollWheelEvent,
786        _window: &mut Window,
787        _cx: &mut Context<Self>,
788    ) {
789        // TODO: Implement scrollback
790        // - Scroll the terminal display up/down
791        // - Send scroll reports if alternate screen is not active
792    }
793
794    /// Process pending terminal events.
795    ///
796    /// This method drains all available events from the event receiver
797    /// and handles them appropriately. Note: bytes are processed in the
798    /// async reader task, not here.
799    fn process_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
800        // Process terminal events (from alacritty event proxy)
801        while let Ok(event) = self.event_rx.try_recv() {
802            match event {
803                TerminalEvent::Wakeup => {
804                    // Terminal has new content - already handled by async task
805                }
806                TerminalEvent::Bell => {
807                    if let Some(ref callback) = self.bell_callback {
808                        callback(window, cx);
809                    }
810                }
811                TerminalEvent::Title(title) => {
812                    if let Some(ref callback) = self.title_callback {
813                        callback(window, cx, &title);
814                    }
815                }
816                TerminalEvent::ClipboardStore(text) => {
817                    if let Some(ref callback) = self.clipboard_store_callback {
818                        callback(window, cx, &text);
819                    }
820                }
821                TerminalEvent::ClipboardLoad => {
822                    // Terminal wants to load data from clipboard
823                    // TODO: Implement clipboard integration
824                }
825                TerminalEvent::Exit => {
826                    if let Some(ref callback) = self.exit_callback {
827                        callback(window, cx);
828                    }
829                }
830            }
831        }
832    }
833
834    /// Get the current terminal dimensions.
835    ///
836    /// # Returns
837    ///
838    /// A tuple of (columns, rows).
839    pub fn dimensions(&self) -> (usize, usize) {
840        (self.state.cols(), self.state.rows())
841    }
842
843    /// Resize the terminal to new dimensions.
844    ///
845    /// This method should be called when the terminal view size changes.
846    /// It updates the internal grid and notifies the terminal process of the new size.
847    ///
848    /// # Arguments
849    ///
850    /// * `cols` - New number of columns
851    /// * `rows` - New number of rows
852    pub fn resize(&mut self, cols: usize, rows: usize) {
853        self.state.resize(cols, rows);
854    }
855
856    /// Get the current terminal configuration.
857    ///
858    /// # Returns
859    ///
860    /// A reference to the current configuration.
861    pub fn config(&self) -> &TerminalConfig {
862        &self.config
863    }
864
865    /// Get the focus handle for this terminal view.
866    ///
867    /// # Returns
868    ///
869    /// A reference to the focus handle.
870    pub fn focus_handle(&self) -> &FocusHandle {
871        &self.focus_handle
872    }
873
874    /// Update the terminal configuration.
875    ///
876    /// This method updates the terminal's configuration, including font settings,
877    /// padding, and color palette. Changes take effect on the next render.
878    ///
879    /// # Arguments
880    ///
881    /// * `config` - The new configuration to apply
882    /// * `cx` - The context for triggering a repaint
883    pub fn update_config(&mut self, config: TerminalConfig, cx: &mut Context<Self>) {
884        // Update renderer with new font settings and palette
885        self.renderer.font_family = config.font_family.clone();
886        self.renderer.font_size = config.font_size;
887        self.renderer.line_height_multiplier = config.line_height_multiplier;
888        self.renderer.palette = config.colors.clone();
889
890        // Store the new config
891        self.config = config;
892
893        // Trigger a repaint - cell dimensions will be recalculated via measure_cell()
894        cx.notify();
895    }
896
897    /// Calculate terminal dimensions from pixel bounds and cell size.
898    ///
899    /// Helper method to determine how many columns and rows fit in the given bounds.
900    #[allow(dead_code)]
901    fn calculate_dimensions(&self, bounds: Bounds<Pixels>) -> (usize, usize) {
902        let width_f32: f32 = bounds.size.width.into();
903        let height_f32: f32 = bounds.size.height.into();
904        let cell_width_f32: f32 = self.renderer.cell_width.into();
905        let cell_height_f32: f32 = self.renderer.cell_height.into();
906
907        let cols = ((width_f32 / cell_width_f32) as usize).max(1);
908        let rows = ((height_f32 / cell_height_f32) as usize).max(1);
909        (cols, rows)
910    }
911}
912
913impl Render for TerminalView {
914    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
915        // Process any pending events
916        self.process_events(window, cx);
917
918        // Get terminal state and renderer for rendering
919        let state_arc = self.state.term_arc();
920        let renderer = self.renderer.clone();
921        let resize_callback = self.resize_callback.clone();
922        let padding = self.config.padding;
923
924        div()
925            .size_full()
926            .bg(rgb(0x1e1e1e))
927            .track_focus(&self.focus_handle)
928            .on_key_down(cx.listener(Self::on_key_down))
929            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
930            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
931            .on_mouse_move(cx.listener(Self::on_mouse_move))
932            .on_scroll_wheel(cx.listener(Self::on_scroll))
933            .child(
934                canvas(
935                    move |bounds, _window, _cx| bounds,
936                    move |bounds, _, window, cx| {
937                        use alacritty_terminal::grid::Dimensions;
938
939                        // Measure actual cell dimensions from the font
940                        let mut measured_renderer = renderer.clone();
941                        measured_renderer.measure_cell(window);
942
943                        // Calculate available space after padding
944                        let available_width: f32 =
945                            (bounds.size.width - padding.left - padding.right).into();
946                        let available_height: f32 =
947                            (bounds.size.height - padding.top - padding.bottom).into();
948                        let cell_width_f32: f32 = measured_renderer.cell_width.into();
949                        let cell_height_f32: f32 = measured_renderer.cell_height.into();
950
951                        let cols = ((available_width / cell_width_f32) as usize).max(1);
952                        let rows = ((available_height / cell_height_f32) as usize).max(1);
953
954                        // Helper struct implementing Dimensions for resize
955                        struct TermSize {
956                            cols: usize,
957                            rows: usize,
958                        }
959                        impl Dimensions for TermSize {
960                            fn total_lines(&self) -> usize {
961                                self.rows
962                            }
963                            fn screen_lines(&self) -> usize {
964                                self.rows
965                            }
966                            fn columns(&self) -> usize {
967                                self.cols
968                            }
969                            fn last_column(&self) -> alacritty_terminal::index::Column {
970                                alacritty_terminal::index::Column(self.cols.saturating_sub(1))
971                            }
972                            fn bottommost_line(&self) -> alacritty_terminal::index::Line {
973                                alacritty_terminal::index::Line(self.rows as i32 - 1)
974                            }
975                            fn topmost_line(&self) -> alacritty_terminal::index::Line {
976                                alacritty_terminal::index::Line(0)
977                            }
978                        }
979
980                        // Resize terminal if dimensions changed
981                        let mut term = state_arc.lock();
982                        let current_cols = term.columns();
983                        let current_rows = term.screen_lines();
984                        if cols != current_cols || rows != current_rows {
985                            // Notify the PTY about the resize
986                            if let Some(ref callback) = resize_callback {
987                                callback(cols, rows);
988                            }
989                            term.resize(TermSize { cols, rows });
990                        }
991
992                        // Paint the terminal with measured dimensions
993                        measured_renderer.paint(bounds, padding, &term, window, cx);
994                    },
995                )
996                .size_full(),
997            )
998    }
999}
1000
1001// Tests are omitted due to macro expansion issues with the test attribute
1002// in this configuration. Integration tests can be added separately.