reedline_repl_rs/
repl.rs

1use crate::command::ReplCommand;
2use crate::completer::ReplCompleter;
3use crate::error::*;
4use crate::prompt::ReplPrompt;
5use crate::{paint_green_bold, paint_yellow_bold, AfterCommandCallback, Callback};
6#[cfg(feature = "async")]
7use crate::{AsyncAfterCommandCallback, AsyncCallback};
8use clap::Command;
9use reedline::MenuBuilder;
10// use crossterm::event::{KeyCode, KeyModifiers};
11use nu_ansi_term::{Color, Style};
12use reedline::{
13    self, default_emacs_keybindings, ColumnarMenu, DefaultHinter, DefaultValidator, Emacs,
14    ExampleHighlighter, ExternalPrinter, FileBackedHistory, KeyCode, KeyModifiers, Keybindings,
15    Reedline, ReedlineEvent, ReedlineMenu, Signal,
16};
17use std::boxed::Box;
18use std::collections::HashMap;
19use std::fmt::Display;
20use std::path::PathBuf;
21
22type ErrorHandler<Context, E> = fn(error: E, repl: &Repl<Context, E>) -> Result<()>;
23
24fn default_error_handler<Context, E: Display>(error: E, _repl: &Repl<Context, E>) -> Result<()> {
25    eprintln!("{}", error);
26    Ok(())
27}
28
29/// Main REPL struct
30pub struct Repl<Context, E: Display> {
31    name: String,
32    banner: Option<String>,
33    version: String,
34    description: String,
35    prompt: ReplPrompt,
36    after_command_callback: Option<AfterCommandCallback<Context, E>>,
37    #[cfg(feature = "async")]
38    after_command_callback_async: Option<AsyncAfterCommandCallback<Context, E>>,
39    commands: HashMap<String, ReplCommand<Context, E>>,
40    history: Option<PathBuf>,
41    history_capacity: Option<usize>,
42    context: Context,
43    keybindings: Keybindings,
44    external_printer: ExternalPrinter<String>,
45    hinter_style: Style,
46    hinter_enabled: bool,
47    quick_completions: bool,
48    partial_completions: bool,
49    stop_on_ctrl_c: bool,
50    stop_on_ctrl_d: bool,
51    error_handler: ErrorHandler<Context, E>,
52}
53
54impl<Context, E> Repl<Context, E>
55where
56    E: Display + From<Error> + std::fmt::Debug,
57{
58    /// Create a new Repl with the given context's initial value.
59    pub fn new(context: Context) -> Self {
60        let name = String::from("repl");
61        let style = Style::new().italic().fg(Color::LightGray);
62        let mut keybindings = default_emacs_keybindings();
63        keybindings.add_binding(
64            KeyModifiers::NONE,
65            KeyCode::Tab,
66            ReedlineEvent::Menu("completion_menu".to_string()),
67        );
68        let prompt = ReplPrompt::new(&paint_green_bold(&format!("{}> ", name)));
69
70        Self {
71            name,
72            banner: None,
73            version: String::new(),
74            description: String::new(),
75            commands: HashMap::new(),
76            history: None,
77            history_capacity: None,
78            after_command_callback: None,
79            #[cfg(feature = "async")]
80            after_command_callback_async: None,
81            quick_completions: true,
82            partial_completions: false,
83            hinter_enabled: true,
84            hinter_style: style,
85            prompt,
86            context,
87            keybindings,
88            external_printer: ExternalPrinter::new(2048),
89            stop_on_ctrl_c: false,
90            stop_on_ctrl_d: true,
91            error_handler: default_error_handler,
92        }
93    }
94
95    /// Give your Repl a name. This is used in the help summary for the Repl.
96    pub fn with_name(mut self, name: &str) -> Self {
97        self.name = name.to_string();
98        self.with_formatted_prompt(name)
99    }
100
101    /// Give your Repl a banner. This is printed at the start of running the Repl.
102    pub fn with_banner(mut self, banner: &str) -> Self {
103        self.banner = Some(banner.to_string());
104
105        self
106    }
107
108    /// Give your Repl a version. This is used in the help summary for the Repl.
109    pub fn with_version(mut self, version: &str) -> Self {
110        self.version = version.to_string();
111
112        self
113    }
114
115    /// Give your Repl a description. This is used in the help summary for the Repl.
116    pub fn with_description(mut self, description: &str) -> Self {
117        self.description = description.to_string();
118
119        self
120    }
121
122    /// Give your REPL a callback which is called after every command and may update the prompt
123    pub fn with_on_after_command(mut self, callback: AfterCommandCallback<Context, E>) -> Self {
124        self.after_command_callback = Some(callback);
125
126        self
127    }
128
129    /// Give your REPL a callback which is called after every command and may update the prompt
130    #[cfg(feature = "async")]
131    pub fn with_on_after_command_async(
132        mut self,
133        callback: AsyncAfterCommandCallback<Context, E>,
134    ) -> Self {
135        self.after_command_callback_async = Some(callback);
136
137        self
138    }
139
140    /// Give your Repl a file based history saved at history_path
141    pub fn with_history(mut self, history_path: PathBuf, capacity: usize) -> Self {
142        self.history = Some(history_path);
143        self.history_capacity = Some(capacity);
144
145        self
146    }
147
148    /// Give your Repl a custom prompt. The default prompt is the Repl name, followed by
149    /// a `>`, all in green and bold, followed by a space:
150    ///
151    /// &Paint::green(format!("{}> ", name)).bold().to_string()
152    pub fn with_prompt(mut self, prompt: &str) -> Self {
153        self.prompt.update_prefix(prompt);
154
155        self
156    }
157
158    /// Give your Repl a custom prompt while applying green/bold formatting automatically
159    ///
160    /// &Paint::green(format!("{}> ", name)).bold().to_string()
161    pub fn with_formatted_prompt(mut self, prompt: &str) -> Self {
162        self.prompt.update_prefix(prompt);
163
164        self
165    }
166
167    /// Pass in a custom error handler. This is really only for testing - the default
168    /// error handler simply prints the error to stderr and then returns
169    pub fn with_error_handler(mut self, handler: ErrorHandler<Context, E>) -> Self {
170        self.error_handler = handler;
171
172        self
173    }
174
175    /// Turn on/off if REPL run is stopped on CTRG+C (Default: false)
176    pub fn with_stop_on_ctrl_c(mut self, stop_on_ctrl_c: bool) -> Self {
177        self.stop_on_ctrl_c = stop_on_ctrl_c;
178
179        self
180    }
181
182    /// Turn on/off if REPL run is stopped on CTRG+D (Default: true)
183    pub fn with_stop_on_ctrl_d(mut self, stop_on_ctrl_d: bool) -> Self {
184        self.stop_on_ctrl_d = stop_on_ctrl_d;
185
186        self
187    }
188
189    /// Turn on quick completions. These completions will auto-select if the completer
190    /// ever narrows down to a single entry.
191    pub fn with_quick_completions(mut self, quick_completions: bool) -> Self {
192        self.quick_completions = quick_completions;
193
194        self
195    }
196
197    /// Turn on partial completions. These completions will fill the buffer with the
198    /// smallest common string from all the options
199    pub fn with_partial_completions(mut self, partial_completions: bool) -> Self {
200        self.partial_completions = partial_completions;
201
202        self
203    }
204
205    /// Sets the style for reedline's fish-style history autosuggestions
206    ///
207    /// Default: `nu_ansi_term::Style::new().italic().fg(nu_ansi_term::Color::LightGray)`
208    ///
209    pub fn with_hinter_style(mut self, style: Style) -> Self {
210        self.hinter_style = style;
211
212        self
213    }
214
215    /// Disables reedline's fish-style history autosuggestions
216    pub fn with_hinter_disabled(mut self) -> Self {
217        self.hinter_enabled = false;
218
219        self
220    }
221
222    /// Adds a reedline keybinding
223    ///
224    /// # Panics
225    ///
226    /// If `comamnd` is an empty [`ReedlineEvent::UntilFound`]
227    pub fn with_keybinding(
228        mut self,
229        modifier: KeyModifiers,
230        key_code: KeyCode,
231        command: ReedlineEvent,
232    ) -> Self {
233        self.keybindings.add_binding(modifier, key_code, command);
234
235        self
236    }
237
238    /// Find a keybinding based on the modifier and keycode
239    pub fn find_keybinding(
240        &self,
241        modifier: KeyModifiers,
242        key_code: KeyCode,
243    ) -> Option<ReedlineEvent> {
244        self.keybindings.find_binding(modifier, key_code)
245    }
246
247    /// Get assigned keybindings
248    pub fn get_keybindings(&self) -> HashMap<(KeyModifiers, KeyCode), ReedlineEvent> {
249        // keybindings.get_keybindings() cannot be returned directly because KeyCombination is not visible
250        self.keybindings
251            .get_keybindings()
252            .iter()
253            .map(|(key, value)| ((key.modifier, key.key_code), value.clone()))
254            .collect()
255    }
256
257    /// Remove a keybinding
258    ///
259    /// Returns `Some(ReedlineEvent)` if the keycombination was previously bound to a particular [`ReedlineEvent`]
260    pub fn without_keybinding(mut self, modifier: KeyModifiers, key_code: KeyCode) -> Self {
261        self.keybindings.remove_binding(modifier, key_code);
262
263        self
264    }
265
266    /// Access the external printer to print messages on the console
267    ///
268    /// The external printer can be used to print messages on the console without a command running.
269    ///
270    /// Example:
271    ///
272    /// ```rust,no_run
273    /// use reedline_repl_rs::{Repl, Error};
274    /// let mut repl: Repl<(), Error> = Repl::new(());
275    /// let printer = repl.external_printer();
276    /// std::thread::spawn(move || {
277    ///     printer.print("hello world".to_owned()).expect("failed to print");
278    /// });
279    /// repl.run().expect("failed to run");
280    /// ```
281    pub fn external_printer(&self) -> ExternalPrinter<String> {
282        self.external_printer.clone()
283    }
284
285    /// Add a command to your REPL
286    pub fn with_command(mut self, command: Command, callback: Callback<Context, E>) -> Self {
287        let name = command.get_name().to_string();
288        self.commands
289            .insert(name.clone(), ReplCommand::new(&name, command, callback));
290        self
291    }
292
293    #[cfg(feature = "async_derive")]
294    pub fn with_async_derived<Clap: clap::Parser>(
295        mut self,
296        callbacks: crate::AsyncCallBackMap<Context, E>,
297    ) -> Self {
298        let derived = Clap::command();
299
300        self = self.with_name(derived.get_name());
301
302        if let Some(version) = derived.get_version() {
303            self = self.with_version(version);
304        }
305
306        if let Some(desc) = derived.get_about() {
307            self = self.with_description(&desc.to_string());
308        }
309
310        let commands = derived.get_subcommands();
311
312        for command in commands {
313            let name = command.get_name();
314            let Some(callback) = callbacks.get(name) else {
315                continue;
316            };
317            let cmd: ReplCommand<Context, E> =
318                ReplCommand::new_async(&name, command.clone(), *callback);
319
320            self.commands.insert(name.to_string(), cmd);
321        }
322
323        self
324    }
325
326    /// Add a command to your REPL using clap's derive parser
327    #[cfg(feature = "derive")]
328    pub fn with_derived<Clap: clap::Parser>(
329        mut self,
330        callbacks: crate::CallBackMap<Context, E>,
331    ) -> Self {
332        let derived = Clap::command();
333
334        self = self.with_name(derived.get_name());
335
336        if let Some(version) = derived.get_version() {
337            self = self.with_version(version);
338        }
339
340        if let Some(desc) = derived.get_about() {
341            self = self.with_description(&desc.to_string());
342        }
343
344        let commands = derived.get_subcommands();
345
346        for command in commands {
347            let name = command.get_name();
348            let Some(callback) = callbacks.get(name) else {
349                continue;
350            };
351            let cmd: ReplCommand<Context, E> = ReplCommand::new(name, command.clone(), *callback);
352
353            self.commands.insert(name.to_string(), cmd);
354        }
355
356        self
357    }
358
359    /// Add a command to your REPL
360    #[cfg(feature = "async")]
361    pub fn with_command_async(
362        mut self,
363        command: Command,
364        callback: AsyncCallback<Context, E>,
365    ) -> Self {
366        let name = command.get_name().to_string();
367        self.commands.insert(
368            name.clone(),
369            ReplCommand::new_async(&name, command, callback),
370        );
371        self
372    }
373
374    fn show_help(&self, args: &[&str]) -> Result<()> {
375        if args.is_empty() {
376            let mut app = Command::new("app");
377
378            for (_, com) in self.commands.iter() {
379                app = app.subcommand(com.command.clone());
380            }
381            let mut help_bytes: Vec<u8> = Vec::new();
382            app.write_help(&mut help_bytes)
383                .expect("failed to print help");
384            let mut help_string =
385                String::from_utf8(help_bytes).expect("Help message was invalid UTF8");
386            let marker = "SUBCOMMANDS:";
387            if let Some(marker_pos) = help_string.find(marker) {
388                help_string = paint_yellow_bold("COMMANDS:")
389                    + &help_string[(marker_pos + marker.len())..help_string.len()];
390            }
391            let header = format!(
392                "{} {}\n{}\n",
393                paint_green_bold(&self.name),
394                self.version,
395                self.description
396            );
397            println!("{}", header);
398            println!("{}", help_string);
399        } else if let Some((_, subcommand)) = self
400            .commands
401            .iter()
402            .find(|(name, _)| name.as_str() == args[0])
403        {
404            subcommand
405                .command
406                .clone()
407                .print_help()
408                .expect("failed to print help");
409            println!();
410        } else {
411            eprintln!("Help not found for command '{}'", args[0]);
412        }
413        Ok(())
414    }
415
416    fn handle_command(&mut self, command: &str, args: &[&str]) -> core::result::Result<(), E> {
417        match self.commands.get(command) {
418            Some(definition) => {
419                let mut argv: Vec<&str> = vec![command];
420                argv.extend(args);
421                match definition.command.clone().try_get_matches_from_mut(argv) {
422                    Ok(matches) => match (definition
423                        .callback
424                        .expect("Must be filled for sync commands"))(
425                        matches, &mut self.context
426                    ) {
427                        Ok(Some(value)) => println!("{}", value),
428                        Ok(None) => (),
429                        Err(error) => return Err(error),
430                    },
431                    Err(err) => {
432                        err.print().expect("failed to print");
433                    }
434                };
435                self.execute_after_command_callback()?;
436            }
437            None => {
438                if command == "help" {
439                    self.show_help(args)?;
440                } else {
441                    return Err(Error::UnknownCommand(command.to_string()).into());
442                }
443            }
444        }
445
446        Ok(())
447    }
448
449    fn execute_after_command_callback(&mut self) -> core::result::Result<(), E> {
450        if let Some(callback) = self.after_command_callback {
451            match callback(&mut self.context) {
452                Ok(Some(new_prompt)) => {
453                    self.prompt.update_prefix(&new_prompt);
454                }
455                Ok(None) => {}
456                Err(err) => {
457                    eprintln!("failed to execute after_command_callback {:?}", err);
458                }
459            }
460        }
461
462        Ok(())
463    }
464
465    #[cfg(feature = "async")]
466    async fn execute_after_command_callback_async(&mut self) -> core::result::Result<(), E> {
467        self.execute_after_command_callback()?;
468        if let Some(callback) = self.after_command_callback_async {
469            match callback(&mut self.context).await {
470                Ok(new_prompt) => {
471                    if let Some(new_prompt) = new_prompt {
472                        self.prompt.update_prefix(&new_prompt);
473                    }
474                }
475                Err(err) => {
476                    eprintln!("failed to execute after_command_callback {:?}", err);
477                }
478            }
479        }
480
481        Ok(())
482    }
483
484    #[cfg(feature = "async")]
485    async fn handle_command_async(
486        &mut self,
487        command: &str,
488        args: &[&str],
489    ) -> core::result::Result<(), E> {
490        match self.commands.get(command) {
491            Some(definition) => {
492                let mut argv: Vec<&str> = vec![command];
493                argv.extend(args);
494                match definition.command.clone().try_get_matches_from_mut(argv) {
495                    Ok(matches) => match if let Some(async_callback) = definition.async_callback {
496                        async_callback(matches, &mut self.context).await
497                    } else {
498                        definition
499                            .callback
500                            .expect("Either async or sync callback must be set")(
501                            matches,
502                            &mut self.context,
503                        )
504                    } {
505                        Ok(Some(value)) => println!("{}", value),
506                        Ok(None) => (),
507                        Err(error) => return Err(error),
508                    },
509                    Err(err) => {
510                        err.print().expect("failed to print");
511                    }
512                };
513                self.execute_after_command_callback_async().await?;
514            }
515            None => {
516                if command == "help" {
517                    self.show_help(args)?;
518                } else {
519                    return Err(Error::UnknownCommand(command.to_string()).into());
520                }
521            }
522        }
523
524        Ok(())
525    }
526
527    #[cfg(not(feature = "shlex"))]
528    fn parse_line(&self, line: &str) -> Option<Vec<String>> {
529        let r = regex::Regex::new(r#"("[^"\n]+"|[\S]+)"#).unwrap();
530        Some(r.captures_iter(line)
531              .map(|a| a[0].to_string().replace('\"', ""))
532              .collect::<Vec<String>>())
533    }
534
535    #[cfg(feature = "shlex")]
536    fn parse_line(&self, line: &str) -> Option<Vec<String>> {
537        shlex::split(line)
538    }
539
540    /// Process an array of arguments directly as command input.
541    /// This method is called internally after splitting an input line.
542    ///
543    /// May be used instead of `run_with_reader` to execute a single command.
544    /// An example use case is to process command line args directly as a command:
545    ///
546    /// ``` no_run
547    /// //
548    /// let mut repl = reedline_repl_rs::Repl::new(());
549    /// // ... set up repl ...
550    /// if std::env::args().len() > 1 {
551    ///     repl.process_argv(std::env::args().skip(1).collect::<Vec<String>>())?;
552    /// } else {
553    ///     repl.run()?;
554    /// }
555    /// ```
556    pub fn process_argv(&mut self, argv: Vec<String>) -> core::result::Result<(), E> {
557        let mut iter = argv.iter();
558        if let Some(command) = iter.next() {
559            self.handle_command(command, &iter.map(AsRef::as_ref).collect::<Vec<&str>>())
560        } else {
561            Ok(())
562        }
563    }
564
565    #[cfg(feature = "async")]
566    /// Async version of `process_argv`, please refer to `process_argv` for documentation
567    pub async fn process_argv_async(&mut self, argv: Vec<String>) -> core::result::Result<(), E> {
568        let mut iter = argv.iter();
569        if let Some(command) = iter.next() {
570            self.handle_command_async(command, &iter.map(AsRef::as_ref).collect::<Vec<&str>>()).await
571        } else {
572            Ok(())
573        }
574    }
575
576    fn process_line(&mut self, line: String) -> core::result::Result<(), E> {
577        if let Some(args) = self.parse_line(&line) {
578            self.process_argv(args)
579        } else {
580            Ok(())
581        }
582    }
583
584    #[cfg(feature = "async")]
585    async fn process_line_async(&mut self, line: String) -> core::result::Result<(), E> {
586        if let Some(args) = self.parse_line(&line) {
587            self.process_argv_async(args).await
588        } else {
589            Ok(())
590        }
591    }
592
593    fn build_line_editor(&mut self) -> Result<Reedline> {
594        let mut valid_commands: Vec<String> = self
595            .commands
596            .iter()
597            .map(|(_, command)| command.name.clone())
598            .collect();
599        valid_commands.push("help".to_string());
600        let completer = Box::new(ReplCompleter::new(&self.commands));
601        let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
602        let validator = Box::new(DefaultValidator);
603        let mut line_editor = Reedline::create()
604            .with_edit_mode(Box::new(Emacs::new(self.keybindings.clone())))
605            .with_completer(completer)
606            .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
607            .with_highlighter(Box::new(ExampleHighlighter::new(valid_commands.clone())))
608            .with_validator(validator)
609            .with_partial_completions(self.partial_completions)
610            .with_quick_completions(self.quick_completions)
611            .with_external_printer(self.external_printer.clone());
612
613        if self.hinter_enabled {
614            line_editor = line_editor.with_hinter(Box::new(
615                DefaultHinter::default().with_style(self.hinter_style),
616            ));
617        }
618
619        if let Some(history_path) = &self.history {
620            let capacity = self.history_capacity.unwrap();
621            let history =
622                FileBackedHistory::with_file(capacity, history_path.to_path_buf()).unwrap();
623            line_editor = line_editor.with_history(Box::new(history));
624        }
625
626        Ok(line_editor)
627    }
628
629    #[cfg(feature = "scripts")]
630    /// Executs REPL taking an object with a `std::io::BufRead` implementation
631    /// as input
632    /// This is useful for executing scripts. Exampel structure that can be used here
633    /// is `std::io::BufReader` built on `std::fs::File`
634    pub fn run_with_reader(&mut self, reader: impl std::io::BufRead) -> Result<()> {
635        let lines = reader.lines();
636        for line in lines {
637            let line = line.expect("failed to read line");
638            if let Err(err) = self.process_line(line) {
639                (self.error_handler)(err, self)?;
640            }
641        }
642
643        Ok(())
644    }
645
646    /// Execute REPL
647    pub fn run(&mut self) -> Result<()> {
648        enable_virtual_terminal_processing();
649        if let Some(banner) = &self.banner {
650            println!("{}", banner);
651        }
652        let mut line_editor = self.build_line_editor()?;
653
654        loop {
655            let sig = line_editor
656                .read_line(&self.prompt)
657                .expect("failed to read_line");
658            match sig {
659                Signal::Success(line) => {
660                    if let Err(err) = self.process_line(line) {
661                        (self.error_handler)(err, self)?;
662                    }
663                }
664                Signal::CtrlC => {
665                    if self.stop_on_ctrl_c {
666                        break;
667                    }
668                }
669                Signal::CtrlD => {
670                    if self.stop_on_ctrl_d {
671                        break;
672                    }
673                }
674            }
675        }
676        disable_virtual_terminal_processing();
677        Ok(())
678    }
679
680    /// Execute REPL
681    #[cfg(feature = "async")]
682    pub async fn run_async(&mut self) -> Result<()> {
683        enable_virtual_terminal_processing();
684        if let Some(banner) = &self.banner {
685            println!("{}", banner);
686        }
687        let mut line_editor = self.build_line_editor()?;
688
689        loop {
690            let sig = line_editor
691                .read_line(&self.prompt)
692                .expect("failed to read_line");
693            match sig {
694                Signal::Success(line) => {
695                    if let Err(err) = self.process_line_async(line).await {
696                        (self.error_handler)(err, self)?;
697                    }
698                }
699                Signal::CtrlC => {
700                    if self.stop_on_ctrl_c {
701                        break;
702                    }
703                }
704                Signal::CtrlD => {
705                    if self.stop_on_ctrl_d {
706                        break;
707                    }
708                }
709            }
710        }
711        disable_virtual_terminal_processing();
712        Ok(())
713    }
714}
715
716#[cfg(windows)]
717pub fn enable_virtual_terminal_processing() {
718    use winapi_util::console::Console;
719    if let Ok(mut term) = Console::stdout() {
720        let _guard = term.set_virtual_terminal_processing(true);
721    }
722    if let Ok(mut term) = Console::stderr() {
723        let _guard = term.set_virtual_terminal_processing(true);
724    }
725}
726
727#[cfg(windows)]
728pub fn disable_virtual_terminal_processing() {
729    use winapi_util::console::Console;
730    if let Ok(mut term) = Console::stdout() {
731        let _guard = term.set_virtual_terminal_processing(false);
732    }
733    if let Ok(mut term) = Console::stderr() {
734        let _guard = term.set_virtual_terminal_processing(false);
735    }
736}
737
738#[cfg(not(windows))]
739pub fn enable_virtual_terminal_processing() {
740    // no-op
741}
742
743#[cfg(not(windows))]
744pub fn disable_virtual_terminal_processing() {
745    // no-op
746}