anveesa 0.3.5 → 0.3.6
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 +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/lib.rs +325 -64
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
package/src/lib.rs
CHANGED
|
@@ -24,8 +24,8 @@ use crate::{
|
|
|
24
24
|
set_default_provider,
|
|
25
25
|
},
|
|
26
26
|
provider::{
|
|
27
|
-
ApprovalDecision, ApprovalPolicy, ChatMessage, DiffKind, ImageAttachment,
|
|
28
|
-
StreamEvent, ToolConfirmPreview, TurnResult, Usage,
|
|
27
|
+
ApprovalDecision, ApprovalPolicy, ChatMessage, ChatRole, DiffKind, ImageAttachment,
|
|
28
|
+
PromptRequest, StreamEvent, ToolConfirmPreview, TurnResult, Usage,
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -123,7 +123,24 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
123
123
|
let session_saved_at = loaded_session.as_ref().filter(|s| s.saved_at > 0).map(|s| s.saved_at);
|
|
124
124
|
// tracks the most recent successful save this run — kept fresh for /session display
|
|
125
125
|
let mut last_saved_at: u64 = session_saved_at.unwrap_or(0);
|
|
126
|
+
// Per-project system prompt: load .anveesa from cwd if no --system was given.
|
|
127
|
+
if session_options.system.is_none() {
|
|
128
|
+
if let Ok(text) = fs::read_to_string(cwd.join(".anveesa")) {
|
|
129
|
+
let trimmed = text.trim().to_string();
|
|
130
|
+
if !trimmed.is_empty() {
|
|
131
|
+
session_options.system = Some(trimmed);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
let history_path = repl_history_path();
|
|
137
|
+
// Load prompt history for ↑/↓ recall (one entry per line, newest at end).
|
|
138
|
+
let input_history: Vec<String> = history_path
|
|
139
|
+
.as_deref()
|
|
140
|
+
.and_then(|p| fs::read_to_string(p).ok())
|
|
141
|
+
.map(|c| c.lines().filter(|l| !l.is_empty()).map(String::from).collect())
|
|
142
|
+
.unwrap_or_default();
|
|
143
|
+
|
|
127
144
|
print_session_header(
|
|
128
145
|
&provider_name,
|
|
129
146
|
session_options.model.as_deref().unwrap_or("-"),
|
|
@@ -144,7 +161,7 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
144
161
|
loop {
|
|
145
162
|
print_input_separator(is_tty, width);
|
|
146
163
|
let (line, ctrl_v_image) =
|
|
147
|
-
match read_prompt_line(&label, width, &mut paste_count, images_available) {
|
|
164
|
+
match read_prompt_line(&label, width, &mut paste_count, images_available, &input_history) {
|
|
148
165
|
Ok(PromptRead::Line(line, img)) => (line, img),
|
|
149
166
|
Ok(PromptRead::Interrupted) => continue,
|
|
150
167
|
Ok(PromptRead::Eof) => {
|
|
@@ -197,6 +214,25 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
197
214
|
);
|
|
198
215
|
continue;
|
|
199
216
|
}
|
|
217
|
+
s if s.starts_with("/export") => {
|
|
218
|
+
let arg = s.strip_prefix("/export").unwrap().trim();
|
|
219
|
+
let path = if arg.is_empty() {
|
|
220
|
+
cwd.join(format!("anveesa-export-{}.md", unix_now()))
|
|
221
|
+
} else {
|
|
222
|
+
std::path::PathBuf::from(arg)
|
|
223
|
+
};
|
|
224
|
+
match export_conversation(&path, &history) {
|
|
225
|
+
Ok(()) => {
|
|
226
|
+
if is_tty {
|
|
227
|
+
eprintln!("\x1b[2m Exported to {}\x1b[0m", path.display());
|
|
228
|
+
} else {
|
|
229
|
+
println!("exported to {}", path.display());
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
Err(e) => eprintln!("\x1b[1;31m✗\x1b[0m {e:#}"),
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
200
236
|
"/status" => {
|
|
201
237
|
print_status_inline(
|
|
202
238
|
is_tty,
|
|
@@ -1107,8 +1143,9 @@ fn print_status_inline(
|
|
|
1107
1143
|
|
|
1108
1144
|
fn print_help_inline(is_tty: bool) {
|
|
1109
1145
|
if !is_tty {
|
|
1110
|
-
println!("commands: /clear, /session, /attach [path], /exit, /quit, /help");
|
|
1111
|
-
println!("
|
|
1146
|
+
println!("commands: /clear, /export [path], /session, /attach [path], /exit, /quit, /help");
|
|
1147
|
+
println!("keys: ↑/↓ history ←/→ cursor Home/End Ctrl+W delete-word Ctrl+U clear-line");
|
|
1148
|
+
println!("images: Ctrl+V to paste clipboard image, or copy then send to auto-attach");
|
|
1112
1149
|
return;
|
|
1113
1150
|
}
|
|
1114
1151
|
println!();
|
|
@@ -1116,6 +1153,7 @@ fn print_help_inline(is_tty: bool) {
|
|
|
1116
1153
|
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
1117
1154
|
println!(" \x1b[1;32m/status\x1b[0m provider, model, turns, token usage");
|
|
1118
1155
|
println!(" \x1b[1;32m/session\x1b[0m show session file, age, and turn count");
|
|
1156
|
+
println!(" \x1b[1;32m/export\x1b[0m \x1b[2m[path]\x1b[0m save conversation to a markdown file");
|
|
1119
1157
|
println!(" \x1b[1;32m/model\x1b[0m \x1b[2m[name]\x1b[0m switch or show current model");
|
|
1120
1158
|
println!(" \x1b[1;32m/provider\x1b[0m \x1b[2m[name]\x1b[0m switch or show current provider");
|
|
1121
1159
|
println!(" \x1b[1;32m/clear\x1b[0m reset conversation and delete saved session");
|
|
@@ -1123,9 +1161,19 @@ fn print_help_inline(is_tty: bool) {
|
|
|
1123
1161
|
println!(" \x1b[1;32m/exit\x1b[0m, \x1b[1;32m/quit\x1b[0m leave the session");
|
|
1124
1162
|
println!(" \x1b[1;32m/help\x1b[0m show this message");
|
|
1125
1163
|
println!();
|
|
1164
|
+
println!("\x1b[2m Keyboard\x1b[0m");
|
|
1165
|
+
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
1166
|
+
println!(" \x1b[2m↑ / ↓\x1b[0m recall previous / next prompt");
|
|
1167
|
+
println!(" \x1b[2m← / →\x1b[0m move cursor left / right");
|
|
1168
|
+
println!(" \x1b[2mHome / End\x1b[0m jump to start / end of line");
|
|
1169
|
+
println!(" \x1b[2mCtrl+W\x1b[0m delete word before cursor");
|
|
1170
|
+
println!(" \x1b[2mCtrl+U\x1b[0m clear entire line \x1b[2m(also Cmd+Delete)\x1b[0m");
|
|
1171
|
+
println!(" \x1b[2mCtrl+V\x1b[0m paste image from clipboard");
|
|
1172
|
+
println!();
|
|
1126
1173
|
println!("\x1b[2m Images\x1b[0m");
|
|
1127
1174
|
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
1128
|
-
println!("
|
|
1175
|
+
println!(" \x1b[2mCtrl+V\x1b[0m to paste a clipboard image inline (shows \x1b[2m[📎]\x1b[0m indicator).");
|
|
1176
|
+
println!(" Or Cmd+C an image and send any message — it attaches automatically.");
|
|
1129
1177
|
println!(" Or use \x1b[1;32m/attach\x1b[0m \x1b[2mpath/to/file.png\x1b[0m for a specific file.");
|
|
1130
1178
|
println!(" For broadest clipboard support: \x1b[2mbrew install pngpaste\x1b[0m");
|
|
1131
1179
|
println!();
|
|
@@ -1167,6 +1215,26 @@ fn print_session_info(is_tty: bool, path: Option<&Path>, turns: usize, saved_at:
|
|
|
1167
1215
|
println!();
|
|
1168
1216
|
}
|
|
1169
1217
|
|
|
1218
|
+
fn export_conversation(path: &std::path::Path, history: &[ChatMessage]) -> Result<()> {
|
|
1219
|
+
let mut out = String::new();
|
|
1220
|
+
for msg in history {
|
|
1221
|
+
match msg.role {
|
|
1222
|
+
ChatRole::User => {
|
|
1223
|
+
out.push_str("## You\n\n");
|
|
1224
|
+
out.push_str(&msg.content);
|
|
1225
|
+
out.push_str("\n\n");
|
|
1226
|
+
}
|
|
1227
|
+
ChatRole::Assistant => {
|
|
1228
|
+
out.push_str("## Assistant\n\n");
|
|
1229
|
+
out.push_str(&msg.content);
|
|
1230
|
+
out.push_str("\n\n");
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
fs::write(path, out.trim_end())
|
|
1235
|
+
.with_context(|| format!("failed to write {}", path.display()))
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1170
1238
|
fn list_providers() -> Result<()> {
|
|
1171
1239
|
let config = AppConfig::load()?;
|
|
1172
1240
|
let is_tty = io::stdout().is_terminal();
|
|
@@ -1336,6 +1404,8 @@ struct PromptBuffer {
|
|
|
1336
1404
|
full: String,
|
|
1337
1405
|
display: String,
|
|
1338
1406
|
segments: Vec<PromptSegment>,
|
|
1407
|
+
/// Byte offset into `full` — where the next insertion goes.
|
|
1408
|
+
cursor: usize,
|
|
1339
1409
|
}
|
|
1340
1410
|
|
|
1341
1411
|
impl PromptBuffer {
|
|
@@ -1343,28 +1413,63 @@ impl PromptBuffer {
|
|
|
1343
1413
|
self.full.is_empty()
|
|
1344
1414
|
}
|
|
1345
1415
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1416
|
+
/// Char offset in `display` that corresponds to the current cursor position in `full`.
|
|
1417
|
+
/// Used to position the terminal cursor after a redraw.
|
|
1418
|
+
fn display_cursor_char(&self) -> usize {
|
|
1419
|
+
let mut full_pos = 0usize;
|
|
1420
|
+
let mut disp_chars = 0usize;
|
|
1421
|
+
for seg in &self.segments {
|
|
1422
|
+
let seg_len = seg.full.len();
|
|
1423
|
+
let next_pos = full_pos + seg_len;
|
|
1424
|
+
if self.cursor <= next_pos {
|
|
1425
|
+
let offset = self.cursor - full_pos;
|
|
1426
|
+
return if seg.hidden {
|
|
1427
|
+
// Hidden spans are atomic: cursor snaps to end of placeholder.
|
|
1428
|
+
disp_chars + seg.display.chars().count()
|
|
1429
|
+
} else {
|
|
1430
|
+
disp_chars + seg.full[..offset].chars().count()
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
full_pos = next_pos;
|
|
1434
|
+
disp_chars += seg.display.chars().count();
|
|
1356
1435
|
}
|
|
1436
|
+
disp_chars
|
|
1437
|
+
}
|
|
1357
1438
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1439
|
+
fn push_text(&mut self, text: &str) {
|
|
1440
|
+
// Find the segment containing the cursor and insert there.
|
|
1441
|
+
let mut pos = 0usize;
|
|
1442
|
+
for seg in self.segments.iter_mut() {
|
|
1443
|
+
let seg_len = seg.full.len();
|
|
1444
|
+
if !seg.hidden && self.cursor >= pos && self.cursor <= pos + seg_len {
|
|
1445
|
+
let offset = self.cursor - pos;
|
|
1446
|
+
seg.full.insert_str(offset, text);
|
|
1447
|
+
seg.display.insert_str(offset, text);
|
|
1448
|
+
self.cursor += text.len();
|
|
1449
|
+
self.rebuild_flat();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
pos += seg_len;
|
|
1453
|
+
}
|
|
1454
|
+
// Cursor is at end or after a hidden segment — append to last visible segment.
|
|
1455
|
+
if let Some(seg) = self.segments.last_mut().filter(|s| !s.hidden) {
|
|
1456
|
+
seg.full.push_str(text);
|
|
1457
|
+
seg.display.push_str(text);
|
|
1458
|
+
} else {
|
|
1459
|
+
self.segments.push(PromptSegment {
|
|
1460
|
+
full: text.to_string(),
|
|
1461
|
+
display: text.to_string(),
|
|
1462
|
+
hidden: false,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
self.cursor += text.len();
|
|
1466
|
+
self.rebuild_flat();
|
|
1363
1467
|
}
|
|
1364
1468
|
|
|
1365
1469
|
fn push_hidden_paste(&mut self, text: String, display: String) {
|
|
1366
1470
|
self.full.push_str(&text);
|
|
1367
1471
|
self.display.push_str(&display);
|
|
1472
|
+
self.cursor = self.full.len();
|
|
1368
1473
|
self.segments.push(PromptSegment {
|
|
1369
1474
|
full: text,
|
|
1370
1475
|
display,
|
|
@@ -1372,48 +1477,118 @@ impl PromptBuffer {
|
|
|
1372
1477
|
});
|
|
1373
1478
|
}
|
|
1374
1479
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1480
|
+
/// Delete the character immediately before the cursor.
|
|
1481
|
+
/// Deletes the entire span atomically if the cursor is just past a hidden span.
|
|
1482
|
+
fn delete_before_cursor(&mut self) {
|
|
1483
|
+
if self.cursor == 0 {
|
|
1377
1484
|
return;
|
|
1378
|
-
}
|
|
1485
|
+
}
|
|
1486
|
+
let mut pos = 0usize;
|
|
1487
|
+
let mut remove_idx: Option<usize> = None;
|
|
1488
|
+
for (i, seg) in self.segments.iter_mut().enumerate() {
|
|
1489
|
+
let seg_len = seg.full.len();
|
|
1490
|
+
let next_pos = pos + seg_len;
|
|
1491
|
+
if seg.hidden && next_pos == self.cursor {
|
|
1492
|
+
// cursor is right after a hidden span — delete the whole span
|
|
1493
|
+
self.cursor -= seg_len;
|
|
1494
|
+
remove_idx = Some(i);
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
if !seg.hidden && self.cursor > pos && self.cursor <= next_pos {
|
|
1498
|
+
let offset = self.cursor - pos;
|
|
1499
|
+
if let Some(ch) = seg.full[..offset].chars().next_back() {
|
|
1500
|
+
let ch_len = ch.len_utf8();
|
|
1501
|
+
seg.full.drain((offset - ch_len)..offset);
|
|
1502
|
+
seg.display.drain((offset - ch_len)..offset);
|
|
1503
|
+
self.cursor -= ch_len;
|
|
1504
|
+
if seg.full.is_empty() {
|
|
1505
|
+
remove_idx = Some(i);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
pos = next_pos;
|
|
1511
|
+
}
|
|
1512
|
+
if let Some(i) = remove_idx {
|
|
1513
|
+
self.segments.remove(i);
|
|
1514
|
+
}
|
|
1515
|
+
self.rebuild_flat();
|
|
1516
|
+
}
|
|
1379
1517
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
let display_len = segment.display.len();
|
|
1383
|
-
self.full.truncate(self.full.len().saturating_sub(full_len));
|
|
1384
|
-
self.display
|
|
1385
|
-
.truncate(self.display.len().saturating_sub(display_len));
|
|
1386
|
-
self.segments.pop();
|
|
1518
|
+
fn move_left(&mut self) {
|
|
1519
|
+
if self.cursor == 0 {
|
|
1387
1520
|
return;
|
|
1388
1521
|
}
|
|
1522
|
+
let mut pos = 0usize;
|
|
1523
|
+
for seg in &self.segments {
|
|
1524
|
+
let next_pos = pos + seg.full.len();
|
|
1525
|
+
if seg.hidden && next_pos == self.cursor {
|
|
1526
|
+
self.cursor = pos;
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if !seg.hidden && self.cursor > pos && self.cursor <= next_pos {
|
|
1530
|
+
let offset = self.cursor - pos;
|
|
1531
|
+
if let Some(ch) = seg.full[..offset].chars().next_back() {
|
|
1532
|
+
self.cursor -= ch.len_utf8();
|
|
1533
|
+
}
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
pos = next_pos;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1389
1539
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1540
|
+
fn move_right(&mut self) {
|
|
1541
|
+
if self.cursor >= self.full.len() {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
let mut pos = 0usize;
|
|
1545
|
+
for seg in &self.segments {
|
|
1546
|
+
let seg_len = seg.full.len();
|
|
1547
|
+
if seg.hidden && pos == self.cursor {
|
|
1548
|
+
self.cursor += seg_len;
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if !seg.hidden && self.cursor >= pos && self.cursor < pos + seg_len {
|
|
1552
|
+
let offset = self.cursor - pos;
|
|
1553
|
+
if let Some(ch) = seg.full[offset..].chars().next() {
|
|
1554
|
+
self.cursor += ch.len_utf8();
|
|
1555
|
+
}
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
pos += seg_len;
|
|
1397
1559
|
}
|
|
1398
1560
|
}
|
|
1399
1561
|
|
|
1562
|
+
fn move_home(&mut self) {
|
|
1563
|
+
self.cursor = 0;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
fn move_end(&mut self) {
|
|
1567
|
+
self.cursor = self.full.len();
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1400
1570
|
/// Ctrl+U / Cmd+Delete — erase the entire line.
|
|
1401
1571
|
fn clear_all(&mut self) {
|
|
1402
1572
|
self.full.clear();
|
|
1403
1573
|
self.display.clear();
|
|
1404
1574
|
self.segments.clear();
|
|
1575
|
+
self.cursor = 0;
|
|
1405
1576
|
}
|
|
1406
1577
|
|
|
1407
|
-
/// Ctrl+W / Option+Delete — erase the last word
|
|
1578
|
+
/// Ctrl+W / Option+Delete — erase the last word before the cursor.
|
|
1408
1579
|
fn pop_word(&mut self) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
self.pop_last();
|
|
1580
|
+
while self.cursor > 0 && self.full[..self.cursor].ends_with(' ') {
|
|
1581
|
+
self.delete_before_cursor();
|
|
1412
1582
|
}
|
|
1413
|
-
while
|
|
1414
|
-
self.
|
|
1583
|
+
while self.cursor > 0 && !self.full[..self.cursor].ends_with(' ') {
|
|
1584
|
+
self.delete_before_cursor();
|
|
1415
1585
|
}
|
|
1416
1586
|
}
|
|
1587
|
+
|
|
1588
|
+
fn rebuild_flat(&mut self) {
|
|
1589
|
+
self.full = self.segments.iter().map(|s| s.full.as_str()).collect();
|
|
1590
|
+
self.display = self.segments.iter().map(|s| s.display.as_str()).collect();
|
|
1591
|
+
}
|
|
1417
1592
|
}
|
|
1418
1593
|
|
|
1419
1594
|
#[cfg(unix)]
|
|
@@ -1473,11 +1648,23 @@ impl RawPromptMode {
|
|
|
1473
1648
|
}
|
|
1474
1649
|
}
|
|
1475
1650
|
|
|
1651
|
+
/// After a redraw (which leaves the terminal cursor at end of display), move it
|
|
1652
|
+
/// back to the buffer's logical cursor position.
|
|
1653
|
+
fn position_prompt_cursor(display: &str, cursor_char: usize) -> io::Result<()> {
|
|
1654
|
+
let back = display.chars().count().saturating_sub(cursor_char);
|
|
1655
|
+
if back > 0 {
|
|
1656
|
+
print!("\x1b[{}D", back);
|
|
1657
|
+
io::stdout().flush()?;
|
|
1658
|
+
}
|
|
1659
|
+
Ok(())
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1476
1662
|
fn read_prompt_line(
|
|
1477
1663
|
label: &str,
|
|
1478
1664
|
width: usize,
|
|
1479
1665
|
paste_count: &mut usize,
|
|
1480
1666
|
images_available: bool,
|
|
1667
|
+
input_history: &[String],
|
|
1481
1668
|
) -> Result<PromptRead> {
|
|
1482
1669
|
let _raw_mode = RawPromptMode::enter()?;
|
|
1483
1670
|
let mut input = io::stdin().lock();
|
|
@@ -1485,6 +1672,10 @@ fn read_prompt_line(
|
|
|
1485
1672
|
let mut display_rows = 1usize;
|
|
1486
1673
|
let mut ctrl_v_image: Option<ImageAttachment> = None;
|
|
1487
1674
|
|
|
1675
|
+
// History navigation state.
|
|
1676
|
+
let mut hist_idx: Option<usize> = None; // None = current live input
|
|
1677
|
+
let mut saved_input = String::new(); // stash live input when navigating into history
|
|
1678
|
+
|
|
1488
1679
|
// Compose the visible prompt label, optionally prefixed with an image indicator.
|
|
1489
1680
|
let effective_label = |img: &Option<ImageAttachment>| -> String {
|
|
1490
1681
|
if img.is_some() {
|
|
@@ -1494,6 +1685,16 @@ fn read_prompt_line(
|
|
|
1494
1685
|
}
|
|
1495
1686
|
};
|
|
1496
1687
|
|
|
1688
|
+
// Redraw the line and position the cursor, returning the new row count.
|
|
1689
|
+
macro_rules! redraw {
|
|
1690
|
+
() => {{
|
|
1691
|
+
let lbl = effective_label(&ctrl_v_image);
|
|
1692
|
+
let rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1693
|
+
let _ = position_prompt_cursor(&buffer.display, buffer.display_cursor_char());
|
|
1694
|
+
rows
|
|
1695
|
+
}};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1497
1698
|
print!("{}", effective_label(&ctrl_v_image));
|
|
1498
1699
|
io::stdout().flush().context("failed to write prompt")?;
|
|
1499
1700
|
|
|
@@ -1509,35 +1710,28 @@ fn read_prompt_line(
|
|
|
1509
1710
|
return Ok(PromptRead::Line(buffer.full, ctrl_v_image));
|
|
1510
1711
|
}
|
|
1511
1712
|
3 => {
|
|
1512
|
-
// Ctrl+C — discard any pasted image too
|
|
1513
|
-
ctrl_v_image = None;
|
|
1514
1713
|
println!("^C");
|
|
1515
1714
|
return Ok(PromptRead::Interrupted);
|
|
1516
1715
|
}
|
|
1517
1716
|
4 if buffer.is_empty() => return Ok(PromptRead::Eof),
|
|
1518
1717
|
8 | 127 => {
|
|
1519
1718
|
// Backspace
|
|
1520
|
-
buffer.
|
|
1521
|
-
|
|
1522
|
-
display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1719
|
+
buffer.delete_before_cursor();
|
|
1720
|
+
display_rows = redraw!();
|
|
1523
1721
|
}
|
|
1524
1722
|
21 => {
|
|
1525
1723
|
// Ctrl+U / Cmd+Delete — erase entire line
|
|
1526
1724
|
buffer.clear_all();
|
|
1527
|
-
|
|
1528
|
-
display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1725
|
+
display_rows = redraw!();
|
|
1529
1726
|
}
|
|
1530
1727
|
22 if images_available => {
|
|
1531
1728
|
// Ctrl+V — paste image from clipboard
|
|
1532
1729
|
match grab_clipboard_image() {
|
|
1533
1730
|
Some(img) => {
|
|
1534
1731
|
ctrl_v_image = Some(img);
|
|
1535
|
-
|
|
1536
|
-
display_rows =
|
|
1537
|
-
redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1732
|
+
display_rows = redraw!();
|
|
1538
1733
|
}
|
|
1539
1734
|
None => {
|
|
1540
|
-
// No image in clipboard — ring the bell
|
|
1541
1735
|
print!("\x07");
|
|
1542
1736
|
let _ = io::stdout().flush();
|
|
1543
1737
|
}
|
|
@@ -1546,23 +1740,90 @@ fn read_prompt_line(
|
|
|
1546
1740
|
23 => {
|
|
1547
1741
|
// Ctrl+W / Option+Delete — erase last word
|
|
1548
1742
|
buffer.pop_word();
|
|
1549
|
-
|
|
1550
|
-
display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1743
|
+
display_rows = redraw!();
|
|
1551
1744
|
}
|
|
1552
1745
|
0x1b => {
|
|
1553
1746
|
let sequence = read_escape_sequence(&mut input)?;
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1747
|
+
match sequence.as_slice() {
|
|
1748
|
+
b"[200~" => {
|
|
1749
|
+
// Bracketed paste
|
|
1750
|
+
let paste = normalize_pasted_text(read_bracketed_paste(&mut input)?);
|
|
1751
|
+
push_paste(&mut buffer, paste, paste_count);
|
|
1752
|
+
display_rows = redraw!();
|
|
1753
|
+
}
|
|
1754
|
+
b"[A" => {
|
|
1755
|
+
// Up arrow — previous history entry
|
|
1756
|
+
if input_history.is_empty() {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
let new_idx = match hist_idx {
|
|
1760
|
+
None => {
|
|
1761
|
+
saved_input = buffer.full.clone();
|
|
1762
|
+
input_history.len() - 1
|
|
1763
|
+
}
|
|
1764
|
+
Some(0) => 0,
|
|
1765
|
+
Some(i) => i - 1,
|
|
1766
|
+
};
|
|
1767
|
+
hist_idx = Some(new_idx);
|
|
1768
|
+
buffer = PromptBuffer::default();
|
|
1769
|
+
buffer.push_text(&input_history[new_idx].clone());
|
|
1770
|
+
display_rows = redraw!();
|
|
1771
|
+
}
|
|
1772
|
+
b"[B" => {
|
|
1773
|
+
// Down arrow — next history entry / back to live input
|
|
1774
|
+
match hist_idx {
|
|
1775
|
+
None => {}
|
|
1776
|
+
Some(i) if i + 1 >= input_history.len() => {
|
|
1777
|
+
hist_idx = None;
|
|
1778
|
+
let text = std::mem::take(&mut saved_input);
|
|
1779
|
+
buffer = PromptBuffer::default();
|
|
1780
|
+
buffer.push_text(&text);
|
|
1781
|
+
display_rows = redraw!();
|
|
1782
|
+
}
|
|
1783
|
+
Some(i) => {
|
|
1784
|
+
hist_idx = Some(i + 1);
|
|
1785
|
+
buffer = PromptBuffer::default();
|
|
1786
|
+
buffer.push_text(&input_history[i + 1].clone());
|
|
1787
|
+
display_rows = redraw!();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
b"[C" => {
|
|
1792
|
+
// Right arrow
|
|
1793
|
+
buffer.move_right();
|
|
1794
|
+
let _ = position_prompt_cursor(
|
|
1795
|
+
&buffer.display,
|
|
1796
|
+
buffer.display_cursor_char(),
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
b"[D" => {
|
|
1800
|
+
// Left arrow
|
|
1801
|
+
buffer.move_left();
|
|
1802
|
+
let _ = position_prompt_cursor(
|
|
1803
|
+
&buffer.display,
|
|
1804
|
+
buffer.display_cursor_char(),
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
b"[H" | b"[1~" => {
|
|
1808
|
+
// Home
|
|
1809
|
+
buffer.move_home();
|
|
1810
|
+
let _ = position_prompt_cursor(&buffer.display, 0);
|
|
1811
|
+
}
|
|
1812
|
+
b"[F" | b"[4~" => {
|
|
1813
|
+
// End
|
|
1814
|
+
buffer.move_end();
|
|
1815
|
+
let _ = position_prompt_cursor(
|
|
1816
|
+
&buffer.display,
|
|
1817
|
+
buffer.display_cursor_char(),
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
_ => {}
|
|
1559
1821
|
}
|
|
1560
1822
|
}
|
|
1561
1823
|
byte if byte >= 0x20 && byte != 0x7f => {
|
|
1562
1824
|
if let Some(ch) = read_utf8_char(byte, &mut input)? {
|
|
1563
1825
|
buffer.push_text(ch.encode_utf8(&mut [0; 4]));
|
|
1564
|
-
|
|
1565
|
-
display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
|
|
1826
|
+
display_rows = redraw!();
|
|
1566
1827
|
}
|
|
1567
1828
|
}
|
|
1568
1829
|
_ => {}
|