diff --git a/youtui/src/app/ui/action.rs b/youtui/src/app/ui/action.rs index 6d71633..9df6148 100644 --- a/youtui/src/app/ui/action.rs +++ b/youtui/src/app/ui/action.rs @@ -1,3 +1,5 @@ +use crate::app::component::actionhandler::Action; + #[derive(Clone, PartialEq, Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AppAction { diff --git a/youtui/src/app/ui/browser.rs b/youtui/src/app/ui/browser.rs index def1fee..025d332 100644 --- a/youtui/src/app/ui/browser.rs +++ b/youtui/src/app/ui/browser.rs @@ -2,28 +2,25 @@ use self::{ artistalbums::{albumsongs::AlbumSongsPanel, artistsearch::ArtistSearchPanel}, draw::draw_browser, }; -use super::AppCallback; +use super::{action::AppAction, AppCallback, WindowContext}; +use crate::app::{ + component::actionhandler::{ + Action, Component, ComponentEffect, DominantKeyRouter, KeyRouter, Suggestable, TextHandler, + }, + server::{ + api::GetArtistSongsProgressUpdate, ArcServer, GetArtistSongs, SearchArtists, TaskMetadata, + }, + structures::{ListStatus, SongListComponent}, + view::{DrawableMut, Scrollable}, +}; use crate::{ app::{component::actionhandler::DynKeybindsIter, keycommand::KeyCommand}, - config::{Config, KeyEnum, KeyEnumKey}, + config::Config, core::send_or_error, }; -use crate::{ - app::{ - component::actionhandler::{ - Component, ComponentEffect, DominantKeyRouter, KeyRouter, Suggestable, TextHandler, - }, - server::{ - api::GetArtistSongsProgressUpdate, ArcServer, GetArtistSongs, SearchArtists, - TaskMetadata, - }, - structures::{ListStatus, SongListComponent}, - view::{DrawableMut, Scrollable}, - }, - config::AppAction, -}; use async_callback_manager::{AsyncTask, Constraint}; use itertools::Either; +use serde::{Deserialize, Serialize}; use std::{iter::Iterator, mem}; use tokio::sync::mpsc; use tracing::error; @@ -52,6 +49,52 @@ pub struct Browser { keybinds: Vec>, } +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BrowserAction { + ViewPlaylist, + Search, + Left, + Right, +} + +impl Action for BrowserAction { + type State = Browser; + fn context(&self) -> std::borrow::Cow { + "Browser".into() + } + fn describe(&self) -> std::borrow::Cow { + match self { + BrowserAction::ViewPlaylist => "View Playlist", + BrowserAction::Search => "Toggle Search", + BrowserAction::Left => "Left", + BrowserAction::Right => "Right", + } + .into() + } + async fn apply( + self, + state: &mut Self::State, + ) -> crate::app::component::actionhandler::ComponentEffect + where + Self: Sized, + { + match self { + BrowserAction::Left => state.left(), + BrowserAction::Right => state.right(), + BrowserAction::ViewPlaylist => { + send_or_error( + &state.callback_tx, + AppCallback::ChangeContext(WindowContext::Playlist), + ) + .await + } + BrowserAction::Search => state.handle_toggle_search(), + } + AsyncTask::new_no_op() + } +} + impl InputRouting { pub fn left(&self) -> Self { match self { diff --git a/youtui/src/config.rs b/youtui/src/config.rs index aff2147..6e335c5 100644 --- a/youtui/src/config.rs +++ b/youtui/src/config.rs @@ -12,6 +12,8 @@ use crate::get_config_dir; use crate::Result; use async_callback_manager::AsyncTask; use clap::ValueEnum; +use keybinds::YoutuiKeymap; +use keybinds::YoutuiModeNames; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::Infallible; @@ -20,6 +22,8 @@ use ytmapi_rs::auth::OAuthToken; const CONFIG_FILE_NAME: &str = "config.toml"; +pub mod keybinds; + #[derive(Serialize, Deserialize)] pub enum ApiKey { OAuthToken(OAuthToken), @@ -37,6 +41,14 @@ impl std::fmt::Debug for ApiKey { } } +#[derive(ValueEnum, Copy, Clone, Default, Debug, Serialize, Deserialize)] +pub enum AuthType { + #[value(name = "oauth")] + OAuth, + #[default] + Browser, +} + #[derive(Debug)] pub struct Config { pub auth_type: AuthType, @@ -68,256 +80,6 @@ impl TryFrom for Config { } } -#[derive(ValueEnum, Copy, Clone, Default, Debug, Serialize, Deserialize)] -pub enum AuthType { - #[value(name = "oauth")] - OAuth, - #[default] - Browser, -} - -#[derive(Debug)] -pub struct YoutuiKeymap { - pub global: HashMap>, - pub playlist: HashMap>, - pub browser: HashMap>, - pub browser_artists: HashMap>, - pub browser_search: HashMap>, - pub browser_songs: HashMap>, - pub help: HashMap>, - pub sort: HashMap>, - pub filter: HashMap>, - pub text_entry: HashMap>, - pub list: HashMap>, - pub log: HashMap>, -} - -#[derive(Default, Debug, Serialize, Deserialize)] -#[serde(default)] -pub struct YoutuiKeymapIR { - pub global: HashMap, - pub playlist: HashMap, - pub browser: HashMap, - pub browser_artists: HashMap, - pub browser_search: HashMap, - pub browser_songs: HashMap, - pub help: HashMap, - pub sort: HashMap, - pub filter: HashMap, - pub text_entry: HashMap, - pub list: HashMap, - pub log: HashMap, -} - -impl TryFrom for YoutuiKeymap { - type Error = String; - fn try_from(value: YoutuiKeymapIR) -> std::result::Result { - let YoutuiKeymapIR { - global, - playlist, - browser, - browser_artists, - browser_search, - browser_songs, - help, - sort, - filter, - text_entry, - list, - log, - } = value; - Ok(Self { - global: global - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - playlist: playlist - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - browser: browser - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - browser_artists: browser_artists - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - browser_search: browser_search - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - browser_songs: browser_songs - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - help: help - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - sort: sort - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - filter: filter - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - text_entry: text_entry - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - list: list - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - log: log - .into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) - .collect::, String>>()?, - }) - } -} - -#[derive(Default, Debug, Serialize, Deserialize)] -#[serde(default)] -pub struct YoutuiModeNames { - global: HashMap, - playlist: HashMap, - browser: HashMap, - browser_artists: HashMap, - browser_search: HashMap, - browser_songs: HashMap, - help: HashMap, - sort: HashMap, - filter: HashMap, - text_entry: HashMap, - list: HashMap, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum KeyEnumString { - #[serde(deserialize_with = "super::core::string_or_struct")] - Key(KeyEnumKey), - Mode(HashMap), -} - -#[derive(Debug)] -pub enum KeyEnum { - Key(KeyEnumKey), - Mode(HashMap>), -} - -impl TryFrom for KeyEnum { - type Error = String; - fn try_from(value: KeyEnumString) -> std::result::Result { - let new: KeyEnum = match value { - KeyEnumString::Key(k) => KeyEnum::Key(k.try_map(TryInto::try_into)?), - KeyEnumString::Mode(m) => KeyEnum::Mode( - m.into_iter() - .map(|(k, a)| Ok::<_, String>((k, KeyEnum::::try_from(a)?))) - .collect::>()?, - ), - }; - Ok(new) - } -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct KeyEnumKey { - // Consider - can there be multiple actions? - // Consider - can an action access global commands? Or commands from another component? - // Consider - case where component has list and help keybinds, but some keybinds share a - // mode. What happens here. - pub action: A, - #[serde(default)] - pub value: usize, - #[serde(default)] - pub visibility: CommandVisibility, -} - -impl KeyEnumKey { - fn try_map( - self, - f: impl FnOnce(A) -> std::result::Result, - ) -> std::result::Result, E> { - let Self { - action, - value, - visibility, - } = self; - Ok(KeyEnumKey { - action: f(action)?, - value, - visibility, - }) - } -} - -impl FromStr for KeyEnumKey { - type Err = Infallible; - fn from_str(s: &str) -> std::result::Result { - Ok(KeyEnumKey { - action: s.to_string(), - value: Default::default(), - visibility: Default::default(), - }) - } -} - -#[derive(PartialEq, Debug, Serialize, Deserialize)] -enum ModeNameEnum { - Submode(HashMap), - #[serde(untagged)] - Name(String), -} - -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum BrowserAction { - ViewPlaylist, - Search, - Left, - Right, -} - -impl Action for BrowserAction { - type State = Browser; - fn context(&self) -> std::borrow::Cow { - "Browser".into() - } - fn describe(&self) -> std::borrow::Cow { - match self { - BrowserAction::ViewPlaylist => "View Playlist", - BrowserAction::Search => "Toggle Search", - BrowserAction::Left => "Left", - BrowserAction::Right => "Right", - } - .into() - } - async fn apply( - self, - state: &mut Self::State, - ) -> crate::app::component::actionhandler::ComponentEffect - where - Self: Sized, - { - match self { - BrowserAction::Left => state.left(), - BrowserAction::Right => state.right(), - BrowserAction::ViewPlaylist => { - send_or_error( - &state.callback_tx, - AppCallback::ChangeContext(WindowContext::Playlist), - ) - .await - } - BrowserAction::Search => state.handle_toggle_search(), - } - AsyncTask::new_no_op() - } -} #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BrowserArtistsAction {