1use std::collections::VecDeque;
28
29pub const HISTORY_CAP: usize = 256;
33
34pub const MAX_LINES: usize = 64;
39
40#[derive(Debug, Default, Clone)]
50pub struct PromptHistory {
51 entries: VecDeque<String>,
52 cap: usize,
53 cursor: Option<usize>,
54}
55
56impl PromptHistory {
57 #[must_use]
58 pub fn new() -> Self {
59 Self::with_capacity(HISTORY_CAP)
60 }
61
62 #[must_use]
63 pub fn with_capacity(cap: usize) -> Self {
64 Self {
65 entries: VecDeque::new(),
66 cap,
67 cursor: None,
68 }
69 }
70
71 pub fn push(&mut self, line: &str) {
76 if line.trim().is_empty() {
77 return;
78 }
79 if self.entries.back().is_some_and(|last| last == line) {
80 self.cursor = None;
81 return;
82 }
83 if self.cap > 0 && self.entries.len() >= self.cap {
84 self.entries.pop_front();
85 }
86 self.entries.push_back(line.to_string());
87 self.cursor = None;
88 }
89
90 pub fn reset_cursor(&mut self) {
93 self.cursor = None;
94 }
95
96 pub fn recall_prev(&mut self) -> Option<&str> {
99 if self.entries.is_empty() {
100 return None;
101 }
102 let next = match self.cursor {
103 None => self.entries.len().saturating_sub(1),
104 Some(0) => 0,
105 Some(i) => i - 1,
106 };
107 self.cursor = Some(next);
108 self.entries.get(next).map(String::as_str)
109 }
110
111 pub fn recall_next(&mut self) -> Option<&str> {
116 let cur = self.cursor?;
117 if cur + 1 >= self.entries.len() {
118 self.cursor = None;
119 return None;
120 }
121 let next = cur + 1;
122 self.cursor = Some(next);
123 self.entries.get(next).map(String::as_str)
124 }
125
126 #[must_use]
129 pub const fn is_recalling(&self) -> bool {
130 self.cursor.is_some()
131 }
132
133 #[must_use]
134 pub fn len(&self) -> usize {
135 self.entries.len()
136 }
137
138 #[must_use]
139 pub fn is_empty(&self) -> bool {
140 self.entries.is_empty()
141 }
142}
143
144#[derive(Debug)]
145pub struct PromptBuffer {
146 lines: Vec<Vec<char>>,
147 row: usize,
148 col: usize,
149 desired_col: Option<usize>,
154 history: PromptHistory,
155 saved_draft: Option<Vec<Vec<char>>>,
159}
160
161impl Default for PromptBuffer {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl PromptBuffer {
168 #[must_use]
169 pub fn new() -> Self {
170 Self {
171 lines: vec![Vec::new()],
172 row: 0,
173 col: 0,
174 desired_col: None,
175 history: PromptHistory::new(),
176 saved_draft: None,
177 }
178 }
179
180 #[must_use]
183 pub fn is_empty(&self) -> bool {
184 self.lines.iter().all(Vec::is_empty)
185 }
186
187 #[must_use]
189 pub fn as_string(&self) -> String {
190 let mut out = String::new();
191 for (i, line) in self.lines.iter().enumerate() {
192 if i > 0 {
193 out.push('\n');
194 }
195 for c in line {
196 out.push(*c);
197 }
198 }
199 out
200 }
201
202 #[must_use]
204 pub fn height(&self) -> usize {
205 self.lines.len()
206 }
207
208 #[must_use]
210 pub const fn cursor_row(&self) -> usize {
211 self.row
212 }
213
214 #[must_use]
216 pub const fn cursor(&self) -> usize {
217 self.col
218 }
219
220 #[must_use]
222 pub fn cursor_column(&self) -> u16 {
223 u16::try_from(self.col).unwrap_or(u16::MAX)
224 }
225
226 #[must_use]
228 pub fn line(&self, row: usize) -> Option<&[char]> {
229 self.lines.get(row).map(Vec::as_slice)
230 }
231
232 #[must_use]
234 pub const fn history(&self) -> &PromptHistory {
235 &self.history
236 }
237
238 pub fn insert(&mut self, c: char) {
244 self.history.reset_cursor();
245 self.saved_draft = None;
246 self.desired_col = None;
247 let line = &mut self.lines[self.row];
248 line.insert(self.col, c);
249 self.col += 1;
250 }
251
252 pub fn insert_newline(&mut self) {
257 if self.lines.len() >= MAX_LINES {
258 return;
259 }
260 self.history.reset_cursor();
261 self.saved_draft = None;
262 self.desired_col = None;
263 let tail = self.lines[self.row].split_off(self.col);
264 self.lines.insert(self.row + 1, tail);
265 self.row += 1;
266 self.col = 0;
267 }
268
269 pub fn backspace(&mut self) {
273 self.desired_col = None;
274 if self.col > 0 {
275 self.col -= 1;
276 self.lines[self.row].remove(self.col);
277 return;
278 }
279 if self.row == 0 {
280 return;
281 }
282 let tail = std::mem::take(&mut self.lines[self.row]);
284 self.lines.remove(self.row);
285 self.row -= 1;
286 self.col = self.lines[self.row].len();
287 self.lines[self.row].extend(tail);
288 }
289
290 pub fn delete(&mut self) {
293 self.desired_col = None;
294 let line_len = self.lines[self.row].len();
295 if self.col < line_len {
296 self.lines[self.row].remove(self.col);
297 return;
298 }
299 if self.row + 1 >= self.lines.len() {
300 return;
301 }
302 let next = self.lines.remove(self.row + 1);
303 self.lines[self.row].extend(next);
304 }
305
306 pub fn move_left(&mut self) {
309 self.desired_col = None;
310 if self.col > 0 {
311 self.col -= 1;
312 return;
313 }
314 if self.row > 0 {
315 self.row -= 1;
316 self.col = self.lines[self.row].len();
317 }
318 }
319
320 pub fn move_right(&mut self) {
321 self.desired_col = None;
322 if self.col < self.lines[self.row].len() {
323 self.col += 1;
324 return;
325 }
326 if self.row + 1 < self.lines.len() {
327 self.row += 1;
328 self.col = 0;
329 }
330 }
331
332 pub fn move_home(&mut self) {
333 self.desired_col = None;
334 self.col = 0;
335 }
336
337 pub fn move_end(&mut self) {
338 self.desired_col = None;
339 self.col = self.lines[self.row].len();
340 }
341
342 pub fn move_up(&mut self) {
347 if self.row == 0 {
348 return;
349 }
350 let want = self.desired_col.unwrap_or(self.col);
351 self.row -= 1;
352 self.col = want.min(self.lines[self.row].len());
353 self.desired_col = Some(want);
354 }
355
356 pub fn move_down(&mut self) {
357 if self.row + 1 >= self.lines.len() {
358 return;
359 }
360 let want = self.desired_col.unwrap_or(self.col);
361 self.row += 1;
362 self.col = want.min(self.lines[self.row].len());
363 self.desired_col = Some(want);
364 }
365
366 #[must_use]
370 pub const fn cursor_on_first_row(&self) -> bool {
371 self.row == 0
372 }
373
374 #[must_use]
376 pub fn cursor_on_last_row(&self) -> bool {
377 self.row + 1 == self.lines.len()
378 }
379
380 pub fn recall_prev(&mut self) {
386 if !self.history.is_recalling() {
387 self.saved_draft = Some(self.lines.clone());
388 }
389 if let Some(line) = self.history.recall_prev().map(str::to_string) {
390 self.replace_with(&line);
391 }
392 }
393
394 pub fn recall_next(&mut self) {
397 if !self.history.is_recalling() {
398 return;
399 }
400 match self.history.recall_next() {
401 Some(line) => {
402 let line = line.to_string();
403 self.replace_with(&line);
404 }
405 None => {
406 if let Some(draft) = self.saved_draft.take() {
408 self.lines = draft;
409 self.move_to_buffer_end();
410 }
411 }
412 }
413 }
414
415 fn replace_with(&mut self, s: &str) {
416 self.lines = s.split('\n').map(|l| l.chars().collect()).collect();
417 if self.lines.is_empty() {
418 self.lines.push(Vec::new());
419 }
420 self.move_to_buffer_end();
421 }
422
423 fn move_to_buffer_end(&mut self) {
424 self.row = self.lines.len() - 1;
425 self.col = self.lines[self.row].len();
426 self.desired_col = None;
427 }
428
429 pub fn take(&mut self) -> Option<String> {
433 if self.is_empty() {
434 return None;
435 }
436 let s = self.as_string();
437 if s.trim().is_empty() {
438 self.clear();
439 return None;
440 }
441 self.history.push(&s);
442 self.clear();
443 Some(s)
444 }
445
446 pub fn clear(&mut self) {
449 self.lines = vec![Vec::new()];
450 self.row = 0;
451 self.col = 0;
452 self.desired_col = None;
453 self.saved_draft = None;
454 self.history.reset_cursor();
455 }
456
457 pub fn replace_all(&mut self, s: &str) {
461 self.replace_with(s);
462 self.history.reset_cursor();
463 self.saved_draft = None;
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::{PromptBuffer, PromptHistory};
470
471 fn type_str(p: &mut PromptBuffer, s: &str) {
472 for c in s.chars() {
473 p.insert(c);
474 }
475 }
476
477 #[test]
478 fn insert_and_backspace() {
479 let mut p = PromptBuffer::new();
480 type_str(&mut p, "hello");
481 assert_eq!(p.as_string(), "hello");
482 assert_eq!(p.cursor(), 5);
483 p.backspace();
484 p.backspace();
485 assert_eq!(p.as_string(), "hel");
486 assert_eq!(p.cursor(), 3);
487 }
488
489 #[test]
490 fn move_and_delete_midway() {
491 let mut p = PromptBuffer::new();
492 type_str(&mut p, "foobar");
493 p.move_home();
494 p.move_right();
495 p.move_right();
496 p.delete();
497 assert_eq!(p.as_string(), "fobar");
498 }
499
500 #[test]
501 fn take_clears_and_pushes_history() {
502 let mut p = PromptBuffer::new();
503 type_str(&mut p, "/status");
504 assert_eq!(p.take().as_deref(), Some("/status"));
505 assert!(p.is_empty());
506 assert!(p.take().is_none());
507 assert_eq!(p.history().len(), 1);
508 }
509
510 #[test]
511 fn shift_enter_creates_new_row() {
512 let mut p = PromptBuffer::new();
513 type_str(&mut p, "first");
514 p.insert_newline();
515 type_str(&mut p, "second");
516 assert_eq!(p.height(), 2);
517 assert_eq!(p.as_string(), "first\nsecond");
518 assert_eq!(p.cursor_row(), 1);
519 assert_eq!(p.cursor(), 6);
520 }
521
522 #[test]
523 fn newline_in_middle_splits_line() {
524 let mut p = PromptBuffer::new();
525 type_str(&mut p, "abcdef");
526 p.move_home();
527 p.move_right();
528 p.move_right();
529 p.move_right();
530 p.insert_newline();
531 assert_eq!(p.as_string(), "abc\ndef");
532 assert_eq!(p.cursor_row(), 1);
533 assert_eq!(p.cursor(), 0);
534 }
535
536 #[test]
537 fn backspace_at_col0_merges_lines() {
538 let mut p = PromptBuffer::new();
539 type_str(&mut p, "foo");
540 p.insert_newline();
541 type_str(&mut p, "bar");
542 p.move_home();
543 p.backspace();
544 assert_eq!(p.as_string(), "foobar");
545 assert_eq!(p.height(), 1);
546 assert_eq!(p.cursor_row(), 0);
547 assert_eq!(p.cursor(), 3);
548 }
549
550 #[test]
551 fn delete_at_eol_merges_with_next_line() {
552 let mut p = PromptBuffer::new();
553 type_str(&mut p, "foo");
554 p.insert_newline();
555 type_str(&mut p, "bar");
556 p.move_up();
558 p.move_end();
559 p.delete();
560 assert_eq!(p.as_string(), "foobar");
561 assert_eq!(p.height(), 1);
562 }
563
564 #[test]
565 fn move_up_keeps_desired_column_across_short_lines() {
566 let mut p = PromptBuffer::new();
567 type_str(&mut p, "longest line");
568 p.insert_newline();
569 type_str(&mut p, "x");
570 p.insert_newline();
571 type_str(&mut p, "another long line");
572 p.move_up();
576 assert_eq!(p.cursor_row(), 1);
577 assert_eq!(p.cursor(), 1);
578 p.move_up();
579 assert_eq!(p.cursor_row(), 0);
580 assert_eq!(
581 p.cursor(),
582 12,
583 "desired col not preserved across short lines"
584 );
585 }
586
587 #[test]
588 fn history_dedupe_and_recall() {
589 let mut h = PromptHistory::with_capacity(8);
590 h.push("a");
591 h.push("b");
592 h.push("b");
593 h.push("c");
594 assert_eq!(h.len(), 3, "consecutive duplicates are deduped");
595 assert_eq!(h.recall_prev(), Some("c"));
596 assert_eq!(h.recall_prev(), Some("b"));
597 assert_eq!(h.recall_prev(), Some("a"));
598 assert_eq!(h.recall_prev(), Some("a"), "clamps at oldest");
599 assert_eq!(h.recall_next(), Some("b"));
600 assert_eq!(h.recall_next(), Some("c"));
601 assert_eq!(
602 h.recall_next(),
603 None,
604 "stepping past newest signals draft restore"
605 );
606 }
607
608 #[test]
609 fn recall_round_trip_preserves_draft() {
610 let mut p = PromptBuffer::new();
611 type_str(&mut p, "/status");
613 let _ = p.take();
614 type_str(&mut p, "/risk");
615 let _ = p.take();
616
617 type_str(&mut p, "abc");
619 assert_eq!(p.as_string(), "abc");
620 p.recall_prev();
621 assert_eq!(p.as_string(), "/risk");
622 p.recall_prev();
623 assert_eq!(p.as_string(), "/status");
624 p.recall_next();
625 assert_eq!(p.as_string(), "/risk");
626 p.recall_next();
627 assert_eq!(
628 p.as_string(),
629 "abc",
630 "draft must be restored at end of history walk"
631 );
632 }
633
634 #[test]
635 fn typing_on_recalled_entry_drops_recall_state() {
636 let mut p = PromptBuffer::new();
637 type_str(&mut p, "/status");
638 let _ = p.take();
639 p.recall_prev();
640 assert!(p.history().is_recalling());
641 p.insert('x');
642 assert!(
643 !p.history().is_recalling(),
644 "edits commit the recalled line"
645 );
646 p.recall_next();
649 assert_eq!(p.as_string(), "/statusx");
650 }
651
652 #[test]
653 fn empty_submit_does_not_pollute_history() {
654 let mut p = PromptBuffer::new();
655 for c in " ".chars() {
656 p.insert(c);
657 }
658 assert!(p.take().is_none());
659 assert_eq!(p.history().len(), 0);
660 }
661
662 #[test]
663 fn replace_all_lands_cursor_at_end() {
664 let mut p = PromptBuffer::new();
665 type_str(&mut p, "ab");
666 p.replace_all("/positions ");
667 assert_eq!(p.as_string(), "/positions ");
668 assert_eq!(p.cursor(), 11);
669 }
670
671 #[test]
672 fn newline_capped_at_max_lines() {
673 let mut p = PromptBuffer::new();
674 for _ in 0..200 {
677 p.insert_newline();
678 }
679 assert_eq!(p.height(), super::MAX_LINES);
680 }
681}