diff --git a/benches/main/ui.rs b/benches/main/ui.rs index c095812..b3285d4 100644 --- a/benches/main/ui.rs +++ b/benches/main/ui.rs @@ -12,7 +12,7 @@ use television::{ action::Action, channels::{ entry::{into_ranges, Entry}, - prototypes::{Cable, ChannelPrototype}, + prototypes::Cable, }, config::{Config, ConfigEnv}, screen::{colors::ResultsColorscheme, results::build_results_list}, @@ -464,6 +464,8 @@ pub fn draw(c: &mut Criterion) { let rt = Runtime::new().unwrap(); + let cable = Cable::default(); + c.bench_function("draw", |b| { b.to_async(&rt).iter_batched( // FIXME: this is kind of hacky @@ -472,7 +474,7 @@ pub fn draw(c: &mut Criterion) { let backend = TestBackend::new(width, height); let terminal = Terminal::new(backend).unwrap(); let (tx, _) = tokio::sync::mpsc::unbounded_channel(); - let channel_prototype = ChannelPrototype::default(); + let channel_prototype = cable.get_channel("files"); // Wait for the channel to finish loading let mut tv = Television::new( tx, diff --git a/television/channels/prototypes.rs b/television/channels/prototypes.rs index 09399ab..bb1f473 100644 --- a/television/channels/prototypes.rs +++ b/television/channels/prototypes.rs @@ -4,7 +4,10 @@ use std::{ ops::Deref, }; -use crate::{cable::CableSpec, channels::preview::PreviewCommand}; +use crate::{ + cable::CableSpec, channels::preview::PreviewCommand, + cli::unknown_channel_exit, +}; /// A prototype for cable channels. /// @@ -73,19 +76,19 @@ impl ChannelPrototype { preview_command: preview, } } + + pub fn set_preview(self, preview_command: Option) -> Self { + Self::new( + &self.name, + &self.source_command, + self.interactive, + preview_command, + ) + } } pub const DEFAULT_PROTOTYPE_NAME: &str = "files"; -impl Default for ChannelPrototype { - fn default() -> Self { - Cable::default() - .get(DEFAULT_PROTOTYPE_NAME) - .cloned() - .unwrap() - } -} - impl Display for ChannelPrototype { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}", self.name) @@ -109,12 +112,14 @@ impl Deref for Cable { } impl Cable { - pub fn default_channel(&self) -> ChannelPrototype { - self.get(DEFAULT_PROTOTYPE_NAME) + pub fn get_channel(&self, name: &str) -> ChannelPrototype { + self.get(name) .cloned() - .unwrap_or_else(|| { - panic!("Default channel '{DEFAULT_PROTOTYPE_NAME}' not found") - }) + .unwrap_or_else(|| unknown_channel_exit(name)) + } + + pub fn has_channel(&self, name: &str) -> bool { + self.contains_key(name) } } diff --git a/television/cli/mod.rs b/television/cli/mod.rs index 44fcd4f..e751d91 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -19,7 +19,7 @@ pub mod args; #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub struct PostProcessedCli { - pub channel: ChannelPrototype, + pub channel: Option, pub preview_command: Option, pub no_preview: bool, pub tick_rate: Option, @@ -40,7 +40,7 @@ pub struct PostProcessedCli { impl Default for PostProcessedCli { fn default() -> Self { Self { - channel: ChannelPrototype::default(), + channel: None, preview_command: None, no_preview: false, tick_rate: None, @@ -60,87 +60,59 @@ impl Default for PostProcessedCli { } } -impl From for PostProcessedCli { - fn from(cli: Cli) -> Self { - // parse literal keybindings passed through the CLI - let keybindings: Option = cli.keybindings.map(|kb| { - parse_keybindings_literal(&kb, CLI_KEYBINDINGS_DELIMITER) - .map_err(|e| { - cli_parsing_error_exit(&e.to_string()); - }) - .unwrap() - }); +pub fn post_process(cli: Cli, cable: &Cable) -> PostProcessedCli { + // Parse literal keybindings passed through the CLI + let keybindings = cli.keybindings.as_ref().map(|kb| { + parse_keybindings_literal(kb, CLI_KEYBINDINGS_DELIMITER) + .unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string())) + }); - // parse the preview command if provided - let preview_command = cli.preview.map(|preview| PreviewCommand { - command: preview, - delimiter: cli.delimiter.clone(), - offset_expr: cli.preview_offset.clone(), - }); + // Parse the preview command if provided + let preview_command = cli.preview.as_ref().map(|preview| PreviewCommand { + command: preview.clone(), + delimiter: cli.delimiter.clone(), + offset_expr: cli.preview_offset.clone(), + }); - let mut channel: ChannelPrototype; - let working_directory: Option; - - let cable = cable::load_cable().unwrap_or_default(); - if cli.channel.is_none() { - channel = cable.default_channel(); - working_directory = cli.working_directory; - } else { - let cli_channel = cli.channel.as_ref().unwrap().to_owned(); - match parse_channel(&cli_channel, &cable) { - Ok(p) => { - channel = p; - working_directory = cli.working_directory; - } - Err(_) => { - // if the path is provided as first argument and it exists, use it as the working - // directory and default to the files channel - if cli.working_directory.is_none() - && Path::new(&cli_channel).exists() - { - channel = cable.default_channel(); - working_directory = Some(cli.channel.unwrap().clone()); - } else { - unknown_channel_exit(&cli.channel.unwrap()); - unreachable!(); - } - } + // Determine channel and working_directory + let (channel, working_directory) = match &cli.channel { + Some(c) if !cable.has_channel(c) => { + if cli.working_directory.is_none() && Path::new(c).exists() { + (None, Some(c.clone())) + } else { + unknown_channel_exit(c); } } + _ => (cli.channel.clone(), cli.working_directory.clone()), + }; - // override the default previewer - if let Some(preview_cmd) = &preview_command { - channel.preview_command = Some(preview_cmd.clone()); - } - - Self { - channel, - preview_command, - no_preview: cli.no_preview, - tick_rate: cli.tick_rate, - frame_rate: cli.frame_rate, - input: cli.input, - custom_header: cli.custom_header, - command: cli.command, - working_directory, - autocomplete_prompt: cli.autocomplete_prompt, - keybindings, - exact: cli.exact, - select_1: cli.select_1, - no_remote: cli.no_remote, - no_help: cli.no_help, - ui_scale: cli.ui_scale, - } + PostProcessedCli { + channel, + preview_command, + no_preview: cli.no_preview, + tick_rate: cli.tick_rate, + frame_rate: cli.frame_rate, + input: cli.input, + custom_header: cli.custom_header, + command: cli.command, + working_directory, + autocomplete_prompt: cli.autocomplete_prompt, + keybindings, + exact: cli.exact, + select_1: cli.select_1, + no_remote: cli.no_remote, + no_help: cli.no_help, + ui_scale: cli.ui_scale, } } -fn cli_parsing_error_exit(message: &str) { +fn cli_parsing_error_exit(message: &str) -> ! { eprintln!("Error parsing CLI arguments: {message}\n"); std::process::exit(1); } -fn unknown_channel_exit(channel: &str) { - eprintln!("Unknown channel: {channel}\n"); +pub fn unknown_channel_exit(channel: &str) -> ! { + eprintln!("Channel not found: {channel}\n"); std::process::exit(1); } @@ -165,20 +137,6 @@ fn parse_keybindings_literal( toml::from_str(&toml_definition).map_err(|e| anyhow!(e)) } -pub fn parse_channel( - channel: &str, - cable_channels: &Cable, -) -> Result { - // try to parse the channel as a cable channel - match cable_channels - .iter() - .find(|(k, _)| k.to_lowercase() == channel) - { - Some((_, v)) => Ok(v.clone()), - None => Err(anyhow!("The following channel wasn't found among cable channels: {channel}")), - } -} - pub fn list_channels() { for c in cable::load_cable().unwrap_or_default().keys() { println!("\t{c}"); @@ -212,17 +170,17 @@ pub fn guess_channel_from_prompt( prompt: &str, command_mapping: &FxHashMap, fallback_channel: &str, - cable_channels: &Cable, -) -> Result { + cable: &Cable, +) -> ChannelPrototype { debug!("Guessing channel from prompt: {}", prompt); // git checkout -qf // --- -------- --- <--------- - let fallback = cable_channels + let fallback = cable .get(fallback_channel) .expect("Fallback channel not found in cable channels") .clone(); if prompt.trim().is_empty() { - return Ok(fallback); + return fallback; } let rev_prompt_words = prompt.split_whitespace().rev(); let mut stack = Vec::new(); @@ -236,7 +194,7 @@ pub fn guess_channel_from_prompt( for word in rev_prompt_words.clone() { // if the stack is empty, we have a match if stack.is_empty() { - return parse_channel(channel, cable_channels); + return cable.get_channel(channel); } // if the word matches the top of the stack, pop it if stack.last() == Some(&word) { @@ -245,14 +203,14 @@ pub fn guess_channel_from_prompt( } // if the stack is empty, we have a match if stack.is_empty() { - return parse_channel(channel, cable_channels); + return cable.get_channel(channel); } // reset the stack stack.clear(); } debug!("No match found, falling back to default channel"); - Ok(fallback) + fallback } const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION"); @@ -304,18 +262,9 @@ mod tests { ..Default::default() }; - let post_processed_cli: PostProcessedCli = cli.into(); + let cable = cable::load_cable().unwrap_or_default(); + let post_processed_cli = post_process(cli, &cable); - let expected = ChannelPrototype { - preview_command: Some(PreviewCommand { - command: "bat -n --color=always {}".to_string(), - delimiter: ":".to_string(), - offset_expr: None, - }), - ..Default::default() - }; - - assert_eq!(post_processed_cli.channel, expected,); assert_eq!( post_processed_cli.preview_command, Some(PreviewCommand { @@ -341,9 +290,9 @@ mod tests { ..Default::default() }; - let post_processed_cli: PostProcessedCli = cli.into(); + let cable = cable::load_cable().unwrap_or_default(); + let post_processed_cli = post_process(cli, &cable); - assert_eq!(post_processed_cli.channel, ChannelPrototype::default(),); assert_eq!( post_processed_cli.working_directory, Some(".".to_string()) @@ -364,7 +313,8 @@ mod tests { ..Default::default() }; - let post_processed_cli: PostProcessedCli = cli.into(); + let cable = cable::load_cable().unwrap_or_default(); + let post_processed_cli = post_process(cli, &cable); let mut expected = KeyBindings::default(); expected.insert(Action::Quit, Binding::SingleKey(Key::Esc)); @@ -402,8 +352,7 @@ mod tests { &command_mapping, fallback, &channels, - ) - .unwrap(); + ); assert_eq!(channel.name, "files"); } @@ -420,8 +369,7 @@ mod tests { &command_mapping, fallback, &channels, - ) - .unwrap(); + ); assert_eq!(channel.name, fallback); } @@ -438,8 +386,7 @@ mod tests { &command_mapping, fallback, &channels, - ) - .unwrap(); + ); assert_eq!(channel.name, fallback); } diff --git a/television/main.rs b/television/main.rs index c496beb..7a9d721 100644 --- a/television/main.rs +++ b/television/main.rs @@ -5,8 +5,9 @@ use std::process::exit; use anyhow::Result; use clap::Parser; +use television::cable::load_cable; +use television::cli::post_process; use television::{ - cable, channels::prototypes::{Cable, ChannelPrototype}, utils::clipboard::CLIPBOARD, }; @@ -35,15 +36,16 @@ async fn main() -> Result<()> { // process the CLI arguments let cli = Cli::parse(); debug!("CLI: {:?}", cli); - let args: PostProcessedCli = cli.into(); - debug!("PostProcessedCli: {:?}", args); // load the configuration file debug!("Loading configuration..."); let mut config = Config::new(&ConfigEnv::init()?)?; debug!("Loading cable channels..."); - let cable_channels = cable::load_cable().unwrap_or_default(); + let cable = load_cable().unwrap_or_default(); + + let args = post_process(cli, &cable); + debug!("PostProcessedCli: {:?}", args); // optionally handle subcommands debug!("Handling subcommands..."); @@ -60,12 +62,8 @@ async fn main() -> Result<()> { // determine the channel to use based on the CLI arguments and configuration debug!("Determining channel..."); - let channel_prototype = determine_channel( - args.clone(), - &config, - is_readable_stdin(), - &cable_channels, - )?; + let channel_prototype = + determine_channel(&args, &config, is_readable_stdin(), &cable); CLIPBOARD.with(<_>::default); @@ -77,13 +75,8 @@ async fn main() -> Result<()> { args.no_help, config.application.tick_rate, ); - let mut app = App::new( - &channel_prototype, - config, - args.input, - options, - &cable_channels, - ); + let mut app = + App::new(&channel_prototype, config, args.input, options, &cable); stdout().flush()?; debug!("Running application..."); let output = app.run(stdout().is_terminal(), false).await?; @@ -156,27 +149,38 @@ pub fn handle_subcommands(command: &Command, config: &Config) -> Result<()> { } pub fn determine_channel( - args: PostProcessedCli, + args: &PostProcessedCli, config: &Config, readable_stdin: bool, - cable_channels: &Cable, -) -> Result { + cable: &Cable, +) -> ChannelPrototype { if readable_stdin { debug!("Using stdin channel"); - Ok(ChannelPrototype::stdin(args.preview_command)) - } else if let Some(prompt) = args.autocomplete_prompt { + ChannelPrototype::stdin(args.preview_command.clone()) + } else if let Some(prompt) = &args.autocomplete_prompt { debug!("Using autocomplete prompt: {:?}", prompt); let channel_prototype = guess_channel_from_prompt( - &prompt, + prompt, &config.shell_integration.commands, &config.shell_integration.fallback_channel, - cable_channels, - )?; + cable, + ); debug!("Using guessed channel: {:?}", channel_prototype); - Ok(channel_prototype) + channel_prototype } else { - debug!("Using {:?} channel", args.channel); - Ok(args.channel) + let channel = args + .channel + .as_ref() + .unwrap_or(&config.application.default_channel) + .clone(); + + let mut prototype = cable.get_channel(&channel); + // use cli preview command if any + if let Some(pc) = &args.preview_command { + prototype.preview_command = Some(pc.clone()); + } + + prototype } } @@ -184,7 +188,8 @@ pub fn determine_channel( mod tests { use rustc_hash::FxHashMap; use television::{ - cable::load_cable, channels::prototypes::ChannelPrototype, + cable::load_cable, + channels::{preview::PreviewCommand, prototypes::ChannelPrototype}, }; use super::*; @@ -199,8 +204,7 @@ mod tests { let channels: Cable = cable_channels.unwrap_or_else(|| load_cable().unwrap_or_default()); let channel = - determine_channel(args.clone(), config, readable_stdin, &channels) - .unwrap(); + determine_channel(args, config, readable_stdin, &channels); assert_eq!( channel.name, expected_channel.name, @@ -209,14 +213,10 @@ mod tests { ); } - #[tokio::test] + #[test] /// Test that the channel is stdin when stdin is readable - async fn test_determine_channel_readable_stdin() { - let channel = ChannelPrototype::default(); - let args = PostProcessedCli { - channel, - ..Default::default() - }; + fn test_determine_channel_readable_stdin() { + let args = PostProcessedCli::default(); let config = Config::default(); assert_is_correct_channel( &args, @@ -227,8 +227,8 @@ mod tests { ); } - #[tokio::test] - async fn test_determine_channel_autocomplete_prompt() { + #[test] + fn test_determine_channel_autocomplete_prompt() { let autocomplete_prompt = Some("cd".to_string()); let expected_channel = ChannelPrototype::new("dirs", "ls {}", false, None); @@ -261,9 +261,9 @@ mod tests { ); } - #[tokio::test] - async fn test_determine_channel_standard_case() { - let channel = ChannelPrototype::new("dirs", "", false, None); + #[test] + fn test_determine_channel_standard_case() { + let channel = Some(String::from("dirs")); let args = PostProcessedCli { channel, ..Default::default() @@ -278,6 +278,50 @@ mod tests { ); } + #[test] + fn test_determine_channel_config_fallback() { + let cable = Cable::default(); + let args = PostProcessedCli { + channel: None, + ..Default::default() + }; + let mut config = Config::default(); + config.application.default_channel = String::from("dirs"); + assert_is_correct_channel( + &args, + &config, + false, + &cable.get_channel("dirs"), + Some(cable), + ); + } + + #[test] + fn test_determine_channel_with_cli_preview() { + let cable = Cable::default(); + + let preview_command = PreviewCommand::new("echo hello", ",", None); + + let args = PostProcessedCli { + channel: Some(String::from("dirs")), + preview_command: Some(preview_command), + ..Default::default() + }; + let config = Config::default(); + + let expected_prototype = cable + .get_channel("dirs") + .set_preview(args.preview_command.clone()); + + assert_is_correct_channel( + &args, + &config, + false, + &expected_prototype, + Some(cable), + ); + } + #[test] fn test_apply_cli_overrides() { let mut config = Config::default(); diff --git a/tests/app.rs b/tests/app.rs index 3076e8d..8172ec6 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -33,7 +33,7 @@ fn setup_app( .join("tests") .join("target_dir"); std::env::set_current_dir(&target_dir).unwrap(); - ChannelPrototype::default() + Cable::default().get("files").unwrap().clone() }); let mut config = default_config_from_file().unwrap(); // this speeds up the tests