feat(cli): allow passing builtin previewers through the cli (e.g. --preview ':files:') (#403)

fixes #392
This commit is contained in:
Alexandre Pasmantier 2025-03-18 19:10:40 +01:00 committed by GitHub
parent 1e4c34fecd
commit 47ea5a2b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 156 additions and 72 deletions

View File

@ -19,8 +19,8 @@ use crate::matcher::Matcher;
use crate::matcher::{config::Config, injector::Injector}; use crate::matcher::{config::Config, injector::Injector};
use crate::utils::command::shell_command; use crate::utils::command::shell_command;
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
enum PreviewKind { pub enum PreviewKind {
Command(PreviewCommand), Command(PreviewCommand),
Builtin(PreviewType), Builtin(PreviewType),
None, None,
@ -63,7 +63,7 @@ impl From<CableChannelPrototype> for Channel {
} }
} }
fn parse_preview_kind(command: &PreviewCommand) -> Result<PreviewKind> { pub fn parse_preview_kind(command: &PreviewCommand) -> Result<PreviewKind> {
debug!("Parsing preview kind for command: {:?}", command); debug!("Parsing preview kind for command: {:?}", command);
let re = Regex::new(r"^\:(\w+)\:$").unwrap(); let re = Regex::new(r"^\:(\w+)\:$").unwrap();
if let Some(captures) = re.captures(&command.command) { if let Some(captures) = re.captures(&command.command) {

View File

@ -18,7 +18,7 @@ pub struct Channel {
} }
impl Channel { impl Channel {
pub fn new(preview_type: Option<PreviewType>) -> Self { pub fn new(preview_type: PreviewType) -> Self {
let matcher = Matcher::new(Config::default()); let matcher = Matcher::new(Config::default());
let injector = matcher.injector(); let injector = matcher.injector();
@ -26,7 +26,7 @@ impl Channel {
Self { Self {
matcher, matcher,
preview_type: preview_type.unwrap_or_default(), preview_type,
selected_entries: HashSet::with_hasher(FxBuildHasher), selected_entries: HashSet::with_hasher(FxBuildHasher),
} }
} }
@ -34,7 +34,7 @@ impl Channel {
impl Default for Channel { impl Default for Channel {
fn default() -> Self { fn default() -> Self {
Self::new(None) Self::new(PreviewType::default())
} }
} }

View File

@ -4,11 +4,11 @@ use std::path::Path;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use tracing::debug; use tracing::debug;
use crate::channels::cable::{parse_preview_kind, PreviewKind};
use crate::channels::{ use crate::channels::{
cable::CableChannelPrototype, entry::PreviewCommand, CliTvChannel, cable::CableChannelPrototype, entry::PreviewCommand, CliTvChannel,
}; };
use crate::cli::args::{Cli, Command, Shell}; use crate::cli::args::{Cli, Command};
use crate::utils::shell::Shell as UtilShell;
use crate::{ use crate::{
cable, cable,
config::{get_config_dir, get_data_dir}, config::{get_config_dir, get_data_dir},
@ -16,22 +16,10 @@ use crate::{
pub mod args; pub mod args;
impl From<Shell> for UtilShell {
fn from(val: Shell) -> Self {
match val {
Shell::Bash => UtilShell::Bash,
Shell::Zsh => UtilShell::Zsh,
Shell::Fish => UtilShell::Fish,
Shell::PowerShell => UtilShell::PowerShell,
Shell::Cmd => UtilShell::Cmd,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PostProcessedCli { pub struct PostProcessedCli {
pub channel: ParsedCliChannel, pub channel: ParsedCliChannel,
pub preview_command: Option<PreviewCommand>, pub preview_kind: PreviewKind,
pub no_preview: bool, pub no_preview: bool,
pub tick_rate: Option<f64>, pub tick_rate: Option<f64>,
pub frame_rate: Option<f64>, pub frame_rate: Option<f64>,
@ -46,7 +34,7 @@ impl Default for PostProcessedCli {
fn default() -> Self { fn default() -> Self {
Self { Self {
channel: ParsedCliChannel::Builtin(CliTvChannel::Files), channel: ParsedCliChannel::Builtin(CliTvChannel::Files),
preview_command: None, preview_kind: PreviewKind::None,
no_preview: false, no_preview: false,
tick_rate: None, tick_rate: None,
frame_rate: None, frame_rate: None,
@ -68,10 +56,17 @@ impl From<Cli> for PostProcessedCli {
.map(std::string::ToString::to_string) .map(std::string::ToString::to_string)
.collect(); .collect();
let preview_command = cli.preview.map(|preview| PreviewCommand { let preview_kind = cli
.preview
.map(|preview| PreviewCommand {
command: preview, command: preview,
delimiter: cli.delimiter.clone(), delimiter: cli.delimiter.clone(),
}); })
.map(|preview_command| {
parse_preview_kind(&preview_command)
.expect("Error parsing preview command")
})
.unwrap_or(PreviewKind::None);
let channel: ParsedCliChannel; let channel: ParsedCliChannel;
let working_directory: Option<String>; let working_directory: Option<String>;
@ -98,7 +93,7 @@ impl From<Cli> for PostProcessedCli {
Self { Self {
channel, channel,
preview_command, preview_kind,
no_preview: cli.no_preview, no_preview: cli.no_preview,
tick_rate: cli.tick_rate, tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate, frame_rate: cli.frame_rate,
@ -264,6 +259,8 @@ Data directory: {data_dir_path}"
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::channels::entry::PreviewType;
use super::*; use super::*;
#[test] #[test]
@ -290,8 +287,8 @@ mod tests {
ParsedCliChannel::Builtin(CliTvChannel::Files) ParsedCliChannel::Builtin(CliTvChannel::Files)
); );
assert_eq!( assert_eq!(
post_processed_cli.preview_command, post_processed_cli.preview_kind,
Some(PreviewCommand { PreviewKind::Command(PreviewCommand {
command: "bat -n --color=always {}".to_string(), command: "bat -n --color=always {}".to_string(),
delimiter: ":".to_string() delimiter: ":".to_string()
}) })
@ -337,4 +334,52 @@ mod tests {
); );
assert_eq!(post_processed_cli.command, None); assert_eq!(post_processed_cli.command, None);
} }
#[test]
fn test_builtin_previewer_files() {
let cli = Cli {
channel: "files".to_string(),
preview: Some(":files:".to_string()),
no_preview: false,
delimiter: ":".to_string(),
tick_rate: Some(50.0),
frame_rate: Some(60.0),
passthrough_keybindings: None,
input: None,
command: None,
working_directory: None,
autocomplete_prompt: None,
};
let post_processed_cli: PostProcessedCli = cli.into();
assert_eq!(
post_processed_cli.preview_kind,
PreviewKind::Builtin(PreviewType::Files)
);
}
#[test]
fn test_builtin_previewer_env() {
let cli = Cli {
channel: "files".to_string(),
preview: Some(":env_var:".to_string()),
no_preview: false,
delimiter: ":".to_string(),
tick_rate: Some(50.0),
frame_rate: Some(60.0),
passthrough_keybindings: None,
input: None,
command: None,
working_directory: None,
autocomplete_prompt: None,
};
let post_processed_cli: PostProcessedCli = cli.into();
assert_eq!(
post_processed_cli.preview_kind,
PreviewKind::Builtin(PreviewType::EnvVar)
);
}
} }

View File

@ -5,6 +5,7 @@ use std::process::exit;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use television::channels::cable::PreviewKind;
use television::utils::clipboard::CLIPBOARD; use television::utils::clipboard::CLIPBOARD;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
@ -30,67 +31,37 @@ async fn main() -> Result<()> {
television::errors::init()?; television::errors::init()?;
television::logging::init()?; television::logging::init()?;
// post-process the CLI arguments
let args: PostProcessedCli = Cli::parse().into(); let args: PostProcessedCli = Cli::parse().into();
debug!("{:?}", args); debug!("{:?}", args);
// load the configuration file
let mut config = Config::new(&ConfigEnv::init()?)?; let mut config = Config::new(&ConfigEnv::init()?)?;
if let Some(command) = args.command { // optionally handle subcommands
match command { args.command.as_ref().map(handle_subcommands);
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);
}
}
}
// optionally change the working directory
args.working_directory.as_ref().map(set_current_dir);
// optionally override configuration values with CLI arguments
config.config.tick_rate = config.config.tick_rate =
args.tick_rate.unwrap_or(config.config.tick_rate); args.tick_rate.unwrap_or(config.config.tick_rate);
if args.no_preview { if args.no_preview {
config.ui.show_preview_panel = false; config.ui.show_preview_panel = false;
} }
if let Some(working_directory) = &args.working_directory { // determine the channel to use based on the CLI arguments and configuration
let path = Path::new(working_directory);
if !path.exists() {
error!(
"Working directory \"{}\" does not exist",
&working_directory
);
println!(
"Error: Working directory \"{}\" does not exist",
&working_directory
);
exit(1);
}
env::set_current_dir(path)?;
}
CLIPBOARD.with(<_>::default);
let channel = let channel =
determine_channel(args.clone(), &config, is_readable_stdin())?; determine_channel(args.clone(), &config, is_readable_stdin())?;
CLIPBOARD.with(<_>::default);
let mut app = let mut app =
App::new(channel, config, &args.passthrough_keybindings, args.input); App::new(channel, config, &args.passthrough_keybindings, args.input);
stdout().flush()?; stdout().flush()?;
let output = app.run(stdout().is_terminal(), false).await?; let output = app.run(stdout().is_terminal(), false).await?;
info!("{:?}", output); info!("{:?}", output);
// lock stdout
let stdout_handle = stdout().lock(); let stdout_handle = stdout().lock();
let mut bufwriter = BufWriter::new(stdout_handle); let mut bufwriter = BufWriter::new(stdout_handle);
if let Some(passthrough) = output.passthrough { if let Some(passthrough) = output.passthrough {
@ -105,6 +76,42 @@ async fn main() -> Result<()> {
exit(0); exit(0);
} }
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) -> 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::default().shell_integration,
)?;
println!("{script}");
exit(0);
}
}
}
pub fn determine_channel( pub fn determine_channel(
args: PostProcessedCli, args: PostProcessedCli,
config: &Config, config: &Config,
@ -113,7 +120,13 @@ pub fn determine_channel(
if readable_stdin { if readable_stdin {
debug!("Using stdin channel"); debug!("Using stdin channel");
Ok(TelevisionChannel::Stdin(StdinChannel::new( Ok(TelevisionChannel::Stdin(StdinChannel::new(
args.preview_command.map(PreviewType::Command), 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 { } else if let Some(prompt) = args.autocomplete_prompt {
let channel = guess_channel_from_prompt( let channel = guess_channel_from_prompt(
@ -175,7 +188,7 @@ mod tests {
&args, &args,
&config, &config,
true, true,
&TelevisionChannel::Stdin(StdinChannel::new(None)), &TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
); );
} }

View File

@ -1,5 +1,7 @@
use crate::cli::args::Shell as CliShell;
use crate::config::shell_integration::ShellIntegrationConfig; use crate::config::shell_integration::ShellIntegrationConfig;
use anyhow::Result; use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Shell { pub enum Shell {
Bash, Bash,
@ -9,6 +11,30 @@ pub enum Shell {
Cmd, Cmd,
} }
impl From<CliShell> for Shell {
fn from(val: CliShell) -> Self {
match val {
CliShell::Bash => Shell::Bash,
CliShell::Zsh => Shell::Zsh,
CliShell::Fish => Shell::Fish,
CliShell::PowerShell => Shell::PowerShell,
CliShell::Cmd => Shell::Cmd,
}
}
}
impl From<&CliShell> for Shell {
fn from(val: &CliShell) -> Self {
match val {
CliShell::Bash => Shell::Bash,
CliShell::Zsh => Shell::Zsh,
CliShell::Fish => Shell::Fish,
CliShell::PowerShell => Shell::PowerShell,
CliShell::Cmd => Shell::Cmd,
}
}
}
const COMPLETION_ZSH: &str = include_str!("shell/completion.zsh"); const COMPLETION_ZSH: &str = include_str!("shell/completion.zsh");
const COMPLETION_BASH: &str = include_str!("shell/completion.bash"); const COMPLETION_BASH: &str = include_str!("shell/completion.bash");
const COMPLETION_FISH: &str = include_str!("shell/completion.fish"); const COMPLETION_FISH: &str = include_str!("shell/completion.fish");