diff --git a/benches/results_list_benchmark.rs b/benches/results_list_benchmark.rs index 1bd280b..61e9857 100644 --- a/benches/results_list_benchmark.rs +++ b/benches/results_list_benchmark.rs @@ -635,7 +635,6 @@ pub fn results_list_benchmark(c: &mut Criterion) { result_line_number_fg: Color::Indexed(222), result_selected_bg: Color::Indexed(222), match_foreground_color: Color::Indexed(222), - pointer_fg: Color::Indexed(222), }; c.bench_function("results_list", |b| { diff --git a/crates/television-channels/src/cable.rs b/crates/television-channels/src/cable.rs index fd21b98..13cb274 100644 --- a/crates/television-channels/src/cable.rs +++ b/crates/television-channels/src/cable.rs @@ -4,7 +4,7 @@ use std::{ ops::Deref, }; -#[derive(Clone, Debug, serde::Deserialize)] +#[derive(Clone, Debug, serde::Deserialize, PartialEq)] pub struct CableChannelPrototype { pub name: String, pub source_command: String, diff --git a/crates/television-derive/src/lib.rs b/crates/television-derive/src/lib.rs index 04b6812..aedf8af 100644 --- a/crates/television-derive/src/lib.rs +++ b/crates/television-derive/src/lib.rs @@ -56,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { ); // create the CliTvChannel enum - let cli_enum_variants = variants + let cli_enum_variants: Vec<_> = variants .iter() .filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI)) .map(|variant| { @@ -64,7 +64,22 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { quote! { #variant_name } - }); + }) + .collect(); + + // Generate lowercase mappings for the TryFrom implementation + let lowercase_match_arms: Vec<_> = variants + .iter() + .filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI)) + .map(|variant| { + let variant_name = &variant.ident; + let lowercase_name = variant_name.to_string().to_lowercase(); + quote! { + #lowercase_name => Ok(Self::#variant_name), + } + }) + .collect(); + let cli_enum = quote! { use clap::ValueEnum; use serde::{Deserialize, Serialize}; @@ -76,6 +91,19 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { #[default] #(#cli_enum_variants),* } + + impl std::convert::TryFrom<&str> for CliTvChannel { + type Error = String; + + fn try_from(channel: &str) -> Result { + match channel { + #( + #lowercase_match_arms + )* + _ => Err(format!("Invalid cli channel name: {}", channel)), + } + } + } }; // Generate the match arms for the `to_channel` method @@ -110,6 +138,14 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { #(#arms),* } } + + pub fn all_channels() -> Vec { + vec![ + #( + stringify!(#cli_enum_variants).to_lowercase(), + )* + ] + } } }; diff --git a/crates/television/cli.rs b/crates/television/cli.rs index a364f6a..05c1fe7 100644 --- a/crates/television/cli.rs +++ b/crates/television/cli.rs @@ -1,14 +1,21 @@ -use clap::Parser; +use clap::{Parser, Subcommand}; +use color_eyre::{eyre::eyre, Result}; -use crate::config::{get_config_dir, get_data_dir}; -use television_channels::{channels::CliTvChannel, entry::PreviewCommand}; +use crate::{ + cable, + config::{get_config_dir, get_data_dir}, +}; +use television_channels::{ + cable::CableChannelPrototype, channels::CliTvChannel, + entry::PreviewCommand, +}; #[derive(Parser, Debug)] #[command(author, version = version(), about)] pub struct Cli { /// Which channel shall we watch? - #[arg(value_enum, default_value = "files")] - pub channel: CliTvChannel, + #[arg(value_enum, default_value = "files", value_parser = channel_parser)] + pub channel: ParsedCliChannel, /// Use a custom preview command (currently only supported by the stdin channel) #[arg(short, long, value_name = "STRING")] @@ -32,15 +39,25 @@ pub struct Cli { /// to be handled by the parent process. #[arg(long, value_name = "STRING")] pub passthrough_keybindings: Option, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Lists available channels + ListChannels, } #[derive(Debug)] pub struct PostProcessedCli { - pub channel: CliTvChannel, + pub channel: ParsedCliChannel, pub preview_command: Option, pub tick_rate: f64, pub frame_rate: f64, pub passthrough_keybindings: Vec, + pub command: Option, } impl From for PostProcessedCli { @@ -63,10 +80,66 @@ impl From for PostProcessedCli { tick_rate: cli.tick_rate, frame_rate: cli.frame_rate, passthrough_keybindings, + command: cli.command, } } } +#[derive(Debug, Clone, PartialEq)] +pub enum ParsedCliChannel { + Builtin(CliTvChannel), + Cable(CableChannelPrototype), +} + +fn channel_parser(channel: &str) -> Result { + let cable_channels = cable::load_cable_channels()?; + CliTvChannel::try_from(channel) + .map(ParsedCliChannel::Builtin) + .or_else(|_| { + cable_channels + .iter() + .find(|(k, _)| k.to_lowercase() == channel) + .map_or_else( + || Err(eyre!("Unknown channel: {}", channel)), + |(_, v)| Ok(ParsedCliChannel::Cable(v.clone())), + ) + }) +} + +pub fn list_cable_channels() -> Vec { + cable::load_cable_channels() + .unwrap_or_default() + .iter() + .map(|(k, _)| k.clone()) + .collect() +} + +pub fn list_builtin_channels() -> Vec { + CliTvChannel::all_channels() + .iter() + .map(std::string::ToString::to_string) + .collect() +} + +pub fn list_channels() { + println!("\x1b[4mBuiltin channels:\x1b[0m"); + for c in list_builtin_channels() { + println!("\t{c}"); + } + println!("\n\x1b[4mCustom channels:\x1b[0m"); + for c in list_cable_channels().iter().map(|c| c.to_lowercase()) { + println!("\t{c}"); + } +} + +fn delimiter_parser(s: &str) -> Result { + Ok(match s { + "" => ":".to_string(), + "\\t" => "\t".to_string(), + _ => s.to_string(), + }) +} + const VERSION_MESSAGE: &str = concat!( env!("CARGO_PKG_VERSION"), "\ntarget triple: ", @@ -116,17 +189,21 @@ mod tests { #[allow(clippy::float_cmp)] fn test_from_cli() { let cli = Cli { - channel: CliTvChannel::Files, + channel: ParsedCliChannel::Builtin(CliTvChannel::Files), preview: Some("bat -n --color=always {}".to_string()), delimiter: ":".to_string(), tick_rate: 50.0, frame_rate: 60.0, passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()), + command: None, }; let post_processed_cli: PostProcessedCli = cli.into(); - assert_eq!(post_processed_cli.channel, CliTvChannel::Files); + assert_eq!( + post_processed_cli.channel, + ParsedCliChannel::Builtin(CliTvChannel::Files) + ); assert_eq!( post_processed_cli.preview_command, Some(PreviewCommand { @@ -142,11 +219,3 @@ mod tests { ); } } - -fn delimiter_parser(s: &str) -> Result { - Ok(match s { - "" => ":".to_string(), - "\\t" => "\t".to_string(), - _ => s.to_string(), - }) -} diff --git a/crates/television/main.rs b/crates/television/main.rs index df8f56c..9d11fc1 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -2,7 +2,7 @@ use std::io::{stdout, IsTerminal, Write}; use std::process::exit; use clap::Parser; -use cli::PostProcessedCli; +use cli::{list_channels, ParsedCliChannel, PostProcessedCli}; use color_eyre::Result; use television_channels::channels::TelevisionChannel; use television_channels::entry::PreviewType; @@ -34,9 +34,17 @@ async fn main() -> Result<()> { logging::init()?; let args: PostProcessedCli = Cli::parse().into(); - debug!("{:?}", args); + if let Some(command) = args.command { + match command { + cli::Command::ListChannels => { + list_channels(); + exit(0); + } + } + } + match App::new( { if is_readable_stdin() { @@ -46,7 +54,12 @@ async fn main() -> Result<()> { )) } else { debug!("Using {:?} channel", args.channel); - args.channel.to_channel() + match args.channel { + ParsedCliChannel::Builtin(c) => c.to_channel(), + ParsedCliChannel::Cable(c) => { + TelevisionChannel::Cable(c.into()) + } + } } }, args.tick_rate,