use crate::get_config_dir;
use anyhow::{Context, Result};
use clap::ValueEnum;
use keymap::{YoutuiKeymap, YoutuiKeymapIR, YoutuiModeNamesIR};
use serde::{Deserialize, Serialize};
use ytmapi_rs::auth::OAuthToken;
const CONFIG_FILE_NAME: &str = "config.toml";
pub mod keymap;
#[derive(Serialize, Deserialize)]
pub enum ApiKey {
OAuthToken(OAuthToken),
BrowserToken(String),
None,
}
impl std::fmt::Debug for ApiKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiKey::OAuthToken(_) => f
.debug_tuple("OAuthToken")
.field(&"/* private fields */")
.finish(),
ApiKey::BrowserToken(_) => f
.debug_tuple("BrowserToken")
.field(&"/* private fields */")
.finish(),
ApiKey::None => f.debug_tuple("NoAuthToken").finish(),
}
}
}
#[derive(ValueEnum, Copy, PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
pub enum AuthType {
#[value(name = "oauth")]
OAuth,
#[default]
Browser,
Unauthenticated,
}
#[derive(ValueEnum, Copy, PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
pub enum DownloaderType {
#[default]
Native,
YtDlp,
}
#[derive(Debug, PartialEq)]
pub struct Config {
pub auth_type: AuthType,
pub downloader_type: DownloaderType,
pub yt_dlp_command: String,
pub keybinds: YoutuiKeymap,
}
fn default_yt_dlp_command() -> String {
String::from("yt-dlp")
}
impl Default for Config {
fn default() -> Self {
Self {
auth_type: Default::default(),
downloader_type: Default::default(),
yt_dlp_command: default_yt_dlp_command(),
keybinds: Default::default(),
}
}
}
#[derive(Default, Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConfigIR {
pub auth_type: AuthType,
pub downloader_type: DownloaderType,
#[serde(default = "default_yt_dlp_command")]
pub yt_dlp_command: String,
pub keybinds: YoutuiKeymapIR,
pub mode_names: YoutuiModeNamesIR,
}
impl TryFrom<ConfigIR> for Config {
type Error = anyhow::Error;
fn try_from(value: ConfigIR) -> std::result::Result<Self, Self::Error> {
let ConfigIR {
auth_type,
downloader_type,
keybinds,
mode_names,
yt_dlp_command,
} = value;
Ok(Config {
auth_type,
downloader_type,
keybinds: YoutuiKeymap::try_from_stringy(keybinds, mode_names)?,
yt_dlp_command,
})
}
}
impl Config {
pub async fn new(debug: bool) -> Result<Self> {
let config_dir = get_config_dir()?;
let config_file_location = config_dir.join(CONFIG_FILE_NAME);
if let Ok(config_file) = tokio::fs::read_to_string(&config_file_location).await {
if debug {
println!(
"Loading config from {}",
config_file_location.to_string_lossy()
);
}
let ir: ConfigIR = toml::from_str(&config_file)
.context("Error deserializing config file from toml")?;
Ok(Config::try_from(ir).context("Error processing config file")?)
} else {
if debug {
println!(
"Config file not found in {}, using defaults",
config_file_location.to_string_lossy()
);
}
Ok(Self::default())
}
}
}
#[cfg(test)]
mod tests {
use crate::config::keymap::YoutuiKeymap;
use crate::config::{Config, ConfigIR};
use pretty_assertions::{assert_eq, assert_ne};
async fn example_config_file() -> String {
tokio::fs::read_to_string("./config/config.toml")
.await
.unwrap()
}
#[tokio::test]
async fn test_deserialize_default_config_to_ir() {
let config_file = example_config_file().await;
toml::from_str::<ConfigIR>(&config_file).unwrap();
}
#[tokio::test]
async fn test_convert_ir_to_config() {
let config_file = example_config_file().await;
let ir: ConfigIR = toml::from_str(&config_file).unwrap();
Config::try_from(ir).unwrap();
}
#[tokio::test]
async fn test_unknown_keys_in_config() {
let config_file = r#"auth_typo = 'Browser'"#;
let config_toml: toml::Value = toml::from_str(config_file).unwrap();
let ir: Result<ConfigIR, _> = config_toml.try_into();
assert!(ir.is_err());
}
#[tokio::test]
async fn test_unknown_keybind_parameters() {
let config_file = r#"[keybinds.global]
raisevolume = {action = "vol_up", visiblity = "hidden"}"#;
let config_toml: toml::Value = toml::from_str(config_file).unwrap();
let ir: Result<ConfigIR, _> = config_toml.try_into();
assert!(ir.is_err());
}
#[tokio::test]
async fn test_default_config_equals_deserialized_config() {
let config_file = example_config_file().await;
let ConfigIR {
auth_type,
keybinds,
mode_names,
downloader_type,
yt_dlp_command,
} = toml::from_str(&config_file).unwrap();
let keybinds = YoutuiKeymap::try_from_stringy_exact(keybinds, mode_names).unwrap();
let YoutuiKeymap {
global,
playlist,
browser,
browser_artists,
browser_search,
browser_songs,
browser_artist_songs,
help,
sort,
filter,
text_entry,
list,
log,
browser_playlists,
browser_playlist_songs,
} = keybinds;
let Config {
auth_type: def_auth_type,
keybinds: def_keybinds,
downloader_type: def_downloader_type,
yt_dlp_command: def_yt_dlp_command,
} = Config::default();
let YoutuiKeymap {
global: def_global,
playlist: def_playlist,
browser: def_browser,
browser_artists: def_browser_artists,
browser_search: def_browser_search,
browser_artist_songs: def_browser_artist_songs,
browser_songs: def_browser_songs,
help: def_help,
sort: def_sort,
filter: def_filter,
text_entry: def_text_entry,
list: def_list,
log: def_log,
browser_playlists: def_browser_playlists,
browser_playlist_songs: def_browser_playlist_songs,
} = def_keybinds;
assert_eq!(auth_type, def_auth_type, "auth_type doesn't match");
assert_eq!(
downloader_type, def_downloader_type,
"downloader_type doesn't match"
);
assert_eq!(
yt_dlp_command, def_yt_dlp_command,
"yt_dlp_command doesn't match"
);
assert_eq!(global, def_global, "global keybinds don't match");
assert_eq!(playlist, def_playlist, "playlist keybinds don't match");
assert_eq!(browser, def_browser, "browser keybinds don't match");
assert_eq!(
browser_artists, def_browser_artists,
"browser_artists keybinds don't match"
);
assert_eq!(
browser_artist_songs, def_browser_artist_songs,
"browser_artist_songs keybinds don't match"
);
assert_eq!(
browser_playlists, def_browser_playlists,
"browser_playlists keybinds don't match"
);
assert_eq!(
browser_playlist_songs, def_browser_playlist_songs,
"browser_playlist_songs keybinds don't match"
);
assert_eq!(
browser_search, def_browser_search,
"browser_search keybinds don't match"
);
assert_eq!(
browser_songs, def_browser_songs,
"browser_songs keybinds don't match"
);
assert_eq!(help, def_help, "help keybinds don't match");
assert_eq!(sort, def_sort, "sort keybinds don't match");
assert_eq!(filter, def_filter, "filter keybinds don't match");
assert_eq!(
text_entry, def_text_entry,
"text_entry keybinds don't match"
);
assert_eq!(list, def_list, "list keybinds don't match");
assert_eq!(log, def_log, "log keybinds don't match");
}
#[tokio::test]
async fn test_default_config_equals_blank_config() {
let ir: ConfigIR = toml::from_str("").unwrap();
let Config {
auth_type,
keybinds,
downloader_type,
yt_dlp_command,
} = Config::try_from(ir).unwrap();
let YoutuiKeymap {
global,
playlist,
browser,
browser_artists,
browser_artist_songs,
browser_search,
browser_songs,
help,
sort,
filter,
text_entry,
list,
log,
browser_playlists,
browser_playlist_songs,
} = keybinds;
let Config {
auth_type: def_auth_type,
keybinds: def_keybinds,
downloader_type: def_downloader_type,
yt_dlp_command: def_yt_dlp_command,
} = Config::default();
let YoutuiKeymap {
global: def_global,
playlist: def_playlist,
browser: def_browser,
browser_artists: def_browser_artists,
browser_search: def_browser_search,
browser_songs: def_browser_songs,
browser_artist_songs: def_browser_artist_songs,
help: def_help,
sort: def_sort,
filter: def_filter,
text_entry: def_text_entry,
list: def_list,
log: def_log,
browser_playlists: def_browser_playlists,
browser_playlist_songs: def_browser_playlist_songs,
} = def_keybinds;
assert_eq!(auth_type, def_auth_type);
assert_eq!(downloader_type, def_downloader_type);
assert_eq!(yt_dlp_command, def_yt_dlp_command);
assert_eq!(global, def_global);
assert_eq!(playlist, def_playlist);
assert_eq!(browser, def_browser);
assert_eq!(browser_artists, def_browser_artists);
assert_eq!(browser_playlists, def_browser_playlists);
assert_eq!(browser_search, def_browser_search);
assert_eq!(browser_songs, def_browser_songs);
assert_eq!(browser_artist_songs, def_browser_artist_songs);
assert_eq!(browser_playlist_songs, def_browser_playlist_songs);
assert_eq!(help, def_help);
assert_eq!(sort, def_sort);
assert_eq!(filter, def_filter);
assert_eq!(text_entry, def_text_entry);
assert_eq!(list, def_list);
assert_eq!(log, def_log);
}
#[tokio::test]
async fn test_different_config_to_default() {
let config_file = tokio::fs::read_to_string("./config/config.toml.vim-example")
.await
.unwrap();
let ir: ConfigIR = toml::from_str(&config_file).unwrap();
let config = Config::try_from(ir).unwrap();
let def_config = Config::default();
assert_ne!(config, def_config)
}
}