Krolyxon
Commit 96dd350

rmptuisrcaction.rs

320 lines · 11.2 KB · Rust

1//! Every user-triggerable action and the key contexts they are bound in.
2
3use std::{fmt, str::FromStr};
4
5use serde::{Deserialize, Deserializer, de};
6
7macro_rules! named_enum {
8 (
9 $(#[$meta:meta])*
10 pub enum $name:ident { $($variant:ident = $str:literal),* $(,)? }
11 ) => {
12 $(#[$meta])*
13 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
14 pub enum $name { $($variant),* }
15
16 impl $name {
17 pub const ALL: &[Self] = &[$(Self::$variant),*];
18
19 pub fn name(self) -> &'static str {
20 match self { $(Self::$variant => $str),* }
21 }
22 }
23
24 impl fmt::Display for $name {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.write_str(self.name())
27 }
28 }
29
30 impl FromStr for $name {
31 type Err = String;
32
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 Self::ALL
35 .iter()
36 .copied()
37 .find(|v| v.name() == s)
38 .ok_or_else(|| {
39 let names: Vec<_> = Self::ALL.iter().map(|v| v.name()).collect();
40 format!("unknown value `{s}`, expected one of: {}", names.join(", "))
41 })
42 }
43 }
44
45 impl<'de> Deserialize<'de> for $name {
46 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
47 let s = String::deserialize(deserializer)?;
48 s.parse().map_err(de::Error::custom)
49 }
50 }
51 };
52}
53
54named_enum! {
55 /// A set of bindings that is active in a certain part of the UI.
56 ///
57 /// Lookups go from the most specific context (popup or screen) through
58 /// `navigation` to `global`, so specific bindings shadow general ones.
59 pub enum Context {
60 Global = "global",
61 Navigation = "navigation",
62 Queue = "queue",
63 Browser = "browser",
64 Playlists = "playlists",
65 Library = "library",
66 TagEditor = "tag_editor",
67 Visualizer = "visualizer",
68 Lyrics = "lyrics",
69 Popup = "popup",
70 Input = "input",
71 }
72}
73
74named_enum! {
75 pub enum Action {
76 // Global
77 Quit = "quit",
78 Help = "help",
79 TogglePause = "toggle_pause",
80 Pause = "pause",
81 Stop = "stop",
82 NextTrack = "next_track",
83 PreviousTrack = "previous_track",
84 SeekForward = "seek_forward",
85 SeekBackward = "seek_backward",
86 VolumeUp = "volume_up",
87 VolumeDown = "volume_down",
88 ToggleMute = "toggle_mute",
89 ToggleRepeat = "toggle_repeat",
90 ToggleRandom = "toggle_random",
91 ToggleSingle = "toggle_single",
92 ToggleConsume = "toggle_consume",
93 UpdateDatabase = "update_database",
94 NextScreen = "next_screen",
95 PreviousScreen = "previous_screen",
96 ScreenQueue = "screen_queue",
97 ScreenBrowser = "screen_browser",
98 ScreenPlaylists = "screen_playlists",
99 ScreenLibrary = "screen_library",
100 ScreenTagEditor = "screen_tag_editor",
101 ScreenVisualizer = "screen_visualizer",
102 ScreenLyrics = "screen_lyrics",
103 // Navigation (lists)
104 Up = "up",
105 Down = "down",
106 Top = "top",
107 Bottom = "bottom",
108 PageUp = "page_up",
109 PageDown = "page_down",
110 HalfPageUp = "half_page_up",
111 HalfPageDown = "half_page_down",
112 Left = "left",
113 Right = "right",
114 Activate = "activate",
115 ToggleMark = "toggle_mark",
116 ClearMarks = "clear_marks",
117 Search = "search",
118 SearchNext = "search_next",
119 SearchPrevious = "search_previous",
120 AddToQueue = "add_to_queue",
121 AddToPlaylist = "add_to_playlist",
122 // Screen specific
123 Delete = "delete",
124 MoveItemUp = "move_item_up",
125 MoveItemDown = "move_item_down",
126 ClearQueue = "clear_queue",
127 ShuffleQueue = "shuffle_queue",
128 SaveQueue = "save_queue",
129 JumpToCurrent = "jump_to_current",
130 ToggleLyricsPane = "toggle_lyrics_pane",
131 DeletePlaylist = "delete_playlist",
132 RenamePlaylist = "rename_playlist",
133 ToggleArtistTag = "toggle_artist_tag",
134 SaveTags = "save_tags",
135 RevertTags = "revert_tags",
136 ReloadLyrics = "reload_lyrics",
137 // Popups and text input
138 Close = "close",
139 Submit = "submit",
140 Cancel = "cancel",
141 Backspace = "backspace",
142 DeleteChar = "delete_char",
143 DeleteWord = "delete_word",
144 CursorLeft = "cursor_left",
145 CursorRight = "cursor_right",
146 CursorHome = "cursor_home",
147 CursorEnd = "cursor_end",
148 }
149}
150
151named_enum! {
152 /// The screens, in tab order.
153 pub enum ScreenId {
154 Queue = "queue",
155 Browser = "browser",
156 Playlists = "playlists",
157 Library = "library",
158 TagEditor = "tag_editor",
159 Visualizer = "visualizer",
160 Lyrics = "lyrics",
161 }
162}
163
164impl ScreenId {
165 pub fn title(self) -> &'static str {
166 match self {
167 Self::Queue => "Queue",
168 Self::Browser => "Browse",
169 Self::Playlists => "Playlists",
170 Self::Library => "Library",
171 Self::TagEditor => "Tags",
172 Self::Visualizer => "Visualizer",
173 Self::Lyrics => "Lyrics",
174 }
175 }
176
177 /// The key context that is active while the screen is focused.
178 pub fn context(self) -> Context {
179 match self {
180 Self::Queue => Context::Queue,
181 Self::Browser => Context::Browser,
182 Self::Playlists => Context::Playlists,
183 Self::Library => Context::Library,
184 Self::TagEditor => Context::TagEditor,
185 Self::Visualizer => Context::Visualizer,
186 Self::Lyrics => Context::Lyrics,
187 }
188 }
189
190 fn index(self) -> usize {
191 Self::ALL.iter().position(|&s| s == self).unwrap_or(0)
192 }
193
194 pub fn next(self) -> Self {
195 Self::ALL[(self.index() + 1) % Self::ALL.len()]
196 }
197
198 pub fn previous(self) -> Self {
199 Self::ALL[(self.index() + Self::ALL.len() - 1) % Self::ALL.len()]
200 }
201}
202
203impl Action {
204 /// The contexts this action may be bound in.
205 pub fn contexts(self) -> &'static [Context] {
206 use Action::*;
207 use Context as C;
208 match self {
209 Quit | Help | TogglePause | Pause | Stop | NextTrack | PreviousTrack | SeekForward
210 | SeekBackward | VolumeUp | VolumeDown | ToggleMute | ToggleRepeat | ToggleRandom
211 | ToggleSingle | ToggleConsume | UpdateDatabase | NextScreen | PreviousScreen
212 | ScreenQueue | ScreenBrowser | ScreenPlaylists | ScreenLibrary | ScreenTagEditor
213 | ScreenVisualizer | ScreenLyrics => &[C::Global],
214 Up | Down | Top | Bottom | PageUp | PageDown | HalfPageUp | HalfPageDown | Left
215 | Right | Activate | ToggleMark | ClearMarks | Search | SearchNext | SearchPrevious
216 | AddToQueue | AddToPlaylist => &[C::Navigation],
217 Delete | MoveItemUp | MoveItemDown => &[C::Queue, C::Playlists],
218 ClearQueue | ShuffleQueue | SaveQueue | JumpToCurrent | ToggleLyricsPane => &[C::Queue],
219 DeletePlaylist | RenamePlaylist => &[C::Playlists],
220 ToggleArtistTag => &[C::Library],
221 SaveTags | RevertTags => &[C::TagEditor],
222 ReloadLyrics => &[C::Lyrics],
223 Close => &[C::Popup],
224 Submit | Cancel | Backspace | DeleteChar | DeleteWord | CursorLeft | CursorRight
225 | CursorHome | CursorEnd => &[C::Input],
226 }
227 }
228
229 /// A short human-readable description for the help popup.
230 pub fn description(self) -> &'static str {
231 use Action::*;
232 match self {
233 Quit => "Quit rmptui",
234 Help => "Show key bindings",
235 TogglePause => "Toggle play/pause",
236 Pause => "Pause",
237 Stop => "Stop playback",
238 NextTrack => "Play next track",
239 PreviousTrack => "Play previous track",
240 SeekForward => "Seek forward",
241 SeekBackward => "Seek backward",
242 VolumeUp => "Increase volume",
243 VolumeDown => "Decrease volume",
244 ToggleMute => "Toggle mute",
245 ToggleRepeat => "Toggle repeat",
246 ToggleRandom => "Toggle random",
247 ToggleSingle => "Toggle single",
248 ToggleConsume => "Toggle consume",
249 UpdateDatabase => "Rescan the music database",
250 NextScreen => "Next screen",
251 PreviousScreen => "Previous screen",
252 ScreenQueue => "Show the queue",
253 ScreenBrowser => "Show the song browser",
254 ScreenPlaylists => "Show the playlist editor",
255 ScreenLibrary => "Show the media library",
256 ScreenTagEditor => "Show the tag editor",
257 ScreenVisualizer => "Show the visualizer",
258 ScreenLyrics => "Show the lyrics",
259 Up => "Move up",
260 Down => "Move down",
261 Top => "Go to top",
262 Bottom => "Go to bottom",
263 PageUp => "Page up",
264 PageDown => "Page down",
265 HalfPageUp => "Half page up",
266 HalfPageDown => "Half page down",
267 Left => "Go back / previous column",
268 Right => "Open / next column",
269 Activate => "Play or open the selected item",
270 ToggleMark => "Mark or unmark the item",
271 ClearMarks => "Clear all marks",
272 Search => "Search the current list",
273 SearchNext => "Next search match",
274 SearchPrevious => "Previous search match",
275 AddToQueue => "Add to (or toggle in) the queue",
276 AddToPlaylist => "Add to a playlist",
277 Delete => "Remove the item",
278 MoveItemUp => "Move the item up",
279 MoveItemDown => "Move the item down",
280 ClearQueue => "Clear the queue",
281 ShuffleQueue => "Shuffle the queue",
282 SaveQueue => "Save the queue as a playlist",
283 JumpToCurrent => "Jump to the playing song",
284 ToggleLyricsPane => "Toggle the lyrics pane",
285 DeletePlaylist => "Delete the playlist",
286 RenamePlaylist => "Rename the playlist",
287 ToggleArtistTag => "Switch between AlbumArtist and Artist",
288 SaveTags => "Write pending tag changes",
289 RevertTags => "Discard pending tag changes",
290 ReloadLyrics => "Fetch lyrics again",
291 Close => "Close the popup",
292 Submit => "Submit",
293 Cancel => "Cancel",
294 Backspace => "Delete previous character",
295 DeleteChar => "Delete next character",
296 DeleteWord => "Delete previous word",
297 CursorLeft => "Cursor left",
298 CursorRight => "Cursor right",
299 CursorHome => "Cursor to start",
300 CursorEnd => "Cursor to end",
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn names_round_trip() {
311 for action in Action::ALL {
312 assert_eq!(action.name().parse::<Action>().unwrap(), *action);
313 assert!(!action.contexts().is_empty());
314 }
315 for context in Context::ALL {
316 assert_eq!(context.name().parse::<Context>().unwrap(), *context);
317 }
318 assert!("nope".parse::<Action>().is_err());
319 }
320}