Skip to main content

shelp/
repl.rs

1mod history;
2pub(crate) mod iter;
3
4use history::History;
5
6use crate::lang::{DefaultLangInterface, LangInterface};
7use crossterm::{cursor, event, execute, queue, style, terminal};
8use std::cmp::min;
9use std::io::prelude::*;
10use std::marker::PhantomData;
11use std::path::PathBuf;
12
13/// `Repl` interacts with the terminal to provide easy interactive shells.
14///
15/// Configuration:
16/// - `leader`
17///   What to print as the prompt:
18///
19///   ```no_lint
20///   > <some-code>
21///   ^^- leader
22///   ```
23/// - `continued_leader`
24///   If the command is more than one line long, what to print on subsequent lines
25///
26///   ```no_lint
27///   > <some-code>
28///   . <some-code>
29///   ```
30/// - `path`
31///   Path to a file to use as persistent history. If given, on construction, the history will be
32///   populated from the contents of this file, and will automatically write it to the file on being
33///   dropped. In case a path is not specified, the history is lost when `Repl` is dropped.
34/// - `capacity`
35///   The maximum amount of commands stored in the history. Default capacity is 64. If there are
36///   already 64 commands in the history, the oldest one will be forgotten.
37/// - `exit_keyword`
38///   The keyword to exit the repl, it exits the process, so should not be used if any cleanup is a
39///   required before closing repl. See [`set_exit_keyword`](Repl::set_exit_keyword)
40/// - `clear_keyword`
41///   Clears the screen. See [`set_clear_keyword`](Repl::set_exit_keyword)
42pub struct Repl<L: LangInterface = DefaultLangInterface> {
43    /// The history of commands run.
44    history: History,
45    /// What to print as the prompt:
46    ///
47    /// > <some-code>
48    /// ^^- leader
49    leader: &'static str,
50    /// The number of characters in the leader, it is stored here since getting number of characters
51    /// is an `O(n)` operation for a utf-8 encoded string
52    leader_len: usize,
53    /// If the command is more than one line long, what to print on subsequent lines
54    ///
55    /// > <some-code>
56    /// . <some-code>
57    /// ^^- continued leader
58    continued_leader: &'static str,
59    /// The number of characters in the continued leader, it is stored here since getting number of
60    /// characters is an `O(n)` operation for a utf-8 encoded string
61    continued_leader_len: usize,
62    /// The keyword which corresponds to the exit command (default is 'exit')
63    exit_keyword: &'static str,
64    /// The keyword which corresponds to the clear command (default is 'clear')
65    clear_keyword: &'static str,
66    _lang_interface: PhantomData<L>,
67}
68
69impl Repl<DefaultLangInterface> {
70    /// Create a `Repl` with default language interface.
71    pub fn newd(
72        leader: &'static str,
73        continued_leader: &'static str,
74        path: Option<PathBuf>,
75    ) -> Self {
76        Self::with_capacity(leader, continued_leader, 64, path)
77    }
78
79    /// Create a `Repl` with default language interface, and specified history capacity.
80    pub fn with_capacityd(
81        leader: &'static str,
82        continued_leader: &'static str,
83        capacity: usize,
84        path: Option<PathBuf>,
85    ) -> Self {
86        let should_persist = path.is_some();
87
88        let mut repl = Self {
89            history: History::with_capacity(capacity, path),
90            leader,
91            leader_len: leader.chars().count(),
92            continued_leader,
93            continued_leader_len: leader.chars().count(),
94            exit_keyword: "exit",
95            clear_keyword: "clear",
96            _lang_interface: PhantomData,
97        };
98
99        if should_persist {
100            let _ = repl.history.read_from_file();
101        }
102
103        repl
104    }
105}
106
107impl<L: LangInterface> Repl<L> {
108    /// Create a `Repl` with specified language interface.
109    pub fn new(
110        leader: &'static str,
111        continued_leader: &'static str,
112        path: Option<PathBuf>,
113    ) -> Self {
114        Self::with_capacity(leader, continued_leader, 64, path)
115    }
116
117    /// Create a `Repl` with specified language interface, and specified history capacity.
118    pub fn with_capacity(
119        leader: &'static str,
120        continued_leader: &'static str,
121        capacity: usize,
122        path: Option<PathBuf>,
123    ) -> Self {
124        let should_persist = path.is_some();
125
126        let mut repl = Self {
127            history: History::with_capacity(capacity, path),
128            leader,
129            leader_len: leader.chars().count(),
130            continued_leader,
131            continued_leader_len: leader.chars().count(),
132            exit_keyword: "exit",
133            clear_keyword: "clear",
134            _lang_interface: PhantomData,
135        };
136
137        if should_persist {
138            let _ = repl.history.read_from_file();
139        }
140
141        repl
142    }
143
144    /// Sets the exit keyword. If you don't want any exit keyword, set it to an empty string
145    pub fn set_exit_keyword(&mut self, exit_keyword: &'static str) {
146        self.exit_keyword = exit_keyword
147    }
148
149    /// Sets the clear keyword. If you don't want any clear keyword, set it to an empty string
150    pub fn set_clear_keyword(&mut self, clear_keyword: &'static str) {
151        self.clear_keyword = clear_keyword
152    }
153
154    /// Gives current command based on the cursor
155    fn cur<'a>(&'a self, c: &Cursor, lines: &'a [String]) -> &'a [String] {
156        if c.use_history {
157            // unwrap because if use_history is enabled, there must be at least one element in
158            // history
159            self.history.cur().unwrap()
160        } else {
161            lines
162        }
163    }
164
165    /// Easy access to the current line
166    fn cur_str<'a>(&'a self, c: &Cursor, lines: &'a [String]) -> &'a str {
167        &self.cur(c, lines)[c.lineno]
168    }
169
170    /// Copy the lines from history into the lines buffer
171    fn replace_with_history(&self, lines: &mut Vec<String>) {
172        let cur = self.history.cur().unwrap();
173        lines.resize(cur.len(), String::new());
174
175        for (i, string) in cur.iter().enumerate() {
176            lines[i].clear();
177            lines[i] += string;
178        }
179
180        self.history.reset_iter();
181    }
182
183    fn pre_exit(&self) {
184        let _ = terminal::disable_raw_mode();
185        println!();
186        let _ = self.history.write_to_file();
187    }
188
189    fn exit(&self) -> ! {
190        self.pre_exit();
191        std::process::exit(0)
192    }
193
194    /// Print a command
195    fn print_lines(
196        &self,
197        stdout: &mut std::io::Stdout,
198        c: &mut Cursor,
199        lines: &[String],
200        colour: style::Color,
201    ) -> crate::Result<()> {
202        if c.lineno > 0 {
203            queue!(stdout, cursor::MoveUp(c.lineno as u16))?;
204        }
205
206        queue!(
207            stdout,
208            terminal::Clear(terminal::ClearType::CurrentLine),
209            terminal::Clear(terminal::ClearType::FromCursorDown),
210        )?;
211        let mut is_first = true;
212
213        for index in 0..lines.len() {
214            let leader = if is_first {
215                is_first = false;
216                self.leader
217            } else {
218                self.continued_leader
219            };
220
221            queue!(
222                stdout,
223                cursor::MoveToColumn(0),
224                style::SetForegroundColor(colour),
225                style::Print(leader),
226            )?;
227            L::print_line(stdout, lines, index)?;
228            queue!(stdout, style::Print("\n"))?;
229        }
230
231        let leader_len = if c.lineno == 0 {
232            self.leader_len
233        } else {
234            self.continued_leader_len
235        };
236
237        c.charno = min(c.charno, lines[c.lineno].chars().count());
238
239        execute!(
240            stdout,
241            cursor::MoveUp((lines.len() - c.lineno) as u16),
242            cursor::MoveToColumn((leader_len + c.charno) as u16)
243        )
244    }
245
246    /// The main function, gives the next command
247    pub fn next(&mut self, colour: style::Color) -> crate::Result<String> {
248        let mut stdout = std::io::stdout();
249        let mut lines = Vec::new();
250        lines.push(String::new());
251
252        let mut c = Cursor::default();
253
254        terminal::enable_raw_mode()?;
255
256        execute!(
257            stdout,
258            style::SetForegroundColor(colour),
259            style::Print(self.leader),
260            style::ResetColor
261        )?;
262
263        loop {
264            if let event::Event::Key(e) = event::read()? {
265                match e.code {
266                    event::KeyCode::Char('c')
267                        if e.modifiers.contains(event::KeyModifiers::CONTROL) =>
268                    {
269                        self.exit()
270                    }
271                    event::KeyCode::Char('l')
272                        if e.modifiers.contains(event::KeyModifiers::CONTROL) =>
273                    {
274                        let lineno = c.lineno;
275                        c.lineno = 0;
276
277                        queue!(
278                            stdout,
279                            terminal::Clear(terminal::ClearType::All),
280                            cursor::MoveTo(0, 0)
281                        )?;
282                        let lines = self.cur(&c, &lines);
283                        self.print_lines(&mut stdout, &mut c, lines, colour)?;
284                        c.lineno = lineno;
285                        c.charno = min(c.charno, lines[c.lineno].chars().count());
286
287                        if c.lineno > 0 {
288                            queue!(stdout, cursor::MoveDown(lineno as u16))?;
289                        }
290                        queue!(
291                            stdout,
292                            cursor::MoveToColumn((self.continued_leader_len + c.charno) as u16),
293                        )?;
294                    }
295                    event::KeyCode::Char(chr) => {
296                        if c.use_history {
297                            self.replace_with_history(&mut lines);
298                            c.use_history = false;
299                        };
300
301                        let byte_i = get_byte_i(&lines[c.lineno], c.charno);
302
303                        lines[c.lineno].insert(byte_i, chr);
304                        c.charno += 1;
305                    }
306                    event::KeyCode::Tab => {
307                        if c.use_history {
308                            self.replace_with_history(&mut lines);
309                            c.use_history = false;
310                        };
311
312                        lines[c.lineno].insert_str(c.charno, "    ");
313                        c.charno += 4;
314                    }
315
316                    event::KeyCode::Home => {
317                        c.charno = 0;
318                    }
319                    event::KeyCode::End => {
320                        c.charno = self.cur_str(&c, &lines).chars().count();
321                    }
322                    event::KeyCode::Left if c.charno > 0 => {
323                        c.charno -= 1;
324                    }
325                    event::KeyCode::Right => {
326                        if c.charno < self.cur_str(&c, &lines).chars().count() {
327                            c.charno += 1;
328                        };
329                    }
330
331                    event::KeyCode::PageUp => history_up!(retain self, stdout, c, lines, colour),
332                    // At the top of the current block, go to previous history block
333                    event::KeyCode::Up if c.lineno == 0 => {
334                        history_up!(self, stdout, c, lines, colour)
335                    }
336                    // In the middle of a block, go up one line
337                    event::KeyCode::Up => {
338                        c.lineno -= 1;
339                        queue!(stdout, cursor::MoveUp(1))?;
340                        c.charno = min(self.cur_str(&c, &lines).chars().count(), c.charno);
341                    }
342
343                    event::KeyCode::PageDown => {
344                        history_down!(retain self, stdout, c, lines, colour)
345                    }
346                    // At the bottom of the block, and in history. This means that there are more
347                    // blocks down, either further down the history or when history is over, the
348                    // editable lines itself
349                    event::KeyCode::Down
350                        if c.use_history && (c.lineno + 1) == self.history.cur().unwrap().len() =>
351                    {
352                        history_down!(self, stdout, c, lines, colour)
353                    }
354                    // When in the end of editable lines, nothing should be done
355                    event::KeyCode::Down if !c.use_history && (c.lineno + 1) == lines.len() => {}
356                    // Somewhere in the block, go to next line
357                    event::KeyCode::Down => {
358                        c.lineno += 1;
359                        queue!(stdout, cursor::MoveDown(1))?;
360                        c.charno = min(self.cur_str(&c, &lines).chars().count(), c.charno);
361                    }
362
363                    // Regular case, just need to delete a character
364                    event::KeyCode::Backspace if c.charno > 0 => {
365                        if c.use_history {
366                            self.replace_with_history(&mut lines);
367                            c.use_history = false;
368                        };
369
370                        c.charno -= 1;
371                        let byte_i = get_byte_i(&lines[c.lineno], c.charno);
372                        lines[c.lineno].remove(byte_i);
373                    }
374                    // It is the last character, and it is not the last line
375                    event::KeyCode::Backspace if c.lineno > 0 => {
376                        if c.use_history {
377                            self.replace_with_history(&mut lines);
378                            c.use_history = false;
379                        };
380
381                        c.lineno -= 1;
382                        c.charno = lines[c.lineno].chars().count();
383                        let line = lines.remove(c.lineno + 1);
384                        lines[c.lineno] += &line;
385
386                        execute!(stdout, cursor::MoveUp(1))?;
387                        self.print_lines(&mut stdout, &mut c, &lines, colour)?;
388                    }
389
390                    // Regular delete, just need to delete one character
391                    event::KeyCode::Delete
392                        if c.charno < self.cur_str(&c, &lines).chars().count() =>
393                    {
394                        if c.use_history {
395                            self.replace_with_history(&mut lines);
396                            c.use_history = false;
397                        };
398
399                        let byte_i = get_byte_i(&lines[c.lineno], c.charno);
400                        lines[c.lineno].remove(byte_i);
401                    }
402                    event::KeyCode::Delete if (c.lineno + 1) < self.cur(&c, &lines).len() => {
403                        if c.use_history {
404                            self.replace_with_history(&mut lines);
405                            c.use_history = false;
406                        };
407
408                        let line = lines.remove(c.lineno + 1);
409                        lines[c.lineno] += &line;
410
411                        self.print_lines(&mut stdout, &mut c, &lines, colour)?;
412                    }
413
414                    event::KeyCode::Enter => {
415                        if self.cur(&c, &lines[..])[0].trim().is_empty() {
416                            execute!(
417                                stdout,
418                                cursor::MoveToNextLine(1),
419                                style::SetForegroundColor(colour),
420                                style::Print(self.leader)
421                            )?;
422                            // Empty line
423                            continue;
424                        }
425
426                        if !c.use_history && lines.len() == 1 {
427                            if lines[0] == self.exit_keyword {
428                                self.exit();
429                            } else if lines[0] == self.clear_keyword {
430                                c.charno = 0;
431                                lines[0].clear();
432
433                                execute!(
434                                    stdout,
435                                    terminal::Clear(terminal::ClearType::All),
436                                    cursor::MoveTo(0, 0),
437                                    style::SetForegroundColor(colour),
438                                    style::Print(self.leader),
439                                    style::ResetColor,
440                                )?;
441
442                                // Command executed, no need to do any other checks
443                                continue;
444                            }
445                        }
446
447                        if c.use_history && (c.lineno + 1) == self.history.cur().unwrap().len() {
448                            // On the last line, break out of loop to return code for execution
449                            break;
450                        }
451                        let indent = L::get_indent(&self.cur(&c, &lines)[0..(c.lineno + 1)]);
452
453                        if !c.use_history && (c.lineno + 1) == lines.len() && indent == 0 {
454                            // On the last line, break out of loop to return code for execution
455                            break;
456                        } else {
457                            if c.use_history {
458                                self.replace_with_history(&mut lines);
459                                c.use_history = false;
460                            }
461
462                            c.lineno += 1;
463                            c.charno = indent;
464                            lines.insert(c.lineno, " ".repeat(indent));
465                            execute!(stdout, style::Print("\n"))?;
466                            self.print_lines(&mut stdout, &mut c, &lines, colour)?;
467                        }
468                    }
469                    _ => {}
470                }
471            };
472
473            queue!(
474                stdout,
475                terminal::Clear(terminal::ClearType::CurrentLine),
476                cursor::MoveToColumn(0),
477                style::SetForegroundColor(colour),
478            )?;
479
480            let (leader, leader_len) = if c.lineno == 0 {
481                (self.leader, self.leader_len)
482            } else {
483                (self.continued_leader, self.continued_leader_len)
484            };
485
486            queue!(stdout, style::Print(leader))?;
487            L::print_line(&mut stdout, self.cur(&c, &lines[..]), c.lineno)?;
488            execute!(
489                stdout,
490                cursor::MoveToColumn((leader_len + c.charno + 1) as u16)
491            )?;
492        }
493
494        terminal::disable_raw_mode()?;
495        println!();
496
497        let src = self.cur(&c, &lines).join("\n");
498
499        if c.use_history {
500            self.history.push(self.history.cur().unwrap().clone());
501        } else {
502            self.history.push(lines);
503        }
504
505        Ok(src)
506    }
507}
508
509impl<L: LangInterface> Drop for Repl<L> {
510    fn drop(&mut self) {
511        self.pre_exit();
512    }
513}
514
515fn get_byte_i(string: &str, i: usize) -> usize {
516    string
517        .char_indices()
518        .nth(i)
519        .map(|c| c.0)
520        .unwrap_or_else(|| string.len())
521}
522
523#[derive(Debug, Default)]
524struct Cursor {
525    use_history: bool,
526    lineno: usize,
527    charno: usize,
528}