Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_tui/src/lib_tui/repl.rs

10.8 KiB, 9 runs

created by r1870400018:1201, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use oxedyne_fe2o3_core::prelude::*;
2use oxedyne_fe2o3_stds::chars::Term;
3use oxedyne_fe2o3_text::{
4 split::StringSplitter,
5};
6
7use std::{
8 fs::OpenOptions,
9 io::{
10 stdout,
11 BufRead,
12 BufReader,
13 Write,
14 },
15 path::Path,
16};
17
18use crossterm::{
19 cursor,
20 event::{
21 read,
22 Event,
23 KeyCode,
24 KeyModifiers,
25 },
26 execute,
27 terminal::{
28 disable_raw_mode,
29 enable_raw_mode,
30 Clear,
31 ClearType,
32 },
33};
34
35
36pub trait ShellContext {
37 fn eval(
38 &mut self,
39 input: &String,
40 cfg: &ShellConfig,
41 splitters: &Splitters,
42 )
43 -> Outcome<Vec<Evaluation>>;
44}
45
46#[derive(Clone)]
47pub struct ShellConfig {
48 pub greeting_msg: String,
49 pub norm_prompt: String,
50 pub alert_prompt: String,
51 pub history_name: String,
52 pub exit_msg: String,
53}
54
55impl Default for ShellConfig {
56 fn default() -> Self {
57 Self {
58 greeting_msg: fmt!("Welcome!"),
59 norm_prompt: fmt!(">"),
60 alert_prompt: fmt!("!"),
61 history_name: fmt!("history.txt"),
62 exit_msg: fmt!("Goodbye!"),
63 }
64 }
65}
66
67#[derive(Clone, Debug)]
68pub struct Splitters {
69 pub command: StringSplitter,
70 pub assignment: StringSplitter,
71 pub word: StringSplitter,
72}
73
74impl Default for Splitters {
75 fn default() -> Self {
76 Self {
77 command: StringSplitter::new().add_separators(Box::new([';'])),
78 assignment: StringSplitter::new().add_separators(Box::new(['='])),
79 word: StringSplitter::default(),
80 }
81 }
82}
83
84pub enum Evaluation {
85 Exit,
86 Error(String),
87 None,
88 Output(String),
89}
90
91pub struct Shell<C: ShellContext> {
92 cfg: ShellConfig,
93 context: C,
94 splitters: Splitters,
95 hist: Vec<String>,
96 hist_ind: usize,
97}
98
99impl<C: ShellContext> Shell<C> {
100
101 pub fn new(
102 cfg: ShellConfig,
103 context: C,
104 )
105 -> Outcome<Self>
106 {
107 let hist_path = "history.txt";
108
109 let file = res!(OpenOptions::new()
110 .read(true)
111 .write(true)
112 .create(true)
113 .open(hist_path)
114 );
115
116 let reader = BufReader::new(file);
117 let hist: Vec<String> = reader.lines().filter_map(Result::ok).collect();
118 let hist_ind = hist.len();
119
120 Ok(Shell {
121 cfg,
122 context,
123 splitters: Splitters::default(),
124 hist,
125 hist_ind,
126 })
127 }
128
129 fn read_line(&mut self) -> Outcome<Option<String>> {
130 res!(enable_raw_mode());
131 let mut input = String::new();
132 let mut cursor_pos = 0;
133
134 loop {
135 if let Event::Key(key_event) = res!(read()) {
136 match key_event.code {
137 KeyCode::Char(c) if key_event.modifiers.contains(KeyModifiers::CONTROL)
138 && (c == 'c' || c == 'd') =>
139 {
140 res!(disable_raw_mode());
141 println!("\n\r{}", self.cfg.exit_msg);
142 return Ok(None);
143 }
144 KeyCode::Char(c) => {
145 input.insert(cursor_pos, c);
146 cursor_pos += 1;
147 print!("{}", c);
148 if cursor_pos < input.len() {
149 // Print the rest of the input after the insertion point
150 print!("{}", &input[cursor_pos..]);
151 // Move the cursor back to the right position after reprinting the rest of the input
152 res!(execute!(stdout(), cursor::MoveLeft((input.len() - cursor_pos) as u16)));
153 }
154 res!(stdout().flush());
155 }
156 KeyCode::Enter => {
157 println!("\r"); // println! behaves differently in crossterm raw mode.
158 res!(disable_raw_mode());
159 return Ok(Some(input));
160 }
161 KeyCode::Left => {
162 if cursor_pos > 0 {
163 cursor_pos -= 1;
164 res!(execute!(stdout(), cursor::MoveLeft(1)));
165 }
166 }
167 KeyCode::Right => {
168 if cursor_pos < input.len() {
169 cursor_pos += 1;
170 res!(execute!(stdout(), cursor::MoveRight(1)));
171 res!(stdout().flush());
172 }
173 }
174 KeyCode::Up => {
175 if self.hist_ind > 0 {
176 self.hist_ind -= 1;
177 input = self.hist[self.hist_ind].clone();
178 cursor_pos = input.len();
179 } else {
180 continue;
181 }
182
183 res!(execute!(stdout(), Clear(ClearType::CurrentLine)));
184 print!("\r{} {}", self.cfg.norm_prompt, input);
185 res!(stdout().flush());
186 }
187 KeyCode::Down => {
188 if self.hist_ind < self.hist.len() - 1 {
189 self.hist_ind += 1;
190 input = self.hist[self.hist_ind].clone();
191 cursor_pos = input.len();
192 } else if self.hist_ind == self.hist.len() - 1 {
193 // Move to blank entry beyond history.
194 self.hist_ind = self.hist.len();
195 input.clear();
196 cursor_pos = 0;
197 } else {
198 continue;
199 }
200
201 res!(execute!(stdout(), Clear(ClearType::CurrentLine)));
202 print!("\r{} {}", self.cfg.norm_prompt, input);
203 res!(stdout().flush());
204 }
205 KeyCode::Backspace => {
206 if cursor_pos > 0 && !input.is_empty() {
207 // Correctly remove the character at the cursor position
208 input.remove(cursor_pos - 1);
209 cursor_pos -= 1;
210
211 // Move cursor back by one to reflect the character removal
212 res!(execute!(stdout(), cursor::MoveLeft(1)));
213
214 // Clear from the cursor to the end of the line to prepare for redraw
215 res!(execute!(stdout(), Clear(ClearType::UntilNewLine)));
216
217 // Print the rest of the string after the cursor
218 print!("{}", &input[cursor_pos..]);
219
220 // Move the cursor back to its correct position after redraw.
221 // This step might not be necessary if the cursor is at the end of the input,
222 // but it's required if the cursor is in the middle of the input.
223 let chars_to_move_left = input.len().saturating_sub(cursor_pos);
224 if chars_to_move_left > 0 {
225 res!(execute!(stdout(), cursor::MoveLeft(chars_to_move_left as u16)));
226 }
227
228 res!(stdout().flush());
229 }
230 }
231 KeyCode::Delete => {
232 if cursor_pos < input.len() {
233 // Remove the character right after the cursor position
234 input.remove(cursor_pos);
235
236 // Clear from the cursor to the end of the line to prepare for redraw
237 res!(execute!(stdout(), Clear(ClearType::UntilNewLine)));
238
239 // Print the rest of the string after the cursor
240 print!("{}", &input[cursor_pos..]);
241
242 // Move the cursor back to its correct position after redrawing.
243 // Calculate the number of characters to move left based on the length of the input
244 // minus the current cursor position, and then move the cursor left that many spaces.
245 let chars_to_move_left = input.len().saturating_sub(cursor_pos);
246 if chars_to_move_left > 0 {
247 res!(execute!(stdout(), cursor::MoveLeft(chars_to_move_left as u16)));
248 }
249
250 res!(stdout().flush());
251 }
252 }
253 _ => {}
254 }
255 }
256 }
257 }
258
259 pub fn start(&mut self) -> Outcome<()> {
260 println!("{}", self.cfg.greeting_msg);
261 let hist_path = Path::new(".").join(self.cfg.history_name.clone());
262 let mut hist_file = res!(OpenOptions::new()
263 .append(true)
264 .create(true)
265 .open(hist_path)
266 );
267
268 print!("{} ", self.cfg.norm_prompt);
269 res!(stdout().flush());
270
271 let mut exit_flag = false;
272
273 while let Some(input) = res!(self.read_line()) {
274 if !input.trim().is_empty() {
275 res!(writeln!(hist_file, "{}", input));
276 res!(hist_file.flush());
277
278 match self.context.eval(
279 &input,
280 &self.cfg,
281 &self.splitters,
282 ) {
283 Ok(evals) => for eval in evals {
284 match eval {
285 Evaluation::Output(s) => println!("{} {}", self.cfg.norm_prompt, s),
286 Evaluation::Error(s) => println!(
287 "{}{} {}{}",
288 Term::SET_BRIGHT_FORE_RED,
289 self.cfg.alert_prompt,
290 s,
291 Term::RESET,
292 ),
293 Evaluation::Exit => exit_flag = true,
294 _ => (),
295 }
296 }
297 Err(e) => println!("Error: {}", e),
298 }
299
300 self.hist.push(input);
301 self.hist_ind = self.hist.len();
302 if exit_flag {
303 break;
304 }
305 }
306
307 res!(execute!(stdout(), Clear(ClearType::CurrentLine)));
308 print!("\r{} ", self.cfg.norm_prompt);
309 res!(stdout().flush());
310 }
311
312 Ok(())
313 }
314}