1use super::*;
2use crate::effects::StackEffect;
3use crate::output::OutputFormat;
4use clap::CommandFactory;
5use rustyline::error::ReadlineError;
6use rustyline::DefaultEditor;
7use std::str::FromStr;
8
9const HUMAN_PROMPT: &str = "🌴 ▶ ";
10
11pub const INTERACT_INSTRUCTIONS: &str = "INTERACTIVE MODE:
12
13Use subcommands in interactive mode directly. \
14No OPTIONS (flags) of subcommands are understood in interactive mode. \
15The ; character can be used to separate commands.
16
17The following additional commands are available:
18 ? Show the short version of \"help\"
19 clear Clear the terminal screen
20 use Change to the specified stack [aliases: stack]
21 exit Quit interactive mode [aliases: quit, q]";
22
23pub const INTERACT_LONG_INSTRUCTIONS: &str = "INTERACTIVE MODE:
24
25Use subcommands in interactive mode directly. For example:
26
27 🌴 ▶ push a new thing
28 Created: a new thing
29 🌴 ▶ peek
30 Now: a new thing
31 🌴 ▶ delete
32 Deleted: a new thing
33 Now: nothing
34 🌴 ▶ exit
35 exit: Buen biåhe!
36
37No OPTIONS (flags) of subcommands are understood in interactive mode.
38
39The ; character can be used to separate commands.
40
41In interactive mode, the following additional commands are available:
42 ?
43 Show the short version of \"help\"
44 clear
45 Clear the terminal screen
46 use
47 Change to the specified stack [aliases: stack]
48 exit
49 Quit interactive mode [aliases: quit, q]";
50
51pub fn interact(original_stack: String, data_store: DataStore, output: OutputFormat) {
54 print_welcome_msg(output);
55
56 let mut rl = DefaultEditor::new().expect("Unable to create readline.");
57 let prompt = if output.is_nonquiet_for_humans() {
58 HUMAN_PROMPT
59 } else {
60 ""
61 };
62
63 let mut stack = original_stack;
64
65 loop {
66 let line = rl.readline(prompt);
67
68 if let Ok(line) = &line {
69 rl.add_history_entry(line).unwrap();
70 }
71
72 use InteractAction::*;
73 let line = line.map_err(handle_error).map(handle_line(&stack));
74 let actions = match line {
75 Ok(actions) => actions,
76 Err(err_action) => vec![err_action],
77 };
78
79 for action in actions {
80 match action {
81 ShortHelp => Cli::command().print_help().unwrap(),
82 LongHelp => Cli::command().print_long_help().unwrap(),
83 Clear => clearscreen::clear().expect("Failed to clear screen"),
84 DoEffect(effect) => effect.run(&data_store, &output),
85 UseStack(new_stack) => {
86 stack = new_stack;
87 output.log(vec!["update", "stack"], vec![vec!["Active stack", &stack]]);
88 }
89 NoContent => (),
90 Exit(reason) => {
91 print_goodbye_msg(&reason, output);
92 return;
93 }
94 MissingArgument(msg) => {
95 output.log(
96 vec!["argument", "error"],
97 vec![vec![&msg, "missing argument"]],
98 );
99 }
100 Error(msg) => {
101 output.log(
102 vec!["exit-message", "exit-reason"],
103 vec![vec!["Error"], vec![&msg]],
104 );
105 return;
106 }
107 Unknown(term) => {
108 if output.is_nonquiet_for_humans() {
109 println!("Oops, I don't know {:?}", term);
110 } else {
111 output.log(vec!["term", "error"], vec![vec![&term, "unknown term"]]);
112 };
113 }
114 };
115 }
116 }
117}
118
119fn print_welcome_msg(output: OutputFormat) {
120 if output.is_nonquiet_for_humans() {
121 println!("sigi {}", SIGI_VERSION);
122 println!(
123 "Type \"exit\", \"quit\", or \"q\" to quit. (On Unixy systems, Ctrl+C or Ctrl+D also work)"
124 );
125 println!("Type \"?\" for quick help, or \"help\" for a more verbose help message.");
126 println!();
127 }
128}
129
130fn print_goodbye_msg(reason: &str, output: OutputFormat) {
131 output.log(
132 vec!["exit-reason", "exit-message"],
133 vec![vec![reason, "Buen biåhe!"]],
134 );
135}
136
137enum InteractAction {
138 ShortHelp,
139 LongHelp,
140 Clear,
141 DoEffect(StackEffect),
142 UseStack(String),
143 NoContent,
144 Exit(String),
145 MissingArgument(String),
146 Error(String),
147 Unknown(String),
148}
149
150fn handle_error(err: ReadlineError) -> InteractAction {
151 match err {
152 ReadlineError::Interrupted => InteractAction::Exit("Ctrl+c".to_string()),
153 ReadlineError::Eof => InteractAction::Exit("Ctrl+d".to_string()),
154 err => InteractAction::Error(format!("{:?}", err)),
155 }
156}
157
158fn handle_line(stack: &str) -> impl Fn(String) -> Vec<InteractAction> + '_ {
159 |line| {
160 line.split(';')
161 .map(|s| s.to_string())
162 .map(|line| parse_line(line, stack.to_string()))
163 .collect()
164 }
165}
166
167fn parse_line(line: String, stack: String) -> InteractAction {
168 let tokens = line.split_ascii_whitespace().collect::<Vec<_>>();
169
170 if tokens.is_empty() {
171 return InteractAction::NoContent;
172 }
173
174 let term = tokens.first().unwrap().to_ascii_lowercase();
175
176 match term.as_str() {
177 "?" => InteractAction::ShortHelp,
178 "help" => InteractAction::LongHelp,
179 "clear" => InteractAction::Clear,
180 "exit" | "quit" | "q" => InteractAction::Exit(term),
181 "use" | "stack" => match tokens.get(1) {
182 Some(stack) => InteractAction::UseStack(stack.to_string()),
183 None => InteractAction::MissingArgument("stack name".to_string()),
184 },
185 _ => match parse_effect(tokens, stack) {
186 ParseEffectResult::Effect(effect) => InteractAction::DoEffect(effect),
187 ParseEffectResult::NotEffect(parse_res) => parse_res,
188 ParseEffectResult::Unknown => InteractAction::Unknown(term),
189 },
190 }
191}
192
193enum ParseEffectResult {
194 Effect(StackEffect),
195 NotEffect(InteractAction),
196 Unknown,
197}
198
199fn parse_effect(tokens: Vec<&str>, stack: String) -> ParseEffectResult {
200 let term = tokens.first().unwrap_or(&"");
201
202 let parse_n = || tokens.get(1).and_then(|&s| usize::from_str(s).ok());
203
204 use ParseEffectResult::*;
205 use StackEffect::*;
206
207 if COMPLETE_TERMS.contains(term) {
208 let index = parse_n().unwrap_or(0);
209 return Effect(Complete { stack, index });
210 }
211 if COUNT_TERMS.contains(term) {
212 return Effect(Count { stack });
213 }
214 if DELETE_TERMS.contains(term) {
215 let index = parse_n().unwrap_or(0);
216 return Effect(Delete { stack, index });
217 }
218 if DELETE_ALL_TERMS.contains(term) {
219 return Effect(DeleteAll { stack });
220 }
221 if EDIT_TERMS.contains(term) {
222 let index = parse_n().unwrap_or(0);
223 return Effect(Edit {
224 stack,
225 editor: resolve_editor(None),
226 index,
227 });
228 }
229 if HEAD_TERMS.contains(term) {
230 let n = parse_n().unwrap_or(DEFAULT_SHORT_LIST_LIMIT);
231 return Effect(Head { stack, n });
232 }
233 if IS_EMPTY_TERMS.contains(term) {
234 return Effect(IsEmpty { stack });
235 }
236 if LIST_TERMS.contains(term) {
237 return Effect(ListAll { stack });
238 }
239 if LIST_STACKS_TERMS.contains(term) {
240 return Effect(ListStacks);
241 }
242 if MOVE_TERMS.contains(term) {
243 match tokens.get(1) {
244 Some(dest) => {
245 let dest = dest.to_string();
246 return Effect(Move { stack, dest });
247 }
248 None => {
249 return NotEffect(InteractAction::MissingArgument(
250 "destination stack".to_string(),
251 ));
252 }
253 };
254 }
255 if MOVE_ALL_TERMS.contains(term) {
256 match tokens.get(1) {
257 Some(dest) => {
258 let dest = dest.to_string();
259 return Effect(MoveAll { stack, dest });
260 }
261 None => {
262 return NotEffect(InteractAction::MissingArgument(
263 "destination stack".to_string(),
264 ));
265 }
266 };
267 }
268 if NEXT_TERMS.contains(term) {
269 return Effect(Next { stack });
270 }
271 if PEEK_TERMS.contains(term) {
272 return Effect(Peek { stack });
273 }
274 if PICK_TERMS.contains(term) {
275 let indices = tokens
276 .iter()
277 .filter_map(|s| usize::from_str(s).ok())
278 .collect();
279 return Effect(Pick { stack, indices });
280 }
281 if PUSH_TERMS.contains(term) {
282 let content = tokens[1..].join(" ");
284 return Effect(Push { stack, content });
285 }
286 if ROT_TERMS.contains(term) {
287 return Effect(Rot { stack });
288 }
289 if SWAP_TERMS.contains(term) {
290 return Effect(Swap { stack });
291 }
292 if TAIL_TERMS.contains(term) {
293 let n = parse_n().unwrap_or(DEFAULT_SHORT_LIST_LIMIT);
294 return Effect(Tail { stack, n });
295 }
296
297 Unknown
298}