1use crossterm::{
2 terminal,
3 cursor,
4 ExecutableCommand,
5 QueueableCommand,
6 csi,
7 Command as ctCommand,
8};
9use std::error::Error;
10use std::fmt;
11use std::io;
12use std::io::Write;
13use rustyline::error::ReadlineError;
14use std::any::Any;
15use std::collections::HashMap;
16
17pub struct DynamicContext {
18 values: HashMap<String, Box<dyn Any>>,
19}
20
21impl DynamicContext {
22 pub fn new() -> Self {
23 DynamicContext {
24 values: HashMap::new(),
25 }
26 }
27
28 pub fn set<T: 'static>(&mut self, key: &str, value: T) {
29 self.values.insert(key.to_owned(), Box::new(value));
30 }
31
32 pub fn get<T: 'static>(&self, key: &str) -> Option<&T> {
33 self.values
34 .get(key)
35 .and_then(|value| value.downcast_ref::<T>())
36 }
37
38 pub fn get_mut<T: 'static>(&mut self, key: &str) -> Option<&mut T> {
39 self.values
40 .get_mut(key)
41 .and_then(|value| value.downcast_mut::<T>())
42 }
43}
44
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47struct SetScrollingRegion(pub u16, pub u16);
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct SetScrollingAll();
51
52#[derive(Debug)]
53pub enum CrosstermError {
54 UnimplementedInWindows,
55}
56
57impl std::error::Error for CrosstermError {}
58
59impl fmt::Display for CrosstermError {
60 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61 match self {
62 CrosstermError::UnimplementedInWindows => write!(f,
63 "This command is unimplemented for Windows"),
64 }
65 }
66}
67
68
69impl ctCommand for SetScrollingRegion {
72 fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
73 write!(f, csi!("{};{}r"), self.0, self.1)
74 }
75
76 #[cfg(windows)]
77 fn execute_winapi(&self) -> Result<(), CrosstermError> {
78 Err(CrosstermError::UnimplementedInWindows)
79 }
80}
81
82impl ctCommand for SetScrollingAll {
86 fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
87 write!(f, csi!("r"))
88 }
89
90 #[cfg(windows)]
91 fn execute_winapi(&self) -> Result<(), CrosstermError> {
92 Err(CrosstermError::UnimplementedInWindows)
93 }
94}
95
96
97pub fn command_loop(commands: &Vec<Command>, context: &mut DynamicContext) -> Result<(), Box<dyn Error>>{
101 setup_screen()?;
102 let default= String::from(">> ");
103
104 println!("info: type 'help' to for a list of commands");
105 let help_str = build_help_str(&commands);
106 loop { if let Err(err) = setup_screen(){
108 eprintln!("error during screen setup: {}", err.to_string());
109 }
110 let mut rl = rustyline::Editor::<()>::new().unwrap();
111 let prompt = context.get::<String>("prompt").unwrap_or(&default);
112 match rl.readline(prompt){
113 Ok(line)=>{
114 if line.is_empty(){continue}
115
116 let mut input_split = line.split(' ').collect::<Vec<_>>(); let input_command = input_split.remove(0);
119 let input_args = &input_split;
120
121 if input_command.eq("help") || input_command.eq("?") {
123 write_output(help_str.clone(), None)?;
124 continue;
125 }
126 if input_command.eq("exit") {
127 break;
128 }
129
130 for cmd in commands.into_iter().filter(|cmd| cmd.command.eq(input_command)) {
131 let output = (cmd.func)(&input_args, context);
132 match output {
133 Err(err) => eprintln!("error executing '{}': {}", input_command, err.to_string()),
134 Ok(output_str) => write_output(output_str, None).expect("error writing output"),
135 }
136 }
137 },
138 Err(ReadlineError::Interrupted) => std::process::exit(0),
139 Err(err)=>{
140 eprintln!("error during readline: {}",err.to_string());
141 break;
142 }
143 }
144 }
145
146 Ok(())
147}
148
149fn build_help_str(commands: &Vec<Command>) -> String {
150 let mut help_output = String::from("---help output------------\n");
151 commands.into_iter().for_each(|cmd| help_output.push_str(&format!("{}\n", cmd.help_output)));
152 help_output.push_str("exit - exit the current prompt");
153 help_output
154}
155
156
157pub fn write_output(output: String, prefix: Option<String>)->Result<(),Box<dyn Error>>{
162 let mut sout = io::stdout().lock();
163
164 let size = crossterm::terminal::size()?;
166 let stdout_end = size.1-1;
167 let mut final_output = String::new();
168
169 if let Some(line) = prefix {
171 final_output.push_str(line.as_str());
172 final_output.push_str(": ");
173 }
174
175 final_output.push_str(output.as_str());
176
177 sout.queue(cursor::SavePosition)?;
178
179 sout.queue(SetScrollingRegion(1,stdout_end))?
182 .queue(terminal::ScrollUp(1))?
183 .queue(cursor::MoveTo(0, stdout_end-1))?; print!("{}", final_output);
186 sout.queue(SetScrollingAll())?
187 .queue(cursor::RestorePosition)?;
188 sout.flush()?;
189 Ok(())
190}
191
192pub fn setup_screen()->Result<(),Box<dyn Error>>{
195 let size = crossterm::terminal::size()?;
196 let mut sout = std::io::stdout().lock();
197 sout.execute(cursor::MoveToRow(size.1))?;
198 Ok(())
199}
200
201pub struct Command<'r> {
202 pub command: &'r str,
203 pub func: fn(&[&str], &mut DynamicContext)->Result<String, Box<dyn Error>>,
204 pub help_output: &'r str,
205}