anveesa 0.3.4 → 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 +371 -65
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("-"),
|
|
@@ -143,15 +160,22 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
143
160
|
|
|
144
161
|
loop {
|
|
145
162
|
print_input_separator(is_tty, width);
|
|
146
|
-
let line
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
let (line, ctrl_v_image) =
|
|
164
|
+
match read_prompt_line(&label, width, &mut paste_count, images_available, &input_history) {
|
|
165
|
+
Ok(PromptRead::Line(line, img)) => (line, img),
|
|
166
|
+
Ok(PromptRead::Interrupted) => continue,
|
|
167
|
+
Ok(PromptRead::Eof) => {
|
|
168
|
+
println!();
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
Err(error) => return Err(error).context("failed to read interactive prompt"),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Ctrl+V image takes precedence over a previously pending image.
|
|
175
|
+
if let Some(img) = ctrl_v_image {
|
|
176
|
+
last_image_fp = Some(image_fingerprint(&img));
|
|
177
|
+
pending_image = Some(img);
|
|
178
|
+
}
|
|
155
179
|
|
|
156
180
|
let prompt = line.trim().to_string();
|
|
157
181
|
if prompt.is_empty() {
|
|
@@ -190,6 +214,25 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
190
214
|
);
|
|
191
215
|
continue;
|
|
192
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
|
+
}
|
|
193
236
|
"/status" => {
|
|
194
237
|
print_status_inline(
|
|
195
238
|
is_tty,
|
|
@@ -1100,8 +1143,9 @@ fn print_status_inline(
|
|
|
1100
1143
|
|
|
1101
1144
|
fn print_help_inline(is_tty: bool) {
|
|
1102
1145
|
if !is_tty {
|
|
1103
|
-
println!("commands: /clear, /session, /attach [path], /exit, /quit, /help");
|
|
1104
|
-
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");
|
|
1105
1149
|
return;
|
|
1106
1150
|
}
|
|
1107
1151
|
println!();
|
|
@@ -1109,6 +1153,7 @@ fn print_help_inline(is_tty: bool) {
|
|
|
1109
1153
|
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
1110
1154
|
println!(" \x1b[1;32m/status\x1b[0m provider, model, turns, token usage");
|
|
1111
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");
|
|
1112
1157
|
println!(" \x1b[1;32m/model\x1b[0m \x1b[2m[name]\x1b[0m switch or show current model");
|
|
1113
1158
|
println!(" \x1b[1;32m/provider\x1b[0m \x1b[2m[name]\x1b[0m switch or show current provider");
|
|
1114
1159
|
println!(" \x1b[1;32m/clear\x1b[0m reset conversation and delete saved session");
|
|
@@ -1116,9 +1161,19 @@ fn print_help_inline(is_tty: bool) {
|
|
|
1116
1161
|
println!(" \x1b[1;32m/exit\x1b[0m, \x1b[1;32m/quit\x1b[0m leave the session");
|
|
1117
1162
|
println!(" \x1b[1;32m/help\x1b[0m show this message");
|
|
1118
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!();
|
|
1119
1173
|
println!("\x1b[2m Images\x1b[0m");
|
|
1120
1174
|
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
1121
|
-
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.");
|
|
1122
1177
|
println!(" Or use \x1b[1;32m/attach\x1b[0m \x1b[2mpath/to/file.png\x1b[0m for a specific file.");
|
|
1123
1178
|
println!(" For broadest clipboard support: \x1b[2mbrew install pngpaste\x1b[0m");
|
|
1124
1179
|
println!();
|
|
@@ -1160,6 +1215,26 @@ fn print_session_info(is_tty: bool, path: Option<&Path>, turns: usize, saved_at:
|
|
|
1160
1215
|
println!();
|
|
1161
1216
|
}
|
|
1162
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
|
+
|
|
1163
1238
|
fn list_providers() -> Result<()> {
|
|
1164
1239
|
let config = AppConfig::load()?;
|
|
1165
1240
|
let is_tty = io::stdout().is_terminal();
|
|
@@ -1313,7 +1388,7 @@ fn print_session_header(
|
|
|
1313
1388
|
}
|
|
1314
1389
|
|
|
1315
1390
|
enum PromptRead {
|
|
1316
|
-
Line(String),
|
|
1391
|
+
Line(String, Option<ImageAttachment>),
|
|
1317
1392
|
Interrupted,
|
|
1318
1393
|
Eof,
|
|
1319
1394
|
}
|
|
@@ -1329,6 +1404,8 @@ struct PromptBuffer {
|
|
|
1329
1404
|
full: String,
|
|
1330
1405
|
display: String,
|
|
1331
1406
|
segments: Vec<PromptSegment>,
|
|
1407
|
+
/// Byte offset into `full` — where the next insertion goes.
|
|
1408
|
+
cursor: usize,
|
|
1332
1409
|
}
|
|
1333
1410
|
|
|
1334
1411
|
impl PromptBuffer {
|
|
@@ -1336,28 +1413,63 @@ impl PromptBuffer {
|
|
|
1336
1413
|
self.full.is_empty()
|
|
1337
1414
|
}
|
|
1338
1415
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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();
|
|
1349
1435
|
}
|
|
1436
|
+
disp_chars
|
|
1437
|
+
}
|
|
1350
1438
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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();
|
|
1356
1467
|
}
|
|
1357
1468
|
|
|
1358
1469
|
fn push_hidden_paste(&mut self, text: String, display: String) {
|
|
1359
1470
|
self.full.push_str(&text);
|
|
1360
1471
|
self.display.push_str(&display);
|
|
1472
|
+
self.cursor = self.full.len();
|
|
1361
1473
|
self.segments.push(PromptSegment {
|
|
1362
1474
|
full: text,
|
|
1363
1475
|
display,
|
|
@@ -1365,29 +1477,94 @@ impl PromptBuffer {
|
|
|
1365
1477
|
});
|
|
1366
1478
|
}
|
|
1367
1479
|
|
|
1368
|
-
|
|
1369
|
-
|
|
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 {
|
|
1370
1484
|
return;
|
|
1371
|
-
}
|
|
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
|
+
}
|
|
1372
1517
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
let display_len = segment.display.len();
|
|
1376
|
-
self.full.truncate(self.full.len().saturating_sub(full_len));
|
|
1377
|
-
self.display
|
|
1378
|
-
.truncate(self.display.len().saturating_sub(display_len));
|
|
1379
|
-
self.segments.pop();
|
|
1518
|
+
fn move_left(&mut self) {
|
|
1519
|
+
if self.cursor == 0 {
|
|
1380
1520
|
return;
|
|
1381
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
|
+
}
|
|
1382
1539
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
let _ = self.display.pop();
|
|
1387
|
-
|
|
1388
|
-
if segment.full.is_empty() {
|
|
1389
|
-
self.segments.pop();
|
|
1540
|
+
fn move_right(&mut self) {
|
|
1541
|
+
if self.cursor >= self.full.len() {
|
|
1542
|
+
return;
|
|
1390
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;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
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();
|
|
1391
1568
|
}
|
|
1392
1569
|
|
|
1393
1570
|
/// Ctrl+U / Cmd+Delete — erase the entire line.
|
|
@@ -1395,18 +1572,23 @@ impl PromptBuffer {
|
|
|
1395
1572
|
self.full.clear();
|
|
1396
1573
|
self.display.clear();
|
|
1397
1574
|
self.segments.clear();
|
|
1575
|
+
self.cursor = 0;
|
|
1398
1576
|
}
|
|
1399
1577
|
|
|
1400
|
-
/// Ctrl+W / Option+Delete — erase the last word
|
|
1578
|
+
/// Ctrl+W / Option+Delete — erase the last word before the cursor.
|
|
1401
1579
|
fn pop_word(&mut self) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
self.pop_last();
|
|
1580
|
+
while self.cursor > 0 && self.full[..self.cursor].ends_with(' ') {
|
|
1581
|
+
self.delete_before_cursor();
|
|
1405
1582
|
}
|
|
1406
|
-
while
|
|
1407
|
-
self.
|
|
1583
|
+
while self.cursor > 0 && !self.full[..self.cursor].ends_with(' ') {
|
|
1584
|
+
self.delete_before_cursor();
|
|
1408
1585
|
}
|
|
1409
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
|
+
}
|
|
1410
1592
|
}
|
|
1411
1593
|
|
|
1412
1594
|
#[cfg(unix)]
|
|
@@ -1466,13 +1648,54 @@ impl RawPromptMode {
|
|
|
1466
1648
|
}
|
|
1467
1649
|
}
|
|
1468
1650
|
|
|
1469
|
-
|
|
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
|
+
|
|
1662
|
+
fn read_prompt_line(
|
|
1663
|
+
label: &str,
|
|
1664
|
+
width: usize,
|
|
1665
|
+
paste_count: &mut usize,
|
|
1666
|
+
images_available: bool,
|
|
1667
|
+
input_history: &[String],
|
|
1668
|
+
) -> Result<PromptRead> {
|
|
1470
1669
|
let _raw_mode = RawPromptMode::enter()?;
|
|
1471
1670
|
let mut input = io::stdin().lock();
|
|
1472
1671
|
let mut buffer = PromptBuffer::default();
|
|
1473
1672
|
let mut display_rows = 1usize;
|
|
1673
|
+
let mut ctrl_v_image: Option<ImageAttachment> = None;
|
|
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
|
+
|
|
1679
|
+
// Compose the visible prompt label, optionally prefixed with an image indicator.
|
|
1680
|
+
let effective_label = |img: &Option<ImageAttachment>| -> String {
|
|
1681
|
+
if img.is_some() {
|
|
1682
|
+
format!("\x1b[2m[📎]\x1b[0m {label}")
|
|
1683
|
+
} else {
|
|
1684
|
+
label.to_string()
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1474
1687
|
|
|
1475
|
-
|
|
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
|
+
|
|
1698
|
+
print!("{}", effective_label(&ctrl_v_image));
|
|
1476
1699
|
io::stdout().flush().context("failed to write prompt")?;
|
|
1477
1700
|
|
|
1478
1701
|
loop {
|
|
@@ -1484,7 +1707,7 @@ fn read_prompt_line(label: &str, width: usize, paste_count: &mut usize) -> Resul
|
|
|
1484
1707
|
match byte[0] {
|
|
1485
1708
|
b'\r' | b'\n' => {
|
|
1486
1709
|
println!();
|
|
1487
|
-
return Ok(PromptRead::Line(buffer.full));
|
|
1710
|
+
return Ok(PromptRead::Line(buffer.full, ctrl_v_image));
|
|
1488
1711
|
}
|
|
1489
1712
|
3 => {
|
|
1490
1713
|
println!("^C");
|
|
@@ -1493,31 +1716,114 @@ fn read_prompt_line(label: &str, width: usize, paste_count: &mut usize) -> Resul
|
|
|
1493
1716
|
4 if buffer.is_empty() => return Ok(PromptRead::Eof),
|
|
1494
1717
|
8 | 127 => {
|
|
1495
1718
|
// Backspace
|
|
1496
|
-
buffer.
|
|
1497
|
-
display_rows =
|
|
1719
|
+
buffer.delete_before_cursor();
|
|
1720
|
+
display_rows = redraw!();
|
|
1498
1721
|
}
|
|
1499
1722
|
21 => {
|
|
1500
1723
|
// Ctrl+U / Cmd+Delete — erase entire line
|
|
1501
1724
|
buffer.clear_all();
|
|
1502
|
-
display_rows =
|
|
1725
|
+
display_rows = redraw!();
|
|
1726
|
+
}
|
|
1727
|
+
22 if images_available => {
|
|
1728
|
+
// Ctrl+V — paste image from clipboard
|
|
1729
|
+
match grab_clipboard_image() {
|
|
1730
|
+
Some(img) => {
|
|
1731
|
+
ctrl_v_image = Some(img);
|
|
1732
|
+
display_rows = redraw!();
|
|
1733
|
+
}
|
|
1734
|
+
None => {
|
|
1735
|
+
print!("\x07");
|
|
1736
|
+
let _ = io::stdout().flush();
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1503
1739
|
}
|
|
1504
1740
|
23 => {
|
|
1505
1741
|
// Ctrl+W / Option+Delete — erase last word
|
|
1506
1742
|
buffer.pop_word();
|
|
1507
|
-
display_rows =
|
|
1743
|
+
display_rows = redraw!();
|
|
1508
1744
|
}
|
|
1509
1745
|
0x1b => {
|
|
1510
1746
|
let sequence = read_escape_sequence(&mut input)?;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
_ => {}
|
|
1515
1821
|
}
|
|
1516
1822
|
}
|
|
1517
1823
|
byte if byte >= 0x20 && byte != 0x7f => {
|
|
1518
1824
|
if let Some(ch) = read_utf8_char(byte, &mut input)? {
|
|
1519
1825
|
buffer.push_text(ch.encode_utf8(&mut [0; 4]));
|
|
1520
|
-
display_rows =
|
|
1826
|
+
display_rows = redraw!();
|
|
1521
1827
|
}
|
|
1522
1828
|
}
|
|
1523
1829
|
_ => {}
|