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