fix(config): use the config default_channel field as a fallback when no channel is specified (#524)

Fixes #520 

fyi @lalvarezt
This commit is contained in:
Alex Pasmantier 2025-05-26 21:16:06 +02:00 committed by GitHub
parent 7bbf538898
commit dfbdd65107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 173 deletions

View File

@ -12,7 +12,7 @@ use television::{
action::Action, action::Action,
channels::{ channels::{
entry::{into_ranges, Entry}, entry::{into_ranges, Entry},
prototypes::{Cable, ChannelPrototype}, prototypes::Cable,
}, },
config::{Config, ConfigEnv}, config::{Config, ConfigEnv},
screen::{colors::ResultsColorscheme, results::build_results_list}, screen::{colors::ResultsColorscheme, results::build_results_list},
@ -464,6 +464,8 @@ pub fn draw(c: &mut Criterion) {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
let cable = Cable::default();
c.bench_function("draw", |b| { c.bench_function("draw", |b| {
b.to_async(&rt).iter_batched( b.to_async(&rt).iter_batched(
// FIXME: this is kind of hacky // FIXME: this is kind of hacky
@ -472,7 +474,7 @@ pub fn draw(c: &mut Criterion) {
let backend = TestBackend::new(width, height); let backend = TestBackend::new(width, height);
let terminal = Terminal::new(backend).unwrap(); let terminal = Terminal::new(backend).unwrap();
let (tx, _) = tokio::sync::mpsc::unbounded_channel(); 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 // Wait for the channel to finish loading
let mut tv = Television::new( let mut tv = Television::new(
tx, tx,

View File

@ -4,7 +4,10 @@ use std::{
ops::Deref, 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. /// A prototype for cable channels.
/// ///
@ -73,19 +76,19 @@ impl ChannelPrototype {
preview_command: preview, preview_command: preview,
} }
} }
pub fn set_preview(self, preview_command: Option<PreviewCommand>) -> Self {
Self::new(
&self.name,
&self.source_command,
self.interactive,
preview_command,
)
}
} }
pub const DEFAULT_PROTOTYPE_NAME: &str = "files"; 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 { impl Display for ChannelPrototype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name) write!(f, "{}", self.name)
@ -109,12 +112,14 @@ impl Deref for Cable {
} }
impl Cable { impl Cable {
pub fn default_channel(&self) -> ChannelPrototype { pub fn get_channel(&self, name: &str) -> ChannelPrototype {
self.get(DEFAULT_PROTOTYPE_NAME) self.get(name)
.cloned() .cloned()
.unwrap_or_else(|| { .unwrap_or_else(|| unknown_channel_exit(name))
panic!("Default channel '{DEFAULT_PROTOTYPE_NAME}' not found") }
})
pub fn has_channel(&self, name: &str) -> bool {
self.contains_key(name)
} }
} }

View File

@ -19,7 +19,7 @@ pub mod args;
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PostProcessedCli { pub struct PostProcessedCli {
pub channel: ChannelPrototype, pub channel: Option<String>,
pub preview_command: Option<PreviewCommand>, pub preview_command: Option<PreviewCommand>,
pub no_preview: bool, pub no_preview: bool,
pub tick_rate: Option<f64>, pub tick_rate: Option<f64>,
@ -40,7 +40,7 @@ pub struct PostProcessedCli {
impl Default for PostProcessedCli { impl Default for PostProcessedCli {
fn default() -> Self { fn default() -> Self {
Self { Self {
channel: ChannelPrototype::default(), channel: None,
preview_command: None, preview_command: None,
no_preview: false, no_preview: false,
tick_rate: None, tick_rate: None,
@ -60,60 +60,33 @@ impl Default for PostProcessedCli {
} }
} }
impl From<Cli> for PostProcessedCli { pub fn post_process(cli: Cli, cable: &Cable) -> PostProcessedCli {
fn from(cli: Cli) -> Self { // Parse literal keybindings passed through the CLI
// parse literal keybindings passed through the CLI let keybindings = cli.keybindings.as_ref().map(|kb| {
let keybindings: Option<KeyBindings> = cli.keybindings.map(|kb| { parse_keybindings_literal(kb, CLI_KEYBINDINGS_DELIMITER)
parse_keybindings_literal(&kb, CLI_KEYBINDINGS_DELIMITER) .unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
.map_err(|e| {
cli_parsing_error_exit(&e.to_string());
})
.unwrap()
}); });
// parse the preview command if provided // Parse the preview command if provided
let preview_command = cli.preview.map(|preview| PreviewCommand { let preview_command = cli.preview.as_ref().map(|preview| PreviewCommand {
command: preview, command: preview.clone(),
delimiter: cli.delimiter.clone(), delimiter: cli.delimiter.clone(),
offset_expr: cli.preview_offset.clone(), offset_expr: cli.preview_offset.clone(),
}); });
let mut channel: ChannelPrototype; // Determine channel and working_directory
let working_directory: Option<String>; let (channel, working_directory) = match &cli.channel {
Some(c) if !cable.has_channel(c) => {
let cable = cable::load_cable().unwrap_or_default(); if cli.working_directory.is_none() && Path::new(c).exists() {
if cli.channel.is_none() { (None, Some(c.clone()))
channel = cable.default_channel();
working_directory = cli.working_directory;
} else { } else {
let cli_channel = cli.channel.as_ref().unwrap().to_owned(); unknown_channel_exit(c);
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!();
}
}
} }
} }
_ => (cli.channel.clone(), cli.working_directory.clone()),
};
// override the default previewer PostProcessedCli {
if let Some(preview_cmd) = &preview_command {
channel.preview_command = Some(preview_cmd.clone());
}
Self {
channel, channel,
preview_command, preview_command,
no_preview: cli.no_preview, no_preview: cli.no_preview,
@ -131,16 +104,15 @@ impl From<Cli> for PostProcessedCli {
no_help: cli.no_help, no_help: cli.no_help,
ui_scale: cli.ui_scale, 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"); eprintln!("Error parsing CLI arguments: {message}\n");
std::process::exit(1); std::process::exit(1);
} }
fn unknown_channel_exit(channel: &str) { pub fn unknown_channel_exit(channel: &str) -> ! {
eprintln!("Unknown channel: {channel}\n"); eprintln!("Channel not found: {channel}\n");
std::process::exit(1); std::process::exit(1);
} }
@ -165,20 +137,6 @@ fn parse_keybindings_literal(
toml::from_str(&toml_definition).map_err(|e| anyhow!(e)) toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
} }
pub fn parse_channel(
channel: &str,
cable_channels: &Cable,
) -> Result<ChannelPrototype> {
// 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() { pub fn list_channels() {
for c in cable::load_cable().unwrap_or_default().keys() { for c in cable::load_cable().unwrap_or_default().keys() {
println!("\t{c}"); println!("\t{c}");
@ -212,17 +170,17 @@ pub fn guess_channel_from_prompt(
prompt: &str, prompt: &str,
command_mapping: &FxHashMap<String, String>, command_mapping: &FxHashMap<String, String>,
fallback_channel: &str, fallback_channel: &str,
cable_channels: &Cable, cable: &Cable,
) -> Result<ChannelPrototype> { ) -> ChannelPrototype {
debug!("Guessing channel from prompt: {}", prompt); debug!("Guessing channel from prompt: {}", prompt);
// git checkout -qf // git checkout -qf
// --- -------- --- <--------- // --- -------- --- <---------
let fallback = cable_channels let fallback = cable
.get(fallback_channel) .get(fallback_channel)
.expect("Fallback channel not found in cable channels") .expect("Fallback channel not found in cable channels")
.clone(); .clone();
if prompt.trim().is_empty() { if prompt.trim().is_empty() {
return Ok(fallback); return fallback;
} }
let rev_prompt_words = prompt.split_whitespace().rev(); let rev_prompt_words = prompt.split_whitespace().rev();
let mut stack = Vec::new(); let mut stack = Vec::new();
@ -236,7 +194,7 @@ pub fn guess_channel_from_prompt(
for word in rev_prompt_words.clone() { for word in rev_prompt_words.clone() {
// if the stack is empty, we have a match // if the stack is empty, we have a match
if stack.is_empty() { 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 the word matches the top of the stack, pop it
if stack.last() == Some(&word) { 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 the stack is empty, we have a match
if stack.is_empty() { if stack.is_empty() {
return parse_channel(channel, cable_channels); return cable.get_channel(channel);
} }
// reset the stack // reset the stack
stack.clear(); stack.clear();
} }
debug!("No match found, falling back to default channel"); debug!("No match found, falling back to default channel");
Ok(fallback) fallback
} }
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION"); const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
@ -304,18 +262,9 @@ mod tests {
..Default::default() ..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!( assert_eq!(
post_processed_cli.preview_command, post_processed_cli.preview_command,
Some(PreviewCommand { Some(PreviewCommand {
@ -341,9 +290,9 @@ mod tests {
..Default::default() ..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!( assert_eq!(
post_processed_cli.working_directory, post_processed_cli.working_directory,
Some(".".to_string()) Some(".".to_string())
@ -364,7 +313,8 @@ mod tests {
..Default::default() ..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(); let mut expected = KeyBindings::default();
expected.insert(Action::Quit, Binding::SingleKey(Key::Esc)); expected.insert(Action::Quit, Binding::SingleKey(Key::Esc));
@ -402,8 +352,7 @@ mod tests {
&command_mapping, &command_mapping,
fallback, fallback,
&channels, &channels,
) );
.unwrap();
assert_eq!(channel.name, "files"); assert_eq!(channel.name, "files");
} }
@ -420,8 +369,7 @@ mod tests {
&command_mapping, &command_mapping,
fallback, fallback,
&channels, &channels,
) );
.unwrap();
assert_eq!(channel.name, fallback); assert_eq!(channel.name, fallback);
} }
@ -438,8 +386,7 @@ mod tests {
&command_mapping, &command_mapping,
fallback, fallback,
&channels, &channels,
) );
.unwrap();
assert_eq!(channel.name, fallback); assert_eq!(channel.name, fallback);
} }

View File

@ -5,8 +5,9 @@ use std::process::exit;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use television::cable::load_cable;
use television::cli::post_process;
use television::{ use television::{
cable,
channels::prototypes::{Cable, ChannelPrototype}, channels::prototypes::{Cable, ChannelPrototype},
utils::clipboard::CLIPBOARD, utils::clipboard::CLIPBOARD,
}; };
@ -35,15 +36,16 @@ async fn main() -> Result<()> {
// process the CLI arguments // process the CLI arguments
let cli = Cli::parse(); let cli = Cli::parse();
debug!("CLI: {:?}", cli); debug!("CLI: {:?}", cli);
let args: PostProcessedCli = cli.into();
debug!("PostProcessedCli: {:?}", args);
// load the configuration file // load the configuration file
debug!("Loading configuration..."); debug!("Loading configuration...");
let mut config = Config::new(&ConfigEnv::init()?)?; let mut config = Config::new(&ConfigEnv::init()?)?;
debug!("Loading cable channels..."); 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 // optionally handle subcommands
debug!("Handling subcommands..."); debug!("Handling subcommands...");
@ -60,12 +62,8 @@ async fn main() -> Result<()> {
// determine the channel to use based on the CLI arguments and configuration // determine the channel to use based on the CLI arguments and configuration
debug!("Determining channel..."); debug!("Determining channel...");
let channel_prototype = determine_channel( let channel_prototype =
args.clone(), determine_channel(&args, &config, is_readable_stdin(), &cable);
&config,
is_readable_stdin(),
&cable_channels,
)?;
CLIPBOARD.with(<_>::default); CLIPBOARD.with(<_>::default);
@ -77,13 +75,8 @@ async fn main() -> Result<()> {
args.no_help, args.no_help,
config.application.tick_rate, config.application.tick_rate,
); );
let mut app = App::new( let mut app =
&channel_prototype, App::new(&channel_prototype, config, args.input, options, &cable);
config,
args.input,
options,
&cable_channels,
);
stdout().flush()?; stdout().flush()?;
debug!("Running application..."); debug!("Running application...");
let output = app.run(stdout().is_terminal(), false).await?; 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( pub fn determine_channel(
args: PostProcessedCli, args: &PostProcessedCli,
config: &Config, config: &Config,
readable_stdin: bool, readable_stdin: bool,
cable_channels: &Cable, cable: &Cable,
) -> Result<ChannelPrototype> { ) -> ChannelPrototype {
if readable_stdin { if readable_stdin {
debug!("Using stdin channel"); debug!("Using stdin channel");
Ok(ChannelPrototype::stdin(args.preview_command)) ChannelPrototype::stdin(args.preview_command.clone())
} else if let Some(prompt) = args.autocomplete_prompt { } else if let Some(prompt) = &args.autocomplete_prompt {
debug!("Using autocomplete prompt: {:?}", prompt); debug!("Using autocomplete prompt: {:?}", prompt);
let channel_prototype = guess_channel_from_prompt( let channel_prototype = guess_channel_from_prompt(
&prompt, prompt,
&config.shell_integration.commands, &config.shell_integration.commands,
&config.shell_integration.fallback_channel, &config.shell_integration.fallback_channel,
cable_channels, cable,
)?; );
debug!("Using guessed channel: {:?}", channel_prototype); debug!("Using guessed channel: {:?}", channel_prototype);
Ok(channel_prototype) channel_prototype
} else { } else {
debug!("Using {:?} channel", args.channel); let channel = args
Ok(args.channel) .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 { mod tests {
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use television::{ use television::{
cable::load_cable, channels::prototypes::ChannelPrototype, cable::load_cable,
channels::{preview::PreviewCommand, prototypes::ChannelPrototype},
}; };
use super::*; use super::*;
@ -199,8 +204,7 @@ mod tests {
let channels: Cable = let channels: Cable =
cable_channels.unwrap_or_else(|| load_cable().unwrap_or_default()); cable_channels.unwrap_or_else(|| load_cable().unwrap_or_default());
let channel = let channel =
determine_channel(args.clone(), config, readable_stdin, &channels) determine_channel(args, config, readable_stdin, &channels);
.unwrap();
assert_eq!( assert_eq!(
channel.name, expected_channel.name, channel.name, expected_channel.name,
@ -209,14 +213,10 @@ mod tests {
); );
} }
#[tokio::test] #[test]
/// Test that the channel is stdin when stdin is readable /// Test that the channel is stdin when stdin is readable
async fn test_determine_channel_readable_stdin() { fn test_determine_channel_readable_stdin() {
let channel = ChannelPrototype::default(); let args = PostProcessedCli::default();
let args = PostProcessedCli {
channel,
..Default::default()
};
let config = Config::default(); let config = Config::default();
assert_is_correct_channel( assert_is_correct_channel(
&args, &args,
@ -227,8 +227,8 @@ mod tests {
); );
} }
#[tokio::test] #[test]
async fn test_determine_channel_autocomplete_prompt() { fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string()); let autocomplete_prompt = Some("cd".to_string());
let expected_channel = let expected_channel =
ChannelPrototype::new("dirs", "ls {}", false, None); ChannelPrototype::new("dirs", "ls {}", false, None);
@ -261,9 +261,9 @@ mod tests {
); );
} }
#[tokio::test] #[test]
async fn test_determine_channel_standard_case() { fn test_determine_channel_standard_case() {
let channel = ChannelPrototype::new("dirs", "", false, None); let channel = Some(String::from("dirs"));
let args = PostProcessedCli { let args = PostProcessedCli {
channel, channel,
..Default::default() ..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] #[test]
fn test_apply_cli_overrides() { fn test_apply_cli_overrides() {
let mut config = Config::default(); let mut config = Config::default();

View File

@ -33,7 +33,7 @@ fn setup_app(
.join("tests") .join("tests")
.join("target_dir"); .join("target_dir");
std::env::set_current_dir(&target_dir).unwrap(); std::env::set_current_dir(&target_dir).unwrap();
ChannelPrototype::default() Cable::default().get("files").unwrap().clone()
}); });
let mut config = default_config_from_file().unwrap(); let mut config = default_config_from_file().unwrap();
// this speeds up the tests // this speeds up the tests