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

This commit is contained in:
Alexandre Pasmantier 2025-03-18 19:07:59 +01:00
parent 1e4c34fecd
commit 2c2ec0e3bb
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::utils::command::shell_command;
#[derive(Debug, Clone)]
enum PreviewKind {
#[derive(Debug, Clone, PartialEq)]
pub enum PreviewKind {
Command(PreviewCommand),
Builtin(PreviewType),
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);
let re = Regex::new(r"^\:(\w+)\:$").unwrap();
if let Some(captures) = re.captures(&command.command) {

View File

@ -18,7 +18,7 @@ pub struct 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 injector = matcher.injector();
@ -26,7 +26,7 @@ impl Channel {
Self {
matcher,
preview_type: preview_type.unwrap_or_default(),
preview_type,
selected_entries: HashSet::with_hasher(FxBuildHasher),
}
}
@ -34,7 +34,7 @@ impl Channel {
impl Default for Channel {
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 tracing::debug;
use crate::channels::cable::{parse_preview_kind, PreviewKind};
use crate::channels::{
cable::CableChannelPrototype, entry::PreviewCommand, CliTvChannel,
};
use crate::cli::args::{Cli, Command, Shell};
use crate::utils::shell::Shell as UtilShell;
use crate::cli::args::{Cli, Command};
use crate::{
cable,
config::{get_config_dir, get_data_dir},
@ -16,22 +16,10 @@ use crate::{
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)]
pub struct PostProcessedCli {
pub channel: ParsedCliChannel,
pub preview_command: Option<PreviewCommand>,
pub preview_kind: PreviewKind,
pub no_preview: bool,
pub tick_rate: Option<f64>,
pub frame_rate: Option<f64>,
@ -46,7 +34,7 @@ impl Default for PostProcessedCli {
fn default() -> Self {
Self {
channel: ParsedCliChannel::Builtin(CliTvChannel::Files),
preview_command: None,
preview_kind: PreviewKind::None,
no_preview: false,
tick_rate: None,
frame_rate: None,
@ -68,10 +56,17 @@ impl From<Cli> for PostProcessedCli {
.map(std::string::ToString::to_string)
.collect();
let preview_command = cli.preview.map(|preview| PreviewCommand {
let preview_kind = cli
.preview
.map(|preview| PreviewCommand {
command: preview,
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 working_directory: Option<String>;
@ -98,7 +93,7 @@ impl From<Cli> for PostProcessedCli {
Self {
channel,
preview_command,
preview_kind,
no_preview: cli.no_preview,
tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate,
@ -264,6 +259,8 @@ Data directory: {data_dir_path}"
#[cfg(test)]
mod tests {
use crate::channels::entry::PreviewType;
use super::*;
#[test]
@ -290,8 +287,8 @@ mod tests {
ParsedCliChannel::Builtin(CliTvChannel::Files)
);
assert_eq!(
post_processed_cli.preview_command,
Some(PreviewCommand {
post_processed_cli.preview_kind,
PreviewKind::Command(PreviewCommand {
command: "bat -n --color=always {}".to_string(),
delimiter: ":".to_string()
})
@ -337,4 +334,52 @@ mod tests {
);
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 clap::Parser;
use television::channels::cable::PreviewKind;
use television::utils::clipboard::CLIPBOARD;
use tracing::{debug, error, info};
@ -30,67 +31,37 @@ async fn main() -> Result<()> {
television::errors::init()?;
television::logging::init()?;
// post-process the CLI arguments
let args: PostProcessedCli = Cli::parse().into();
debug!("{:?}", args);
// load the configuration file
let mut config = Config::new(&ConfigEnv::init()?)?;
if let Some(command) = args.command {
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);
}
}
}
// optionally handle 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
config.config.tick_rate =
args.tick_rate.unwrap_or(config.config.tick_rate);
if args.no_preview {
config.ui.show_preview_panel = false;
}
if let Some(working_directory) = &args.working_directory {
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);
// determine the channel to use based on the CLI arguments and configuration
let channel =
determine_channel(args.clone(), &config, is_readable_stdin())?;
CLIPBOARD.with(<_>::default);
let mut app =
App::new(channel, config, &args.passthrough_keybindings, args.input);
stdout().flush()?;
let output = app.run(stdout().is_terminal(), false).await?;
info!("{:?}", output);
// lock stdout
let stdout_handle = stdout().lock();
let mut bufwriter = BufWriter::new(stdout_handle);
if let Some(passthrough) = output.passthrough {
@ -105,6 +76,42 @@ async fn main() -> Result<()> {
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(
args: PostProcessedCli,
config: &Config,
@ -113,7 +120,13 @@ pub fn determine_channel(
if readable_stdin {
debug!("Using stdin channel");
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 {
let channel = guess_channel_from_prompt(
@ -175,7 +188,7 @@ mod tests {
&args,
&config,
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 anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Shell {
Bash,
@ -9,6 +11,30 @@ pub enum 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,
}
}
}
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_BASH: &str = include_str!("shell/completion.bash");
const COMPLETION_FISH: &str = include_str!("shell/completion.fish");