Krolyxon
Commit b139f20

rmptuisrcaction.rs

323 lines · 11.4 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 CycleVisualizer = "cycle_visualizer",
137 ReloadLyrics = "reload_lyrics",
138 // Popups and text input
139 Close = "close",
140 Submit = "submit",
141 Cancel = "cancel",
142 Backspace = "backspace",
143 DeleteChar = "delete_char",
144 DeleteWord = "delete_word",
145 CursorLeft = "cursor_left",
146 CursorRight = "cursor_right",
147 CursorHome = "cursor_home",
148 CursorEnd = "cursor_end",
149 }
150}
151
152named_enum! {
153 /// The screens, in tab order.
154 pub enum ScreenId {
155 Queue = "queue",
156 Browser = "browser",
157 Playlists = "playlists",
158 Library = "library",
159 TagEditor = "tag_editor",
160 Visualizer = "visualizer",
161 Lyrics = "lyrics",
162 }
163}
164
165impl ScreenId {
166 pub fn title(self) -> &'static str {
167 match self {
168 Self::Queue => "Queue",
169 Self::Browser => "Browse",
170 Self::Playlists => "Playlists",
171 Self::Library => "Library",
172 Self::TagEditor => "Tags",
173 Self::Visualizer => "Visualizer",
174 Self::Lyrics => "Lyrics",
175 }
176 }
177
178 /// The key context that is active while the screen is focused.
179 pub fn context(self) -> Context {
180 match self {
181 Self::Queue => Context::Queue,
182 Self::Browser => Context::Browser,
183 Self::Playlists => Context::Playlists,
184 Self::Library => Context::Library,
185 Self::TagEditor => Context::TagEditor,
186 Self::Visualizer => Context::Visualizer,
187 Self::Lyrics => Context::Lyrics,
188 }
189 }
190
191 fn index(self) -> usize {
192 Self::ALL.iter().position(|&s| s == self).unwrap_or(0)
193 }
194
195 pub fn next(self) -> Self {
196 Self::ALL[(self.index() + 1) % Self::ALL.len()]
197 }
198
199 pub fn previous(self) -> Self {
200 Self::ALL[(self.index() + Self::ALL.len() - 1) % Self::ALL.len()]
201 }
202}
203
204impl Action {
205 /// The contexts this action may be bound in.
206 pub fn contexts(self) -> &'static [Context] {
207 use Action::*;
208 use Context as C;
209 match self {
210 Quit | Help | TogglePause | Pause | Stop | NextTrack | PreviousTrack | SeekForward
211 | SeekBackward | VolumeUp | VolumeDown | ToggleMute | ToggleRepeat | ToggleRandom
212 | ToggleSingle | ToggleConsume | UpdateDatabase | NextScreen | PreviousScreen
213 | ScreenQueue | ScreenBrowser | ScreenPlaylists | ScreenLibrary | ScreenTagEditor
214 | ScreenVisualizer | ScreenLyrics | ClearQueue => &[C::Global],
215 Up | Down | Top | Bottom | PageUp | PageDown | HalfPageUp | HalfPageDown | Left
216 | Right | Activate | ToggleMark | ClearMarks | Search | SearchNext | SearchPrevious
217 | AddToQueue | AddToPlaylist => &[C::Navigation],
218 Delete | MoveItemUp | MoveItemDown => &[C::Queue, C::Playlists],
219 ShuffleQueue | SaveQueue | JumpToCurrent | ToggleLyricsPane => &[C::Queue],
220 DeletePlaylist | RenamePlaylist => &[C::Playlists],
221 ToggleArtistTag => &[C::Library],
222 SaveTags | RevertTags => &[C::TagEditor],
223 CycleVisualizer => &[C::Visualizer],
224 ReloadLyrics => &[C::Lyrics],
225 Close => &[C::Popup],
226 Submit | Cancel | Backspace | DeleteChar | DeleteWord | CursorLeft | CursorRight
227 | CursorHome | CursorEnd => &[C::Input],
228 }
229 }
230
231 /// A short human-readable description for the help popup.
232 pub fn description(self) -> &'static str {
233 use Action::*;
234 match self {
235 Quit => "Quit rmptui",
236 Help => "Show key bindings",
237 TogglePause => "Toggle play/pause",
238 Pause => "Pause",
239 Stop => "Stop playback",
240 NextTrack => "Play next track",
241 PreviousTrack => "Play previous track",
242 SeekForward => "Seek forward",
243 SeekBackward => "Seek backward",
244 VolumeUp => "Increase volume",
245 VolumeDown => "Decrease volume",
246 ToggleMute => "Toggle mute",
247 ToggleRepeat => "Toggle repeat",
248 ToggleRandom => "Toggle random",
249 ToggleSingle => "Toggle single",
250 ToggleConsume => "Toggle consume",
251 UpdateDatabase => "Rescan the music database",
252 NextScreen => "Next screen",
253 PreviousScreen => "Previous screen",
254 ScreenQueue => "Show the queue",
255 ScreenBrowser => "Show the song browser",
256 ScreenPlaylists => "Show the playlist editor",
257 ScreenLibrary => "Show the media library",
258 ScreenTagEditor => "Show the tag editor",
259 ScreenVisualizer => "Show the visualizer",
260 ScreenLyrics => "Show the lyrics",
261 Up => "Move up",
262 Down => "Move down",
263 Top => "Go to top",
264 Bottom => "Go to bottom",
265 PageUp => "Page up",
266 PageDown => "Page down",
267 HalfPageUp => "Half page up",
268 HalfPageDown => "Half page down",
269 Left => "Go back / previous column",
270 Right => "Open / next column",
271 Activate => "Play or open the selected item",
272 ToggleMark => "Mark or unmark the item",
273 ClearMarks => "Clear all marks",
274 Search => "Search the current list",
275 SearchNext => "Next search match",
276 SearchPrevious => "Previous search match",
277 AddToQueue => "Add to (or toggle in) the queue",
278 AddToPlaylist => "Add to a playlist",
279 Delete => "Remove the item",
280 MoveItemUp => "Move the item up",
281 MoveItemDown => "Move the item down",
282 ClearQueue => "Clear the queue",
283 ShuffleQueue => "Shuffle the queue",
284 SaveQueue => "Save the queue as a playlist",
285 JumpToCurrent => "Jump to the playing song",
286 ToggleLyricsPane => "Toggle the lyrics pane",
287 DeletePlaylist => "Delete the playlist",
288 RenamePlaylist => "Rename the playlist",
289 ToggleArtistTag => "Switch between AlbumArtist and Artist",
290 SaveTags => "Write pending tag changes",
291 RevertTags => "Discard pending tag changes",
292 CycleVisualizer => "Switch to the next visualizer style",
293 ReloadLyrics => "Fetch lyrics again",
294 Close => "Close the popup",
295 Submit => "Submit",
296 Cancel => "Cancel",
297 Backspace => "Delete previous character",
298 DeleteChar => "Delete next character",
299 DeleteWord => "Delete previous word",
300 CursorLeft => "Cursor left",
301 CursorRight => "Cursor right",
302 CursorHome => "Cursor to start",
303 CursorEnd => "Cursor to end",
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn names_round_trip() {
314 for action in Action::ALL {
315 assert_eq!(action.name().parse::<Action>().unwrap(), *action);
316 assert!(!action.contexts().is_empty());
317 }
318 for context in Context::ALL {
319 assert_eq!(context.name().parse::<Context>().unwrap(), *context);
320 }
321 assert!("nope".parse::<Action>().is_err());
322 }
323}