1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use tui::{backend::Backend, layout::Rect, Frame, Terminal};

use super::remove_newlines;
use crate::theme::Theme;

/// Output of a process
pub struct ProcessOutput {
    pub message: Option<String>,
    pub output: Option<String>,
}

impl ProcessOutput {
    pub fn new(message: impl Into<String>, output: impl Into<String>) -> Self {
        Self {
            message: Some(message.into()),
            output: Some(output.into()),
        }
    }

    pub fn empty() -> Self {
        Self {
            message: None,
            output: None,
        }
    }

    pub fn message(message: impl Into<String>) -> Self {
        Self {
            message: Some(message.into()),
            output: None,
        }
    }

    pub fn output(output: impl Into<String>) -> Self {
        Self {
            output: Some(output.into()),
            message: None,
        }
    }
}

/// Context of an execution
#[derive(Clone, Copy)]
pub struct ExecutionContext {
    pub inline: bool,
    pub theme: Theme,
}

/// Trait to display a process on the shell
pub trait Process {
    /// Minimum height needed to render the process
    fn min_height(&self) -> usize;

    /// Peeks into the result to check wether the UI should be shown ([None]) or we can give a straight result
    /// ([Some])
    fn peek(&mut self) -> Result<Option<ProcessOutput>> {
        Ok(None)
    }

    /// Render `self` in the given area from the frame
    fn render<B: Backend>(&mut self, frame: &mut Frame<B>, area: Rect);

    /// Process raw user input event and return [Some] to end user interaction or [None] to keep waiting for user input
    fn process_raw_event(&mut self, event: Event) -> Result<Option<ProcessOutput>>;

    /// Run this process `render` and `process_event` until we've got an output
    fn show<B, F>(mut self, terminal: &mut Terminal<B>, mut area: F) -> Result<ProcessOutput>
    where
        B: Backend,
        F: FnMut(&Frame<B>) -> Rect,
        Self: Sized,
    {
        loop {
            // Draw UI
            terminal.draw(|f| {
                let area = area(f);
                self.render(f, area);
            })?;

            let event = event::read()?;
            if let Event::Key(k) = &event {
                // Ignore release & repeat events, we're only counting Press
                if k.kind != KeyEventKind::Press {
                    continue;
                }
                // Exit on Ctrl+C
                if let KeyCode::Char(c) = k.code {
                    if c == 'c' && k.modifiers.contains(KeyModifiers::CONTROL) {
                        return Ok(ProcessOutput::empty());
                    }
                }
            }

            // Process event
            if let Some(res) = self.process_raw_event(event)? {
                return Ok(res);
            }
        }
    }
}

/// Utility trait to implement an interactive process
pub trait InteractiveProcess: Process {
    /// Process user input event and return [Some] to end user interaction or [None] to keep waiting for user input
    fn process_event(&mut self, event: Event) -> Result<Option<ProcessOutput>> {
        match event {
            Event::Paste(content) => self.insert_text(remove_newlines(content))?,
            Event::Key(key) => {
                let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
                match key.code {
                    // `ctrl + d` - Delete
                    KeyCode::Char(c) if has_ctrl && c == 'd' => self.delete_current()?,
                    // `ctrl + u` | `ctrl + e` | F2 - Edit / Update
                    KeyCode::F(f) if f == 2 => {
                        // TODO edit - delegate to process?
                    }
                    KeyCode::Char(c) if has_ctrl && (c == 'e' || c == 'u') => {
                        // TODO edit
                    }
                    // Selection
                    KeyCode::Char(c) if has_ctrl && c == 'k' => self.prev(),
                    KeyCode::Char(c) if has_ctrl && c == 'j' => self.next(),
                    KeyCode::Up => self.move_up(),
                    KeyCode::Down => self.move_down(),
                    KeyCode::Right => self.move_right(),
                    KeyCode::Left => self.move_left(),
                    // Text edit
                    KeyCode::Char(c) => self.insert_char(c)?,
                    KeyCode::Backspace => self.delete_char(true)?,
                    KeyCode::Delete => self.delete_char(false)?,
                    // Control flow
                    KeyCode::Enter | KeyCode::Tab => return self.accept_current(),
                    KeyCode::Esc => return self.exit().map(Some),
                    _ => (),
                }
            }
            _ => (),
        };

        // Keep waiting for input
        Ok(None)
    }

    /// Moves the selection up
    fn move_up(&mut self);
    /// Moves the selection down
    fn move_down(&mut self);
    /// Moves the selection left
    fn move_left(&mut self);
    /// Moves the selection right
    fn move_right(&mut self);

    /// Moves the selection to the previous item
    fn prev(&mut self);
    /// Moves the selection to the next item
    fn next(&mut self);

    /// Inserts the given text into the currently selected input, if any
    fn insert_text(&mut self, text: String) -> Result<()>;
    /// Inserts the given char into the currently selected input, if any
    fn insert_char(&mut self, c: char) -> Result<()>;
    /// Removes a character from the currently selected input, if any
    fn delete_char(&mut self, backspace: bool) -> Result<()>;

    /// Deleted the currently selected item, if any
    fn delete_current(&mut self) -> Result<()>;
    /// Accepts the currently selected item, if any
    fn accept_current(&mut self) -> Result<Option<ProcessOutput>>;
    /// Exits with the current state
    fn exit(&mut self) -> Result<ProcessOutput>;
}