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.