1use crate::{
4 cmd::{Cmd, Line},
5 error::ReplBlockResult,
6 history::{History, HistIdx},
7 macros::key,
8};
9use camino::{Utf8Path, Utf8PathBuf};
10use crossterm::{
11 cursor, execute, queue, style, terminal,
12 event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
13 style::{Stylize, StyledContent},
14 terminal::ClearType,
15};
16use std::io::{Stdout, Write};
17use unicode_segmentation::UnicodeSegmentation;
18
19
20type Evaluator<'eval> =
21 dyn for<'src> FnMut(&'src str) -> ReplBlockResult<()> + 'eval;
22
23pub struct ReplBuilder<'eval, W: Write> {
24 sink: W,
25 default_prompt: Vec<StyledContent<char>>,
26 continue_prompt: Vec<StyledContent<char>>,
27 reverse_search_prompt: Vec<StyledContent<char>>,
28 history_filepath: Utf8PathBuf,
29 evaluator: Box<Evaluator<'eval>>,
30 hello_msg: String,
31 goodbye_msg: String,
32}
33
34impl<'eval> Default for ReplBuilder<'eval, Stdout> {
35 fn default() -> ReplBuilder<'eval, Stdout> {
36 #[inline(always)]
37 fn nop<'eval>() -> Box<Evaluator<'eval>> {
38 Box::new(|_| Ok(()))
39 }
40 ReplBuilder {
41 sink: std::io::stdout(),
42 default_prompt: vec!['■'.yellow(), '>'.green().bold(), ' '.reset()],
43 continue_prompt: vec!['.'.yellow(), '.'.yellow(), ' '.reset()],
44 reverse_search_prompt: vec![
45 'r'.yellow().italic(),
46 'e'.yellow().italic(),
47 'v'.yellow().italic(),
48 'e'.yellow().italic(),
49 'r'.yellow().italic(),
50 's'.yellow().italic(),
51 'e'.yellow().italic(),
52 ' '.reset(),
53 's'.yellow().italic(),
54 'e'.yellow().italic(),
55 'a'.yellow().italic(),
56 'r'.yellow().italic(),
57 'c'.yellow().italic(),
58 'h'.yellow().italic(),
59 ':'.blue().italic(),
60 ' '.reset(),
61 ],
62 history_filepath: Utf8PathBuf::from(".repl.history"),
63 evaluator: nop(),
64 hello_msg: format!("🖐 Press {} to exit.", "Ctrl-D".magenta()),
65 goodbye_msg: "👋".to_string(),
66 }
67 }
68}
69
70impl<'eval, W: Write> ReplBuilder<'eval, W> {
71 pub fn sink<S: Write>(self, sink: S) -> ReplBuilder<'eval, S> {
72 ReplBuilder {
73 sink,
74 default_prompt: self.default_prompt,
75 continue_prompt: self.continue_prompt,
76 reverse_search_prompt: self.reverse_search_prompt,
77 history_filepath: self.history_filepath,
78 evaluator: self.evaluator,
79 hello_msg: self.hello_msg,
80 goodbye_msg: self.goodbye_msg,
81 }
82 }
83
84 pub fn default_prompt(mut self, prompt: Vec<StyledContent<char>>) -> Self {
85 self.default_prompt = prompt;
86 self
87 }
88
89 pub fn continue_prompt(mut self, prompt: Vec<StyledContent<char>>) -> Self {
90 self.continue_prompt = prompt;
91 self
92 }
93
94 pub fn reverse_search_prompt(mut self, prompt: Vec<StyledContent<char>>) -> Self {
95 self.reverse_search_prompt = prompt;
96 self
97 }
98
99 pub fn history_filepath(mut self, filepath: impl AsRef<Utf8Path>) -> Self {
100 self.history_filepath = filepath.as_ref().to_path_buf();
101 self
102 }
103
104 pub fn evaluator<E>(mut self, evaluator: E) -> Self
105 where
106 E: for<'src> FnMut(&'src str) -> ReplBlockResult<()> + 'eval
107 {
108 self.evaluator = Box::new(evaluator);
109 self
110 }
111
112 pub fn hello(mut self, hello_msg: impl Into<String>) -> Self {
113 self.hello_msg = hello_msg.into();
114 self
115 }
116
117 pub fn goodbye(mut self, goodbye_msg: impl Into<String>) -> Self {
118 self.goodbye_msg = goodbye_msg.into();
119 self
120 }
121
122 pub fn build(self) -> ReplBlockResult<Repl<'eval, W>> {
123 assert_eq!(
124 self.default_prompt.len(), self.continue_prompt.len(),
125 "default_prompt.len() != continue_prompt.len()"
126 );
127 let mut repl = Repl::new(
128 self.sink,
129 self.history_filepath,
130 self.evaluator,
131 self.default_prompt,
132 self.continue_prompt,
133 self.reverse_search_prompt,
134 self.hello_msg,
135 self.goodbye_msg,
136 )?;
137 repl.render_default_prompt()?;
138 repl.sink.flush()?;
139 Ok(repl)
140 }
141}
142
143
144
145pub struct Repl<'eval, W: Write> {
146 sink: W,
147 state: State,
148 height: u16,
150 history: History,
152 history_filepath: Utf8PathBuf,
154 evaluator: Box<Evaluator<'eval>>,
156 default_prompt: Vec<StyledContent<char>>,
158 continue_prompt: Vec<StyledContent<char>>,
160 reverse_search_prompt: Vec<StyledContent<char>>,
162 hello_msg: String,
163 goodbye_msg: String,
164}
165
166impl<'eval, W: Write> Repl<'eval, W> {
167 fn new(
168 mut sink: W,
169 history_filepath: impl AsRef<Utf8Path>,
170 evaluator: Box<Evaluator<'eval>>,
171 default_prompt: Vec<StyledContent<char>>,
172 continue_prompt: Vec<StyledContent<char>>,
173 reverse_search_prompt: Vec<StyledContent<char>>,
174 hello_msg: String,
175 goodbye_msg: String,
176 ) -> ReplBlockResult<Repl<'eval, W>> {
177 sink.flush()?;
178 let mut repl = Self {
179 sink,
180 state: State::Edit(EditState {
181 buffer: Cmd::default(),
182 cursor: ORIGIN,
183 }),
184 height: 1,
185 history: History::read_from_file(history_filepath.as_ref())?,
186 history_filepath: history_filepath.as_ref().to_path_buf(),
187 evaluator,
188 default_prompt,
189 continue_prompt,
190 reverse_search_prompt,
191 hello_msg,
192 goodbye_msg,
193 };
194 execute!(
195 repl.sink,
196 cursor::SetCursorStyle::BlinkingBar,
197 cursor::MoveToColumn(0),
198 style::Print(&repl.hello_msg),
199 style::Print("\n"),
200 )?;
201 Ok(repl)
202 }
203}
204
205impl<'eval, W: Write> Repl<'eval, W> {
206 pub fn start(&mut self) -> ReplBlockResult<()> {
207 loop {
208 let old_height = self.height;
209 self.dispatch_key_event()?; self.render_ui(old_height)?;
211 }
212 }
213
214 fn dispatch_key_event(&mut self) -> ReplBlockResult<()> {
215 terminal::enable_raw_mode()?;
216 let event = event::read()?;
217 terminal::disable_raw_mode()?;
218 match event {
219 Event::Key(key!(CONTROL-'c')) => self.cmd_nop()?,
220
221 Event::Key(key!(CONTROL-'d')) => self.cmd_exit_repl()?,
223 Event::Key(key!(CONTROL-'g')) => self.cmd_cancel_nav()?,
224 Event::Key(key!(@name Enter)) => self.cmd_eval()?,
225
226 Event::Key(key!(CONTROL-'p')) => self.cmd_nav_up()?,
228 Event::Key(key!(@name Up)) => self.cmd_nav_up()?,
229 Event::Key(key!(CONTROL-'n')) => self.cmd_nav_down()?,
230 Event::Key(key!(@name Down)) => self.cmd_nav_down()?,
231 Event::Key(key!(CONTROL-'b')) => self.cmd_nav_cmd_left()?,
232 Event::Key(key!(@name Left)) => self.cmd_nav_cmd_left()?,
233 Event::Key(key!(CONTROL-'f')) => self.cmd_nav_cmd_right()?,
234 Event::Key(key!(@name Right)) => self.cmd_nav_cmd_right()?,
235 Event::Key(key!(CONTROL-'a')) => self.cmd_nav_to_start_of_cmd()?,
236 Event::Key(key!(@name Home)) => self.cmd_nav_to_start_of_cmd()?,
237 Event::Key(key!(CONTROL-'e')) => self.cmd_nav_to_end_of_cmd()?,
238 Event::Key(key!(@name End)) => self.cmd_nav_to_end_of_cmd()?,
239 Event::Key(key!(CONTROL-'r')) => self.cmd_reverse_search_history()?,
240 Event::Key(key!(@name PageUp)) => self.cmd_nav_history_up()?,
241 Event::Key(key!(@name PageDown)) => self.cmd_nav_history_down()?,
242
243 Event::Key(key!(@c)) => self.cmd_insert_char(c)?,
245 Event::Key(key!(SHIFT-@c)) => self.cmd_insert_char(c)?,
246 Event::Key(key!(@name SHIFT-Enter)) => self.cmd_insert_newline()?,
249 Event::Key(key!(CONTROL-'o')) => self.cmd_insert_newline()?,
250 Event::Key(key!(@name Backspace)) => self.cmd_rm_grapheme_before_cursor()?,
251 Event::Key(key!(@name Delete)) => self.cmd_rm_grapheme_at_cursor()?,
252
253 _event => {},
254 }
255 Ok(())
256 }
257
258 fn render_ui(&mut self, old_input_area_height: u16) -> ReplBlockResult<()> {
259 let dims = self.input_area_dims()?;
260 let prompt_len = self.prompt_len();
261
262 let calculate_uncursor = |cmd: &Cmd, uncompressed: &Cmd, cursor: Coords| {
263 let prev_unlines: Vec<Vec<Line>> = (0..cursor.y)
264 .map(|y| cmd[y].uncompress(dims.width, prompt_len))
265 .collect();
266 let mut uncursor = Coords {
267 x: cursor.x,
268 y: prev_unlines.iter()
269 .map(|unline| unline.len())
270 .sum::<usize>() as u16,
271 };
272 let line = &cmd[cursor.y];
273 let unlines_for_line = line.uncompress(dims.width, prompt_len);
274 for unline in unlines_for_line.iter() {
275 let unline_len = unline.count_graphemes();
276 let width = std::cmp::min(dims.width, unline_len);
277 if uncursor.x > width {
278 uncursor.x -= width;
279 uncursor.y += 1;
280 } else {
281 break;
282 }
283 }
284 if uncompressed[uncursor.y].is_start() {
285 uncursor.x += prompt_len;
286 }
287 uncursor
288 };
289
290 macro_rules! render {
291 ($cmd:expr, $cursor:expr) => {{
292 let (cmd, cursor): (&Cmd, Coords) = ($cmd, $cursor);
293 let uncompressed = cmd.uncompress(dims.width, prompt_len);
294
295 let num_unlines = uncompressed.count_lines() as u16;
297 let content_height = num_unlines;
298 self.height = std::cmp::max(self.height, content_height);
299
300 let uncursor = calculate_uncursor(cmd, &uncompressed, cursor);
302
303 for _ in old_input_area_height..content_height {
305 queue!(self.sink, terminal::ScrollUp(1))?;
306 }
307
308 self.clear_input_area()?;
323 self.move_cursor_to_origin()?;
324 self.render_cmd(&uncompressed)?;
325
326 let o = self.origin()?;
328 queue!(self.sink, cursor::MoveToColumn(o.x + uncursor.x))?;
329 queue!(self.sink, cursor::MoveToRow(o.y + uncursor.y))?;
330
331 ReplBlockResult::Ok(())
332 }};
333 }
334
335 match &self.state {
336 State::Edit(EditState { buffer, cursor }) => {
337 render!(buffer, *cursor)?;
338 }
339 State::Navigate(NavigateState { preview, cursor, .. }) => {
340 render!(preview, *cursor)?;
341 }
342 State::Search(SearchState { regex, preview, cursor, .. }) => {
343 let (cmd, cursor): (&Cmd, Coords) = (preview, *cursor);
344 let uncompressed = cmd.uncompress(dims.width, prompt_len);
345 let regex = regex.clone();
346
347 let num_unlines = uncompressed.count_lines() as u16;
349 const SEARCH_PROMPT_LINE: u16 = 1;
350 let content_height = num_unlines + SEARCH_PROMPT_LINE;
351 self.height = std::cmp::max(self.height, content_height);
352
353 for _ in old_input_area_height..content_height {
355 queue!(self.sink, terminal::ScrollUp(1))?;
356 }
357
358 self.clear_input_area()?;
359 self.move_cursor_to_origin()?;
360 self.render_cmd(&uncompressed)?;
361 self.render_reverse_search_prompt()?;
362
363 queue!(self.sink, style::Print(regex))?;
365
366 let o = self.origin()?;
367 queue!(self.sink, cursor::MoveToRow(o.y + cursor.y + self.height))?;
369 queue!(self.sink, cursor::MoveToColumn(o.x + cursor.x))?;
370 }
371 }
372
373 self.sink.flush()?;
374 Ok(())
375 }
376
377 fn render_cmd(&mut self, uncompressed: &Cmd, ) -> ReplBlockResult<()> {
378 for (ulidx, unline) in uncompressed.lines().iter().enumerate() {
379 if ulidx == 0 {
380 self.render_default_prompt()?;
381 queue!(self.sink, style::Print(unline))?;
382 queue!(self.sink, cursor::MoveDown(1))?;
383 queue!(self.sink, cursor::MoveToColumn(0))?;
384 } else if unline.is_start() {
385 self.render_continue_prompt()?;
386 queue!(self.sink, style::Print(unline))?;
387 queue!(self.sink, cursor::MoveDown(1))?;
388 } else {
390 queue!(self.sink, style::Print(unline))?;
391 queue!(self.sink, cursor::MoveDown(1))?;
392 queue!(self.sink, cursor::MoveToColumn(0))?;
393 }
394 }
395 Ok(())
396 }
397
398 fn render_default_prompt(
399 &mut self,
400 ) -> ReplBlockResult<&mut Self> {
401 queue!(self.sink, cursor::MoveToColumn(0))?;
402 for &c in &self.default_prompt {
403 queue!(self.sink, style::Print(c))?;
404 }
405 Ok(self)
406 }
407
408 fn render_continue_prompt(
409 &mut self,
410 ) -> ReplBlockResult<()> {
411 queue!(self.sink, cursor::MoveToColumn(0))?;
412 for &c in &self.continue_prompt {
413 queue!(self.sink, style::Print(c))?;
414 }
415 Ok(())
416 }
417
418 fn render_reverse_search_prompt(
419 &mut self,
420 ) -> ReplBlockResult<()> {
421 let origin = self.origin()?;
422 queue!(self.sink, cursor::MoveTo(origin.x, origin.y + self.height))?;
424 for c in &self.reverse_search_prompt {
426 queue!(self.sink, style::Print(c))?;
427 }
428 Ok(())
429 }
430
431 fn move_cursor_to_origin(
432 &mut self,
433 ) -> ReplBlockResult<()> {
434 let origin = self.origin()?;
435 queue!(self.sink, cursor::MoveTo(origin.x, origin.y))?;
436 Ok(())
437 }
438
439 fn clear_input_area(
440 &mut self,
441 ) -> ReplBlockResult<()> {
442 self.move_cursor_to_origin()?;
443 for _ in 0..self.height {
444 queue!(self.sink, terminal::Clear(ClearType::CurrentLine))?;
445 queue!(self.sink, cursor::MoveDown(1))?;
446 }
447 self.move_cursor_to_origin()?;
448 Ok(())
449 }
450
451
452 fn origin(&self) -> ReplBlockResult<Coords> {
454 let (_term_width, term_height) = terminal::size()?;
455 Ok(Coords { x: 0, y: term_height - self.height })
456 }
457
458 fn input_area_dims(&self) -> ReplBlockResult<Dims> {
461 let (term_width, _term_height) = terminal::size()?;
462 Ok(Dims { width: term_width, height: self.height })
463 }
464
465 fn prompt_len(&self) -> u16 {
466 assert_eq!(
467 self.default_prompt.len(), self.continue_prompt.len(),
468 "default_prompt.len() != continue_prompt.len()"
469 );
470 self.default_prompt.len() as u16
471 }
472
473
474
475 fn cmd_nop(&mut self) -> ReplBlockResult<()> {
476 Ok(()) }
478
479 fn cmd_exit_repl(&mut self) -> ReplBlockResult<()> {
481 execute!(
482 self.sink,
483 cursor::SetCursorStyle::DefaultUserShape,
484 cursor::MoveToColumn(0),
485 style::Print(&self.goodbye_msg),
486 terminal::Clear(ClearType::FromCursorDown),
487 )?;
488 self.sink.flush()?;
489 std::process::exit(0);
490 }
491
492 fn cmd_cancel_nav(&mut self) -> ReplBlockResult<()> {
493 match &mut self.state {
494 State::Edit(EditState { .. }) => {
495 }
497 State::Navigate(NavigateState { backup, .. }) => {
498 self.state = State::Edit(EditState {
499 cursor: backup.end_of_cmd(),
500 buffer: std::mem::take(backup),
501 });
502 }
503 State::Search(SearchState { backup, .. }) => {
504 self.state = State::Edit(EditState {
505 cursor: backup.end_of_cmd(),
506 buffer: std::mem::take(backup),
507 });
508 }
509 }
510 Ok(())
511 }
512
513 fn cmd_nav_up(&mut self) -> ReplBlockResult<()> {
514 let is_at_top_line = |cursor: Coords| cursor.y == ORIGIN.x;
515 match &mut self.state {
516 State::Edit(EditState { buffer, cursor }) => {
517 if is_at_top_line(*cursor) {
518 self.cmd_nav_history_up()?;
519 } else {
520 cursor.y -= 1;
521 let line_len = buffer[cursor.y].count_graphemes();
522 cursor.x = std::cmp::min(cursor.x, line_len);
523 }
524 }
525 State::Navigate(NavigateState { preview, cursor, .. }) => {
526 if is_at_top_line(*cursor) {
527 self.cmd_nav_history_up()?;
528 } else {
529 cursor.y -= 1;
530 let line_len = preview[cursor.y].count_graphemes();
531 cursor.x = std::cmp::min(cursor.x, line_len);
532 }
533 }
534 State::Search(SearchState { .. }) => {
535 self.cmd_nav_history_up()?;
536 }
537 }
538 Ok(())
539 }
540
541 fn cmd_nav_down(&mut self) -> ReplBlockResult<()> {
542 let is_at_bottom_line = |cursor: Coords, cmd: &Cmd| cursor.y == cmd.count_lines() - 1;
543 match &mut self.state {
544 State::Edit(EditState { buffer, cursor }) => {
545 if is_at_bottom_line(*cursor, buffer) {
546 self.cmd_nav_history_down()?;
547 } else {
548 cursor.y += 1;
549 let line_len = buffer[cursor.y].count_graphemes();
550 cursor.x = std::cmp::min(cursor.x, line_len);
551 }
552 }
553 State::Navigate(NavigateState { preview, cursor, .. }) => {
554 if is_at_bottom_line(*cursor, preview) {
555 self.cmd_nav_history_down()?;
556 } else {
557 cursor.y += 1;
558 let line_len = preview[cursor.y].count_graphemes();
559 cursor.x = std::cmp::min(cursor.x, line_len);
560 }
561 }
562 State::Search(SearchState { .. }) => {
563 self.cmd_nav_history_down()?;
564 }
565 }
566 Ok(())
567 }
568
569 fn cmd_nav_history_up(&mut self) -> ReplBlockResult<()> {
570 match &mut self.state {
571 State::Edit(EditState { buffer, cursor: _ }) => {
572 let Some(max_hidx) = self.history.max_idx() else {
573 return Ok(()); };
575 self.state = State::Navigate(NavigateState {
576 hidx: max_hidx,
577 backup: std::mem::take(buffer),
578 preview: self.history[max_hidx].clone(),
579 cursor: self.history[max_hidx].end_of_cmd(),
580 });
581 }
582 State::Navigate(NavigateState { hidx, preview, cursor, .. }) => {
583 let min_hidx = HistIdx(0);
584 if *hidx == min_hidx {
585 } else {
587 *hidx -= 1;
588 *preview = self.history[*hidx].clone(); *cursor = preview.end_of_cmd();
590 }
591 }
592 State::Search(SearchState { preview, matches, current, .. }) => {
593 if *current >= matches.len() - 1 {
594 } else {
596 *current += 1;
597 *preview = if matches.is_empty() {
598 Cmd::default()
599 } else {
600 let hidx = matches[*current];
601 self.history[hidx].clone()
602 };
603 }
604 }
605 }
606 Ok(())
607 }
608
609 fn cmd_nav_history_down(&mut self) -> ReplBlockResult<()> {
610 match &mut self.state {
611 State::Edit(EditState { .. }) => {}
612 State::Navigate(NavigateState { hidx, backup, preview, cursor }) => {
613 let max_hidx = self.history.max_idx();
614 if Some(*hidx) == max_hidx { self.state = State::Edit(EditState {
616 cursor: backup.end_of_cmd(),
617 buffer: std::mem::take(backup),
618 });
619 } else {
620 *hidx += 1;
621 *preview = self.history[*hidx].clone(); *cursor = preview.end_of_cmd();
623 }
624 }
625 State::Search(SearchState { preview, matches, current, .. }) => {
626 if *current == 0 {
627 } else {
629 *current -= 1;
630 *preview = if matches.is_empty() {
631 Cmd::default()
632 } else {
633 let hidx = matches[*current];
634 self.history[hidx].clone()
635 };
636 }
637 }
638 }
639 Ok(())
640 }
641
642 fn cmd_nav_cmd_left(&mut self) -> ReplBlockResult<()> {
643 let update_cursor = |cmd: &Cmd, cursor: &mut Coords| {
644 if *cursor == ORIGIN {
645 } else {
647 let is_start_of_cursor_line = cursor.x == ORIGIN.x;
648 let has_prev_line = cursor.y >= 1;
649 if is_start_of_cursor_line && has_prev_line {
650 *cursor = Coords {
651 x: cmd[cursor.y - 1].count_graphemes(),
652 y: cursor.y - 1,
653 };
654 } else if is_start_of_cursor_line && !has_prev_line {
655 } else { cursor.x -= 1;
658 }
659 }
660 };
661 match &mut self.state {
662 State::Edit(EditState { buffer, cursor }) => {
663 update_cursor(buffer, cursor);
664 },
665 State::Navigate(NavigateState { preview, cursor, .. }) => {
666 update_cursor(preview, cursor);
667 },
668 State::Search(SearchState { cursor, .. }) => {
669 let prompt_len = self.reverse_search_prompt.len() as u16;
670 if cursor.x <= prompt_len {
671 cursor.x = prompt_len; } else {
673 cursor.x -= 1;
674 }
675 },
676 }
677 Ok(())
678 }
679
680 fn cmd_nav_cmd_right(&mut self) -> ReplBlockResult<()> {
681 let update_cursor = |cmd: &Cmd, cursor: &mut Coords| {
682 if *cursor == cmd.end_of_cmd() {
683 } else {
685 let is_end_of_cursor_line =
686 cursor.x == cmd[cursor.y].count_graphemes();
687 let has_next_line = cursor.y + 1 < cmd.count_lines();
688 if is_end_of_cursor_line && has_next_line {
689 *cursor = Coords {
690 x: ORIGIN.x,
691 y: cursor.y + 1,
692 };
693 } else if is_end_of_cursor_line && !has_next_line {
694 } else { cursor.x += 1;
697 }
698 }
699 };
700 match &mut self.state {
701 State::Edit(EditState { buffer, cursor }) => {
702 update_cursor(buffer, cursor);
703 },
704 State::Navigate(NavigateState { preview, cursor, .. }) => {
705 update_cursor(preview, cursor);
706 },
707 State::Search(SearchState { regex, cursor, .. }) => {
708 let prompt_len = self.reverse_search_prompt.len() as u16;
709 let regex_line_len = regex.graphemes(true).count() as u16;
710 if cursor.x >= prompt_len + regex_line_len {
711 cursor.x = prompt_len + regex_line_len; } else {
713 cursor.x += 1;
714 }
715 },
716 }
717 Ok(())
718 }
719
720 fn cmd_nav_to_start_of_cmd(&mut self) -> ReplBlockResult<()> {
722 match &mut self.state {
723 State::Edit(EditState { cursor, .. }) => {
724 *cursor = ORIGIN;
725 },
726 State::Navigate(NavigateState { cursor, .. }) => {
727 *cursor = ORIGIN;
728 },
729 State::Search(SearchState { cursor, .. }) => {
730 let prompt_len = self.reverse_search_prompt.len() as u16;
731 cursor.x = prompt_len;
732 },
733 }
734 Ok(())
735 }
736
737 fn cmd_nav_to_end_of_cmd(&mut self) -> ReplBlockResult<()> {
739 match &mut self.state {
740 State::Edit(EditState { buffer, cursor }) => {
741 *cursor = buffer.end_of_cmd();
742 },
743 State::Navigate(NavigateState { preview, cursor, .. }) => {
744 *cursor = preview.end_of_cmd();
745 },
746 State::Search(SearchState { regex, cursor, .. }) => {
747 let prompt_len = self.reverse_search_prompt.len() as u16;
748 let regex_line_len = regex.graphemes(true).count() as u16;
749 cursor.x = prompt_len + regex_line_len;
750 },
751 }
752 Ok(())
753 }
754
755 fn cmd_reverse_search_history(&mut self) -> ReplBlockResult<()> {
756 match &mut self.state {
757 State::Edit(EditState { buffer, cursor }) => {
758 self.state = State::Search(SearchState {
759 regex: String::new(),
760 backup: std::mem::take(buffer),
761 preview: Cmd::default(),
762 cursor: *cursor,
763 matches: vec![],
764 current: 0,
765 });
766 self.cmd_reverse_search_history()?;
767 }
768 State::Navigate(NavigateState { hidx: _, backup, preview, cursor }) => {
769 self.state = State::Search(SearchState {
770 regex: String::new(),
771 backup: std::mem::take(backup),
772 preview: std::mem::take(preview),
773 cursor: *cursor,
774 matches: vec![],
775 current: 0,
776 });
777 self.cmd_reverse_search_history()?;
778 }
779 State::Search(SearchState {
780 regex,
781 backup: _,
782 preview,
783 cursor,
784 matches,
785 current,
786 }) => {
787 *matches = self.history.reverse_search(regex);
788 *current = 0;
789 *preview = if matches.is_empty() {
790 Cmd::default()
791 } else {
792 self.history[matches[*current]].clone()
793 };
794 let prompt_len = self.reverse_search_prompt.len() as u16;
795 *cursor = Coords { x: prompt_len, y: ORIGIN.y };
796 }
797 }
798 Ok(())
799 }
800
801 fn cmd_insert_char(&mut self, c: char) -> ReplBlockResult<()> {
803 let dims = self.input_area_dims()?;
804 match &mut self.state {
805 State::Edit(EditState { buffer, cursor }) => {
806 buffer.insert_char(*cursor, c);
807 cursor.x += 1;
808 }
809 State::Navigate(NavigateState { preview, cursor, .. }) => {
810 self.state = State::Edit(EditState {
811 buffer: std::mem::take(preview),
812 cursor: *cursor,
813 });
814 self.cmd_insert_char(c)?;
815 }
816 State::Search(SearchState {
817 regex,
818 backup: _,
819 preview,
820 cursor,
821 matches,
822 current,
823 }) => {
824 let prompt_len = self.reverse_search_prompt.len();
825 if regex.len() >= dims.width as usize - prompt_len - 1 {
826 return Ok(()); }
828 let mut re: Vec<&str> = regex.graphemes(true).collect();
829 let c = c.to_string();
830 re.insert(cursor.x as usize - prompt_len, &c);
831 *regex = re.into_iter().collect::<String>();
832 cursor.x += 1;
833 *matches = self.history.reverse_search(regex);
834 *current = 0;
835 *preview = if matches.is_empty() {
836 Cmd::default()
837 } else {
838 let hidx = matches[*current];
839 self.history[hidx].clone()
840 };
841 }
842 }
843 Ok(())
844 }
845
846 fn cmd_insert_newline(&mut self) -> ReplBlockResult<()> {
848 match &mut self.state {
849 State::Edit(EditState { buffer, cursor }) => {
850 buffer.insert_empty_line(*cursor);
851 *cursor = Coords {
852 x: ORIGIN.x,
853 y: cursor.y + 1
854 };
855 }
856 State::Navigate(NavigateState { preview, cursor, .. }) => {
857 self.state = State::Edit(EditState {
858 buffer: std::mem::take(preview),
859 cursor: *cursor,
860 });
861 self.cmd_insert_newline()?;
862 }
863 State::Search(SearchState { .. }) => {
864 }
866 }
867 Ok(())
868 }
869
870 fn cmd_rm_grapheme_before_cursor(&mut self) -> ReplBlockResult<()> {
873 match &mut self.state {
874 State::Edit(EditState { buffer, cursor }) => {
875 if cursor.y == 0 && cursor.x == 0 {
876 } else if cursor.y == 0 && cursor.x > 0 {
878 buffer.rm_grapheme_before(*cursor);
879 cursor.x -= 1;
880 } else if cursor.y > 0 && cursor.x == 0 {
881 let old_len = buffer[cursor.y - 1].count_graphemes();
882 buffer.rm_grapheme_before(*cursor);
883 *cursor = Coords { x: old_len, y: cursor.y - 1 };
884 } else if cursor.y > 0 && cursor.x > 0 {
885 buffer.rm_grapheme_before(*cursor);
886 cursor.x -= 1;
887 } else {
888 let tag = "cmd_rm_grapheme_before_cursor";
889 unreachable!("[{tag}] cursor={cursor:?}");
890 }
891 }
892 State::Navigate(NavigateState { preview, cursor, .. }) => {
893 self.state = State::Edit(EditState {
894 buffer: std::mem::take(preview),
895 cursor: *cursor,
896 });
897 self.cmd_rm_grapheme_before_cursor()?;
898 }
899 State::Search(SearchState {
900 regex,
901 backup: _,
902 preview,
903 cursor,
904 matches,
905 current,
906 }) => {
907 let prompt_len = self.reverse_search_prompt.len();
908 let rmidx = cursor.x as usize - prompt_len;
909 if regex.len() == 0 || rmidx == 0 {
910 return Ok(()); }
912 let mut re: Vec<&str> = regex.graphemes(true).collect();
913 re.remove(cursor.x as usize - prompt_len - 1);
914 *regex = re.into_iter().collect::<String>();
915 cursor.x -= 1;
916 *matches = self.history.reverse_search(regex);
917 *preview = if matches.is_empty() {
918 Cmd::default()
919 } else {
920 let hidx = matches[*current];
921 self.history[hidx].clone()
922 };
923 },
924 }
925 Ok(())
926 }
927
928 fn cmd_rm_grapheme_at_cursor(&mut self) -> ReplBlockResult<()> {
931 match &mut self.state {
932 State::Edit(EditState { buffer, cursor }) => {
933 let is_end_of_line = cursor.x == buffer[cursor.y].count_graphemes();
934 let has_next_line = cursor.y + 1 < buffer.count_lines();
935 if is_end_of_line && has_next_line {
936 buffer.rm_grapheme_at(*cursor);
937 } else if is_end_of_line && !has_next_line {
938 } else if !is_end_of_line {
940 buffer.rm_grapheme_at(*cursor);
941 } else {
942 let tag = "cmd_rm_grapheme_at_cursor";
943 unreachable!("[{tag}] cursor={cursor:?}");
944 }
945 }
946 State::Navigate(NavigateState { preview, cursor, .. }) => {
947 self.state = State::Edit(EditState {
948 buffer: std::mem::take(preview),
949 cursor: *cursor,
950 });
951 self.cmd_rm_grapheme_at_cursor()?;
952 }
953 State::Search(SearchState {
954 regex,
955 backup: _,
956 preview,
957 cursor,
958 matches,
959 current,
960 }) => {
961 let prompt_len = self.reverse_search_prompt.len();
962 let rmidx = cursor.x as usize - prompt_len;
963 let is_end_of_regex_line = rmidx == regex.graphemes(true).count();
964 if regex.len() == 0 || is_end_of_regex_line {
965 return Ok(()); }
967 let mut re: Vec<&str> = regex.graphemes(true).collect();
968 re.remove(cursor.x as usize - prompt_len);
969 *regex = re.into_iter().collect::<String>();
970 *matches = self.history.reverse_search(regex);
971 *preview = if matches.is_empty() {
972 Cmd::default()
973 } else {
974 let hidx = matches[*current];
975 self.history[hidx].clone()
976 };
977 }
978 }
979 Ok(())
980 }
981
982 fn cmd_eval(&mut self) -> ReplBlockResult<()> {
984 match &mut self.state {
985 State::Edit(EditState { buffer, cursor }) => {
986 let source_code = buffer.to_source_code();
987 if source_code.is_empty() {
988 return Ok(());
989 }
990 { writeln!(self.sink)?;
992 self.sink.flush()?;
993 }
994 let cmd = std::mem::take(buffer);
995 let _hidx = self.history.add_cmd(cmd);
996 self.history.write_to_file(&self.history_filepath)?;
997 (*self.evaluator)(source_code.as_str())?;
998 self.height = 1; *cursor = ORIGIN;
1000 }
1001 State::Navigate(NavigateState { preview, cursor, .. }) => {
1002 self.state = State::Edit(EditState {
1003 buffer: std::mem::take(preview),
1004 cursor: *cursor,
1005 });
1006 self.cmd_eval()?;
1007 }
1008 State::Search(SearchState { preview, cursor, .. }) => {
1009 self.state = State::Edit(EditState {
1010 buffer: std::mem::take(preview),
1011 cursor: *cursor,
1012 });
1013 self.cmd_eval()?;
1014 }
1015 }
1016 Ok(())
1017 }
1018}
1019
1020#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1021pub struct Dims { pub width: u16, pub height: u16 }
1022
1023#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1024pub struct Coords { pub x: u16, pub y: u16 }
1025
1026pub(crate) const ORIGIN: Coords = Coords { x: 0, y: 0 };
1027
1028impl std::fmt::Display for Coords {
1029 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1030 write!(f, "({}, {})", self.x, self.y)
1031 }
1032}
1033
1034
1035#[derive(Debug)]
1036enum State {
1037 Edit(EditState),
1038 Navigate(NavigateState),
1039 Search(SearchState),
1040}
1041
1042#[derive(Debug)]
1044struct EditState {
1045 buffer: Cmd,
1047 cursor: Coords,
1049}
1050
1051#[derive(Debug)]
1053struct NavigateState {
1054 hidx: HistIdx,
1056 backup: Cmd,
1058 preview: Cmd,
1060 cursor: Coords,
1062}
1063
1064#[derive(Debug)]
1066struct SearchState {
1067 regex: String,
1069 backup: Cmd,
1071 preview: Cmd,
1073 cursor: Coords,
1075 matches: Vec<HistIdx>,
1077 current: usize,
1079}