Skip to main content

vtcode_tui/core_tui/session/
mouse_selection.rs

1use ratatui::buffer::Buffer;
2use ratatui::crossterm::{clipboard::CopyToClipboard, execute};
3use ratatui::layout::Rect;
4use std::io::Write;
5
6/// Tracks mouse-driven text selection state for the TUI transcript.
7#[derive(Debug, Default)]
8pub struct MouseSelectionState {
9    /// Whether the user is currently dragging to select text.
10    pub is_selecting: bool,
11    /// Screen coordinates where the selection started (column, row).
12    pub start: (u16, u16),
13    /// Screen coordinates where the selection currently ends (column, row).
14    pub end: (u16, u16),
15    /// Whether a completed selection exists (ready for highlight rendering).
16    pub has_selection: bool,
17    /// Whether the current selection has already been copied to clipboard.
18    copied: bool,
19}
20
21impl MouseSelectionState {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Begin a new selection at the given screen position.
27    pub fn start_selection(&mut self, col: u16, row: u16) {
28        self.is_selecting = true;
29        self.has_selection = false;
30        self.copied = false;
31        self.start = (col, row);
32        self.end = (col, row);
33    }
34
35    /// Update the end position while dragging.
36    pub fn update_selection(&mut self, col: u16, row: u16) {
37        if self.is_selecting {
38            self.end = (col, row);
39            self.has_selection = true;
40        }
41    }
42
43    /// Finalize the selection on mouse-up.
44    pub fn finish_selection(&mut self, col: u16, row: u16) {
45        if self.is_selecting {
46            self.end = (col, row);
47            self.is_selecting = false;
48            // Only mark as having a selection if start != end
49            self.has_selection = self.start != self.end;
50        }
51    }
52
53    /// Clear any active selection.
54    #[allow(dead_code)]
55    pub fn clear(&mut self) {
56        self.is_selecting = false;
57        self.has_selection = false;
58    }
59
60    /// Returns the selection range normalized so that `from` is before `to`.
61    fn normalized(&self) -> ((u16, u16), (u16, u16)) {
62        let (s, e) = (self.start, self.end);
63        if s.1 < e.1 || (s.1 == e.1 && s.0 <= e.0) {
64            (s, e)
65        } else {
66            (e, s)
67        }
68    }
69
70    /// Extract selected text from a ratatui `Buffer`.
71    pub fn extract_text(&self, buf: &Buffer, area: Rect) -> String {
72        if !self.has_selection && !self.is_selecting {
73            return String::new();
74        }
75
76        let ((start_col, start_row), (end_col, end_row)) = self.normalized();
77        let mut result = String::new();
78
79        for row in start_row..=end_row {
80            if row < area.y || row >= area.y + area.height {
81                continue;
82            }
83            let line_start = if row == start_row {
84                start_col.max(area.x)
85            } else {
86                area.x
87            };
88            let line_end = if row == end_row {
89                end_col.min(area.x + area.width)
90            } else {
91                area.x + area.width
92            };
93
94            for col in line_start..line_end {
95                if col < area.x || col >= area.x + area.width {
96                    continue;
97                }
98                let cell = &buf[(col, row)];
99                let symbol = cell.symbol();
100                if !symbol.is_empty() {
101                    result.push_str(symbol);
102                }
103            }
104
105            // Add newline between rows (but not after the last)
106            if row < end_row {
107                // Trim trailing whitespace from each line
108                let trimmed = result.trim_end().len();
109                result.truncate(trimmed);
110                result.push('\n');
111            }
112        }
113
114        // Trim trailing whitespace from the final line
115        let trimmed = result.trim_end();
116        trimmed.to_string()
117    }
118
119    /// Apply selection highlight (inverted colors) to the frame buffer.
120    pub fn apply_highlight(&self, buf: &mut Buffer, area: Rect) {
121        if !self.has_selection && !self.is_selecting {
122            return;
123        }
124
125        let ((start_col, start_row), (end_col, end_row)) = self.normalized();
126
127        for row in start_row..=end_row {
128            if row < area.y || row >= area.y + area.height {
129                continue;
130            }
131            let line_start = if row == start_row {
132                start_col.max(area.x)
133            } else {
134                area.x
135            };
136            let line_end = if row == end_row {
137                end_col.min(area.x + area.width)
138            } else {
139                area.x + area.width
140            };
141
142            for col in line_start..line_end {
143                if col < area.x || col >= area.x + area.width {
144                    continue;
145                }
146                let cell = &mut buf[(col, row)];
147                // Swap foreground and background to show selection
148                let fg = cell.fg;
149                let bg = cell.bg;
150                cell.set_fg(bg);
151                cell.set_bg(fg);
152            }
153        }
154    }
155
156    /// Returns `true` if the selection needs to be copied (finalized and not yet copied).
157    pub fn needs_copy(&self) -> bool {
158        self.has_selection && !self.is_selecting && !self.copied
159    }
160
161    /// Mark the selection as already copied.
162    pub fn mark_copied(&mut self) {
163        self.copied = true;
164    }
165
166    /// Copy the selected text to the system clipboard using crossterm clipboard commands.
167    pub fn copy_to_clipboard(text: &str) {
168        if text.is_empty() {
169            return;
170        }
171        let _ = execute!(
172            std::io::stderr(),
173            CopyToClipboard::to_clipboard_from(text.as_bytes())
174        );
175        let _ = std::io::stderr().flush();
176    }
177}