Krolyxon
Commit 3de1a21

rmptuisrcaction.rs

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