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 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}