intelli_shell/common/
process.rs

1use anyhow::Result;
2use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
3use ratatui::{backend::Backend, layout::Rect, Frame, Terminal};
4
5use super::remove_newlines;
6use crate::theme::Theme;
7
8/// Output of a process
9pub struct ProcessOutput {
10    pub message: Option<String>,
11    pub output: Option<String>,
12}
13
14impl ProcessOutput {
15    pub fn new(message: impl Into<String>, output: impl Into<String>) -> Self {
16        Self {
17            message: Some(message.into()),
18            output: Some(output.into()),
19        }
20    }
21
22    pub fn empty() -> Self {
23        Self {
24            message: None,
25            output: None,
26        }
27    }
28
29    pub fn message(message: impl Into<String>) -> Self {
30        Self {
31            message: Some(message.into()),
32            output: None,
33        }
34    }
35
36    pub fn output(output: impl Into<String>) -> Self {
37        Self {
38            output: Some(output.into()),
39            message: None,
40        }
41    }
42}
43
44/// Context of an execution
45#[derive(Clone, Copy)]
46pub struct ExecutionContext {
47    pub inline: bool,
48    pub theme: Theme,
49}
50
51/// Trait to display a process on the shell
52pub trait Process {
53    /// Minimum height needed to render the process
54    fn min_height(&self) -> usize;
55
56    /// Peeks into the result to check wether the UI should be shown ([None]) or we can give a straight result
57    /// ([Some])
58    fn peek(&mut self) -> Result<Option<ProcessOutput>> {
59        Ok(None)
60    }
61
62    /// Render `self` in the given area from the frame
63    fn render<B: Backend>(&mut self, frame: &mut Frame<B>, area: Rect);
64
65    /// Process raw user input event and return [Some] to end user interaction or [None] to keep waiting for user input
66    fn process_raw_event(&mut self, event: Event) -> Result<Option<ProcessOutput>>;
67
68    /// Run this process `render` and `process_event` until we've got an output
69    fn show<B, F>(mut self, terminal: &mut Terminal<B>, mut area: F) -> Result<ProcessOutput>
70    where
71        B: Backend,
72        F: FnMut(&Frame<B>) -> Rect,
73        Self: Sized,
74    {
75        loop {
76            // Draw UI
77            terminal.draw(|f| {
78                let area = area(f);
79                self.render(f, area);
80            })?;
81
82            let event = event::read()?;
83            if let Event::Key(k) = &event {
84                // Ignore release & repeat events, we're only counting Press
85                if k.kind != KeyEventKind::Press {
86                    continue;
87                }
88                // Exit on Ctrl+C
89                if let KeyCode::Char(c) = k.code {
90                    if c == 'c' && k.modifiers.contains(KeyModifiers::CONTROL) {
91                        return Ok(ProcessOutput::empty());
92                    }
93                }
94            }
95
96            // Process event
97            if let Some(res) = self.process_raw_event(event)? {
98                return Ok(res);
99            }
100        }
101    }
102}
103
104/// Utility trait to implement an interactive process
105pub trait InteractiveProcess: Process {
106    /// Process user input event and return [Some] to end user interaction or [None] to keep waiting for user input
107    fn process_event(&mut self, event: Event) -> Result<Option<ProcessOutput>> {
108        match event {
109            Event::Paste(content) => self.insert_text(remove_newlines(content))?,
110            Event::Key(key) => {
111                let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
112                match key.code {
113                    // `ctrl + d` - Delete
114                    KeyCode::Char(c) if has_ctrl && c == 'd' => self.delete_current()?,
115                    // `ctrl + u` | `ctrl + e` | F2 - Edit / Update
116                    KeyCode::F(f) if f == 2 => self.edit_current()?,
117                    KeyCode::Char(c) if has_ctrl && (c == 'e' || c == 'u') => self.edit_current()?,
118                    // Selection
119                    KeyCode::Home => self.home(),
120                    KeyCode::End => self.end(),
121                    KeyCode::Char(c) if has_ctrl && c == 'k' => self.prev(),
122                    KeyCode::Char(c) if has_ctrl && c == 'j' => self.next(),
123                    KeyCode::Up => self.move_up(),
124                    KeyCode::Down => self.move_down(),
125                    KeyCode::Right => self.move_right(),
126                    KeyCode::Left => self.move_left(),
127                    // Text edit
128                    KeyCode::Char(c) => self.insert_char(c)?,
129                    KeyCode::Backspace => self.delete_char(true)?,
130                    KeyCode::Delete => self.delete_char(false)?,
131                    // Control flow
132                    KeyCode::Enter | KeyCode::Tab => return self.accept_current(),
133                    KeyCode::Esc => return self.exit().map(Some),
134                    _ => (),
135                }
136            }
137            _ => (),
138        };
139
140        // Keep waiting for input
141        Ok(None)
142    }
143
144    /// Moves the selection up
145    fn move_up(&mut self);
146    /// Moves the selection down
147    fn move_down(&mut self);
148    /// Moves the selection left
149    fn move_left(&mut self);
150    /// Moves the selection right
151    fn move_right(&mut self);
152
153    /// Moves the selection to the previous item
154    fn prev(&mut self);
155    /// Moves the selection to the next item
156    fn next(&mut self);
157
158    /// Home button, usually moving selection to the first
159    fn home(&mut self);
160    /// End button, usually moving selection to the last
161    fn end(&mut self);
162
163    /// Inserts the given text into the currently selected input, if any
164    fn insert_text(&mut self, text: String) -> Result<()>;
165    /// Inserts the given char into the currently selected input, if any
166    fn insert_char(&mut self, c: char) -> Result<()>;
167    /// Removes a character from the currently selected input, if any
168    fn delete_char(&mut self, backspace: bool) -> Result<()>;
169
170    /// Deletes the currently selected item, if any
171    fn delete_current(&mut self) -> Result<()>;
172    /// Edits the currently selected item, if any
173    fn edit_current(&mut self) -> Result<()>;
174    /// Accepts the currently selected item, if any
175    fn accept_current(&mut self) -> Result<Option<ProcessOutput>>;
176    /// Exits with the current state
177    fn exit(&mut self) -> Result<ProcessOutput>;
178}