vtcode_tui/core_tui/session/
mouse_selection.rs1use ratatui::buffer::Buffer;
2use ratatui::crossterm::{clipboard::CopyToClipboard, execute};
3use ratatui::layout::Rect;
4use std::io::Write;
5
6#[derive(Debug, Default)]
8pub struct MouseSelectionState {
9 pub is_selecting: bool,
11 pub start: (u16, u16),
13 pub end: (u16, u16),
15 pub has_selection: bool,
17 copied: bool,
19}
20
21impl MouseSelectionState {
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 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 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 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 self.has_selection = self.start != self.end;
50 }
51 }
52
53 #[allow(dead_code)]
55 pub fn clear(&mut self) {
56 self.is_selecting = false;
57 self.has_selection = false;
58 }
59
60 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 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 area = area.intersection(buf.area);
78 if area.width == 0 || area.height == 0 {
79 return String::new();
80 }
81
82 let ((start_col, start_row), (end_col, end_row)) = self.normalized();
83 let mut result = String::new();
84
85 for row in start_row..=end_row {
86 if row < area.y || row >= area.bottom() {
87 continue;
88 }
89 let line_start = if row == start_row {
90 start_col.max(area.x)
91 } else {
92 area.x
93 };
94 let line_end = if row == end_row {
95 end_col.min(area.right())
96 } else {
97 area.right()
98 };
99
100 for col in line_start..line_end {
101 if col < area.x || col >= area.right() {
102 continue;
103 }
104 let cell = &buf[(col, row)];
105 let symbol = cell.symbol();
106 if !symbol.is_empty() {
107 result.push_str(symbol);
108 }
109 }
110
111 if row < end_row {
113 let trimmed = result.trim_end().len();
115 result.truncate(trimmed);
116 result.push('\n');
117 }
118 }
119
120 let trimmed = result.trim_end();
122 trimmed.to_string()
123 }
124
125 pub fn apply_highlight(&self, buf: &mut Buffer, area: Rect) {
127 if !self.has_selection && !self.is_selecting {
128 return;
129 }
130
131 let area = area.intersection(buf.area);
133 if area.width == 0 || area.height == 0 {
134 return;
135 }
136
137 let ((start_col, start_row), (end_col, end_row)) = self.normalized();
138
139 for row in start_row..=end_row {
140 if row < area.y || row >= area.bottom() {
141 continue;
142 }
143 let line_start = if row == start_row {
144 start_col.max(area.x)
145 } else {
146 area.x
147 };
148 let line_end = if row == end_row {
149 end_col.min(area.right())
150 } else {
151 area.right()
152 };
153
154 for col in line_start..line_end {
155 if col < area.x || col >= area.right() {
156 continue;
157 }
158 let cell = &mut buf[(col, row)];
159 let fg = cell.fg;
161 let bg = cell.bg;
162 cell.set_fg(bg);
163 cell.set_bg(fg);
164 }
165 }
166 }
167
168 pub fn needs_copy(&self) -> bool {
170 self.has_selection && !self.is_selecting && !self.copied
171 }
172
173 pub fn mark_copied(&mut self) {
175 self.copied = true;
176 }
177
178 pub fn copy_to_clipboard(text: &str) {
180 if text.is_empty() {
181 return;
182 }
183 let _ = execute!(
184 std::io::stderr(),
185 CopyToClipboard::to_clipboard_from(text.as_bytes())
186 );
187 let _ = std::io::stderr().flush();
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use ratatui::style::Color;
195
196 #[test]
197 fn extract_text_clamps_area_to_buffer_bounds() {
198 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 2));
199 buf[(0, 0)].set_symbol("A");
200 buf[(1, 0)].set_symbol("B");
201 buf[(0, 1)].set_symbol("C");
202 buf[(1, 1)].set_symbol("D");
203
204 let mut selection = MouseSelectionState::new();
205 selection.start_selection(0, 0);
206 selection.finish_selection(5, 5);
207
208 let text = selection.extract_text(&buf, Rect::new(0, 0, 10, 10));
209 assert_eq!(text, "AB\nCD");
210 }
211
212 #[test]
213 fn apply_highlight_clamps_area_to_buffer_bounds() {
214 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
215 buf[(0, 0)].set_fg(Color::Red);
216 buf[(0, 0)].set_bg(Color::Blue);
217
218 let mut selection = MouseSelectionState::new();
219 selection.start_selection(0, 0);
220 selection.finish_selection(5, 5);
221
222 selection.apply_highlight(&mut buf, Rect::new(0, 0, 10, 10));
223
224 assert_eq!(buf[(0, 0)].fg, Color::Blue);
225 assert_eq!(buf[(0, 0)].bg, Color::Red);
226 }
227}