glues_tui/
context.rs

1pub mod entry;
2pub mod notebook;
3pub mod theme_selector;
4
5use theme_selector::ThemeSelector;
6use {
7    crate::{
8        Action,
9        config::{self, LAST_THEME},
10        input::{Input, KeyCode, KeyEvent, to_textarea_input},
11        log,
12        logger::*,
13        theme::{self, THEME},
14    },
15    glues_core::transition::VimKeymapKind,
16    ratatui::{
17        style::Style,
18        text::Line,
19        widgets::{Block, Borders},
20    },
21    std::time::SystemTime,
22    tui_textarea::TextArea,
23};
24pub use {entry::EntryContext, notebook::NotebookContext};
25
26pub enum ContextState {
27    Entry,
28    Notebook,
29}
30
31pub struct ContextPrompt {
32    pub widget: TextArea<'static>,
33    pub message: Vec<Line<'static>>,
34    pub action: Action,
35}
36
37impl ContextPrompt {
38    pub fn new(message: Vec<Line<'static>>, action: Action, default: Option<String>) -> Self {
39        Self::with_mask(message, action, default, None)
40    }
41
42    pub fn new_masked(
43        message: Vec<Line<'static>>,
44        action: Action,
45        default: Option<String>,
46        mask_char: char,
47    ) -> Self {
48        Self::with_mask(message, action, default, Some(mask_char))
49    }
50
51    fn with_mask(
52        message: Vec<Line<'static>>,
53        action: Action,
54        default: Option<String>,
55        mask: Option<char>,
56    ) -> Self {
57        let mut widget = TextArea::new(vec![default.unwrap_or_default()]);
58        if let Some(mask_char) = mask {
59            widget.set_mask_char(mask_char);
60        }
61        widget.set_cursor_style(Style::default().fg(THEME.accent_text).bg(THEME.accent));
62        widget.set_block(
63            Block::default()
64                .border_style(Style::default())
65                .borders(Borders::ALL),
66        );
67        Self {
68            widget,
69            message,
70            action,
71        }
72    }
73}
74
75pub struct QuitMenu {
76    pub message: String,
77    pub quit_action: Action,
78    pub menu_action: Action,
79}
80
81impl QuitMenu {
82    pub fn new(message: impl Into<String>, quit_action: Action, menu_action: Action) -> Self {
83        Self {
84            message: message.into(),
85            quit_action,
86            menu_action,
87        }
88    }
89}
90
91pub struct Context {
92    pub entry: EntryContext,
93    pub notebook: NotebookContext,
94
95    pub state: ContextState,
96
97    pub quit_menu: Option<QuitMenu>,
98    pub confirm: Option<(String, Action)>,
99    pub alert: Option<String>,
100    pub prompt: Option<ContextPrompt>,
101    pub theme_selector: Option<ThemeSelector>,
102    pub last_log: Option<(String, SystemTime)>,
103
104    pub help: bool,
105    pub editor_keymap: bool,
106    pub vim_keymap: Option<VimKeymapKind>,
107
108    pub keymap: bool,
109}
110
111impl Default for Context {
112    fn default() -> Self {
113        Self {
114            entry: EntryContext::default(),
115            notebook: NotebookContext::default(),
116
117            state: ContextState::Entry,
118            quit_menu: None,
119            confirm: None,
120            alert: None,
121            prompt: None,
122            theme_selector: None,
123            last_log: None,
124
125            help: false,
126            editor_keymap: false,
127            vim_keymap: None,
128
129            keymap: false,
130        }
131    }
132}
133
134impl Context {
135    pub fn take_prompt_input(&mut self) -> Option<String> {
136        self.prompt
137            .take()?
138            .widget
139            .lines()
140            .first()
141            .map(ToOwned::to_owned)
142    }
143
144    pub async fn consume(&mut self, input: &Input) -> Action {
145        if self.vim_keymap.is_some() {
146            self.vim_keymap = None;
147            return Action::None;
148        } else if self.editor_keymap {
149            self.editor_keymap = false;
150            return Action::None;
151        } else if self.help {
152            self.help = false;
153            return Action::None;
154        } else if self.alert.is_some() {
155            // any key pressed will close the alert
156            self.alert = None;
157            return Action::None;
158        } else if self.quit_menu.is_some() {
159            let code = match input {
160                Input::Key(key) => key.code,
161                _ => return Action::None,
162            };
163
164            match code {
165                #[cfg(not(target_arch = "wasm32"))]
166                KeyCode::Char('q') => {
167                    let menu = self.quit_menu.take().log_expect("quit menu must be some");
168                    return menu.quit_action;
169                }
170                KeyCode::Char('m') => {
171                    let menu = self.quit_menu.take().log_expect("quit menu must be some");
172                    return menu.menu_action;
173                }
174                KeyCode::Esc => {
175                    self.quit_menu = None;
176                    return Action::None;
177                }
178                _ => return Action::None,
179            }
180        } else if self.confirm.is_some() {
181            let code = match input {
182                Input::Key(key) => key.code,
183                _ => return Action::None,
184            };
185
186            match code {
187                KeyCode::Char('y') => {
188                    let (_, action) = self.confirm.take().log_expect("confirm must be some");
189                    log!("Context::consume - remove note!!!");
190                    return action;
191                }
192                KeyCode::Char('n') => {
193                    self.confirm = None;
194                    return Action::None;
195                }
196                _ => return Action::None,
197            }
198        } else if let Some(selector) = self.theme_selector.as_mut() {
199            let key = match input {
200                Input::Key(key) => key,
201                _ => return Action::None,
202            };
203
204            match key.code {
205                KeyCode::Char('j') | KeyCode::Down => {
206                    selector.select_next();
207                    return Action::None;
208                }
209                KeyCode::Char('k') | KeyCode::Up => {
210                    selector.select_previous();
211                    return Action::None;
212                }
213                KeyCode::Enter => {
214                    let preset = selector.selected();
215                    theme::set_theme(preset.id);
216                    config::update(LAST_THEME, preset.id.as_str()).await;
217                    self.theme_selector = None;
218                    return Action::None;
219                }
220                KeyCode::Esc => {
221                    self.theme_selector = None;
222                    return Action::None;
223                }
224                KeyCode::Char(char) => {
225                    if let Some(preset) = selector.select_by_key(char) {
226                        theme::set_theme(preset.id);
227                        config::update(LAST_THEME, preset.id.as_str()).await;
228                        self.theme_selector = None;
229                    }
230                    return Action::None;
231                }
232                _ => return Action::None,
233            }
234        } else if let Some(prompt) = self.prompt.as_ref() {
235            match input {
236                Input::Key(KeyEvent {
237                    code: KeyCode::Enter,
238                    ..
239                }) => {
240                    return prompt.action.clone();
241                }
242                Input::Key(KeyEvent {
243                    code: KeyCode::Esc, ..
244                }) => {
245                    self.prompt = None;
246                    return Action::None;
247                }
248                _ => {
249                    if let Some(text_input) = to_textarea_input(input) {
250                        self.prompt
251                            .as_mut()
252                            .log_expect("prompt must be some")
253                            .widget
254                            .input(text_input);
255                    }
256
257                    return Action::None;
258                }
259            }
260        }
261
262        match self.state {
263            ContextState::Entry => match input {
264                Input::Key(key) => self.entry.consume(key.code).await,
265                _ => Action::None,
266            },
267            ContextState::Notebook => self.notebook.consume(input),
268        }
269    }
270}