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

View File

@ -4,7 +4,10 @@ use std::{
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.
///
@ -73,19 +76,19 @@ impl ChannelPrototype {
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";
impl Default for ChannelPrototype {
fn default() -> Self {
Cable::default()
.get(DEFAULT_PROTOTYPE_NAME)
.cloned()
.unwrap()
}
}
impl Display for ChannelPrototype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
@ -109,12 +112,14 @@ impl Deref for Cable {
}
impl Cable {
pub fn default_channel(&self) -> ChannelPrototype {
self.get(DEFAULT_PROTOTYPE_NAME)
pub fn get_channel(&self, name: &str) -> ChannelPrototype {
self.get(name)
.cloned()
.unwrap_or_else(|| {
panic!("Default channel '{DEFAULT_PROTOTYPE_NAME}' not found")
})
.unwrap_or_else(|| unknown_channel_exit(name))
}
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)]
#[derive(Debug, Clone)]
pub struct PostProcessedCli {
pub channel: ChannelPrototype,
pub channel: Option<String>,
pub preview_command: Option<PreviewCommand>,
pub no_preview: bool,
pub tick_rate: Option<f64>,
@ -40,7 +40,7 @@ pub struct PostProcessedCli {
impl Default for PostProcessedCli {
fn default() -> Self {
Self {
channel: ChannelPrototype::default(),
channel: None,
preview_command: None,
no_preview: false,
tick_rate: None,
@ -60,87 +60,59 @@ impl Default for PostProcessedCli {
}
}
impl From<Cli> for PostProcessedCli {
fn from(cli: Cli) -> Self {
// parse literal keybindings passed through the CLI
let keybindings: Option<KeyBindings> = cli.keybindings.map(|kb| {
parse_keybindings_literal(&kb, CLI_KEYBINDINGS_DELIMITER)
.map_err(|e| {
cli_parsing_error_exit(&e.to_string());
})
.unwrap()
});
pub fn post_process(cli: Cli, cable: &Cable) -> PostProcessedCli {
// Parse literal keybindings passed through the CLI
let keybindings = cli.keybindings.as_ref().map(|kb| {
parse_keybindings_literal(kb, CLI_KEYBINDINGS_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
// parse the preview command if provided
let preview_command = cli.preview.map(|preview| PreviewCommand {
command: preview,
delimiter: cli.delimiter.clone(),
offset_expr: cli.preview_offset.clone(),
});
// Parse the preview command if provided
let preview_command = cli.preview.as_ref().map(|preview| PreviewCommand {
command: preview.clone(),
delimiter: cli.delimiter.clone(),
offset_expr: cli.preview_offset.clone(),
});
let mut channel: ChannelPrototype;
let working_directory: Option<String>;
let cable = cable::load_cable().unwrap_or_default();
if cli.channel.is_none() {
channel = cable.default_channel();
working_directory = cli.working_directory;
} else {
let cli_channel = cli.channel.as_ref().unwrap().to_owned();
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!();
}
}
// Determine channel and working_directory
let (channel, working_directory) = match &cli.channel {
Some(c) if !cable.has_channel(c) => {
if cli.working_directory.is_none() && Path::new(c).exists() {
(None, Some(c.clone()))
} else {
unknown_channel_exit(c);
}
}
_ => (cli.channel.clone(), cli.working_directory.clone()),
};
// override the default previewer
if let Some(preview_cmd) = &preview_command {
channel.preview_command = Some(preview_cmd.clone());
}
Self {
channel,
preview_command,
no_preview: cli.no_preview,
tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate,
input: cli.input,
custom_header: cli.custom_header,
command: cli.command,
working_directory,
autocomplete_prompt: cli.autocomplete_prompt,
keybindings,
exact: cli.exact,
select_1: cli.select_1,
no_remote: cli.no_remote,
no_help: cli.no_help,
ui_scale: cli.ui_scale,
}
PostProcessedCli {
channel,
preview_command,
no_preview: cli.no_preview,
tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate,
input: cli.input,
custom_header: cli.custom_header,
command: cli.command,
working_directory,
autocomplete_prompt: cli.autocomplete_prompt,
keybindings,
exact: cli.exact,
select_1: cli.select_1,
no_remote: cli.no_remote,
no_help: cli.no_help,
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");
std::process::exit(1);
}
fn unknown_channel_exit(channel: &str) {
eprintln!("Unknown channel: {channel}\n");
pub fn unknown_channel_exit(channel: &str) -> ! {
eprintln!("Channel not found: {channel}\n");
std::process::exit(1);
}
@ -165,20 +137,6 @@ fn parse_keybindings_literal(
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() {
for c in cable::load_cable().unwrap_or_default().keys() {
println!("\t{c}");
@ -212,17 +170,17 @@ pub fn guess_channel_from_prompt(
prompt: &str,
command_mapping: &FxHashMap<String, String>,
fallback_channel: &str,
cable_channels: &Cable,
) -> Result<ChannelPrototype> {
cable: &Cable,
) -> ChannelPrototype {
debug!("Guessing channel from prompt: {}", prompt);
// git checkout -qf
// --- -------- --- <---------
let fallback = cable_channels
let fallback = cable
.get(fallback_channel)
.expect("Fallback channel not found in cable channels")
.clone();
if prompt.trim().is_empty() {
return Ok(fallback);
return fallback;
}
let rev_prompt_words = prompt.split_whitespace().rev();
let mut stack = Vec::new();
@ -236,7 +194,7 @@ pub fn guess_channel_from_prompt(
for word in rev_prompt_words.clone() {
// if the stack is empty, we have a match
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 stack.last() == Some(&word) {
@ -245,14 +203,14 @@ pub fn guess_channel_from_prompt(
}
// if the stack is empty, we have a match
if stack.is_empty() {
return parse_channel(channel, cable_channels);
return cable.get_channel(channel);
}
// reset the stack
stack.clear();
}
debug!("No match found, falling back to default channel");
Ok(fallback)
fallback
}
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
@ -304,18 +262,9 @@ mod tests {
..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!(
post_processed_cli.preview_command,
Some(PreviewCommand {
@ -341,9 +290,9 @@ mod tests {
..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!(
post_processed_cli.working_directory,
Some(".".to_string())
@ -364,7 +313,8 @@ mod tests {
..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();
expected.insert(Action::Quit, Binding::SingleKey(Key::Esc));
@ -402,8 +352,7 @@ mod tests {
&command_mapping,
fallback,
&channels,
)
.unwrap();
);
assert_eq!(channel.name, "files");
}
@ -420,8 +369,7 @@ mod tests {
&command_mapping,
fallback,
&channels,
)
.unwrap();
);
assert_eq!(channel.name, fallback);
}
@ -438,8 +386,7 @@ mod tests {
&command_mapping,
fallback,
&channels,
)
.unwrap();
);
assert_eq!(channel.name, fallback);
}

View File

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

View File

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