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