anveesa 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2250 -0
- package/Cargo.toml +30 -0
- package/LICENSE +21 -0
- package/bin/anveesa.js +50 -0
- package/package.json +35 -22
- package/scripts/install.js +203 -0
- package/src/cli.rs +126 -0
- package/src/config.rs +743 -0
- package/src/display.rs +794 -0
- package/src/image.rs +344 -0
- package/src/lib.rs +777 -0
- package/src/main.rs +4 -0
- package/src/mcp.rs +271 -0
- package/src/prompt.rs +616 -0
- package/src/provider/command.rs +310 -0
- package/src/provider/mod.rs +210 -0
- package/src/provider/openai_compatible.rs +1635 -0
- package/src/provider/openai_compatible_tests.rs +533 -0
- package/src/session.rs +565 -0
- package/src/tools.rs +2729 -0
- package/src/tools_scenarios.rs +2026 -0
- package/src/tui/commands.rs +515 -0
- package/src/tui/format.rs +439 -0
- package/src/tui/input.rs +198 -0
- package/src/tui/render.rs +735 -0
- package/src/tui/stream.rs +439 -0
- package/src/tui.rs +709 -0
- package/src/web.rs +185 -0
- package/src/web_ui.html +213 -0
- package/src/workspace.rs +216 -0
- package/bin/anveesa +0 -12
- package/install.js +0 -92
package/src/prompt.rs
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
use std::io::{self, Read, Write};
|
|
2
|
+
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
|
|
5
|
+
use crate::{
|
|
6
|
+
image::{grab_clipboard_image, read_clipboard_text},
|
|
7
|
+
provider::ImageAttachment,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[cfg(unix)]
|
|
11
|
+
use libc;
|
|
12
|
+
|
|
13
|
+
pub enum PromptRead {
|
|
14
|
+
Line(String, Option<ImageAttachment>),
|
|
15
|
+
Interrupted,
|
|
16
|
+
Eof,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub struct PromptSegment {
|
|
20
|
+
pub full: String,
|
|
21
|
+
pub display: String,
|
|
22
|
+
pub hidden: bool,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Default)]
|
|
26
|
+
pub struct PromptBuffer {
|
|
27
|
+
pub full: String,
|
|
28
|
+
pub display: String,
|
|
29
|
+
pub segments: Vec<PromptSegment>,
|
|
30
|
+
/// Byte offset into `full` — where the next insertion goes.
|
|
31
|
+
pub cursor: usize,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl PromptBuffer {
|
|
35
|
+
pub fn is_empty(&self) -> bool {
|
|
36
|
+
self.full.is_empty()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Char offset in `display` that corresponds to the current cursor position in `full`.
|
|
40
|
+
/// Used to position the terminal cursor after a redraw.
|
|
41
|
+
pub fn display_cursor_char(&self) -> usize {
|
|
42
|
+
let mut full_pos = 0usize;
|
|
43
|
+
let mut disp_chars = 0usize;
|
|
44
|
+
for seg in &self.segments {
|
|
45
|
+
let seg_len = seg.full.len();
|
|
46
|
+
let next_pos = full_pos + seg_len;
|
|
47
|
+
if self.cursor <= next_pos {
|
|
48
|
+
let offset = self.cursor - full_pos;
|
|
49
|
+
return if seg.hidden {
|
|
50
|
+
// Hidden spans are atomic: cursor snaps to end of placeholder.
|
|
51
|
+
disp_chars + seg.display.chars().count()
|
|
52
|
+
} else {
|
|
53
|
+
disp_chars + seg.full[..offset].chars().count()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
full_pos = next_pos;
|
|
57
|
+
disp_chars += seg.display.chars().count();
|
|
58
|
+
}
|
|
59
|
+
disp_chars
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn push_text(&mut self, text: &str) {
|
|
63
|
+
// Find the segment containing the cursor and insert there.
|
|
64
|
+
let mut pos = 0usize;
|
|
65
|
+
for seg in self.segments.iter_mut() {
|
|
66
|
+
let seg_len = seg.full.len();
|
|
67
|
+
if !seg.hidden && self.cursor >= pos && self.cursor <= pos + seg_len {
|
|
68
|
+
let offset = self.cursor - pos;
|
|
69
|
+
seg.full.insert_str(offset, text);
|
|
70
|
+
seg.display.insert_str(offset, text);
|
|
71
|
+
self.cursor += text.len();
|
|
72
|
+
self.rebuild_flat();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
pos += seg_len;
|
|
76
|
+
}
|
|
77
|
+
// Cursor is at end or after a hidden segment — append to last visible segment.
|
|
78
|
+
if let Some(seg) = self.segments.last_mut().filter(|s| !s.hidden) {
|
|
79
|
+
seg.full.push_str(text);
|
|
80
|
+
seg.display.push_str(text);
|
|
81
|
+
} else {
|
|
82
|
+
self.segments.push(PromptSegment {
|
|
83
|
+
full: text.to_string(),
|
|
84
|
+
display: text.to_string(),
|
|
85
|
+
hidden: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
self.cursor += text.len();
|
|
89
|
+
self.rebuild_flat();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn push_hidden_paste(&mut self, text: String, display: String) {
|
|
93
|
+
self.full.push_str(&text);
|
|
94
|
+
self.display.push_str(&display);
|
|
95
|
+
self.cursor = self.full.len();
|
|
96
|
+
self.segments.push(PromptSegment {
|
|
97
|
+
full: text,
|
|
98
|
+
display,
|
|
99
|
+
hidden: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Delete the character immediately before the cursor.
|
|
104
|
+
/// Deletes the entire span atomically if the cursor is just past a hidden span.
|
|
105
|
+
pub fn delete_before_cursor(&mut self) {
|
|
106
|
+
if self.cursor == 0 {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let mut pos = 0usize;
|
|
110
|
+
let mut remove_idx: Option<usize> = None;
|
|
111
|
+
for (i, seg) in self.segments.iter_mut().enumerate() {
|
|
112
|
+
let seg_len = seg.full.len();
|
|
113
|
+
let next_pos = pos + seg_len;
|
|
114
|
+
if seg.hidden && next_pos == self.cursor {
|
|
115
|
+
// cursor is right after a hidden span — delete the whole span
|
|
116
|
+
self.cursor -= seg_len;
|
|
117
|
+
remove_idx = Some(i);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if !seg.hidden && self.cursor > pos && self.cursor <= next_pos {
|
|
121
|
+
let offset = self.cursor - pos;
|
|
122
|
+
if let Some(ch) = seg.full[..offset].chars().next_back() {
|
|
123
|
+
let ch_len = ch.len_utf8();
|
|
124
|
+
seg.full.drain((offset - ch_len)..offset);
|
|
125
|
+
seg.display.drain((offset - ch_len)..offset);
|
|
126
|
+
self.cursor -= ch_len;
|
|
127
|
+
if seg.full.is_empty() {
|
|
128
|
+
remove_idx = Some(i);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
pos = next_pos;
|
|
134
|
+
}
|
|
135
|
+
if let Some(i) = remove_idx {
|
|
136
|
+
self.segments.remove(i);
|
|
137
|
+
}
|
|
138
|
+
self.rebuild_flat();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub fn move_left(&mut self) {
|
|
142
|
+
if self.cursor == 0 {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let mut pos = 0usize;
|
|
146
|
+
for seg in &self.segments {
|
|
147
|
+
let next_pos = pos + seg.full.len();
|
|
148
|
+
if seg.hidden && next_pos == self.cursor {
|
|
149
|
+
self.cursor = pos;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if !seg.hidden && self.cursor > pos && self.cursor <= next_pos {
|
|
153
|
+
let offset = self.cursor - pos;
|
|
154
|
+
if let Some(ch) = seg.full[..offset].chars().next_back() {
|
|
155
|
+
self.cursor -= ch.len_utf8();
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
pos = next_pos;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pub fn move_right(&mut self) {
|
|
164
|
+
if self.cursor >= self.full.len() {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let mut pos = 0usize;
|
|
168
|
+
for seg in &self.segments {
|
|
169
|
+
let seg_len = seg.full.len();
|
|
170
|
+
if seg.hidden && pos == self.cursor {
|
|
171
|
+
self.cursor += seg_len;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if !seg.hidden && self.cursor >= pos && self.cursor < pos + seg_len {
|
|
175
|
+
let offset = self.cursor - pos;
|
|
176
|
+
if let Some(ch) = seg.full[offset..].chars().next() {
|
|
177
|
+
self.cursor += ch.len_utf8();
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pos += seg_len;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pub fn move_home(&mut self) {
|
|
186
|
+
self.cursor = 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub fn move_end(&mut self) {
|
|
190
|
+
self.cursor = self.full.len();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Ctrl+U / Cmd+Delete — erase the entire line.
|
|
194
|
+
pub fn clear_all(&mut self) {
|
|
195
|
+
self.full.clear();
|
|
196
|
+
self.display.clear();
|
|
197
|
+
self.segments.clear();
|
|
198
|
+
self.cursor = 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Ctrl+W / Option+Delete — erase the last word before the cursor.
|
|
202
|
+
pub fn pop_word(&mut self) {
|
|
203
|
+
while self.cursor > 0 && self.full[..self.cursor].ends_with(' ') {
|
|
204
|
+
self.delete_before_cursor();
|
|
205
|
+
}
|
|
206
|
+
while self.cursor > 0 && !self.full[..self.cursor].ends_with(' ') {
|
|
207
|
+
self.delete_before_cursor();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pub fn rebuild_flat(&mut self) {
|
|
212
|
+
self.full = self.segments.iter().map(|s| s.full.as_str()).collect();
|
|
213
|
+
self.display = self.segments.iter().map(|s| s.display.as_str()).collect();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[cfg(unix)]
|
|
218
|
+
struct RawPromptMode {
|
|
219
|
+
fd: i32,
|
|
220
|
+
saved: libc::termios,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[cfg(unix)]
|
|
224
|
+
impl RawPromptMode {
|
|
225
|
+
fn enter() -> Result<Self> {
|
|
226
|
+
let fd = libc::STDIN_FILENO;
|
|
227
|
+
let mut saved = std::mem::MaybeUninit::<libc::termios>::uninit();
|
|
228
|
+
if unsafe { libc::tcgetattr(fd, saved.as_mut_ptr()) } != 0 {
|
|
229
|
+
return Err(io::Error::last_os_error()).context("failed to read terminal mode");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let saved = unsafe { saved.assume_init() };
|
|
233
|
+
let mut raw = saved;
|
|
234
|
+
raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
|
|
235
|
+
raw.c_oflag &= !libc::OPOST;
|
|
236
|
+
raw.c_cflag |= libc::CS8;
|
|
237
|
+
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG);
|
|
238
|
+
raw.c_cc[libc::VMIN] = 1;
|
|
239
|
+
raw.c_cc[libc::VTIME] = 0;
|
|
240
|
+
|
|
241
|
+
if unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &raw) } != 0 {
|
|
242
|
+
return Err(io::Error::last_os_error()).context("failed to set terminal raw mode");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
print!("\x1b[?2004h");
|
|
246
|
+
let _ = io::stdout().flush();
|
|
247
|
+
|
|
248
|
+
Ok(Self { fd, saved })
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[cfg(unix)]
|
|
253
|
+
impl Drop for RawPromptMode {
|
|
254
|
+
fn drop(&mut self) {
|
|
255
|
+
print!("\x1b[?2004l");
|
|
256
|
+
let _ = io::stdout().flush();
|
|
257
|
+
|
|
258
|
+
unsafe {
|
|
259
|
+
libc::tcsetattr(self.fd, libc::TCSAFLUSH, &self.saved);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[cfg(not(unix))]
|
|
265
|
+
struct RawPromptMode;
|
|
266
|
+
|
|
267
|
+
#[cfg(not(unix))]
|
|
268
|
+
impl RawPromptMode {
|
|
269
|
+
fn enter() -> Result<Self> {
|
|
270
|
+
Ok(Self)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// After a redraw (which leaves the terminal cursor at end of display), move it
|
|
275
|
+
/// back to the buffer's logical cursor position.
|
|
276
|
+
fn position_prompt_cursor(display: &str, cursor_char: usize) -> io::Result<()> {
|
|
277
|
+
let back = display.chars().count().saturating_sub(cursor_char);
|
|
278
|
+
if back > 0 {
|
|
279
|
+
print!("\x1b[{}D", back);
|
|
280
|
+
io::stdout().flush()?;
|
|
281
|
+
}
|
|
282
|
+
Ok(())
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
pub fn read_prompt_line(
|
|
286
|
+
label: &str,
|
|
287
|
+
width: usize,
|
|
288
|
+
paste_count: &mut usize,
|
|
289
|
+
images_available: bool,
|
|
290
|
+
input_history: &[String],
|
|
291
|
+
) -> Result<PromptRead> {
|
|
292
|
+
let _raw_mode = RawPromptMode::enter()?;
|
|
293
|
+
let mut input = io::stdin().lock();
|
|
294
|
+
let mut buffer = PromptBuffer::default();
|
|
295
|
+
let mut display_rows = 1usize;
|
|
296
|
+
let mut ctrl_v_image: Option<ImageAttachment> = None;
|
|
297
|
+
|
|
298
|
+
// History navigation state.
|
|
299
|
+
let mut hist_idx: Option<usize> = None; // None = current live input
|
|
300
|
+
let mut saved_input = String::new(); // stash live input when navigating into history
|
|
301
|
+
|
|
302
|
+
// Compose the visible prompt label, optionally prefixed with an image indicator.
|
|
303
|
+
let effective_label = |img: &Option<ImageAttachment>| -> String {
|
|
304
|
+
if img.is_some() {
|
|
305
|
+
format!("\x1b[2m[📎]\x1b[0m {label}")
|
|
306
|
+
} else {
|
|
307
|
+
label.to_string()
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Redraw the line and position the cursor, returning the new row count.
|
|
312
|
+
macro_rules! redraw {
|
|
313
|
+
() => {{
|
|
314
|
+
let lbl = effective_label(&ctrl_v_image);
|
|
315
|
+
let rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
316
|
+
let _ = position_prompt_cursor(&buffer.display, buffer.display_cursor_char());
|
|
317
|
+
rows
|
|
318
|
+
}};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
print!("{}", effective_label(&ctrl_v_image));
|
|
322
|
+
io::stdout().flush().context("failed to write prompt")?;
|
|
323
|
+
|
|
324
|
+
loop {
|
|
325
|
+
let mut byte = [0u8; 1];
|
|
326
|
+
input
|
|
327
|
+
.read_exact(&mut byte)
|
|
328
|
+
.context("failed to read prompt input")?;
|
|
329
|
+
|
|
330
|
+
match byte[0] {
|
|
331
|
+
b'\r' | b'\n' => {
|
|
332
|
+
println!();
|
|
333
|
+
return Ok(PromptRead::Line(buffer.full, ctrl_v_image));
|
|
334
|
+
}
|
|
335
|
+
3 => {
|
|
336
|
+
println!("^C");
|
|
337
|
+
return Ok(PromptRead::Interrupted);
|
|
338
|
+
}
|
|
339
|
+
4 if buffer.is_empty() => return Ok(PromptRead::Eof),
|
|
340
|
+
8 | 127 => {
|
|
341
|
+
// Backspace
|
|
342
|
+
buffer.delete_before_cursor();
|
|
343
|
+
display_rows = redraw!();
|
|
344
|
+
}
|
|
345
|
+
21 => {
|
|
346
|
+
// Ctrl+U / Cmd+Delete — erase entire line
|
|
347
|
+
buffer.clear_all();
|
|
348
|
+
display_rows = redraw!();
|
|
349
|
+
}
|
|
350
|
+
22 => {
|
|
351
|
+
// Ctrl+V — universal paste: image first, then clipboard text
|
|
352
|
+
if images_available && let Some(img) = grab_clipboard_image() {
|
|
353
|
+
ctrl_v_image = Some(img);
|
|
354
|
+
display_rows = redraw!();
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
// Fall back to clipboard text via pbpaste / xclip
|
|
358
|
+
if let Some(text) = read_clipboard_text()
|
|
359
|
+
&& !text.is_empty()
|
|
360
|
+
{
|
|
361
|
+
buffer.push_text(&text.replace('\r', "\n"));
|
|
362
|
+
display_rows = redraw!();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
23 => {
|
|
366
|
+
// Ctrl+W / Option+Delete — erase last word
|
|
367
|
+
buffer.pop_word();
|
|
368
|
+
display_rows = redraw!();
|
|
369
|
+
}
|
|
370
|
+
0x1b => {
|
|
371
|
+
let sequence = read_escape_sequence(&mut input)?;
|
|
372
|
+
match sequence.as_slice() {
|
|
373
|
+
b"[200~" => {
|
|
374
|
+
// Bracketed paste
|
|
375
|
+
let paste = normalize_pasted_text(read_bracketed_paste(&mut input)?);
|
|
376
|
+
push_paste(&mut buffer, paste, paste_count);
|
|
377
|
+
display_rows = redraw!();
|
|
378
|
+
}
|
|
379
|
+
b"[A" => {
|
|
380
|
+
// Up arrow — previous history entry
|
|
381
|
+
if input_history.is_empty() {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
let new_idx = match hist_idx {
|
|
385
|
+
None => {
|
|
386
|
+
saved_input = buffer.full.clone();
|
|
387
|
+
input_history.len() - 1
|
|
388
|
+
}
|
|
389
|
+
Some(0) => 0,
|
|
390
|
+
Some(i) => i - 1,
|
|
391
|
+
};
|
|
392
|
+
hist_idx = Some(new_idx);
|
|
393
|
+
buffer = PromptBuffer::default();
|
|
394
|
+
buffer.push_text(&input_history[new_idx].clone());
|
|
395
|
+
display_rows = redraw!();
|
|
396
|
+
}
|
|
397
|
+
b"[B" => {
|
|
398
|
+
// Down arrow — next history entry / back to live input
|
|
399
|
+
match hist_idx {
|
|
400
|
+
None => {}
|
|
401
|
+
Some(i) if i + 1 >= input_history.len() => {
|
|
402
|
+
hist_idx = None;
|
|
403
|
+
let text = std::mem::take(&mut saved_input);
|
|
404
|
+
buffer = PromptBuffer::default();
|
|
405
|
+
buffer.push_text(&text);
|
|
406
|
+
display_rows = redraw!();
|
|
407
|
+
}
|
|
408
|
+
Some(i) => {
|
|
409
|
+
hist_idx = Some(i + 1);
|
|
410
|
+
buffer = PromptBuffer::default();
|
|
411
|
+
buffer.push_text(&input_history[i + 1].clone());
|
|
412
|
+
display_rows = redraw!();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
b"[C" => {
|
|
417
|
+
// Right arrow
|
|
418
|
+
buffer.move_right();
|
|
419
|
+
let _ =
|
|
420
|
+
position_prompt_cursor(&buffer.display, buffer.display_cursor_char());
|
|
421
|
+
}
|
|
422
|
+
b"[D" => {
|
|
423
|
+
// Left arrow
|
|
424
|
+
buffer.move_left();
|
|
425
|
+
let _ =
|
|
426
|
+
position_prompt_cursor(&buffer.display, buffer.display_cursor_char());
|
|
427
|
+
}
|
|
428
|
+
b"[H" | b"[1~" => {
|
|
429
|
+
// Home
|
|
430
|
+
buffer.move_home();
|
|
431
|
+
let _ = position_prompt_cursor(&buffer.display, 0);
|
|
432
|
+
}
|
|
433
|
+
b"[F" | b"[4~" => {
|
|
434
|
+
// End
|
|
435
|
+
buffer.move_end();
|
|
436
|
+
let _ =
|
|
437
|
+
position_prompt_cursor(&buffer.display, buffer.display_cursor_char());
|
|
438
|
+
}
|
|
439
|
+
_ => {}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
byte if byte >= 0x20 && byte != 0x7f => {
|
|
443
|
+
if let Some(ch) = read_utf8_char(byte, &mut input)? {
|
|
444
|
+
buffer.push_text(ch.encode_utf8(&mut [0; 4]));
|
|
445
|
+
display_rows = redraw!();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
_ => {}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pub fn push_paste(buffer: &mut PromptBuffer, text: String, paste_count: &mut usize) {
|
|
454
|
+
let line_count = pasted_line_count(&text);
|
|
455
|
+
if should_collapse_paste(&text) {
|
|
456
|
+
*paste_count += 1;
|
|
457
|
+
buffer.push_hidden_paste(
|
|
458
|
+
text,
|
|
459
|
+
pasted_text_display_placeholder(*paste_count, line_count),
|
|
460
|
+
);
|
|
461
|
+
} else {
|
|
462
|
+
buffer.push_text(&text);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn redraw_prompt_line(
|
|
467
|
+
label: &str,
|
|
468
|
+
display: &str,
|
|
469
|
+
previous_rows: usize,
|
|
470
|
+
width: usize,
|
|
471
|
+
) -> Result<usize> {
|
|
472
|
+
if previous_rows > 1 {
|
|
473
|
+
print!("\x1b[{}A", previous_rows - 1);
|
|
474
|
+
}
|
|
475
|
+
print!("\r\x1b[J{label}{display}");
|
|
476
|
+
io::stdout().flush().context("failed to redraw prompt")?;
|
|
477
|
+
Ok(input_screen_rows(display, width, 2))
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fn read_escape_sequence(input: &mut impl Read) -> io::Result<Vec<u8>> {
|
|
481
|
+
let mut sequence = Vec::new();
|
|
482
|
+
let mut byte = [0u8; 1];
|
|
483
|
+
|
|
484
|
+
input.read_exact(&mut byte)?;
|
|
485
|
+
sequence.push(byte[0]);
|
|
486
|
+
|
|
487
|
+
if byte[0] == b'[' {
|
|
488
|
+
loop {
|
|
489
|
+
input.read_exact(&mut byte)?;
|
|
490
|
+
sequence.push(byte[0]);
|
|
491
|
+
if (0x40..=0x7e).contains(&byte[0]) {
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
if sequence.len() >= 16 {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
Ok(sequence)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fn read_bracketed_paste(input: &mut impl Read) -> io::Result<String> {
|
|
504
|
+
const END: &[u8] = b"\x1b[201~";
|
|
505
|
+
|
|
506
|
+
let mut bytes = Vec::new();
|
|
507
|
+
let mut byte = [0u8; 1];
|
|
508
|
+
|
|
509
|
+
loop {
|
|
510
|
+
input.read_exact(&mut byte)?;
|
|
511
|
+
bytes.push(byte[0]);
|
|
512
|
+
if bytes.ends_with(END) {
|
|
513
|
+
let new_len = bytes.len() - END.len();
|
|
514
|
+
bytes.truncate(new_len);
|
|
515
|
+
return Ok(String::from_utf8_lossy(&bytes).into_owned());
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fn read_utf8_char(first: u8, input: &mut impl Read) -> io::Result<Option<char>> {
|
|
521
|
+
let expected_len = match first {
|
|
522
|
+
0x00..=0x7f => 1,
|
|
523
|
+
0xc2..=0xdf => 2,
|
|
524
|
+
0xe0..=0xef => 3,
|
|
525
|
+
0xf0..=0xf4 => 4,
|
|
526
|
+
_ => return Ok(None),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
let mut bytes = vec![first];
|
|
530
|
+
if expected_len > 1 {
|
|
531
|
+
let mut rest = vec![0u8; expected_len - 1];
|
|
532
|
+
input.read_exact(&mut rest)?;
|
|
533
|
+
bytes.extend(rest);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
Ok(std::str::from_utf8(&bytes)
|
|
537
|
+
.ok()
|
|
538
|
+
.and_then(|text| text.chars().next()))
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn normalize_pasted_text(text: String) -> String {
|
|
542
|
+
text.replace("\r\n", "\n").replace('\r', "\n")
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
pub fn should_collapse_paste(text: &str) -> bool {
|
|
546
|
+
pasted_line_count(text) > 3 || text.len() > 200
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
pub fn pasted_line_count(text: &str) -> usize {
|
|
550
|
+
text.lines().count().max(1)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
pub fn pasted_text_display_placeholder(paste_count: usize, line_count: usize) -> String {
|
|
554
|
+
format!("[Pasted text #{paste_count} +{line_count} lines]")
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
pub fn input_screen_rows(
|
|
558
|
+
input: &str,
|
|
559
|
+
terminal_width: usize,
|
|
560
|
+
first_row_prefix_width: usize,
|
|
561
|
+
) -> usize {
|
|
562
|
+
let width = terminal_width.max(1);
|
|
563
|
+
|
|
564
|
+
input
|
|
565
|
+
.split('\n')
|
|
566
|
+
.enumerate()
|
|
567
|
+
.map(|(index, line)| {
|
|
568
|
+
let prompt_prefix_width = if index == 0 {
|
|
569
|
+
first_row_prefix_width
|
|
570
|
+
} else {
|
|
571
|
+
0
|
|
572
|
+
};
|
|
573
|
+
let columns = line.chars().count() + prompt_prefix_width;
|
|
574
|
+
columns.div_ceil(width).max(1)
|
|
575
|
+
})
|
|
576
|
+
.sum::<usize>()
|
|
577
|
+
.max(1)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#[cfg(test)]
|
|
581
|
+
mod tests {
|
|
582
|
+
use super::*;
|
|
583
|
+
|
|
584
|
+
#[test]
|
|
585
|
+
fn pasted_input_screen_rows_accounts_for_prompt_and_wrapping() {
|
|
586
|
+
assert_eq!(input_screen_rows("hello", 80, 2), 1);
|
|
587
|
+
assert_eq!(input_screen_rows("one\ntwo\nthree", 80, 2), 3);
|
|
588
|
+
assert_eq!(input_screen_rows(&"x".repeat(78), 80, 2), 1);
|
|
589
|
+
assert_eq!(input_screen_rows(&"x".repeat(79), 80, 2), 2);
|
|
590
|
+
assert_eq!(input_screen_rows("", 80, 2), 1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#[test]
|
|
594
|
+
fn pasted_text_placeholder_does_not_look_like_a_prompt() {
|
|
595
|
+
let placeholder = pasted_text_display_placeholder(2, 157);
|
|
596
|
+
|
|
597
|
+
assert!(placeholder.contains("[Pasted text #2 +157 lines]"));
|
|
598
|
+
assert!(!placeholder.contains("❯"));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn prompt_buffer_hidden_paste_preserves_full_text() {
|
|
603
|
+
let mut buffer = PromptBuffer::default();
|
|
604
|
+
let mut paste_count = 0;
|
|
605
|
+
let pasted = "warning: one\nwarning: two\nwarning: three\nwarning: four".to_string();
|
|
606
|
+
|
|
607
|
+
buffer.push_text("please read this: ");
|
|
608
|
+
push_paste(&mut buffer, pasted.clone(), &mut paste_count);
|
|
609
|
+
|
|
610
|
+
assert_eq!(buffer.full, format!("please read this: {pasted}"));
|
|
611
|
+
assert_eq!(
|
|
612
|
+
buffer.display,
|
|
613
|
+
"please read this: [Pasted text #1 +4 lines]"
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|