nkxxll bbbdcb0271
feat(cli): add substring matching with --exact flag (#477)
fixes #368 

@alexpasmantier I tried to parse down the exact flag to the find method,
but I can't find an elegant way to do so because the channel creates its
own configurations for the matcher inside of the `new` method. I started
adding the exact flag to the new function of each of the channels but it
just does not seem right to do so. Do you have an idea on how to improve
the change? I would think of passing the config struct to the new
function and exposing the config of the channel for channel transitions,
but I'm not sure... 🤔

I have done it here for the stdin channel just to show you how it would
look like and it works fine😄! Looking forward to hearing your thoughts
on that 👍

ref issue: #368

---------

Co-authored-by: Alexandre Pasmantier <alex.pasmant@gmail.com>
2025-04-22 00:37:07 +02:00

291 lines
9.0 KiB
Rust

use std::env;
use std::io::{stdout, BufWriter, IsTerminal, Write};
use std::path::Path;
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};
use television::app::{App, AppOptions};
use television::channels::{
entry::PreviewType, stdin::Channel as StdinChannel, TelevisionChannel,
};
use television::cli::{
args::{Cli, Command},
guess_channel_from_prompt, list_channels, ParsedCliChannel,
PostProcessedCli,
};
use television::config::{merge_keybindings, Config, ConfigEnv};
use television::utils::shell::render_autocomplete_script_template;
use television::utils::{
shell::{completion_script, Shell},
stdin::is_readable_stdin,
};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
television::errors::init()?;
television::logging::init()?;
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(|x| handle_subcommands(x, &config));
// 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 options = AppOptions::new(
args.exact,
args.select_1,
args.no_remote,
args.no_help,
config.application.tick_rate,
);
let mut app = App::new(channel, config, args.input, options);
stdout().flush()?;
debug!("Running application...");
let output = app.run(stdout().is_terminal(), false).await?;
info!("App output: {:?}", output);
let stdout_handle = stdout().lock();
let mut bufwriter = BufWriter::new(stdout_handle);
if let Some(entries) = output.selected_entries {
for entry in &entries {
writeln!(bufwriter, "{}", entry.stdout_repr())?;
}
}
bufwriter.flush()?;
exit(0);
}
/// Apply overrides from the CLI arguments to the configuration.
///
/// This function mutates the configuration in place.
fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
if let Some(tick_rate) = args.tick_rate {
config.application.tick_rate = tick_rate;
}
if args.no_preview {
config.ui.show_preview_panel = false;
}
if let Some(keybindings) = &args.keybindings {
config.keybindings =
merge_keybindings(config.keybindings.clone(), keybindings);
}
if let Some(header) = &args.custom_header {
config.ui.custom_header = Some(header.to_string());
}
}
pub fn set_current_dir(path: &String) -> Result<()> {
let path = Path::new(path);
if !path.exists() {
error!("Working directory \"{}\" does not exist", path.display());
println!(
"Error: Working directory \"{}\" does not exist",
path.display()
);
exit(1);
}
env::set_current_dir(path)?;
Ok(())
}
pub fn handle_subcommands(command: &Command, config: &Config) -> Result<()> {
match command {
Command::ListChannels => {
list_channels();
exit(0);
}
Command::InitShell { shell } => {
let target_shell = Shell::from(shell);
// the completion scripts for the various shells are templated
// so that it's possible to override the keybindings triggering
// shell autocomplete and command history in tv
let script = render_autocomplete_script_template(
target_shell,
completion_script(target_shell)?,
&config.shell_integration,
)?;
println!("{script}");
exit(0);
}
}
}
pub fn determine_channel(
args: PostProcessedCli,
config: &Config,
readable_stdin: bool,
) -> Result<TelevisionChannel> {
if readable_stdin {
debug!("Using stdin channel");
Ok(TelevisionChannel::Stdin(StdinChannel::new(
match &args.preview_kind {
PreviewKind::Command(ref preview_command) => {
PreviewType::Command(preview_command.clone())
}
PreviewKind::Builtin(preview_type) => preview_type.clone(),
PreviewKind::None => PreviewType::None,
},
)))
} 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 {
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
ParsedCliChannel::Cable(c) => {
Ok(TelevisionChannel::Cable(c.into()))
}
}
} else {
debug!("Using {:?} channel", args.channel);
match args.channel {
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
ParsedCliChannel::Cable(c) => {
Ok(TelevisionChannel::Cable(c.into()))
}
}
}
}
#[cfg(test)]
mod tests {
use rustc_hash::FxHashMap;
use super::*;
fn assert_is_correct_channel(
args: &PostProcessedCli,
config: &Config,
readable_stdin: bool,
expected_channel: &TelevisionChannel,
) {
let channel =
determine_channel(args.clone(), config, readable_stdin).unwrap();
assert!(
channel.name() == expected_channel.name(),
"Expected {:?} but got {:?}",
expected_channel.name(),
channel.name()
);
}
#[tokio::test]
async fn test_determine_channel_readable_stdin() {
let channel = television::cli::ParsedCliChannel::Builtin(
television::channels::CliTvChannel::Env,
);
let args = PostProcessedCli {
channel,
..Default::default()
};
let config = Config::default();
assert_is_correct_channel(
&args,
&config,
true,
&TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
);
}
#[tokio::test]
async fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string());
let expected_channel = television::channels::TelevisionChannel::Dirs(
television::channels::dirs::Channel::default(),
);
let args = PostProcessedCli {
autocomplete_prompt,
..Default::default()
};
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();
m.insert("dirs".to_string(), vec!["cd".to_string()]);
m
},
keybindings: FxHashMap::default(),
},
..Default::default()
};
config.shell_integration.merge_triggers();
assert_is_correct_channel(&args, &config, false, &expected_channel);
}
#[tokio::test]
async fn test_determine_channel_standard_case() {
let channel = television::cli::ParsedCliChannel::Builtin(
television::channels::CliTvChannel::Dirs,
);
let args = PostProcessedCli {
channel,
..Default::default()
};
let config = Config::default();
assert_is_correct_channel(
&args,
&config,
false,
&TelevisionChannel::Dirs(
television::channels::dirs::Channel::default(),
),
);
}
#[test]
fn test_apply_cli_overrides() {
let mut config = Config::default();
let args = PostProcessedCli {
tick_rate: Some(100_f64),
no_preview: true,
..Default::default()
};
apply_cli_overrides(&args, &mut config);
assert_eq!(config.application.tick_rate, 100_f64);
assert!(!config.ui.show_preview_panel);
}
}