tui_clap/
lib.rs

1use clap::{App, ArgMatches, ErrorKind};
2use crossterm::event::{poll, read, Event, KeyCode};
3use std::borrow::BorrowMut;
4use std::cmp::{max, min};
5use std::str::Lines;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::mpsc::{RecvError, TryRecvError};
8use std::sync::{mpsc, Arc};
9use std::thread;
10use std::time::Duration;
11use tui::backend::Backend;
12use tui::buffer::Buffer;
13use tui::layout::Rect;
14use tui::style::Style;
15use tui::widgets::{StatefulWidget, Widget};
16use tui::Frame;
17
18/// Helper struct to read from `crossterm`'s input events
19pub struct Events {
20    rx: mpsc::Receiver<Event>,
21    ignore_exit_key: Arc<AtomicBool>,
22}
23
24/// The command input widget itself
25#[derive(Default, Clone)]
26pub struct CommandInput {
27    prompt: String,
28}
29
30#[derive(Default)]
31pub struct CommandInputState {
32    history: Vec<String>,
33    index_of_history: usize,
34    content: String,
35}
36
37#[derive(Default, Clone)]
38pub struct CommandOutput {}
39
40#[derive(Default)]
41pub struct CommandOutputState {
42    history: Vec<String>,
43}
44
45impl CommandInputState {
46    pub fn add_char(&mut self, c: char) {
47        self.content.push(c);
48    }
49
50    pub fn del_char(&mut self) {
51        self.content.pop();
52    }
53
54    pub fn reset(&mut self) {
55        self.content.drain(..);
56    }
57
58    pub fn enter(&mut self) -> String {
59        let command = self.content.clone();
60        self.history.push(command.clone());
61        self.reset();
62
63        command
64    }
65
66    pub fn back_in_history(&mut self) {
67        if self.history.is_empty() {
68            return;
69        }
70
71        self.index_of_history = min(self.index_of_history + 1, self.history.len() - 1);
72
73        self.content = self.history[self.index_of_history].clone();
74    }
75
76    pub fn forward_in_history(&mut self) {
77        if self.history.is_empty() {
78            return;
79        }
80
81        self.index_of_history = max(self.index_of_history - 1, 0);
82
83        self.content = self.history[self.index_of_history].clone();
84    }
85}
86
87impl CommandInput {
88    pub fn prompt(&mut self, prompt: &str) {
89        self.prompt = prompt.to_string();
90    }
91}
92
93impl StatefulWidget for CommandInput {
94    type State = CommandInputState;
95
96    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
97        buf.set_string(area.left(), area.top(), &self.prompt, Style::default());
98        buf.set_string(
99            area.left() + self.prompt.len() as u16,
100            area.top(),
101            &state.content,
102            Style::default(),
103        );
104    }
105}
106
107impl Widget for CommandInput {
108    fn render(self, area: Rect, buf: &mut Buffer) {
109        StatefulWidget::render(self, area, buf, &mut CommandInputState::default())
110    }
111}
112
113impl StatefulWidget for CommandOutput {
114    type State = CommandOutputState;
115
116    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
117        let max_lines = area.height - 1;
118        let max_chars_per_line = area.width - 1;
119
120        let mut lines_to_render: Vec<&str> = vec![];
121
122        let history_to_show = state.history.iter().rev().take(max_lines as usize).rev();
123        let mut y = 0;
124        for line in history_to_show {
125            if line.len() > max_chars_per_line as usize {
126                let mut rest_of_line = line.as_str();
127                loop {
128                    if rest_of_line.len() > max_chars_per_line as usize {
129                        let split_line = rest_of_line.split_at(max_chars_per_line as usize);
130                        lines_to_render.push(split_line.0);
131                        rest_of_line = split_line.1;
132                    } else {
133                        lines_to_render.push(rest_of_line);
134                        break;
135                    }
136                }
137            } else {
138                lines_to_render.push(line);
139            }
140        }
141
142        for line in lines_to_render.iter().rev().take(max_lines as usize).rev() {
143            buf.set_string(area.left(), area.top() + y, line, Style::default());
144            y += 1;
145        }
146    }
147}
148
149impl Widget for CommandOutput {
150    fn render(self, area: Rect, buf: &mut Buffer) {
151        StatefulWidget::render(self, area, buf, &mut CommandOutputState::default())
152    }
153}
154
155#[derive(Debug, Clone, Copy)]
156pub struct Config {
157    pub exit_key: KeyCode,
158    pub tick_rate: Duration,
159}
160
161impl Default for Config {
162    fn default() -> Config {
163        Config {
164            exit_key: KeyCode::Char('q'),
165            tick_rate: Duration::from_millis(250),
166        }
167    }
168}
169
170impl Default for Events {
171    fn default() -> Self {
172        Events::from_config(Config::default())
173    }
174}
175
176impl Events {
177    /// Creates an `Events` instance from `Config` and starts a thread to listen on `crossterm` input events
178    pub fn from_config(config: Config) -> Events {
179        let (tx, rx) = mpsc::channel();
180        let ignore_exit_key = Arc::new(AtomicBool::new(false));
181        {
182            let ignore_exit_key = ignore_exit_key.clone();
183            thread::spawn(move || loop {
184                if let Ok(b) = poll(config.tick_rate) {
185                    if !b {
186                        continue;
187                    }
188                    let read = read();
189                    if let Ok(event) = read {
190                        if let Err(err) = tx.send(event) {
191                            eprintln!("{}", err);
192                            return;
193                        }
194                        if !ignore_exit_key.load(Ordering::Relaxed) {
195                            if let Event::Key(key) = event {
196                                if key.code == config.exit_key {
197                                    return;
198                                }
199                            }
200                        }
201                    }
202                }
203            })
204        };
205        Events {
206            rx,
207            ignore_exit_key,
208        }
209    }
210
211    /// Checks if there was a new event to read from.
212    /// Returns `Some(Event)` if there was some, `None` if not and `Result::Err` if the connection was disconnected.
213    pub fn next(&self) -> Result<Option<Event>, mpsc::RecvError> {
214        match self.rx.try_recv() {
215            Ok(event) => Ok(Some(event)),
216            Err(err) => match err {
217                TryRecvError::Empty => Ok(None),
218                TryRecvError::Disconnected => Err(RecvError {}),
219            },
220        }
221    }
222
223    pub fn disable_exit_key(&mut self) {
224        self.ignore_exit_key.store(true, Ordering::Relaxed);
225    }
226
227    pub fn enable_exit_key(&mut self) {
228        self.ignore_exit_key.store(false, Ordering::Relaxed);
229    }
230}
231
232/// A struct holding widgets for input and output for interaction with a `clap:App`
233pub struct TuiClap<'a> {
234    command_input_state: CommandInputState,
235    command_output_state: CommandOutputState,
236    command_input_widget: CommandInput,
237    command_output_widget: CommandOutput,
238    clap: App<'a>
239}
240
241impl TuiClap<'_> {
242    /// Creates a `TuiClap` struct from a `clap:App`
243    pub fn from_app<'a>(
244        app: App<'a>,
245    ) -> TuiClap {
246        TuiClap {
247            command_input_state: CommandInputState::default(),
248            command_output_state: CommandOutputState::default(),
249            command_input_widget: Default::default(),
250            command_output_widget: Default::default(),
251            clap: app,
252        }
253    }
254
255    /// Write `string` to the output widget
256    pub fn write_to_output(&mut self, string: String) {
257        let lines: Lines = string.lines();
258        for str in lines {
259            self.command_output_state.history.push(str.to_string());
260        }
261    }
262
263    /// Access the input widget's state
264    pub fn state(&mut self) -> &mut CommandInputState {
265        self.command_input_state.borrow_mut()
266    }
267
268    /// Parses the current content of the input widget, resets it and returns the matches if successful.
269    /// If the command was not matched by clap, the error will be written to the output widget and a `Result::Err` is returned.
270    pub fn parse(&mut self) -> Result<ArgMatches, ()> {
271        let content = self.command_input_state.content.clone();
272        self.state().enter();
273
274        let commands_vec = content.split(' ').collect::<Vec<&str>>();
275        let matches_result = self.clap.try_get_matches_from_mut(commands_vec.clone());
276
277        match matches_result {
278            Ok(matches) => Ok(matches),
279            Err(err) => match err.kind {
280                ErrorKind::DisplayHelp => {
281                    let mut buf = Vec::new();
282                    let mut writer = Box::new(&mut buf);
283                    self.clap
284                        .write_help(&mut writer)
285                        .expect("Could not write help");
286                    self.write_to_output(std::str::from_utf8(buf.as_slice()).unwrap().to_string());
287                    Err(())
288                }
289                ErrorKind::DisplayVersion => {
290                    self.write_to_output(self.clap.render_long_version());
291                    Err(())
292                }
293                ErrorKind::Format => {
294                    Err(())
295                }
296                _ => {
297                    self.write_to_output(format!("error: {}", err));
298                    Err(())
299                },
300            },
301        }
302    }
303
304    /// Access the input widget
305    pub fn input_widget(&mut self) -> &mut CommandInput {
306        self.command_input_widget.borrow_mut()
307    }
308
309    /// Render the input widget on `tui:Frame`
310    pub fn render_input<B: Backend>(&mut self, frame: &mut Frame<B>, area: Rect) {
311        frame.render_stateful_widget(
312            self.command_input_widget.clone(),
313            area,
314            self.command_input_state.borrow_mut(),
315        );
316    }
317
318    /// Access the output widget
319    pub fn output_widget(&mut self) -> &mut CommandOutput {
320        self.command_output_widget.borrow_mut()
321    }
322
323    /// Render the output widget on `tui:Frame`
324    pub fn render_output<B: Backend>(&mut self, frame: &mut Frame<B>, area: Rect) {
325        frame.render_stateful_widget(
326            self.command_output_widget.clone(),
327            area,
328            self.command_output_state.borrow_mut(),
329        );
330    }
331}