Skip to main content

freya_terminal/
handle.rs

1use std::{
2    cell::{
3        Ref,
4        RefCell,
5    },
6    io::Write,
7    path::PathBuf,
8    rc::Rc,
9    time::Instant,
10};
11
12use freya_core::{
13    notify::ArcNotify,
14    prelude::{
15        Platform,
16        TaskHandle,
17        UseId,
18        UserEvent,
19    },
20};
21use keyboard_types::{
22    Key,
23    Modifiers,
24    NamedKey,
25};
26use portable_pty::{
27    MasterPty,
28    PtySize,
29};
30use vt100::Parser;
31
32use crate::{
33    buffer::{
34        TerminalBuffer,
35        TerminalSelection,
36    },
37    parser::{
38        TerminalMouseButton,
39        encode_mouse_move,
40        encode_mouse_press,
41        encode_mouse_release,
42        encode_wheel_event,
43    },
44    pty::{
45        extract_buffer,
46        query_max_scrollback,
47        spawn_pty,
48    },
49};
50
51/// Unique identifier for a terminal instance
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct TerminalId(pub usize);
54
55impl TerminalId {
56    pub fn new() -> Self {
57        Self(UseId::<TerminalId>::get_in_hook())
58    }
59}
60
61impl Default for TerminalId {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67/// Error type for terminal operations
68#[derive(Debug, thiserror::Error)]
69pub enum TerminalError {
70    #[error("PTY error: {0}")]
71    PtyError(String),
72
73    #[error("Write error: {0}")]
74    WriteError(String),
75
76    #[error("Terminal not initialized")]
77    NotInitialized,
78}
79
80/// Internal cleanup handler for terminal resources.
81pub(crate) struct TerminalCleaner {
82    /// Writer handle for the PTY.
83    pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
84    /// Async tasks
85    pub(crate) reader_task: TaskHandle,
86    pub(crate) pty_task: TaskHandle,
87    /// Notifier that signals when the terminal should close.
88    pub(crate) closer_notifier: ArcNotify,
89}
90
91impl Drop for TerminalCleaner {
92    fn drop(&mut self) {
93        *self.writer.borrow_mut() = None;
94        self.reader_task.try_cancel();
95        self.pty_task.try_cancel();
96        self.closer_notifier.notify();
97    }
98}
99
100/// Handle to a running terminal instance.
101///
102/// The handle allows you to write input to the terminal and resize it.
103/// Multiple Terminal components can share the same handle.
104///
105/// The PTY is automatically closed when the handle is dropped.
106#[derive(Clone)]
107#[allow(dead_code)]
108pub struct TerminalHandle {
109    /// Unique identifier for this terminal instance.
110    pub(crate) id: TerminalId,
111    /// Terminal buffer containing the current screen state.
112    pub(crate) buffer: Rc<RefCell<TerminalBuffer>>,
113    /// VT100 parser for accessing full scrollback content.
114    pub(crate) parser: Rc<RefCell<Parser>>,
115    /// Writer for sending input to the PTY process.
116    pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
117    /// PTY master handle for resizing.
118    pub(crate) master: Rc<RefCell<Box<dyn MasterPty + Send>>>,
119    /// Current working directory reported by the shell via OSC 7.
120    pub(crate) cwd: Rc<RefCell<Option<PathBuf>>>,
121    /// Window title reported by the shell via OSC 0 or OSC 2.
122    pub(crate) title: Rc<RefCell<Option<String>>>,
123    /// Notifier that signals when the terminal/PTY closes.
124    pub(crate) closer_notifier: ArcNotify,
125    /// Handles cleanup when the terminal is dropped.
126    pub(crate) cleaner: Rc<TerminalCleaner>,
127    /// Notifier that signals when new output is received from the PTY.
128    pub(crate) output_notifier: ArcNotify,
129    /// Notifier that signals when the window title changes via OSC 0 or OSC 2.
130    pub(crate) title_notifier: ArcNotify,
131    /// Tracks when user last wrote input to the PTY.
132    pub(crate) last_write_time: Rc<RefCell<Instant>>,
133    /// Currently pressed mouse button (for drag/motion tracking).
134    pub(crate) pressed_button: Rc<RefCell<Option<TerminalMouseButton>>>,
135    /// Current modifier keys state (shift, ctrl, alt, etc.).
136    pub(crate) modifiers: Rc<RefCell<Modifiers>>,
137}
138
139impl PartialEq for TerminalHandle {
140    fn eq(&self, other: &Self) -> bool {
141        self.id == other.id
142    }
143}
144
145impl TerminalHandle {
146    /// Create a new terminal with the specified command and default scrollback size (1000 lines).
147    ///
148    /// # Example
149    ///
150    /// ```rust,no_run
151    /// use freya_terminal::prelude::*;
152    /// use portable_pty::CommandBuilder;
153    ///
154    /// let mut cmd = CommandBuilder::new("bash");
155    /// cmd.env("TERM", "xterm-256color");
156    ///
157    /// let handle = TerminalHandle::new(TerminalId::new(), cmd, None).unwrap();
158    /// ```
159    pub fn new(
160        id: TerminalId,
161        command: portable_pty::CommandBuilder,
162        scrollback_length: Option<usize>,
163    ) -> Result<Self, TerminalError> {
164        spawn_pty(id, command, scrollback_length.unwrap_or(1000))
165    }
166
167    /// Refresh the terminal buffer from the parser, preserving selection state.
168    fn refresh_buffer(&self) {
169        let mut parser = self.parser.borrow_mut();
170        let total_scrollback = query_max_scrollback(&mut parser);
171
172        let mut buffer = self.buffer.borrow_mut();
173        buffer.scroll_offset = buffer.scroll_offset.min(total_scrollback);
174
175        parser.screen_mut().set_scrollback(buffer.scroll_offset);
176        let mut new_buffer = extract_buffer(&parser, buffer.scroll_offset, total_scrollback);
177        parser.screen_mut().set_scrollback(0);
178
179        new_buffer.selection = buffer.selection.take();
180        *buffer = new_buffer;
181    }
182
183    /// Write data to the terminal.
184    ///
185    /// # Example
186    ///
187    /// ```rust,no_run
188    /// # use freya_terminal::prelude::*;
189    /// # let handle: TerminalHandle = unimplemented!();
190    /// handle.write(b"ls -la\n").unwrap();
191    /// ```
192    pub fn write(&self, data: &[u8]) -> Result<(), TerminalError> {
193        self.write_raw(data)?;
194        let mut buffer = self.buffer.borrow_mut();
195        buffer.selection = None;
196        buffer.scroll_offset = 0;
197        drop(buffer);
198        *self.last_write_time.borrow_mut() = Instant::now();
199        self.scroll_to_bottom();
200        Ok(())
201    }
202
203    /// Process a key event and write the corresponding terminal escape sequence to the PTY.
204    ///
205    /// Handles standard terminal keys (Enter, Backspace, Tab, Escape, arrows, Delete),
206    /// Ctrl+letter control codes, modified Enter (Shift/Ctrl via CSI u encoding),
207    /// regular character input, and shift state tracking for mouse selection.
208    ///
209    /// Returns `Ok(true)` if the key was handled, `Ok(false)` if not recognized.
210    ///
211    /// Application-level shortcuts like clipboard copy/paste should be handled
212    /// by the caller before calling this method.
213    ///
214    /// # Example
215    ///
216    /// ```rust,no_run
217    /// # use freya_terminal::prelude::*;
218    /// # use keyboard_types::{Key, Modifiers};
219    /// # let handle: TerminalHandle = unimplemented!();
220    /// # let key = Key::Character("a".into());
221    /// # let modifiers = Modifiers::empty();
222    /// let _ = handle.write_key(&key, modifiers);
223    /// ```
224    pub fn write_key(&self, key: &Key, modifiers: Modifiers) -> Result<bool, TerminalError> {
225        let shift = modifiers.contains(Modifiers::SHIFT);
226        let ctrl = modifiers.contains(Modifiers::CONTROL);
227        let alt = modifiers.contains(Modifiers::ALT);
228
229        match key {
230            Key::Character(ch) if ctrl && ch.len() == 1 => {
231                self.write(&[ch.as_bytes()[0] & 0x1f])?;
232                Ok(true)
233            }
234            Key::Named(NamedKey::Enter) if shift || ctrl => {
235                if self.parser.borrow().screen().alternate_screen() {
236                    let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
237                    let seq = format!("\x1b[13;{m}u");
238                    self.write(seq.as_bytes())?;
239                } else {
240                    self.write(b"\r")?;
241                }
242                Ok(true)
243            }
244            Key::Named(NamedKey::Enter) => {
245                self.write(b"\r")?;
246                Ok(true)
247            }
248            Key::Named(NamedKey::Backspace) if ctrl => {
249                self.write(&[0x08])?;
250                Ok(true)
251            }
252            Key::Named(NamedKey::Backspace) if alt => {
253                self.write(&[0x1b, 0x7f])?;
254                Ok(true)
255            }
256            Key::Named(NamedKey::Backspace) => {
257                self.write(&[0x7f])?;
258                Ok(true)
259            }
260            Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
261                let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
262                let seq = format!("\x1b[3;{m}~");
263                self.write(seq.as_bytes())?;
264                Ok(true)
265            }
266            Key::Named(NamedKey::Delete) => {
267                self.write(b"\x1b[3~")?;
268                Ok(true)
269            }
270            Key::Named(NamedKey::Shift) => {
271                self.shift_pressed(true);
272                Ok(true)
273            }
274            Key::Named(NamedKey::Tab) => {
275                self.write(b"\t")?;
276                Ok(true)
277            }
278            Key::Named(NamedKey::Escape) => {
279                self.write(&[0x1b])?;
280                Ok(true)
281            }
282            Key::Named(
283                dir @ (NamedKey::ArrowUp
284                | NamedKey::ArrowDown
285                | NamedKey::ArrowLeft
286                | NamedKey::ArrowRight),
287            ) => {
288                let ch = match dir {
289                    NamedKey::ArrowUp => 'A',
290                    NamedKey::ArrowDown => 'B',
291                    NamedKey::ArrowRight => 'C',
292                    NamedKey::ArrowLeft => 'D',
293                    _ => unreachable!(),
294                };
295                if shift || ctrl {
296                    let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
297                    let seq = format!("\x1b[1;{m}{ch}");
298                    self.write(seq.as_bytes())?;
299                } else {
300                    self.write(&[0x1b, b'[', ch as u8])?;
301                }
302                Ok(true)
303            }
304            Key::Character(ch) => {
305                self.write(ch.as_bytes())?;
306                Ok(true)
307            }
308            _ => Ok(false),
309        }
310    }
311
312    /// Write text to the PTY as a paste operation.
313    ///
314    /// If bracketed paste mode is enabled, wraps the text with `\x1b[200~` ... `\x1b[201~`.
315    pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
316        let bracketed = self.parser.borrow().screen().bracketed_paste();
317
318        let mut data = Vec::with_capacity(text.len() + 12);
319        if bracketed {
320            data.extend_from_slice(b"\x1b[200~");
321        }
322        data.extend_from_slice(text.as_bytes());
323        if bracketed {
324            data.extend_from_slice(b"\x1b[201~");
325        }
326
327        self.write(&data)
328    }
329
330    /// Write data to the PTY without resetting scroll or selection state.
331    fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
332        match &mut *self.writer.borrow_mut() {
333            Some(w) => {
334                w.write_all(data)
335                    .map_err(|e| TerminalError::WriteError(e.to_string()))?;
336                w.flush()
337                    .map_err(|e| TerminalError::WriteError(e.to_string()))?;
338                Ok(())
339            }
340            None => Err(TerminalError::NotInitialized),
341        }
342    }
343
344    /// Resize the terminal to the specified rows and columns.
345    ///
346    /// # Example
347    ///
348    /// ```rust,no_run
349    /// # use freya_terminal::prelude::*;
350    /// # let handle: TerminalHandle = unimplemented!();
351    /// handle.resize(24, 80);
352    /// ```
353    pub fn resize(&self, rows: u16, cols: u16) {
354        self.parser.borrow_mut().screen_mut().set_size(rows, cols);
355        self.refresh_buffer();
356        let _ = self.master.borrow().resize(PtySize {
357            rows,
358            cols,
359            pixel_width: 0,
360            pixel_height: 0,
361        });
362    }
363
364    /// Scroll the terminal by the specified delta.
365    ///
366    /// # Example
367    ///
368    /// ```rust,no_run
369    /// # use freya_terminal::prelude::*;
370    /// # let handle: TerminalHandle = unimplemented!();
371    /// handle.scroll(-3); // Scroll up 3 lines
372    /// handle.scroll(3); // Scroll down 3 lines
373    /// ```
374    pub fn scroll(&self, delta: i32) {
375        if self.parser.borrow().screen().alternate_screen() {
376            return;
377        }
378
379        {
380            let mut buffer = self.buffer.borrow_mut();
381            let new_offset = (buffer.scroll_offset as i64 + delta as i64).max(0) as usize;
382            buffer.scroll_offset = new_offset.min(buffer.total_scrollback);
383        }
384
385        self.refresh_buffer();
386        Platform::get().send(UserEvent::RequestRedraw);
387    }
388
389    /// Scroll the terminal to the bottom.
390    ///
391    /// # Example
392    ///
393    /// ```rust,no_run
394    /// # use freya_terminal::prelude::*;
395    /// # let handle: TerminalHandle = unimplemented!();
396    /// handle.scroll_to_bottom();
397    /// ```
398    pub fn scroll_to_bottom(&self) {
399        if self.parser.borrow().screen().alternate_screen() {
400            return;
401        }
402
403        self.buffer.borrow_mut().scroll_offset = 0;
404        self.refresh_buffer();
405        Platform::get().send(UserEvent::RequestRedraw);
406    }
407
408    /// Get the current scrollback position (scroll offset from buffer).
409    ///
410    /// # Example
411    ///
412    /// ```rust,no_run
413    /// # use freya_terminal::prelude::*;
414    /// # let handle: TerminalHandle = unimplemented!();
415    /// let position = handle.scrollback_position();
416    /// ```
417    pub fn scrollback_position(&self) -> usize {
418        self.buffer.borrow().scroll_offset
419    }
420
421    /// Get the current working directory reported by the shell via OSC 7.
422    ///
423    /// Returns `None` if the shell hasn't reported a CWD yet.
424    pub fn cwd(&self) -> Option<PathBuf> {
425        self.cwd.borrow().clone()
426    }
427
428    /// Get the window title reported by the shell via OSC 0 or OSC 2.
429    ///
430    /// Returns `None` if the shell hasn't reported a title yet.
431    pub fn title(&self) -> Option<String> {
432        self.title.borrow().clone()
433    }
434
435    /// Send a wheel event to the PTY.
436    ///
437    /// This sends mouse wheel events as escape sequences to the running process.
438    /// Uses the currently active mouse protocol encoding based on what
439    /// the application has requested via DECSET sequences.
440    pub fn send_wheel_to_pty(&self, row: usize, col: usize, delta_y: f64) {
441        let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
442        let seq = encode_wheel_event(row, col, delta_y, encoding);
443        let _ = self.write_raw(seq.as_bytes());
444    }
445
446    /// Send a mouse move/drag event to the PTY based on the active mouse mode.
447    ///
448    /// - `AnyMotion` (DECSET 1003): sends motion events regardless of button state.
449    /// - `ButtonMotion` (DECSET 1002): sends motion events only while a button is held.
450    ///
451    /// When dragging, the held button is encoded in the motion event so TUI apps
452    /// can implement their own text selection.
453    ///
454    /// If shift is held and a button is pressed, updates the text selection instead
455    /// of sending events to the PTY.
456    pub fn mouse_move(&self, row: usize, col: usize) {
457        let is_dragging = self.pressed_button.borrow().is_some();
458
459        if self.modifiers.borrow().contains(Modifiers::SHIFT) && is_dragging {
460            // Shift+drag updates text selection (raw mode, bypasses PTY)
461            self.update_selection(row, col);
462            return;
463        }
464
465        let parser = self.parser.borrow();
466        let mouse_mode = parser.screen().mouse_protocol_mode();
467        let encoding = parser.screen().mouse_protocol_encoding();
468
469        let held = *self.pressed_button.borrow();
470
471        match mouse_mode {
472            vt100::MouseProtocolMode::AnyMotion => {
473                let seq = encode_mouse_move(row, col, held, encoding);
474                let _ = self.write_raw(seq.as_bytes());
475            }
476            vt100::MouseProtocolMode::ButtonMotion => {
477                if let Some(button) = held {
478                    let seq = encode_mouse_move(row, col, Some(button), encoding);
479                    let _ = self.write_raw(seq.as_bytes());
480                }
481            }
482            vt100::MouseProtocolMode::None => {
483                // No mouse tracking - do text selection if dragging
484                if is_dragging {
485                    self.update_selection(row, col);
486                }
487            }
488            _ => {}
489        }
490    }
491
492    /// Returns whether the running application has enabled mouse tracking.
493    fn is_mouse_tracking_enabled(&self) -> bool {
494        let parser = self.parser.borrow();
495        parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None
496    }
497
498    /// Handle a mouse button press event.
499    ///
500    /// When the running application has enabled mouse tracking (e.g. vim,
501    /// helix, htop), this sends the press escape sequence to the PTY.
502    /// Otherwise it starts a text selection.
503    ///
504    /// If shift is held, text selection is always performed regardless of
505    /// the application's mouse tracking state.
506    pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
507        *self.pressed_button.borrow_mut() = Some(button);
508
509        if self.modifiers.borrow().contains(Modifiers::SHIFT) {
510            // Shift+drag always does raw text selection
511            self.start_selection(row, col);
512        } else if self.is_mouse_tracking_enabled() {
513            let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
514            let seq = encode_mouse_press(row, col, button, encoding);
515            let _ = self.write_raw(seq.as_bytes());
516        } else {
517            self.start_selection(row, col);
518        }
519    }
520
521    /// Handle a mouse button release event.
522    ///
523    /// When the running application has enabled mouse tracking, this sends the
524    /// release escape sequence to the PTY. Only `PressRelease`, `ButtonMotion`,
525    /// and `AnyMotion` modes receive release events — `Press` mode does not.
526    /// Otherwise it ends the current text selection.
527    ///
528    /// If shift is held, always ends the text selection instead of sending
529    /// events to the PTY.
530    pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
531        *self.pressed_button.borrow_mut() = None;
532
533        if self.modifiers.borrow().contains(Modifiers::SHIFT) {
534            // Shift+drag ends text selection
535            self.end_selection();
536            return;
537        }
538
539        let parser = self.parser.borrow();
540        let mouse_mode = parser.screen().mouse_protocol_mode();
541        let encoding = parser.screen().mouse_protocol_encoding();
542
543        match mouse_mode {
544            vt100::MouseProtocolMode::PressRelease
545            | vt100::MouseProtocolMode::ButtonMotion
546            | vt100::MouseProtocolMode::AnyMotion => {
547                let seq = encode_mouse_release(row, col, button, encoding);
548                let _ = self.write_raw(seq.as_bytes());
549            }
550            vt100::MouseProtocolMode::Press => {
551                // Press-only mode doesn't send release events
552            }
553            vt100::MouseProtocolMode::None => {
554                self.end_selection();
555            }
556        }
557    }
558
559    /// Number of arrow key presses to send per wheel tick in alternate scroll mode.
560    const ALTERNATE_SCROLL_LINES: usize = 3;
561
562    /// Handle a mouse button release from outside the terminal viewport.
563    ///
564    /// Clears the pressed state and ends any active text selection without
565    /// sending an encoded event to the PTY.
566    pub fn release(&self) {
567        *self.pressed_button.borrow_mut() = None;
568        self.end_selection();
569    }
570
571    /// Handle a wheel event intelligently.
572    ///
573    /// The behavior depends on the terminal state:
574    /// - If viewing scrollback history: scrolls the scrollback buffer.
575    /// - If mouse tracking is enabled (e.g., vim, helix): sends wheel escape
576    ///   sequences to the PTY.
577    /// - If on the alternate screen without mouse tracking (e.g., gitui, less):
578    ///   sends arrow key sequences to the PTY (alternate scroll mode, like
579    ///   wezterm/kitty/alacritty).
580    /// - Otherwise (normal shell): scrolls the scrollback buffer.
581    pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
582        let scroll_delta = if delta_y > 0.0 { 3 } else { -3 };
583        let scroll_offset = self.buffer.borrow().scroll_offset;
584        let (mouse_mode, alt_screen, app_cursor) = {
585            let parser = self.parser.borrow();
586            let screen = parser.screen();
587            (
588                screen.mouse_protocol_mode(),
589                screen.alternate_screen(),
590                screen.application_cursor(),
591            )
592        };
593
594        if scroll_offset > 0 {
595            // User is viewing scrollback history
596            let delta = scroll_delta;
597            self.scroll(delta);
598        } else if mouse_mode != vt100::MouseProtocolMode::None {
599            // App has enabled mouse tracking (vim, helix, etc.)
600            self.send_wheel_to_pty(row, col, delta_y);
601        } else if alt_screen {
602            // Alternate screen without mouse tracking (gitui, less, etc.)
603            // Send arrow key presses, matching wezterm/kitty/alacritty behavior
604            let key = match (delta_y > 0.0, app_cursor) {
605                (true, true) => "\x1bOA",
606                (true, false) => "\x1b[A",
607                (false, true) => "\x1bOB",
608                (false, false) => "\x1b[B",
609            };
610            for _ in 0..Self::ALTERNATE_SCROLL_LINES {
611                let _ = self.write_raw(key.as_bytes());
612            }
613        } else {
614            // Normal screen, no mouse tracking — scroll scrollback
615            let delta = scroll_delta;
616            self.scroll(delta);
617        }
618    }
619
620    /// Read the current terminal buffer.
621    pub fn read_buffer(&'_ self) -> Ref<'_, TerminalBuffer> {
622        self.buffer.borrow()
623    }
624
625    /// Returns a future that completes when new output is received from the PTY.
626    ///
627    /// Can be called repeatedly in a loop to detect ongoing output activity.
628    pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
629        self.output_notifier.notified()
630    }
631
632    /// Returns a future that completes when the window title changes via OSC 0 or OSC 2.
633    ///
634    /// Can be called repeatedly in a loop to react to title updates from the shell.
635    pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
636        self.title_notifier.notified()
637    }
638
639    pub fn last_write_elapsed(&self) -> std::time::Duration {
640        self.last_write_time.borrow().elapsed()
641    }
642
643    /// Returns a future that completes when the terminal/PTY closes.
644    ///
645    /// This can be used to detect when the shell process exits and update the UI accordingly.
646    ///
647    /// # Example
648    ///
649    /// ```rust,ignore
650    /// use_future(move || async move {
651    ///     terminal_handle.closed().await;
652    ///     // Terminal has exited, update UI state
653    /// });
654    /// ```
655    pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
656        self.closer_notifier.notified()
657    }
658
659    /// Returns the unique identifier for this terminal instance.
660    pub fn id(&self) -> TerminalId {
661        self.id
662    }
663
664    /// Track whether shift is currently pressed.
665    ///
666    /// This should be called from your key event handlers to track shift state
667    /// for shift+drag text selection.
668    pub fn shift_pressed(&self, pressed: bool) {
669        let mut mods = self.modifiers.borrow_mut();
670        if pressed {
671            mods.insert(Modifiers::SHIFT);
672        } else {
673            mods.remove(Modifiers::SHIFT);
674        }
675    }
676
677    /// Get the current text selection.
678    pub fn get_selection(&self) -> Option<TerminalSelection> {
679        self.buffer.borrow().selection.clone()
680    }
681
682    /// Set the text selection.
683    pub fn set_selection(&self, selection: Option<TerminalSelection>) {
684        self.buffer.borrow_mut().selection = selection;
685    }
686
687    pub fn start_selection(&self, row: usize, col: usize) {
688        let mut buffer = self.buffer.borrow_mut();
689        let scroll = buffer.scroll_offset;
690        buffer.selection = Some(TerminalSelection {
691            dragging: true,
692            start_row: row,
693            start_col: col,
694            start_scroll: scroll,
695            end_row: row,
696            end_col: col,
697            end_scroll: scroll,
698        });
699        Platform::get().send(UserEvent::RequestRedraw);
700    }
701
702    pub fn update_selection(&self, row: usize, col: usize) {
703        let mut buffer = self.buffer.borrow_mut();
704        let scroll = buffer.scroll_offset;
705        if let Some(selection) = &mut buffer.selection
706            && selection.dragging
707        {
708            selection.end_row = row;
709            selection.end_col = col;
710            selection.end_scroll = scroll;
711            Platform::get().send(UserEvent::RequestRedraw);
712        }
713    }
714
715    pub fn end_selection(&self) {
716        if let Some(selection) = &mut self.buffer.borrow_mut().selection {
717            selection.dragging = false;
718            Platform::get().send(UserEvent::RequestRedraw);
719        }
720    }
721
722    /// Clear the current selection.
723    pub fn clear_selection(&self) {
724        self.buffer.borrow_mut().selection = None;
725        Platform::get().send(UserEvent::RequestRedraw);
726    }
727
728    pub fn get_selected_text(&self) -> Option<String> {
729        let buffer = self.buffer.borrow();
730        let selection = buffer.selection.clone()?;
731        if selection.is_empty() {
732            return None;
733        }
734
735        let scroll = buffer.scroll_offset;
736        let (display_start, start_col, display_end, end_col) = selection.display_positions(scroll);
737
738        let mut parser = self.parser.borrow_mut();
739        let saved_scrollback = parser.screen().scrollback();
740        let (_rows, cols) = parser.screen().size();
741
742        let mut lines = Vec::new();
743
744        for d in display_start..=display_end {
745            let cp = d - scroll as i64;
746            let needed_scrollback = (-cp).max(0) as usize;
747            let viewport_row = cp.max(0) as u16;
748
749            parser.screen_mut().set_scrollback(needed_scrollback);
750
751            let row_cells: Vec<_> = (0..cols)
752                .filter_map(|c| parser.screen().cell(viewport_row, c).cloned())
753                .collect();
754
755            let is_single = display_start == display_end;
756            let is_first = d == display_start;
757            let is_last = d == display_end;
758
759            let cells = if is_single {
760                let s = start_col.min(row_cells.len());
761                let e = end_col.min(row_cells.len());
762                &row_cells[s..e]
763            } else if is_first {
764                let s = start_col.min(row_cells.len());
765                &row_cells[s..]
766            } else if is_last {
767                &row_cells[..end_col.min(row_cells.len())]
768            } else {
769                &row_cells
770            };
771
772            let line: String = cells
773                .iter()
774                .map(|cell| {
775                    if cell.has_contents() {
776                        cell.contents()
777                    } else {
778                        " "
779                    }
780                })
781                .collect::<String>();
782
783            lines.push(line);
784        }
785
786        parser.screen_mut().set_scrollback(saved_scrollback);
787
788        Some(lines.join("\n"))
789    }
790}