1use gut::prelude::*;
3use std::path::{Path, PathBuf};
4mod helper;
8use rustyline::{history::FileHistory, Editor};
12type MyEditor<R> = Editor<helper::MyHelper<R>, FileHistory>;
13
14pub struct Interpreter<A> {
16 prompt: String,
17 history_file: Option<PathBuf>,
18 action: A,
19}
20impl<A: Actionable> Interpreter<A> {
24 fn continue_interpret_line(&mut self, line: &str) -> bool {
26 if let Some(mut args) = shlex::split(line) {
27 assert!(args.len() >= 1);
28 args.insert(0, self.prompt.to_owned());
29
30 match A::try_parse_from(&args) {
31 Ok(x) => match self.action.act_on(&x) {
33 Ok(exit) => {
34 if exit {
35 return false;
36 }
37 }
38 Err(e) => {
39 eprintln!("{:?}", e);
40 }
41 },
42 Err(e) => println!("{:}", e),
44 }
45 true
46 } else {
47 eprintln!("Invalid quoting: {line:?}");
48 false
49 }
50 }
51
52 fn continue_read_eval_print<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> bool {
53 match editor.readline(&self.prompt) {
54 Err(rustyline::error::ReadlineError::Eof) => false,
55 Ok(line) => {
56 let line = line.trim();
57 if !line.is_empty() {
58 let _ = editor.add_history_entry(line);
59 self.continue_interpret_line(&line)
60 } else {
61 true
62 }
63 }
64 Err(e) => {
65 eprintln!("{}", e);
66 false
67 }
68 }
69 }
70}
71
72fn create_readline_editor<R: HelpfulCommand>() -> Result<Editor<helper::MyHelper<R>, FileHistory>> {
73 use rustyline::{ColorMode, CompletionType, Config};
74
75 #[cfg(not(windows))]
76 let builder = Config::builder().completion_type(CompletionType::Fuzzy);
77 #[cfg(windows)]
78 let builder = Config::builder().completion_type(CompletionType::Circular);
79 let config = builder
80 .color_mode(ColorMode::Enabled)
81 .history_ignore_dups(true)?
82 .history_ignore_space(true)
83 .max_history_size(1000)?
84 .build();
85
86 let mut rl = Editor::with_config(config)?;
87 let h = self::helper::MyHelper::new();
88 rl.set_helper(Some(h));
89 Ok(rl)
90}
91impl<A: Actionable> Interpreter<A> {
95 fn load_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
96 if let Some(h) = self.history_file.as_ref() {
97 editor.load_history(h).context("no history")?;
98 }
99 Ok(())
100 }
101
102 fn save_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
103 if let Some(h) = self.history_file.as_ref() {
104 editor.save_history(h).context("write history file")?;
105 }
106 Ok(())
107 }
108}
109impl<A: Actionable> Interpreter<A> {
113 pub fn interpret_script(&mut self, script: &str) -> Result<()> {
114 let lines = script.lines().filter(|s| !s.trim().is_empty());
115 for line in lines {
116 debug!("Execute: {:?}", line);
117 if !self.continue_interpret_line(&line) {
118 break;
119 }
120 }
121
122 Ok(())
123 }
124
125 pub fn interpret_script_file(&mut self, script_file: &Path) -> Result<()> {
126 let s = gut::fs::read_file(script_file)?;
127 self.interpret_script(&s)?;
128 Ok(())
129 }
130}
131pub trait Actionable {
136 type Command: clap::Parser;
137
138 fn act_on(&mut self, cmd: &Self::Command) -> Result<bool>;
141
142 fn try_parse_from<I, T>(iter: I) -> Result<Self::Command>
144 where
145 I: IntoIterator<Item = T>,
146 T: Into<std::ffi::OsString> + Clone,
147 {
148 use clap::Parser;
149
150 let r = Self::Command::try_parse_from(iter)?;
151 Ok(r)
152 }
153}
154
155pub trait HelpfulCommand {
157 fn get_subcommands() -> Vec<String>;
158 fn suitable_for_path_complete(line: &str, pos: usize) -> bool;
159}
160
161impl<T: clap::CommandFactory> HelpfulCommand for T {
162 fn get_subcommands() -> Vec<String> {
163 let app = Self::command();
164 app.get_subcommands().map(|s| s.get_name().into()).collect()
165 }
166
167 fn suitable_for_path_complete(line: &str, pos: usize) -> bool {
169 line[..pos]
170 .chars()
171 .last()
172 .map(|x| std::path::is_separator(x))
173 .unwrap_or(false)
174 }
175}
176impl<A: Actionable> Interpreter<A> {
180 #[track_caller]
181 pub fn new(action: A) -> Self {
182 Self {
183 prompt: "> ".to_string(),
184 history_file: None,
186 action,
187 }
188 }
189}
190
191impl<A: Actionable> Interpreter<A> {
192 pub fn with_history_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
194 let p = path.into();
195 self.history_file = Some(p);
196 self
197 }
198
199 pub fn with_prompt(mut self, s: &str) -> Self {
201 self.prompt = s.into();
202 self
203 }
204
205 pub fn run(&mut self) -> Result<()> {
207 let version = env!("CARGO_PKG_VERSION");
208 println!("This is the interactive parser, version {}.", version);
209 println!("Enter \"help\" or \"?\" for a list of commands.");
210 println!("Press Ctrl-D or enter \"quit\" or \"q\" to exit.");
211 println!("");
212
213 let mut editor = create_readline_editor::<A::Command>()?;
214 let _ = self.load_history(&mut editor);
215 while self.continue_read_eval_print(&mut editor) {
216 debug!("excuted one loop");
217 }
218 self.save_history(&mut editor)?;
219
220 Ok(())
221 }
222}
223