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 self.copied = false;
59 }
60
61 fn normalized(&self) -> ((u16, u16), (u16, u16)) {
63 let (s, e) = (self.start, self.end);
64 if s.1 < e.1 || (s.1 == e.1 && s.0 <= e.0) {
65 (s, e)
66 } else {
67 (e, s)
68 }
69 }
70
71 pub fn extract_text(&self, buf: &Buffer, area: Rect) -> String {
73 if !self.has_selection && !self.is_selecting {
74 return String::new();
75 }
76
77 let area = area.intersection(buf.area);
79 if area.width == 0 || area.height == 0 {
80 return String::new();
81 }
82
83 let ((start_col, start_row), (end_col, end_row)) = self.normalized();
84 let mut result = String::new();
85
86 for row in start_row..=end_row {
87 if row < area.y || row >= area.bottom() {
88 continue;
89 }
90 let line_start = if row == start_row {
91 start_col.max(area.x)
92 } else {
93 area.x
94 };
95 let line_end = if row == end_row {
96 end_col.min(area.right())
97 } else {
98 area.right()
99 };
100
101 for col in line_start..line_end {
102 if col < area.x || col >= area.right() {
103 continue;
104 }
105 let cell = &buf[(col, row)];
106 let symbol = cell.symbol();
107 if !symbol.is_empty() {
108 result.push_str(symbol);
109 }
110 }
111
112 if row < end_row {
114 let trimmed = result.trim_end().len();
116 result.truncate(trimmed);
117 result.push('\n');
118 }
119 }
120
121 let trimmed = result.trim_end();
123 trimmed.to_string()
124 }
125
126 pub fn apply_highlight(&self, buf: &mut Buffer, area: Rect) {
128 if !self.has_selection && !self.is_selecting {
129 return;
130 }
131
132 let area = area.intersection(buf.area);
134 if area.width == 0 || area.height == 0 {
135 return;
136 }
137
138 let ((start_col, start_row), (end_col, end_row)) = self.normalized();
139
140 for row in start_row..=end_row {
141 if row < area.y || row >= area.bottom() {
142 continue;
143 }
144 let line_start = if row == start_row {
145 start_col.max(area.x)
146 } else {
147 area.x
148 };
149 let line_end = if row == end_row {
150 end_col.min(area.right())
151 } else {
152 area.right()
153 };
154
155 for col in line_start..line_end {
156 if col < area.x || col >= area.right() {
157 continue;
158 }
159 let cell = &mut buf[(col, row)];
160 let fg = cell.fg;
162 let bg = cell.bg;
163 cell.set_fg(bg);
164 cell.set_bg(fg);
165 }
166 }
167 }
168
169 pub fn needs_copy(&self) -> bool {
171 self.has_selection && !self.is_selecting && !self.copied
172 }
173
174 pub fn mark_copied(&mut self) {
176 self.copied = true;
177 }
178
179 pub fn copy_to_clipboard(text: &str) {
181 if text.is_empty() {
182 return;
183 }
184 let _ = execute!(
185 std::io::stderr(),
186 CopyToClipboard::to_clipboard_from(text.as_bytes())
187 );
188 let _ = std::io::stderr().flush();
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use ratatui::style::Color;
196
197 #[test]
198 fn extract_text_clamps_area_to_buffer_bounds() {
199 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 2));
200 buf[(0, 0)].set_symbol("A");
201 buf[(1, 0)].set_symbol("B");
202 buf[(0, 1)].set_symbol("C");
203 buf[(1, 1)].set_symbol("D");
204
205 let mut selection = MouseSelectionState::new();
206 selection.start_selection(0, 0);
207 selection.finish_selection(5, 5);
208
209 let text = selection.extract_text(&buf, Rect::new(0, 0, 10, 10));
210 assert_eq!(text, "AB\nCD");
211 }
212
213 #[test]
214 fn apply_highlight_clamps_area_to_buffer_bounds() {
215 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
216 buf[(0, 0)].set_fg(Color::Red);
217 buf[(0, 0)].set_bg(Color::Blue);
218
219 let mut selection = MouseSelectionState::new();
220 selection.start_selection(0, 0);
221 selection.finish_selection(5, 5);
222
223 selection.apply_highlight(&mut buf, Rect::new(0, 0, 10, 10));
224
225 assert_eq!(buf[(0, 0)].fg, Color::Blue);
226 assert_eq!(buf[(0, 0)].bg, Color::Red);
227 }
228}