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) => {
249                self.write(&[0x7f])?;
250                Ok(true)
251            }
252            Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
253                let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
254                let seq = format!("\x1b[3;{m}~");
255                self.write(seq.as_bytes())?;
256                Ok(true)
257            }
258            Key::Named(NamedKey::Delete) => {
259                self.write(b"\x1b[3~")?;
260                Ok(true)
261            }
262            Key::Named(NamedKey::Shift) => {
263                self.shift_pressed(true);
264                Ok(true)
265            }
266            Key::Named(NamedKey::Tab) => {
267                self.write(b"\t")?;
268                Ok(true)
269            }
270            Key::Named(NamedKey::Escape) => {
271                self.write(&[0x1b])?;
272                Ok(true)
273            }
274            Key::Named(
275                dir @ (NamedKey::ArrowUp
276                | NamedKey::ArrowDown
277                | NamedKey::ArrowLeft
278                | NamedKey::ArrowRight),
279            ) => {
280                let ch = match dir {
281                    NamedKey::ArrowUp => 'A',
282                    NamedKey::ArrowDown => 'B',
283                    NamedKey::ArrowRight => 'C',
284                    NamedKey::ArrowLeft => 'D',
285                    _ => unreachable!(),
286                };
287                if shift || ctrl {
288                    let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
289                    let seq = format!("\x1b[1;{m}{ch}");
290                    self.write(seq.as_bytes())?;
291                } else {
292                    self.write(&[0x1b, b'[', ch as u8])?;
293                }
294                Ok(true)
295            }
296            Key::Character(ch) => {
297                self.write(ch.as_bytes())?;
298                Ok(true)
299            }
300            _ => Ok(false),
301        }
302    }
303
304    /// Write text to the PTY as a paste operation.
305    ///
306    /// If bracketed paste mode is enabled, wraps the text with `\x1b[200~` ... `\x1b[201~`.
307    pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
308        let bracketed = self.parser.borrow().screen().bracketed_paste();
309
310        let mut data = Vec::with_capacity(text.len() + 12);
311        if bracketed {
312            data.extend_from_slice(b"\x1b[200~");
313        }
314        data.extend_from_slice(text.as_bytes());
315        if bracketed {
316            data.extend_from_slice(b"\x1b[201~");
317        }
318
319        self.write(&data)
320    }
321
322    /// Write data to the PTY without resetting scroll or selection state.
323    fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
324        match &mut *self.writer.borrow_mut() {
325            Some(w) => {
326                w.write_all(data)
327                    .map_err(|e| TerminalError::WriteError(e.to_string()))?;
328                w.flush()
329                    .map_err(|e| TerminalError::WriteError(e.to_string()))?;
330                Ok(())
331            }
332            None => Err(TerminalError::NotInitialized),
333        }
334    }
335
336    /// Resize the terminal to the specified rows and columns.
337    ///
338    /// # Example
339    ///
340    /// ```rust,no_run
341    /// # use freya_terminal::prelude::*;
342    /// # let handle: TerminalHandle = unimplemented!();
343    /// handle.resize(24, 80);
344    /// ```
345    pub fn resize(&self, rows: u16, cols: u16) {
346        self.parser.borrow_mut().screen_mut().set_size(rows, cols);
347        self.refresh_buffer();
348        let _ = self.master.borrow().resize(PtySize {
349            rows,
350            cols,
351            pixel_width: 0,
352            pixel_height: 0,
353        });
354    }
355
356    /// Scroll the terminal by the specified delta.
357    ///
358    /// # Example
359    ///
360    /// ```rust,no_run
361    /// # use freya_terminal::prelude::*;
362    /// # let handle: TerminalHandle = unimplemented!();
363    /// handle.scroll(-3); // Scroll up 3 lines
364    /// handle.scroll(3); // Scroll down 3 lines
365    /// ```
366    pub fn scroll(&self, delta: i32) {
367        if self.parser.borrow().screen().alternate_screen() {
368            return;
369        }
370
371        {
372            let mut buffer = self.buffer.borrow_mut();
373            let new_offset = (buffer.scroll_offset as i64 + delta as i64).max(0) as usize;
374            buffer.scroll_offset = new_offset.min(buffer.total_scrollback);
375        }
376
377        self.refresh_buffer();
378        Platform::get().send(UserEvent::RequestRedraw);
379    }
380
381    /// Scroll the terminal to the bottom.
382    ///
383    /// # Example
384    ///
385    /// ```rust,no_run
386    /// # use freya_terminal::prelude::*;
387    /// # let handle: TerminalHandle = unimplemented!();
388    /// handle.scroll_to_bottom();
389    /// ```
390    pub fn scroll_to_bottom(&self) {
391        if self.parser.borrow().screen().alternate_screen() {
392            return;
393        }
394
395        self.buffer.borrow_mut().scroll_offset = 0;
396        self.refresh_buffer();
397        Platform::get().send(UserEvent::RequestRedraw);
398    }
399
400    /// Get the current scrollback position (scroll offset from buffer).
401    ///
402    /// # Example
403    ///
404    /// ```rust,no_run
405    /// # use freya_terminal::prelude::*;
406    /// # let handle: TerminalHandle = unimplemented!();
407    /// let position = handle.scrollback_position();
408    /// ```
409    pub fn scrollback_position(&self) -> usize {
410        self.buffer.borrow().scroll_offset
411    }
412
413    /// Get the current working directory reported by the shell via OSC 7.
414    ///
415    /// Returns `None` if the shell hasn't reported a CWD yet.
416    pub fn cwd(&self) -> Option<PathBuf> {
417        self.cwd.borrow().clone()
418    }
419
420    /// Get the window title reported by the shell via OSC 0 or OSC 2.
421    ///
422    /// Returns `None` if the shell hasn't reported a title yet.
423    pub fn title(&self) -> Option<String> {
424        self.title.borrow().clone()
425    }
426
427    /// Send a wheel event to the PTY.
428    ///
429    /// This sends mouse wheel events as escape sequences to the running process.
430    /// Uses the currently active mouse protocol encoding based on what
431    /// the application has requested via DECSET sequences.
432    pub fn send_wheel_to_pty(&self, row: usize, col: usize, delta_y: f64) {
433        let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
434        let seq = encode_wheel_event(row, col, delta_y, encoding);
435        let _ = self.write_raw(seq.as_bytes());
436    }
437
438    /// Send a mouse move/drag event to the PTY based on the active mouse mode.
439    ///
440    /// - `AnyMotion` (DECSET 1003): sends motion events regardless of button state.
441    /// - `ButtonMotion` (DECSET 1002): sends motion events only while a button is held.
442    ///
443    /// When dragging, the held button is encoded in the motion event so TUI apps
444    /// can implement their own text selection.
445    ///
446    /// If shift is held and a button is pressed, updates the text selection instead
447    /// of sending events to the PTY.
448    pub fn mouse_move(&self, row: usize, col: usize) {
449        let is_dragging = self.pressed_button.borrow().is_some();
450
451        if self.modifiers.borrow().contains(Modifiers::SHIFT) && is_dragging {
452            // Shift+drag updates text selection (raw mode, bypasses PTY)
453            self.update_selection(row, col);
454            return;
455        }
456
457        let parser = self.parser.borrow();
458        let mouse_mode = parser.screen().mouse_protocol_mode();
459        let encoding = parser.screen().mouse_protocol_encoding();
460
461        let held = *self.pressed_button.borrow();
462
463        match mouse_mode {
464            vt100::MouseProtocolMode::AnyMotion => {
465                let seq = encode_mouse_move(row, col, held, encoding);
466                let _ = self.write_raw(seq.as_bytes());
467            }
468            vt100::MouseProtocolMode::ButtonMotion => {
469                if let Some(button) = held {
470                    let seq = encode_mouse_move(row, col, Some(button), encoding);
471                    let _ = self.write_raw(seq.as_bytes());
472                }
473            }
474            vt100::MouseProtocolMode::None => {
475                // No mouse tracking - do text selection if dragging
476                if is_dragging {
477                    self.update_selection(row, col);
478                }
479            }
480            _ => {}
481        }
482    }
483
484    /// Returns whether the running application has enabled mouse tracking.
485    fn is_mouse_tracking_enabled(&self) -> bool {
486        let parser = self.parser.borrow();
487        parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None
488    }
489
490    /// Handle a mouse button press event.
491    ///
492    /// When the running application has enabled mouse tracking (e.g. vim,
493    /// helix, htop), this sends the press escape sequence to the PTY.
494    /// Otherwise it starts a text selection.
495    ///
496    /// If shift is held, text selection is always performed regardless of
497    /// the application's mouse tracking state.
498    pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
499        *self.pressed_button.borrow_mut() = Some(button);
500
501        if self.modifiers.borrow().contains(Modifiers::SHIFT) {
502            // Shift+drag always does raw text selection
503            self.start_selection(row, col);
504        } else if self.is_mouse_tracking_enabled() {
505            let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
506            let seq = encode_mouse_press(row, col, button, encoding);
507            let _ = self.write_raw(seq.as_bytes());
508        } else {
509            self.start_selection(row, col);
510        }
511    }
512
513    /// Handle a mouse button release event.
514    ///
515    /// When the running application has enabled mouse tracking, this sends the
516    /// release escape sequence to the PTY. Only `PressRelease`, `ButtonMotion`,
517    /// and `AnyMotion` modes receive release events — `Press` mode does not.
518    /// Otherwise it ends the current text selection.
519    ///
520    /// If shift is held, always ends the text selection instead of sending
521    /// events to the PTY.
522    pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
523        *self.pressed_button.borrow_mut() = None;
524
525        if self.modifiers.borrow().contains(Modifiers::SHIFT) {
526            // Shift+drag ends text selection
527            self.end_selection();
528            return;
529        }
530
531        let parser = self.parser.borrow();
532        let mouse_mode = parser.screen().mouse_protocol_mode();
533        let encoding = parser.screen().mouse_protocol_encoding();
534
535        match mouse_mode {
536            vt100::MouseProtocolMode::PressRelease
537            | vt100::MouseProtocolMode::ButtonMotion
538            | vt100::MouseProtocolMode::AnyMotion => {
539                let seq = encode_mouse_release(row, col, button, encoding);
540                let _ = self.write_raw(seq.as_bytes());
541            }
542            vt100::MouseProtocolMode::Press => {
543                // Press-only mode doesn't send release events
544            }
545            vt100::MouseProtocolMode::None => {
546                self.end_selection();
547            }
548        }
549    }
550
551    /// Number of arrow key presses to send per wheel tick in alternate scroll mode.
552    const ALTERNATE_SCROLL_LINES: usize = 3;
553
554    /// Handle a mouse button release from outside the terminal viewport.
555    ///
556    /// Clears the pressed state and ends any active text selection without
557    /// sending an encoded event to the PTY.
558    pub fn release(&self) {
559        *self.pressed_button.borrow_mut() = None;
560        self.end_selection();
561    }
562
563    /// Handle a wheel event intelligently.
564    ///
565    /// The behavior depends on the terminal state:
566    /// - If viewing scrollback history: scrolls the scrollback buffer.
567    /// - If mouse tracking is enabled (e.g., vim, helix): sends wheel escape
568    ///   sequences to the PTY.
569    /// - If on the alternate screen without mouse tracking (e.g., gitui, less):
570    ///   sends arrow key sequences to the PTY (alternate scroll mode, like
571    ///   wezterm/kitty/alacritty).
572    /// - Otherwise (normal shell): scrolls the scrollback buffer.
573    pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
574        let scroll_delta = if delta_y > 0.0 { 3 } else { -3 };
575        let scroll_offset = self.buffer.borrow().scroll_offset;
576        let (mouse_mode, alt_screen, app_cursor) = {
577            let parser = self.parser.borrow();
578            let screen = parser.screen();
579            (
580                screen.mouse_protocol_mode(),
581                screen.alternate_screen(),
582                screen.application_cursor(),
583            )
584        };
585
586        if scroll_offset > 0 {
587            // User is viewing scrollback history
588            let delta = scroll_delta;
589            self.scroll(delta);
590        } else if mouse_mode != vt100::MouseProtocolMode::None {
591            // App has enabled mouse tracking (vim, helix, etc.)
592            self.send_wheel_to_pty(row, col, delta_y);
593        } else if alt_screen {
594            // Alternate screen without mouse tracking (gitui, less, etc.)
595            // Send arrow key presses, matching wezterm/kitty/alacritty behavior
596            let key = match (delta_y > 0.0, app_cursor) {
597                (true, true) => "\x1bOA",
598                (true, false) => "\x1b[A",
599                (false, true) => "\x1bOB",
600                (false, false) => "\x1b[B",
601            };
602            for _ in 0..Self::ALTERNATE_SCROLL_LINES {
603                let _ = self.write_raw(key.as_bytes());
604            }
605        } else {
606            // Normal screen, no mouse tracking — scroll scrollback
607            let delta = scroll_delta;
608            self.scroll(delta);
609        }
610    }
611
612    /// Read the current terminal buffer.
613    pub fn read_buffer(&'_ self) -> Ref<'_, TerminalBuffer> {
614        self.buffer.borrow()
615    }
616
617    /// Returns a future that completes when new output is received from the PTY.
618    ///
619    /// Can be called repeatedly in a loop to detect ongoing output activity.
620    pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
621        self.output_notifier.notified()
622    }
623
624    /// Returns a future that completes when the window title changes via OSC 0 or OSC 2.
625    ///
626    /// Can be called repeatedly in a loop to react to title updates from the shell.
627    pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
628        self.title_notifier.notified()
629    }
630
631    pub fn last_write_elapsed(&self) -> std::time::Duration {
632        self.last_write_time.borrow().elapsed()
633    }
634
635    /// Returns a future that completes when the terminal/PTY closes.
636    ///
637    /// This can be used to detect when the shell process exits and update the UI accordingly.
638    ///
639    /// # Example
640    ///
641    /// ```rust,ignore
642    /// use_future(move || async move {
643    ///     terminal_handle.closed().await;
644    ///     // Terminal has exited, update UI state
645    /// });
646    /// ```
647    pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
648        self.closer_notifier.notified()
649    }
650
651    /// Returns the unique identifier for this terminal instance.
652    pub fn id(&self) -> TerminalId {
653        self.id
654    }
655
656    /// Track whether shift is currently pressed.
657    ///
658    /// This should be called from your key event handlers to track shift state
659    /// for shift+drag text selection.
660    pub fn shift_pressed(&self, pressed: bool) {
661        let mut mods = self.modifiers.borrow_mut();
662        if pressed {
663            mods.insert(Modifiers::SHIFT);
664        } else {
665            mods.remove(Modifiers::SHIFT);
666        }
667    }
668
669    /// Get the current text selection.
670    pub fn get_selection(&self) -> Option<TerminalSelection> {
671        self.buffer.borrow().selection.clone()
672    }
673
674    /// Set the text selection.
675    pub fn set_selection(&self, selection: Option<TerminalSelection>) {
676        self.buffer.borrow_mut().selection = selection;
677    }
678
679    pub fn start_selection(&self, row: usize, col: usize) {
680        let mut buffer = self.buffer.borrow_mut();
681        let scroll = buffer.scroll_offset;
682        buffer.selection = Some(TerminalSelection {
683            dragging: true,
684            start_row: row,
685            start_col: col,
686            start_scroll: scroll,
687            end_row: row,
688            end_col: col,
689            end_scroll: scroll,
690        });
691        Platform::get().send(UserEvent::RequestRedraw);
692    }
693
694    pub fn update_selection(&self, row: usize, col: usize) {
695        let mut buffer = self.buffer.borrow_mut();
696        let scroll = buffer.scroll_offset;
697        if let Some(selection) = &mut buffer.selection
698            && selection.dragging
699        {
700            selection.end_row = row;
701            selection.end_col = col;
702            selection.end_scroll = scroll;
703            Platform::get().send(UserEvent::RequestRedraw);
704        }
705    }
706
707    pub fn end_selection(&self) {
708        if let Some(selection) = &mut self.buffer.borrow_mut().selection {
709            selection.dragging = false;
710            Platform::get().send(UserEvent::RequestRedraw);
711        }
712    }
713
714    /// Clear the current selection.
715    pub fn clear_selection(&self) {
716        self.buffer.borrow_mut().selection = None;
717        Platform::get().send(UserEvent::RequestRedraw);
718    }
719
720    pub fn get_selected_text(&self) -> Option<String> {
721        let buffer = self.buffer.borrow();
722        let selection = buffer.selection.clone()?;
723        if selection.is_empty() {
724            return None;
725        }
726
727        let scroll = buffer.scroll_offset;
728        let (display_start, start_col, display_end, end_col) = selection.display_positions(scroll);
729
730        let mut parser = self.parser.borrow_mut();
731        let saved_scrollback = parser.screen().scrollback();
732        let (_rows, cols) = parser.screen().size();
733
734        let mut lines = Vec::new();
735
736        for d in display_start..=display_end {
737            let cp = d - scroll as i64;
738            let needed_scrollback = (-cp).max(0) as usize;
739            let viewport_row = cp.max(0) as u16;
740
741            parser.screen_mut().set_scrollback(needed_scrollback);
742
743            let row_cells: Vec<_> = (0..cols)
744                .filter_map(|c| parser.screen().cell(viewport_row, c).cloned())
745                .collect();
746
747            let is_single = display_start == display_end;
748            let is_first = d == display_start;
749            let is_last = d == display_end;
750
751            let cells = if is_single {
752                let s = start_col.min(row_cells.len());
753                let e = end_col.min(row_cells.len());
754                &row_cells[s..e]
755            } else if is_first {
756                let s = start_col.min(row_cells.len());
757                &row_cells[s..]
758            } else if is_last {
759                &row_cells[..end_col.min(row_cells.len())]
760            } else {
761                &row_cells
762            };
763
764            let line: String = cells
765                .iter()
766                .map(|cell| {
767                    if cell.has_contents() {
768                        cell.contents()
769                    } else {
770                        " "
771                    }
772                })
773                .collect::<String>();
774
775            lines.push(line);
776        }
777
778        parser.screen_mut().set_scrollback(saved_scrollback);
779
780        Some(lines.join("\n"))
781    }
782}