From d09f6708bc873bf130cabed08958949444f185d8 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Thu, 20 Mar 2025 02:42:58 +0100 Subject: [PATCH] feat(shell): add fallback channel to the config for smart autocomplete (#413) Fixes #321 --- .config/config.toml | 4 ++ television/cli/mod.rs | 80 +++++++++++++++++++++++--- television/config/mod.rs | 6 ++ television/config/shell_integration.rs | 1 + television/event.rs | 15 +++++ television/main.rs | 19 +++++- 6 files changed, 115 insertions(+), 10 deletions(-) diff --git a/.config/config.toml b/.config/config.toml index e8b9c1b..268de0b 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -109,6 +109,10 @@ toggle_preview = "ctrl-o" # E.g. typing `git checkout ` will open television with a list of # branches to choose from. +[shell_integration] +# This specifies the default fallback channel if no other channel is matched. +fallback_channel = "files" + [shell_integration.channel_triggers] # Add your channel triggers here. Each key is a channel that will be triggered # by the corresponding commands. diff --git a/television/cli/mod.rs b/television/cli/mod.rs index 1cfcbb4..91e7b38 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -136,6 +136,15 @@ pub enum ParsedCliChannel { Cable(CableChannelPrototype), } +impl ParsedCliChannel { + pub fn name(&self) -> String { + match self { + Self::Builtin(c) => c.to_string(), + Self::Cable(c) => c.name.clone(), + } + } +} + const CLI_KEYBINDINGS_DELIMITER: char = ';'; /// Parse the keybindings string into a hashmap of key -> action. @@ -154,7 +163,7 @@ fn parse_keybindings(cli_keybindings: &str) -> Result { toml::from_str(&toml_definition).map_err(|e| anyhow!(e)) } -fn parse_channel(channel: &str) -> Result { +pub fn parse_channel(channel: &str) -> Result { let cable_channels = cable::load_cable_channels().unwrap_or_default(); // try to parse the channel as a cable channel cable_channels @@ -165,7 +174,7 @@ fn parse_channel(channel: &str) -> Result { // try to parse the channel as a builtin channel CliTvChannel::try_from(channel) .map(ParsedCliChannel::Builtin) - .map_err(|_| anyhow!("Unknown channel: {}", channel)) + .map_err(|_| anyhow!("Unknown channel: '{}'", channel)) }, |(_, v)| Ok(ParsedCliChannel::Cable(v.clone())), ) @@ -223,15 +232,13 @@ pub fn list_channels() { pub fn guess_channel_from_prompt( prompt: &str, command_mapping: &FxHashMap, + fallback_channel: ParsedCliChannel, ) -> Result { debug!("Guessing channel from prompt: {}", prompt); // git checkout -qf // --- -------- --- <--------- if prompt.trim().is_empty() { - return match command_mapping.get("") { - Some(channel) => parse_channel(channel), - None => Err(anyhow!("No channel found for prompt: {}", prompt)), - }; + return Ok(fallback_channel); } let rev_prompt_words = prompt.split_whitespace().rev(); let mut stack = Vec::new(); @@ -259,7 +266,9 @@ pub fn guess_channel_from_prompt( // reset the stack stack.clear(); } - Err(anyhow!("No channel found for prompt: {}", prompt)) + + debug!("No match found, falling back to default channel"); + Ok(fallback_channel) } const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION"); @@ -458,4 +467,61 @@ mod tests { assert_eq!(post_processed_cli.keybindings, Some(expected)); } + + fn guess_channel_from_prompt_setup( + ) -> (FxHashMap, ParsedCliChannel) { + let mut command_mapping = FxHashMap::default(); + command_mapping.insert("vim".to_string(), "files".to_string()); + command_mapping.insert("export".to_string(), "env".to_string()); + + ( + command_mapping, + ParsedCliChannel::Builtin(CliTvChannel::Env), + ) + } + + #[test] + fn test_guess_channel_from_prompt_present() { + let prompt = "vim -d file1"; + + let (command_mapping, fallback) = guess_channel_from_prompt_setup(); + + let channel = + guess_channel_from_prompt(prompt, &command_mapping, fallback) + .unwrap(); + + assert_eq!(channel.name(), "files"); + } + + #[test] + fn test_guess_channel_from_prompt_fallback() { + let prompt = "git checkout "; + + let (command_mapping, fallback) = guess_channel_from_prompt_setup(); + + let channel = guess_channel_from_prompt( + prompt, + &command_mapping, + fallback.clone(), + ) + .unwrap(); + + assert_eq!(channel, fallback); + } + + #[test] + fn test_guess_channel_from_prompt_empty() { + let prompt = ""; + + let (command_mapping, fallback) = guess_channel_from_prompt_setup(); + + let channel = guess_channel_from_prompt( + prompt, + &command_mapping, + fallback.clone(), + ) + .unwrap(); + + assert_eq!(channel, fallback); + } } diff --git a/television/config/mod.rs b/television/config/mod.rs index e6f7df7..de7a47e 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -157,6 +157,12 @@ impl Config { mut default: Config, mut user: Config, ) -> Config { + // merge shell integration fallback channel with default fallback channel + if user.shell_integration.fallback_channel.is_empty() { + user.shell_integration + .fallback_channel + .clone_from(&default.shell_integration.fallback_channel); + } // merge shell integration triggers with commands default.shell_integration.merge_triggers(); user.shell_integration.merge_triggers(); diff --git a/television/config/shell_integration.rs b/television/config/shell_integration.rs index 35ada93..e85481a 100644 --- a/television/config/shell_integration.rs +++ b/television/config/shell_integration.rs @@ -15,6 +15,7 @@ pub struct ShellIntegrationConfig { pub commands: FxHashMap, /// {channel: [commands]} pub channel_triggers: FxHashMap>, + pub fallback_channel: String, pub keybindings: FxHashMap, } diff --git a/television/event.rs b/television/event.rs index 91e8abb..842a94a 100644 --- a/television/event.rs +++ b/television/event.rs @@ -149,13 +149,28 @@ async fn poll_event(timeout: Duration) -> bool { PollFuture { timeout }.await } +fn flush_existing_events() { + let mut counter = 0; + while let Ok(true) = crossterm::event::poll(Duration::from_millis(0)) { + if let Ok(crossterm::event::Event::Key(_)) = crossterm::event::read() { + counter += 1; + } + } + if counter > 0 { + debug!("Flushed {} existing events", counter); + } +} + impl EventLoop { + // FIXME: this init parameter doesn't seem to be used anymore pub fn new(tick_rate: f64, init: bool) -> Self { let (tx, rx) = mpsc::unbounded_channel(); let tick_interval = Duration::from_secs_f64(1.0 / tick_rate); let (abort, mut abort_recv) = mpsc::unbounded_channel(); + flush_existing_events(); + if init { //let mut reader = crossterm::event::EventStream::new(); tokio::spawn(async move { diff --git a/television/main.rs b/television/main.rs index 6bdeabf..a0e11b1 100644 --- a/television/main.rs +++ b/television/main.rs @@ -6,6 +6,7 @@ use std::process::exit; use anyhow::Result; use clap::Parser; use television::channels::cable::PreviewKind; +use television::cli::parse_channel; use television::utils::clipboard::CLIPBOARD; use tracing::{debug, error, info}; @@ -31,28 +32,37 @@ async fn main() -> Result<()> { television::errors::init()?; television::logging::init()?; - // post-process the CLI arguments - let args: PostProcessedCli = Cli::parse().into(); - debug!("{:?}", args); + debug!("\n\n==== NEW SESSION =====\n"); + + // 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()?)?; // optionally handle subcommands + debug!("Handling subcommands..."); args.command.as_ref().map(handle_subcommands); // optionally change the working directory args.working_directory.as_ref().map(set_current_dir); // optionally override configuration values with CLI arguments + debug!("Applying CLI overrides..."); apply_cli_overrides(&args, &mut config); // determine the channel to use based on the CLI arguments and configuration + debug!("Determining channel..."); let channel = determine_channel(args.clone(), &config, is_readable_stdin())?; CLIPBOARD.with(<_>::default); + debug!("Creating application..."); let mut app = App::new(channel, config, &args.passthrough_keybindings, args.input); stdout().flush()?; @@ -141,9 +151,11 @@ pub fn determine_channel( }, ))) } else if let Some(prompt) = args.autocomplete_prompt { + debug!("Using autocomplete prompt: {:?}", prompt); let channel = guess_channel_from_prompt( &prompt, &config.shell_integration.commands, + parse_channel(&config.shell_integration.fallback_channel)?, )?; debug!("Using guessed channel: {:?}", channel); match channel { @@ -217,6 +229,7 @@ mod tests { let mut config = Config { shell_integration: television::config::shell_integration::ShellIntegrationConfig { + fallback_channel: "files".to_string(), commands: FxHashMap::default(), channel_triggers: { let mut m = FxHashMap::default();