feat(cable): support cable channel invocation through the cli (#116)

This commit is contained in:
Alexandre Pasmantier 2024-12-14 17:47:36 +01:00 committed by GitHub
parent 913aa85af0
commit 937d0f0758
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 23 deletions

View File

@ -635,7 +635,6 @@ pub fn results_list_benchmark(c: &mut Criterion) {
result_line_number_fg: Color::Indexed(222), result_line_number_fg: Color::Indexed(222),
result_selected_bg: Color::Indexed(222), result_selected_bg: Color::Indexed(222),
match_foreground_color: Color::Indexed(222), match_foreground_color: Color::Indexed(222),
pointer_fg: Color::Indexed(222),
}; };
c.bench_function("results_list", |b| { c.bench_function("results_list", |b| {

View File

@ -4,7 +4,7 @@ use std::{
ops::Deref, ops::Deref,
}; };
#[derive(Clone, Debug, serde::Deserialize)] #[derive(Clone, Debug, serde::Deserialize, PartialEq)]
pub struct CableChannelPrototype { pub struct CableChannelPrototype {
pub name: String, pub name: String,
pub source_command: String, pub source_command: String,

View File

@ -56,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
); );
// create the CliTvChannel enum // create the CliTvChannel enum
let cli_enum_variants = variants let cli_enum_variants: Vec<_> = variants
.iter() .iter()
.filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI)) .filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI))
.map(|variant| { .map(|variant| {
@ -64,7 +64,22 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
quote! { quote! {
#variant_name #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! { let cli_enum = quote! {
use clap::ValueEnum; use clap::ValueEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -76,6 +91,19 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
#[default] #[default]
#(#cli_enum_variants),* #(#cli_enum_variants),*
} }
impl std::convert::TryFrom<&str> for CliTvChannel {
type Error = String;
fn try_from(channel: &str) -> Result<Self, Self::Error> {
match channel {
#(
#lowercase_match_arms
)*
_ => Err(format!("Invalid cli channel name: {}", channel)),
}
}
}
}; };
// Generate the match arms for the `to_channel` method // Generate the match arms for the `to_channel` method
@ -110,6 +138,14 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
#(#arms),* #(#arms),*
} }
} }
pub fn all_channels() -> Vec<String> {
vec![
#(
stringify!(#cli_enum_variants).to_lowercase(),
)*
]
}
} }
}; };

View File

@ -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 crate::{
use television_channels::{channels::CliTvChannel, entry::PreviewCommand}; cable,
config::{get_config_dir, get_data_dir},
};
use television_channels::{
cable::CableChannelPrototype, channels::CliTvChannel,
entry::PreviewCommand,
};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version = version(), about)] #[command(author, version = version(), about)]
pub struct Cli { pub struct Cli {
/// Which channel shall we watch? /// Which channel shall we watch?
#[arg(value_enum, default_value = "files")] #[arg(value_enum, default_value = "files", value_parser = channel_parser)]
pub channel: CliTvChannel, pub channel: ParsedCliChannel,
/// Use a custom preview command (currently only supported by the stdin channel) /// Use a custom preview command (currently only supported by the stdin channel)
#[arg(short, long, value_name = "STRING")] #[arg(short, long, value_name = "STRING")]
@ -32,15 +39,25 @@ pub struct Cli {
/// to be handled by the parent process. /// to be handled by the parent process.
#[arg(long, value_name = "STRING")] #[arg(long, value_name = "STRING")]
pub passthrough_keybindings: Option<String>, pub passthrough_keybindings: Option<String>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Lists available channels
ListChannels,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct PostProcessedCli { pub struct PostProcessedCli {
pub channel: CliTvChannel, pub channel: ParsedCliChannel,
pub preview_command: Option<PreviewCommand>, pub preview_command: Option<PreviewCommand>,
pub tick_rate: f64, pub tick_rate: f64,
pub frame_rate: f64, pub frame_rate: f64,
pub passthrough_keybindings: Vec<String>, pub passthrough_keybindings: Vec<String>,
pub command: Option<Command>,
} }
impl From<Cli> for PostProcessedCli { impl From<Cli> for PostProcessedCli {
@ -63,10 +80,66 @@ impl From<Cli> for PostProcessedCli {
tick_rate: cli.tick_rate, tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate, frame_rate: cli.frame_rate,
passthrough_keybindings, passthrough_keybindings,
command: cli.command,
} }
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum ParsedCliChannel {
Builtin(CliTvChannel),
Cable(CableChannelPrototype),
}
fn channel_parser(channel: &str) -> Result<ParsedCliChannel> {
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<String> {
cable::load_cable_channels()
.unwrap_or_default()
.iter()
.map(|(k, _)| k.clone())
.collect()
}
pub fn list_builtin_channels() -> Vec<String> {
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<String, String> {
Ok(match s {
"" => ":".to_string(),
"\\t" => "\t".to_string(),
_ => s.to_string(),
})
}
const VERSION_MESSAGE: &str = concat!( const VERSION_MESSAGE: &str = concat!(
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
"\ntarget triple: ", "\ntarget triple: ",
@ -116,17 +189,21 @@ mod tests {
#[allow(clippy::float_cmp)] #[allow(clippy::float_cmp)]
fn test_from_cli() { fn test_from_cli() {
let cli = Cli { let cli = Cli {
channel: CliTvChannel::Files, channel: ParsedCliChannel::Builtin(CliTvChannel::Files),
preview: Some("bat -n --color=always {}".to_string()), preview: Some("bat -n --color=always {}".to_string()),
delimiter: ":".to_string(), delimiter: ":".to_string(),
tick_rate: 50.0, tick_rate: 50.0,
frame_rate: 60.0, frame_rate: 60.0,
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()), passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
command: None,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); 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!( assert_eq!(
post_processed_cli.preview_command, post_processed_cli.preview_command,
Some(PreviewCommand { Some(PreviewCommand {
@ -142,11 +219,3 @@ mod tests {
); );
} }
} }
fn delimiter_parser(s: &str) -> Result<String, String> {
Ok(match s {
"" => ":".to_string(),
"\\t" => "\t".to_string(),
_ => s.to_string(),
})
}

View File

@ -2,7 +2,7 @@ use std::io::{stdout, IsTerminal, Write};
use std::process::exit; use std::process::exit;
use clap::Parser; use clap::Parser;
use cli::PostProcessedCli; use cli::{list_channels, ParsedCliChannel, PostProcessedCli};
use color_eyre::Result; use color_eyre::Result;
use television_channels::channels::TelevisionChannel; use television_channels::channels::TelevisionChannel;
use television_channels::entry::PreviewType; use television_channels::entry::PreviewType;
@ -34,9 +34,17 @@ async fn main() -> Result<()> {
logging::init()?; logging::init()?;
let args: PostProcessedCli = Cli::parse().into(); let args: PostProcessedCli = Cli::parse().into();
debug!("{:?}", args); debug!("{:?}", args);
if let Some(command) = args.command {
match command {
cli::Command::ListChannels => {
list_channels();
exit(0);
}
}
}
match App::new( match App::new(
{ {
if is_readable_stdin() { if is_readable_stdin() {
@ -46,7 +54,12 @@ async fn main() -> Result<()> {
)) ))
} else { } else {
debug!("Using {:?} channel", args.channel); 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, args.tick_rate,