| 1 | //! Every user-triggerable action and the key contexts they are bound in. |
| 2 | |
| 3 | use std::{fmt, str::FromStr}; |
| 4 | |
| 5 | use serde::{Deserialize, Deserializer, de}; |
| 6 | |
| 7 | macro_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 | |
| 54 | named_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 | |
| 74 | named_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 | |
| 151 | named_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 | |
| 164 | impl 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 | |
| 203 | impl 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 | ClearQueue => &[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 | 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)] |
| 306 | mod 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 | } |