1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8pub enum ShortcutCategory {
9 Navigation,
10 Notes,
11 TextEditing,
12 Other,
13}
14
15impl Display for ShortcutCategory {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 ShortcutCategory::Navigation => write!(f, "Navigation"),
19 ShortcutCategory::Notes => write!(f, "Notes"),
20 ShortcutCategory::TextEditing => write!(f, "Text Editing"),
21 ShortcutCategory::Other => write!(f, "Other"),
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
27#[serde(try_from = "String", into = "String")]
28pub enum ActionShortcuts {
29 Quit,
30 OpenPreferences,
31 SearchNotes,
32 OpenNote,
33 NewJournal,
34 Text(TextAction),
35 ToggleSidebar,
37 OpenFileBrowser,
38 FocusEditor,
39 FocusSidebar,
40 OpenSortDialog,
41 FileOperations,
43 FollowLink,
45 QuickNote,
47 YankRow,
52 ToggleQueryPanel,
54 OpenSavedSearches,
55 SaveCurrentQuery,
56 OpenAsk,
59 SwitchWorkspace,
61 FindInBuffer,
64 ReplaceInBuffer,
68 Leader,
71 OpenCommandPalette,
74}
75
76impl ActionShortcuts {
77 pub fn category(&self) -> ShortcutCategory {
78 match self {
79 ActionShortcuts::Leader
80 | ActionShortcuts::OpenCommandPalette
81 | ActionShortcuts::ToggleSidebar
82 | ActionShortcuts::OpenFileBrowser
83 | ActionShortcuts::FocusSidebar
84 | ActionShortcuts::FocusEditor
85 | ActionShortcuts::OpenSortDialog
86 | ActionShortcuts::ToggleQueryPanel
87 | ActionShortcuts::OpenSavedSearches
88 | ActionShortcuts::SaveCurrentQuery
89 | ActionShortcuts::SwitchWorkspace => ShortcutCategory::Navigation,
90
91 ActionShortcuts::SearchNotes
92 | ActionShortcuts::OpenNote
93 | ActionShortcuts::NewJournal
94 | ActionShortcuts::FileOperations
95 | ActionShortcuts::FollowLink
96 | ActionShortcuts::QuickNote
97 | ActionShortcuts::FindInBuffer
98 | ActionShortcuts::ReplaceInBuffer
99 | ActionShortcuts::YankRow
100 | ActionShortcuts::OpenAsk => ShortcutCategory::Notes,
101
102 ActionShortcuts::Text(_) => ShortcutCategory::TextEditing,
103
104 ActionShortcuts::Quit | ActionShortcuts::OpenPreferences => ShortcutCategory::Other,
105 }
106 }
107
108 pub fn label(&self) -> String {
109 match self {
110 ActionShortcuts::Quit => "Quit".into(),
111 ActionShortcuts::OpenPreferences => "Preferences".into(),
112 ActionShortcuts::SearchNotes => "Search notes".into(),
113 ActionShortcuts::OpenNote => "Open note".into(),
114 ActionShortcuts::NewJournal => "New journal entry".into(),
115 ActionShortcuts::ToggleSidebar => "Toggle drawer".into(),
116 ActionShortcuts::OpenFileBrowser => "Open file browser".into(),
117 ActionShortcuts::FocusEditor => "Focus right".into(),
118 ActionShortcuts::FocusSidebar => "Focus left".into(),
119 ActionShortcuts::OpenSortDialog => "Sort options".into(),
120 ActionShortcuts::FileOperations => "File operations".into(),
121 ActionShortcuts::FollowLink => "Follow link".into(),
122 ActionShortcuts::QuickNote => "Quick note".into(),
123 ActionShortcuts::YankRow => "Copy selected row".into(),
124 ActionShortcuts::ToggleQueryPanel => "Toggle query drawer".into(),
125 ActionShortcuts::OpenSavedSearches => "Saved searches".into(),
126 ActionShortcuts::OpenAsk => "Ask".into(),
127 ActionShortcuts::SaveCurrentQuery => "Save current query".into(),
128 ActionShortcuts::SwitchWorkspace => "Switch workspace".into(),
129 ActionShortcuts::FindInBuffer => "Find in note".into(),
130 ActionShortcuts::ReplaceInBuffer => "Replace in note".into(),
131 ActionShortcuts::Leader => "Leader menu".into(),
132 ActionShortcuts::OpenCommandPalette => "Command palette".into(),
133 ActionShortcuts::Text(ta) => match ta {
134 TextAction::Bold => "Bold".into(),
135 TextAction::Italic => "Italic".into(),
136 TextAction::Link => "Insert link".into(),
137 TextAction::Image => "Insert image".into(),
138 TextAction::ToggleHeader => "Toggle header".into(),
139 TextAction::Header(n) => format!("Header {n}"),
140 TextAction::Underline => "Underline".into(),
141 TextAction::Strikethrough => "Strikethrough".into(),
142 },
143 }
144 }
145}
146
147impl Display for ActionShortcuts {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 let action = match self {
150 ActionShortcuts::Quit => "Quit".to_string(),
151 ActionShortcuts::OpenPreferences => "OpenSettings".to_string(),
152 ActionShortcuts::SearchNotes => "SearchNotes".to_string(),
153 ActionShortcuts::OpenNote => "OpenNote".to_string(),
154 ActionShortcuts::NewJournal => "NewJournal".to_string(),
155 ActionShortcuts::Text(text_action) => format!("TextEditor-{}", text_action),
156 ActionShortcuts::ToggleSidebar => "ToggleSidebar".to_string(),
157 ActionShortcuts::OpenFileBrowser => "OpenFileBrowser".to_string(),
158 ActionShortcuts::FocusEditor => "FocusEditor".to_string(),
159 ActionShortcuts::FocusSidebar => "FocusSidebar".to_string(),
160 ActionShortcuts::OpenSortDialog => "OpenSortDialog".to_string(),
161 ActionShortcuts::FileOperations => "FileOperations".to_string(),
162 ActionShortcuts::FollowLink => "FollowLink".to_string(),
163 ActionShortcuts::QuickNote => "QuickNote".to_string(),
164 ActionShortcuts::YankRow => "YankRow".to_string(),
165 ActionShortcuts::ToggleQueryPanel => "ToggleQueryPanel".to_string(),
166 ActionShortcuts::OpenSavedSearches => "OpenSavedSearches".to_string(),
167 ActionShortcuts::OpenAsk => "OpenAsk".to_string(),
168 ActionShortcuts::SaveCurrentQuery => "SaveCurrentQuery".to_string(),
169 ActionShortcuts::SwitchWorkspace => "SwitchWorkspace".to_string(),
170 ActionShortcuts::FindInBuffer => "FindInBuffer".to_string(),
171 ActionShortcuts::ReplaceInBuffer => "ReplaceInBuffer".to_string(),
172 ActionShortcuts::Leader => "Leader".to_string(),
173 ActionShortcuts::OpenCommandPalette => "OpenCommandPalette".to_string(),
174 };
175 write!(f, "{}", action)
176 }
177}
178
179impl TryFrom<String> for ActionShortcuts {
180 type Error = String;
181
182 fn try_from(value: String) -> Result<Self, Self::Error> {
183 let action = match value.as_str() {
184 "Quit" => ActionShortcuts::Quit,
185 "OpenSettings" | "OpenPreferences" => ActionShortcuts::OpenPreferences,
188 "SearchNotes" => ActionShortcuts::SearchNotes,
189 "OpenNote" => ActionShortcuts::OpenNote,
190 "NewJournal" => ActionShortcuts::NewJournal,
191 "ToggleSidebar" => ActionShortcuts::ToggleSidebar,
192 "OpenFileBrowser" => ActionShortcuts::OpenFileBrowser,
193 "FocusEditor" => ActionShortcuts::FocusEditor,
194 "FocusSidebar" => ActionShortcuts::FocusSidebar,
195 "OpenSortDialog" => ActionShortcuts::OpenSortDialog,
196 "CycleSortField" => ActionShortcuts::OpenSortDialog,
197 "SortReverseOrder" => ActionShortcuts::OpenSortDialog,
198 "FileOperations" => ActionShortcuts::FileOperations,
199 "FollowLink" => ActionShortcuts::FollowLink,
200 "QuickNote" => ActionShortcuts::QuickNote,
201 "YankRow" => ActionShortcuts::YankRow,
202 "ToggleQueryPanel" => ActionShortcuts::ToggleQueryPanel,
203 "ToggleBacklinks" => ActionShortcuts::ToggleQueryPanel,
204 "OpenSavedSearches" => ActionShortcuts::OpenSavedSearches,
205 "OpenAsk" | "OpenRagAnswer" => ActionShortcuts::OpenAsk,
209 "SaveCurrentQuery" => ActionShortcuts::SaveCurrentQuery,
210 "SwitchWorkspace" => ActionShortcuts::SwitchWorkspace,
211 "FindInBuffer" => ActionShortcuts::FindInBuffer,
212 "ReplaceInBuffer" => ActionShortcuts::ReplaceInBuffer,
213 "Leader" => ActionShortcuts::Leader,
214 "OpenCommandPalette" => ActionShortcuts::OpenCommandPalette,
215 _ => {
216 if let Some(text_action) = value.strip_prefix("TextEditor-") {
217 match TextAction::try_from(text_action.to_string()) {
218 Ok(ta) => ActionShortcuts::Text(ta),
219 Err(e) => return Err(format!("Error extracting Text Action: {}", e)),
220 }
221 } else {
222 return Err(format!("Error, non valid Action: {}", value));
223 }
224 }
225 };
226 Ok(action)
227 }
228}
229
230impl From<ActionShortcuts> for String {
231 fn from(value: ActionShortcuts) -> Self {
232 value.to_string()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn shortcut_category_order() {
242 assert!(ShortcutCategory::Navigation < ShortcutCategory::Notes);
243 assert!(ShortcutCategory::Notes < ShortcutCategory::TextEditing);
244 assert!(ShortcutCategory::TextEditing < ShortcutCategory::Other);
245 }
246
247 #[test]
248 fn shortcut_category_display() {
249 assert_eq!(ShortcutCategory::Navigation.to_string(), "Navigation");
250 assert_eq!(ShortcutCategory::Notes.to_string(), "Notes");
251 assert_eq!(ShortcutCategory::TextEditing.to_string(), "Text Editing");
252 assert_eq!(ShortcutCategory::Other.to_string(), "Other");
253 }
254
255 #[test]
256 fn action_shortcuts_categories() {
257 assert_eq!(
258 ActionShortcuts::ToggleSidebar.category(),
259 ShortcutCategory::Navigation
260 );
261 assert_eq!(
262 ActionShortcuts::FocusSidebar.category(),
263 ShortcutCategory::Navigation
264 );
265 assert_eq!(
266 ActionShortcuts::FocusEditor.category(),
267 ShortcutCategory::Navigation
268 );
269 assert_eq!(
270 ActionShortcuts::OpenSortDialog.category(),
271 ShortcutCategory::Navigation
272 );
273 assert_eq!(
274 ActionShortcuts::ToggleQueryPanel.category(),
275 ShortcutCategory::Navigation
276 );
277 assert_eq!(
278 ActionShortcuts::OpenSavedSearches.category(),
279 ShortcutCategory::Navigation
280 );
281 assert_eq!(
282 ActionShortcuts::SaveCurrentQuery.category(),
283 ShortcutCategory::Navigation
284 );
285 assert_eq!(
286 ActionShortcuts::SwitchWorkspace.category(),
287 ShortcutCategory::Navigation
288 );
289
290 assert_eq!(
291 ActionShortcuts::SearchNotes.category(),
292 ShortcutCategory::Notes
293 );
294 assert_eq!(
295 ActionShortcuts::OpenNote.category(),
296 ShortcutCategory::Notes
297 );
298 assert_eq!(
299 ActionShortcuts::NewJournal.category(),
300 ShortcutCategory::Notes
301 );
302 assert_eq!(
303 ActionShortcuts::FileOperations.category(),
304 ShortcutCategory::Notes
305 );
306 assert_eq!(
307 ActionShortcuts::FollowLink.category(),
308 ShortcutCategory::Notes
309 );
310 assert_eq!(
311 ActionShortcuts::QuickNote.category(),
312 ShortcutCategory::Notes
313 );
314 assert_eq!(
315 ActionShortcuts::FindInBuffer.category(),
316 ShortcutCategory::Notes
317 );
318
319 assert_eq!(
320 ActionShortcuts::Text(TextAction::Bold).category(),
321 ShortcutCategory::TextEditing
322 );
323 assert_eq!(
324 ActionShortcuts::Text(TextAction::Header(2)).category(),
325 ShortcutCategory::TextEditing
326 );
327
328 assert_eq!(ActionShortcuts::Quit.category(), ShortcutCategory::Other);
329 assert_eq!(
330 ActionShortcuts::OpenPreferences.category(),
331 ShortcutCategory::Other
332 );
333 }
334
335 #[test]
336 fn action_shortcuts_labels() {
337 assert_eq!(ActionShortcuts::Quit.label(), "Quit");
338 assert_eq!(ActionShortcuts::OpenPreferences.label(), "Preferences");
339 assert_eq!(ActionShortcuts::SearchNotes.label(), "Search notes");
340 assert_eq!(ActionShortcuts::OpenNote.label(), "Open note");
341 assert_eq!(ActionShortcuts::NewJournal.label(), "New journal entry");
342 assert_eq!(ActionShortcuts::ToggleSidebar.label(), "Toggle drawer");
343 assert_eq!(
344 ActionShortcuts::OpenFileBrowser.label(),
345 "Open file browser"
346 );
347 assert_eq!(ActionShortcuts::FocusEditor.label(), "Focus right");
348 assert_eq!(ActionShortcuts::FocusSidebar.label(), "Focus left");
349 assert_eq!(ActionShortcuts::OpenSortDialog.label(), "Sort options");
350 assert_eq!(ActionShortcuts::FileOperations.label(), "File operations");
351 assert_eq!(ActionShortcuts::FollowLink.label(), "Follow link");
352 assert_eq!(ActionShortcuts::QuickNote.label(), "Quick note");
353 assert_eq!(
354 ActionShortcuts::ToggleQueryPanel.label(),
355 "Toggle query drawer"
356 );
357 assert_eq!(ActionShortcuts::OpenSavedSearches.label(), "Saved searches");
358 assert_eq!(
359 ActionShortcuts::SaveCurrentQuery.label(),
360 "Save current query"
361 );
362 assert_eq!(ActionShortcuts::SwitchWorkspace.label(), "Switch workspace");
363 assert_eq!(ActionShortcuts::FindInBuffer.label(), "Find in note");
364 assert_eq!(ActionShortcuts::Text(TextAction::Bold).label(), "Bold");
365 assert_eq!(ActionShortcuts::Text(TextAction::Italic).label(), "Italic");
366 assert_eq!(
367 ActionShortcuts::Text(TextAction::Link).label(),
368 "Insert link"
369 );
370 assert_eq!(
371 ActionShortcuts::Text(TextAction::Image).label(),
372 "Insert image"
373 );
374 assert_eq!(
375 ActionShortcuts::Text(TextAction::ToggleHeader).label(),
376 "Toggle header"
377 );
378 assert_eq!(
379 ActionShortcuts::Text(TextAction::Header(1)).label(),
380 "Header 1"
381 );
382 assert_eq!(
383 ActionShortcuts::Text(TextAction::Header(2)).label(),
384 "Header 2"
385 );
386 assert_eq!(
387 ActionShortcuts::Text(TextAction::Underline).label(),
388 "Underline"
389 );
390 assert_eq!(
391 ActionShortcuts::Text(TextAction::Strikethrough).label(),
392 "Strikethrough"
393 );
394 }
395
396 #[test]
397 fn file_operations_roundtrip() {
398 assert_eq!(
399 ActionShortcuts::FileOperations.to_string(),
400 "FileOperations"
401 );
402 assert_eq!(
403 ActionShortcuts::try_from("FileOperations".to_string()),
404 Ok(ActionShortcuts::FileOperations)
405 );
406 }
407
408 #[test]
409 fn open_file_browser_roundtrip() {
410 assert_eq!(
411 ActionShortcuts::OpenFileBrowser.to_string(),
412 "OpenFileBrowser"
413 );
414 assert_eq!(
415 ActionShortcuts::try_from("OpenFileBrowser".to_string()),
416 Ok(ActionShortcuts::OpenFileBrowser)
417 );
418 assert_eq!(
419 ActionShortcuts::OpenFileBrowser.category(),
420 ShortcutCategory::Navigation
421 );
422 }
423
424 #[test]
425 fn saved_search_actions_roundtrip() {
426 assert_eq!(
427 ActionShortcuts::ToggleQueryPanel.to_string(),
428 "ToggleQueryPanel"
429 );
430 assert_eq!(
431 ActionShortcuts::try_from("ToggleQueryPanel".to_string()),
432 Ok(ActionShortcuts::ToggleQueryPanel)
433 );
434 assert_eq!(
436 ActionShortcuts::try_from("ToggleBacklinks".to_string()),
437 Ok(ActionShortcuts::ToggleQueryPanel)
438 );
439 assert_eq!(
440 ActionShortcuts::try_from("OpenSavedSearches".to_string()),
441 Ok(ActionShortcuts::OpenSavedSearches)
442 );
443 assert_eq!(
444 ActionShortcuts::try_from("SaveCurrentQuery".to_string()),
445 Ok(ActionShortcuts::SaveCurrentQuery)
446 );
447 }
448
449 #[test]
450 fn open_ask_roundtrip_and_legacy_alias() {
451 assert_eq!(ActionShortcuts::OpenAsk.to_string(), "OpenAsk");
452 assert_eq!(
453 ActionShortcuts::try_from("OpenAsk".to_string()),
454 Ok(ActionShortcuts::OpenAsk)
455 );
456 assert_eq!(
458 ActionShortcuts::try_from("OpenRagAnswer".to_string()),
459 Ok(ActionShortcuts::OpenAsk)
460 );
461 }
462
463 #[test]
464 fn open_sort_dialog_roundtrip_and_legacy_alias() {
465 assert_eq!(
466 ActionShortcuts::OpenSortDialog.to_string(),
467 "OpenSortDialog"
468 );
469 assert_eq!(
470 ActionShortcuts::try_from("OpenSortDialog".to_string()),
471 Ok(ActionShortcuts::OpenSortDialog)
472 );
473 assert_eq!(
474 ActionShortcuts::try_from("CycleSortField".to_string()),
475 Ok(ActionShortcuts::OpenSortDialog)
476 );
477 assert_eq!(
478 ActionShortcuts::try_from("SortReverseOrder".to_string()),
479 Ok(ActionShortcuts::OpenSortDialog)
480 );
481 }
482}
483
484#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
485pub enum TextAction {
486 Bold,
487 Italic,
488 Link,
489 Image,
490 ToggleHeader,
491 Header(u8),
492 Underline,
493 Strikethrough,
494}
495
496impl Display for TextAction {
497 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498 let name = match self {
499 TextAction::Bold => "Bold".to_string(),
500 TextAction::Italic => "Italic".to_string(),
501 TextAction::Link => "Link".to_string(),
502 TextAction::Image => "Image".to_string(),
503 TextAction::ToggleHeader => "ToggleHeader".to_string(),
504 TextAction::Header(level) => format!("Header{}", level),
505 TextAction::Underline => "Underline".to_string(),
506 TextAction::Strikethrough => "Strikethrough".to_string(),
507 };
508 write!(f, "{}", name)
509 }
510}
511
512impl TryFrom<String> for TextAction {
513 type Error = String;
514
515 fn try_from(value: String) -> Result<Self, Self::Error> {
516 let action = match value.as_str() {
517 "Bold" => TextAction::Bold,
518 "Italic" => TextAction::Italic,
519 "Link" => TextAction::Link,
520 "Image" => TextAction::Image,
521 "ToggleHeader" => TextAction::ToggleHeader,
522 "Underline" => TextAction::Underline,
523 "Strikethrough" => TextAction::Strikethrough,
524 _ => {
525 if let Some(level) = value.strip_prefix("Header") {
526 match level.parse::<u8>() {
527 Ok(lvl) => TextAction::Header(lvl),
528 Err(e) => return Err(format!("Error parsing header level: {}", e)),
529 }
530 } else {
531 return Err(format!("Error, not valid Text Action: {}", value));
532 }
533 }
534 };
535 Ok(action)
536 }
537}