This commit is contained in:
Alex Pasmantier 2025-07-27 01:01:56 +02:00
parent e3279854fd
commit 1957d8d6ee
8 changed files with 339 additions and 483 deletions

View File

@ -16,7 +16,7 @@ use television::{
action::Action,
cable::Cable,
channels::entry::{Entry, into_ranges},
cli::PostProcessedCli,
cli::ProcessedCli,
config::{Config, ConfigEnv},
screen::{colors::ResultsColorscheme, result_item::build_results_list},
television::Television,
@ -486,7 +486,7 @@ pub fn draw(c: &mut Criterion) {
channel_prototype,
config,
cable.clone(),
PostProcessedCli::default(),
ProcessedCli::default(),
);
tv.find("television");
for _ in 0..5 {

View File

@ -2,7 +2,7 @@ use crate::{
action::Action,
cable::Cable,
channels::{entry::Entry, prototypes::ChannelPrototype},
cli::PostProcessedCli,
cli::ProcessedCli,
config::{Config, DEFAULT_PREVIEW_SIZE, default_tick_rate},
event::{Event, EventLoop, InputEvent, Key, MouseInputEvent},
history::History,
@ -190,7 +190,7 @@ impl App {
config: Config,
options: AppOptions,
cable_channels: Cable,
cli_args: &PostProcessedCli,
cli_args: &ProcessedCli,
) -> Self {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, render_rx) = mpsc::unbounded_channel();

View File

@ -36,17 +36,6 @@ pub struct Cli {
#[arg(index = 1, verbatim_doc_comment)]
pub channel: Option<String>,
/// A preview line number offset template to use to scroll the preview to for each
/// entry.
///
/// When a channel is specified: This overrides the offset defined in the channel prototype.
/// When no channel is specified: This flag requires --preview-command to be set.
///
/// This template uses the same syntax as the `preview` option and will be formatted
/// using the currently selected entry.
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
pub preview_offset: Option<String>,
/// Disable the preview panel entirely on startup.
///
/// This flag works identically in both channel mode and ad-hoc mode.
@ -310,6 +299,29 @@ pub struct Cli {
)]
pub preview_command: Option<String>,
/// A preview line number offset template to use to scroll the preview to for each
/// entry.
///
/// When a channel is specified: This overrides the offset defined in the channel prototype.
/// When no channel is specified: This flag requires --preview-command to be set.
///
/// This template uses the same syntax as the `preview` option and will be formatted
/// using the currently selected entry.
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
pub preview_offset: Option<String>,
/// Whether to cache the preview command output.
///
/// This can be useful when the preview command is expensive to run or when the
/// output is static and doesn't change often.
#[arg(
long,
default_value = "false",
verbatim_doc_comment,
conflicts_with = "no_preview"
)]
pub preview_cached: bool,
/// Layout orientation for the UI.
///
/// When a channel is specified: Overrides the layout/orientation defined in the channel prototype.

View File

@ -4,8 +4,7 @@ use crate::{
channels::prototypes::{ChannelPrototype, Template},
cli::args::{Cli, Command},
config::{
DEFAULT_PREVIEW_SIZE, KeyBindings, get_config_dir, get_data_dir,
merge_bindings,
KeyBindings, get_config_dir, get_data_dir, merge_bindings,
shell_integration::ShellIntegrationConfig,
ui::{BorderType, Padding},
},
@ -26,269 +25,131 @@ use tracing::debug;
pub mod args;
pub struct ChannelArguments {
pub channel: Option<String>,
pub working_directory: Option<PathBuf>,
pub prototype: Option<CliPrototype>,
pub no_preview: bool,
pub hide_preview: bool,
pub show_preview: bool,
pub preview_size: Option<u16>,
pub preview_header: Option<String>,
pub preview_footer: Option<String>,
pub autocomplete_prompt: Option<String>,
}
struct CliPrototype {
// source
pub source_command: Option<Template>,
pub source_entry_delimiter: Option<char>,
pub ansi: bool,
pub source_display: Option<Template>,
pub source_output: Option<Template>,
// preview
pub preview_command: Option<Template>,
pub preview_offset: Option<Template>,
pub preview_cached: Option<bool>,
// ui
pub ui_scale: Option<u16>,
pub
}
/// # CLI Use Cases
///
/// The CLI interface supports two primary use cases:
///
/// ## 1. Channel-based mode (channel is specified)
/// When a channel is provided, the CLI operates in **override mode**:
/// - The channel provides the base configuration (source, preview, UI settings)
/// - All CLI flags act as **overrides** to the channel's defaults
/// - Most restrictions are enforced at the clap level using `conflicts_with`
/// - Templates and keybindings are validated after clap parsing
/// - More permissive - allows any combination of flags as they override channel defaults
///
/// ## 2. Ad-hoc mode (no channel specified)
/// When no channel is provided, the CLI creates an **ad-hoc channel**:
/// - Stricter validation rules apply for interdependent flags
/// - `--preview-*` flags require `--preview-command` to be set
/// - `--source-*` flags require `--source-command` to be set
/// - This ensures the ad-hoc channel has all necessary components to function
///
/// The validation logic in `post_process()` enforces these constraints for ad-hoc mode
/// while allowing full flexibility in channel-based mode.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct PostProcessedCli {
// Channel and source configuration
pub struct ProcessedCli {
// general
pub workdir: Option<PathBuf>,
pub input: Option<String>,
pub global_history: bool,
pub config_file: Option<PathBuf>,
pub cable_dir: Option<PathBuf>,
pub command: Option<Command>,
// shell integration
pub autocomplete_prompt: Option<String>,
// channel arguments
pub channel: Option<String>,
pub prototype: CliPrototype,
// matching behavior
pub exact: bool,
pub select_1: bool,
pub take_1: bool,
pub take_1_fast: bool,
// performance
pub tick_rate: Option<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct CliPrototype {
// source
// ---------------------------------------
pub source_command: Option<Template>,
pub source_entry_delimiter: Option<char>,
pub ansi: bool,
pub source_display: Option<Template>,
pub source_output: Option<Template>,
pub source_entry_delimiter: Option<char>,
pub working_directory: Option<PathBuf>,
pub autocomplete_prompt: Option<String>,
pub ansi: bool,
// Preview configuration
pub watch: Option<f64>,
// preview
// ---------------------------------------
pub preview_command: Option<Template>,
pub preview_offset: Option<Template>,
pub preview_cached: bool,
// ui
// ---------------------------------------
pub ui_scale: Option<u16>,
pub layout: Option<Orientation>,
pub height: Option<u16>,
pub width: Option<u16>,
pub inline: bool,
// ui features
pub no_preview: bool,
pub hide_preview: bool,
pub show_preview: bool,
pub no_status_bar: bool,
pub hide_status_bar: bool,
pub show_status_bar: bool,
pub no_remote: bool,
pub hide_remote: bool,
pub show_remote: bool,
pub no_help_panel: bool,
pub hide_help_panel: bool,
pub show_help_panel: bool,
// preview
pub preview_size: Option<u16>,
pub preview_header: Option<String>,
pub preview_footer: Option<String>,
pub preview_border: Option<BorderType>,
pub preview_padding: Option<Padding>,
// Results panel configuration
// results
pub results_border: Option<BorderType>,
pub results_padding: Option<Padding>,
// Status bar configuration
pub no_status_bar: bool,
pub hide_status_bar: bool,
pub show_status_bar: bool,
// Remote configuration
pub no_remote: bool,
pub hide_remote: bool,
pub show_remote: bool,
// Help panel configuration
pub no_help_panel: bool,
pub hide_help_panel: bool,
pub show_help_panel: bool,
// Input configuration
pub input: Option<String>,
// input
pub input_header: Option<String>,
pub input_prompt: Option<String>,
pub input_border: Option<BorderType>,
pub input_padding: Option<Padding>,
// UI and layout configuration
pub layout: Option<Orientation>,
pub ui_scale: Option<u16>,
pub height: Option<u16>,
pub width: Option<u16>,
pub inline: bool,
// Behavior and matching configuration
pub exact: bool,
pub select_1: bool,
pub take_1: bool,
pub take_1_fast: bool,
// keybindings
pub keybindings: Option<KeyBindings>,
// Performance configuration
pub tick_rate: Option<f64>,
pub watch_interval: Option<f64>,
// History configuration
pub global_history: bool,
// Configuration sources
pub config_file: Option<PathBuf>,
pub cable_dir: Option<PathBuf>,
// Command handling
pub command: Option<Command>,
}
const DEFAULT_BORDER_TYPE: BorderType = BorderType::Rounded;
impl Default for PostProcessedCli {
fn default() -> Self {
Self {
// Channel and source configuration
channel: None,
source_command: None,
source_display: None,
source_output: None,
source_entry_delimiter: None,
working_directory: None,
autocomplete_prompt: None,
ansi: false,
// Preview configuration
preview_command: None,
preview_offset: None,
no_preview: false,
hide_preview: false,
show_preview: false,
preview_size: Some(DEFAULT_PREVIEW_SIZE),
preview_header: None,
preview_footer: None,
preview_border: Some(DEFAULT_BORDER_TYPE),
preview_padding: None,
// Results panel configuration
results_border: Some(DEFAULT_BORDER_TYPE),
results_padding: None,
// Status bar configuration
no_status_bar: false,
hide_status_bar: false,
show_status_bar: false,
// Remote configuration
no_remote: false,
hide_remote: false,
show_remote: false,
// Help panel configuration
no_help_panel: false,
hide_help_panel: false,
show_help_panel: false,
// Input configuration
input: None,
input_header: None,
input_prompt: None,
input_border: Some(DEFAULT_BORDER_TYPE),
input_padding: None,
// UI and layout configuration
layout: None,
ui_scale: None,
height: None,
width: None,
inline: false,
// Behavior and matching configuration
exact: false,
select_1: false,
take_1: false,
take_1_fast: false,
keybindings: None,
// Performance configuration
tick_rate: None,
watch_interval: None,
// History configuration
global_history: false,
// Configuration sources
config_file: None,
cable_dir: None,
// Command handling
command: None,
}
}
}
/// Post-processes the raw CLI arguments into a structured format with validation.
///
/// This function handles the two main CLI use cases:
///
/// **Channel-based mode**: When `cli.channel` is provided, all flags are treated as
/// overrides to the channel's configuration. Validation is minimal since the channel
/// provides sensible defaults.
///
/// **Ad-hoc mode**: When no channel is specified, stricter validation ensures that
/// interdependent flags are used correctly:
/// - Preview flags (`--preview-offset`, `--preview-size`, etc.) require `--preview-command`
/// - Source flags (`--source-display`, `--source-output`) require `--source-command`
///
/// This prevents creating broken ad-hoc channels that reference non-existent commands.
pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
// Parse literal keybindings passed through the CLI
let mut 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()))
});
// if `--expect` is used, parse and add to the keybindings
if let Some(expect) = &cli.expect {
let expect_bindings = parse_expect_bindings(expect)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()));
keybindings = match keybindings {
Some(kb) => {
// Merge expect bindings into existing keybindings
Some(merge_bindings(kb, &expect_bindings))
}
None => {
// Initialize keybindings with expect bindings
Some(expect_bindings)
}
}
}
// Parse preview overrides if provided
let preview_command_override =
cli.preview_command.as_ref().map(|preview_cmd| {
Template::parse(preview_cmd).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing preview command: {e}"
))
})
impl ProcessedCli {
/// Processes the raw CLI arguments into a structured format with validation.
///
/// This function handles the two main CLI use cases:
///
/// **Channel-based mode**: When `cli.channel` is provided, all flags are treated as
/// overrides to the channel's configuration. Validation is minimal since the channel
/// provides sensible defaults.
///
/// **Ad-hoc mode**: When no channel is specified, stricter validation ensures that
/// interdependent flags are used correctly:
/// - Preview flags (`--preview-offset`, `--preview-size`, etc.) require `--preview-command`
/// - Source flags (`--source-display`, `--source-output`) require `--source-command`
///
/// This prevents creating broken ad-hoc channels that reference non-existent commands.
pub fn process(cli: Cli, readable_stdin: bool) -> Self {
// Parse literal keybindings passed through the CLI
let mut 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()))
});
let preview_offset_override =
cli.preview_offset.as_ref().map(|offset_str| {
// if `--expect` is used, parse and add to the keybindings
if let Some(expect) = &cli.expect {
let expect_bindings = parse_expect_bindings(expect)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()));
keybindings = match keybindings {
Some(kb) => {
// Merge expect bindings into existing keybindings
Some(merge_bindings(kb, &expect_bindings))
}
None => {
// Initialize keybindings with expect bindings
Some(expect_bindings)
}
}
}
// Parse preview overrides if provided
let preview_command =
cli.preview_command.as_ref().map(|preview_cmd| {
Template::parse(preview_cmd).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing preview command: {e}"
))
})
});
let preview_offset = cli.preview_offset.as_ref().map(|offset_str| {
Template::parse(offset_str).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing preview offset: {e}"
@ -296,47 +157,46 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
})
});
if cli.autocomplete_prompt.is_some() {
if let Some(ch) = &cli.channel {
if !Path::new(ch).exists() {
let mut cmd = Cli::command();
let arg1 = "'--autocomplete-prompt <STRING>'".yellow();
let arg2 = "'[CHANNEL]'".yellow();
let msg = format!(
"The argument {} cannot be used with {}",
arg1, arg2
);
cmd.error(ErrorKind::ArgumentConflict, msg).exit();
if cli.autocomplete_prompt.is_some() {
if let Some(ch) = &cli.channel {
if !Path::new(ch).exists() {
let mut cmd = Cli::command();
let arg1 = "'--autocomplete-prompt <STRING>'".yellow();
let arg2 = "'[CHANNEL]'".yellow();
let msg = format!(
"The argument {} cannot be used with {}",
arg1, arg2
);
cmd.error(ErrorKind::ArgumentConflict, msg).exit();
}
}
}
}
// Validate interdependent flags for ad-hoc mode (when no channel is specified)
// This ensures ad-hoc channels have all necessary components to function properly
validate_adhoc_mode_constraints(&cli, readable_stdin);
// Validate interdependent flags for ad-hoc mode (when no channel is specified)
// This ensures ad-hoc channels have all necessary components to function properly
validate_adhoc_mode_constraints(&cli, readable_stdin);
// Validate width flag requires inline or height
if cli.width.is_some() && !cli.inline && cli.height.is_none() {
cli_parsing_error_exit(
"--width can only be used in combination with --inline or --height",
);
}
// Determine channel and working_directory
let (channel, working_directory) = match &cli.channel {
Some(c) if Path::new(c).exists() => {
// If the channel is a path, use it as the working directory
(None, Some(PathBuf::from(c)))
// Validate width flag requires inline or height
if cli.width.is_some() && !cli.inline && cli.height.is_none() {
cli_parsing_error_exit(
"--width can only be used in combination with --inline or --height",
);
}
_ => (
cli.channel.clone(),
cli.working_directory.as_ref().map(PathBuf::from),
),
};
// Parse source overrides if any source fields are provided
let source_command_override =
cli.source_command.as_ref().map(|source_cmd| {
// Determine channel and working_directory
let (channel, working_directory) = match &cli.channel {
Some(c) if Path::new(c).exists() => {
// If the channel is a path, use it as the working directory
(None, Some(PathBuf::from(c)))
}
_ => (
cli.channel.clone(),
cli.working_directory.as_ref().map(PathBuf::from),
),
};
// Parse source overrides if any source fields are provided
let source_command = cli.source_command.as_ref().map(|source_cmd| {
Template::parse(source_cmd).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing source command: {e}"
@ -344,8 +204,7 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
})
});
let source_display_override =
cli.source_display.as_ref().map(|display_str| {
let source_display = cli.source_display.as_ref().map(|display_str| {
Template::parse(display_str).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing source display: {e}"
@ -353,8 +212,7 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
})
});
let source_output_override =
cli.source_output.as_ref().map(|output_str| {
let source_output = cli.source_output.as_ref().map(|output_str| {
Template::parse(output_str).unwrap_or_else(|e| {
cli_parsing_error_exit(&format!(
"Error parsing source output: {e}"
@ -362,111 +220,102 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
})
});
// Validate that the source entry delimiter is a single character
let source_entry_delimiter =
cli.source_entry_delimiter.as_ref().map(|delimiter| {
parse_source_entry_delimiter(delimiter)
// Validate that the source entry delimiter is a single character
let source_entry_delimiter =
cli.source_entry_delimiter.as_ref().map(|delimiter| {
parse_source_entry_delimiter(delimiter)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
// Determine layout
let layout: Option<Orientation> = cli.layout.map(Orientation::from);
// borders
let input_border = cli.input_border.map(BorderType::from);
let results_border = cli.results_border.map(BorderType::from);
let preview_border = cli.preview_border.map(BorderType::from);
// padding
let input_padding = cli.input_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
let results_padding = cli.results_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
let preview_padding = cli.preview_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
// Determine layout
let layout: Option<Orientation> = cli.layout.map(Orientation::from);
let prototype = CliPrototype {
source_command,
source_entry_delimiter,
ansi: cli.ansi,
source_display,
source_output,
watch: cli.watch,
// borders
let input_border = cli.input_border.map(BorderType::from);
let results_border = cli.results_border.map(BorderType::from);
let preview_border = cli.preview_border.map(BorderType::from);
preview_command,
preview_offset,
preview_cached: cli.preview_cached,
// padding
let input_padding = cli.input_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
let results_padding = cli.results_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
let preview_padding = cli.preview_padding.map(|p| {
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
});
ui_scale: cli.ui_scale,
layout,
height: cli.height,
width: cli.width,
inline: cli.inline,
PostProcessedCli {
// Channel and source configuration
channel,
source_command: source_command_override,
source_display: source_display_override,
source_output: source_output_override,
source_entry_delimiter,
working_directory,
autocomplete_prompt: cli.autocomplete_prompt,
ansi: cli.ansi,
no_preview: cli.no_preview,
hide_preview: cli.hide_preview,
show_preview: cli.show_preview,
// Preview configuration
preview_command: preview_command_override,
preview_offset: preview_offset_override,
no_preview: cli.no_preview,
hide_preview: cli.hide_preview,
show_preview: cli.show_preview,
preview_size: cli.preview_size,
preview_header: cli.preview_header,
preview_footer: cli.preview_footer,
preview_border,
preview_padding,
no_status_bar: cli.no_status_bar,
hide_status_bar: cli.hide_status_bar,
show_status_bar: cli.show_status_bar,
// Results configuration
results_border,
results_padding,
no_remote: cli.no_remote,
hide_remote: cli.hide_remote,
show_remote: cli.show_remote,
// Status bar configuration
no_status_bar: cli.no_status_bar,
hide_status_bar: cli.hide_status_bar,
show_status_bar: cli.show_status_bar,
no_help_panel: cli.no_help_panel,
hide_help_panel: cli.hide_help_panel,
show_help_panel: cli.show_help_panel,
// Remote configuration
no_remote: cli.no_remote,
hide_remote: cli.hide_remote,
show_remote: cli.show_remote,
preview_size: cli.preview_size,
preview_header: cli.preview_header,
preview_footer: cli.preview_footer,
preview_border,
preview_padding,
// Help panel configuration
no_help_panel: cli.no_help_panel,
hide_help_panel: cli.hide_help_panel,
show_help_panel: cli.show_help_panel,
results_border,
results_padding,
// Input configuration
input: cli.input,
input_header: cli.input_header,
input_prompt: cli.input_prompt,
input_border,
input_padding,
input_header: cli.input_header.clone(),
input_prompt: cli.input_prompt.clone(),
input_border,
input_padding,
// UI and layout configuration
layout,
ui_scale: cli.ui_scale,
height: cli.height,
width: cli.width,
inline: cli.inline,
keybindings,
};
// Behavior and matching configuration
exact: cli.exact,
select_1: cli.select_1,
take_1: cli.take_1,
take_1_fast: cli.take_1_fast,
keybindings,
// Performance configuration
tick_rate: cli.tick_rate,
watch_interval: cli.watch,
// History configuration
global_history: cli.global_history,
// Configuration sources
config_file: cli.config_file.map(|p| expand_tilde(&p)),
cable_dir: cli.cable_dir.map(|p| expand_tilde(&p)),
// Command handling
command: cli.command,
Self {
workdir: working_directory,
input: cli.input,
global_history: cli.global_history,
config_file: cli.config_file.map(|p| expand_tilde(&p)),
cable_dir: cli.cable_dir.map(|p| expand_tilde(&p)),
command: cli.command,
autocomplete_prompt: cli.autocomplete_prompt,
channel,
prototype,
exact: cli.exact,
select_1: cli.select_1,
take_1: cli.take_1,
take_1_fast: cli.take_1_fast,
tick_rate: cli.tick_rate,
}
}
}
@ -725,6 +574,8 @@ Data directory: {data_dir_path}"
#[cfg(test)]
mod tests {
use rustc_hash::FxHashMap;
use crate::{action::Action, event::Key};
use super::*;
@ -739,17 +590,14 @@ mod tests {
..Default::default()
};
let post_processed_cli = post_process(cli, false);
let p = ProcessedCli::process(cli, false);
assert_eq!(
post_processed_cli.preview_command.unwrap().raw(),
p.prototype.preview_command.unwrap().raw(),
"bat -n --color=always {}".to_string(),
);
assert_eq!(post_processed_cli.tick_rate, None);
assert_eq!(
post_processed_cli.working_directory,
Some(PathBuf::from("/home/user"))
);
assert_eq!(p.tick_rate, None);
assert_eq!(p.workdir, Some(PathBuf::from("/home/user")));
}
#[test]
@ -760,13 +608,10 @@ mod tests {
..Default::default()
};
let post_processed_cli = post_process(cli, false);
let p = ProcessedCli::process(cli, false);
assert_eq!(
post_processed_cli.working_directory,
Some(PathBuf::from("."))
);
assert_eq!(post_processed_cli.command, None);
assert_eq!(p.workdir, Some(PathBuf::from(".")));
assert_eq!(p.command, None);
}
#[test]
@ -782,14 +627,14 @@ mod tests {
..Default::default()
};
let post_processed_cli = post_process(cli, false);
let p = ProcessedCli::process(cli, false);
let mut expected = KeyBindings::default();
expected.insert(Key::Esc, Action::Quit.into());
expected.insert(Key::Down, Action::SelectNextEntry.into());
expected.insert(Key::Ctrl('j'), Action::SelectNextEntry.into());
assert_eq!(post_processed_cli.keybindings, Some(expected));
assert_eq!(p.prototype.keybindings, Some(expected));
}
/// Returns a tuple containing a command mapping and a fallback channel.
@ -817,12 +662,14 @@ mod tests {
let (command_mapping, fallback, channels) =
guess_channel_from_prompt_setup();
let channel = guess_channel_from_prompt(
prompt,
&command_mapping,
fallback,
&channels,
);
let si_config = ShellIntegrationConfig {
commands: command_mapping,
channel_triggers: FxHashMap::default(),
fallback_channel: fallback.to_string(),
keybindings: FxHashMap::default(),
};
let channel = guess_channel_from_prompt(prompt, &si_config, &channels);
assert_eq!(channel.metadata.name, "files");
}
@ -834,12 +681,14 @@ mod tests {
let (command_mapping, fallback, channels) =
guess_channel_from_prompt_setup();
let channel = guess_channel_from_prompt(
prompt,
&command_mapping,
fallback,
&channels,
);
let si_config = ShellIntegrationConfig {
commands: command_mapping,
channel_triggers: FxHashMap::default(),
fallback_channel: fallback.to_string(),
keybindings: FxHashMap::default(),
};
let channel = guess_channel_from_prompt(prompt, &si_config, &channels);
assert_eq!(channel.metadata.name, fallback);
}
@ -851,12 +700,14 @@ mod tests {
let (command_mapping, fallback, channels) =
guess_channel_from_prompt_setup();
let channel = guess_channel_from_prompt(
prompt,
&command_mapping,
fallback,
&channels,
);
let si_config = ShellIntegrationConfig {
commands: command_mapping,
channel_triggers: FxHashMap::default(),
fallback_channel: fallback.to_string(),
keybindings: FxHashMap::default(),
};
let channel = guess_channel_from_prompt(prompt, &si_config, &channels);
assert_eq!(channel.metadata.name, fallback);
}

View File

@ -2,7 +2,7 @@ use crate::{
action::Action,
cable::CABLE_DIR_NAME,
channels::prototypes::{DEFAULT_PROTOTYPE_NAME, Template, UiSpec},
cli::PostProcessedCli,
cli::ProcessedCli,
features::FeatureFlags,
history::DEFAULT_HISTORY_SIZE,
screen::keybindings::remove_action_bindings,
@ -267,7 +267,7 @@ impl Config {
}
/// Apply CLI overrides to this config
pub fn apply_cli_overrides(&mut self, args: &PostProcessedCli) {
pub fn apply_cli_overrides(&mut self, args: &ProcessedCli) {
debug!("Applying CLI overrides to config after channel merging");
if let Some(cable_dir) = &args.cable_dir {

View File

@ -13,7 +13,7 @@ use television::{
ChannelPrototype, CommandSpec, PreviewSpec, Template, UiSpec,
},
cli::{
PostProcessedCli,
ProcessedCli,
args::{Cli, Command},
guess_channel_from_prompt, list_channels, post_process,
},
@ -39,71 +39,67 @@ async fn main() -> Result<()> {
debug!("\n\n==== NEW SESSION =====\n");
// process the CLI arguments
let cli = Cli::parse();
debug!("CLI: {:?}", cli);
let readable_stdin = is_readable_stdin();
let args = post_process(cli, readable_stdin);
debug!("PostProcessedCli: {:?}", args);
// process CLI arguments
let cli = ProcessedCli::process(Cli::parse(), readable_stdin);
debug!("Processed CLI: {:?}", cli);
// load the configuration file
debug!("Loading configuration...");
let mut config =
Config::new(&ConfigEnv::init()?, args.config_file.as_deref())?;
Config::new(&ConfigEnv::init()?, cli.config_file.as_deref())?;
// override configuration values with provided CLI arguments
let cable_dir = args
// load cable channels
debug!("Loading cable channels...");
let cable_dir = cli
.cable_dir
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| config.application.cable_dir.clone());
let cable = load_cable(&cable_dir).unwrap_or_default();
// handle subcommands
debug!("Handling subcommands...");
if let Some(subcommand) = &args.command {
if let Some(subcommand) = &cli.command {
handle_subcommand(subcommand, &cable_dir, &config.shell_integration)?;
}
debug!("Loading cable channels...");
let cable = load_cable(cable_dir).unwrap_or_default();
// optionally change the working directory
if let Some(ref working_dir) = args.working_directory {
if let Some(ref working_dir) = cli.workdir {
set_current_dir(working_dir)
.unwrap_or_else(|e| os_error_exit(&e.to_string()));
}
// TODO: [27/07] this is where I left off
debug!("Applying CLI overrides...");
config.apply_cli_overrides(&args);
config.apply_cli_overrides(&cli);
// determine the channel to use based on the CLI arguments and configuration
debug!("Determining channel...");
let channel_prototype =
determine_channel(&args, &config, readable_stdin, &cable);
determine_channel(&cli, &config, readable_stdin, &cable);
CLIPBOARD.with(<_>::default);
debug!("Creating application...");
// Determine the effective watch interval (CLI override takes precedence)
let watch_interval =
args.watch_interval.unwrap_or(channel_prototype.watch);
let watch_interval = cli.watch_interval.unwrap_or(channel_prototype.watch);
let options = AppOptions::new(
args.exact,
args.select_1,
args.take_1,
args.take_1_fast,
args.no_remote,
args.no_preview,
args.preview_size,
cli.exact,
cli.select_1,
cli.take_1,
cli.take_1_fast,
cli.no_remote,
cli.no_preview,
cli.preview_size,
config.application.tick_rate,
watch_interval,
args.height,
args.width,
args.inline,
cli.height,
cli.width,
cli.inline,
);
let mut app = App::new(channel_prototype, config, options, cable, &args);
let mut app = App::new(channel_prototype, config, options, cable, &cli);
// If the user requested to show the remote control on startup, switch the
// television into Remote Control mode before the application event loop
@ -111,7 +107,7 @@ async fn main() -> Result<()> {
// the corresponding keybinding after launch, ensuring the panel is
// visible from the start.
// TODO: This is a hack, preview is not initialised yet, find a better way to do it.
if args.show_remote && app.television.remote_control.is_some() {
if cli.show_remote && app.television.remote_control.is_some() {
app.television.mode = Mode::RemoteControl;
}
@ -203,7 +199,7 @@ const CUSTOM_CHANNEL_NAME: &str = "custom channel";
const DEFAULT_ADHOC_CHANNEL_HEADER: &str = "Custom Channel";
/// Creates an ad-hoc channel prototype from CLI arguments
fn create_adhoc_channel(args: &PostProcessedCli) -> ChannelPrototype {
fn create_adhoc_channel(args: &ProcessedCli) -> ChannelPrototype {
debug!("Creating ad-hoc channel");
let mut prototype = ChannelPrototype::new(
Metadata {
@ -226,7 +222,7 @@ fn create_adhoc_channel(args: &PostProcessedCli) -> ChannelPrototype {
/// Applies source-related CLI overrides to the channel prototype
fn apply_source_overrides(
prototype: &mut ChannelPrototype,
args: &PostProcessedCli,
args: &ProcessedCli,
) {
if let Some(source_cmd) = &args.source_command {
prototype.source.command = CommandSpec::from(source_cmd.clone());
@ -245,7 +241,7 @@ fn apply_source_overrides(
/// Applies preview-related CLI overrides to the channel prototype
fn apply_preview_overrides(
prototype: &mut ChannelPrototype,
args: &PostProcessedCli,
args: &ProcessedCli,
) {
if let Some(preview_cmd) = &args.preview_command {
if let Some(ref mut preview) = prototype.preview {
@ -266,10 +262,7 @@ fn apply_preview_overrides(
}
/// Applies UI-related CLI overrides to the channel prototype
fn apply_ui_overrides(
prototype: &mut ChannelPrototype,
args: &PostProcessedCli,
) {
fn apply_ui_overrides(prototype: &mut ChannelPrototype, args: &ProcessedCli) {
let mut ui_changes_needed = false;
let mut ui_spec = prototype.ui.clone().unwrap_or(UiSpec {
ui_scale: None,
@ -400,7 +393,7 @@ fn apply_ui_overrides(
///
/// After determining the base channel, it applies all relevant CLI overrides.
pub fn determine_channel(
args: &PostProcessedCli,
args: &ProcessedCli,
config: &Config,
readable_stdin: bool,
cable: &Cable,
@ -462,7 +455,7 @@ mod tests {
use super::*;
fn assert_is_correct_channel(
args: &PostProcessedCli,
args: &ProcessedCli,
config: &Config,
readable_stdin: bool,
expected_channel: &ChannelPrototype,
@ -487,7 +480,7 @@ mod tests {
#[test]
/// Test that the channel is stdin when stdin is readable
fn test_determine_channel_readable_stdin() {
let args = PostProcessedCli::default();
let args = ProcessedCli::default();
let config = Config::default();
assert_is_correct_channel(
&args,
@ -500,7 +493,7 @@ mod tests {
#[test]
fn test_determine_channel_stdin_disables_preview_without_command() {
let args = PostProcessedCli::default();
let args = ProcessedCli::default();
let config = Config::default();
let cable = Cable::from_prototypes(vec![]);
@ -520,7 +513,7 @@ mod tests {
#[test]
fn test_determine_channel_stdin_enables_preview_with_command() {
let args = PostProcessedCli {
let args = ProcessedCli {
preview_command: Some(Template::parse("cat {}").unwrap()),
..Default::default()
};
@ -545,7 +538,7 @@ mod tests {
fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string());
let expected_channel = ChannelPrototype::simple("dirs", "ls {}");
let args = PostProcessedCli {
let args = ProcessedCli {
autocomplete_prompt,
..Default::default()
};
@ -577,7 +570,7 @@ mod tests {
#[test]
fn test_determine_channel_standard_case() {
let channel = Some(String::from("dirs"));
let args = PostProcessedCli {
let args = ProcessedCli {
channel,
..Default::default()
};
@ -593,7 +586,7 @@ mod tests {
#[test]
fn test_determine_channel_config_fallback() {
let args = PostProcessedCli {
let args = ProcessedCli {
channel: None,
..Default::default()
};
@ -620,7 +613,7 @@ mod tests {
None,
);
let args = PostProcessedCli {
let args = ProcessedCli {
channel: Some(String::from("dirs")),
preview_command: Some(preview_command),
..Default::default()
@ -641,7 +634,7 @@ mod tests {
#[test]
fn test_determine_channel_adhoc_with_source_command() {
let args = PostProcessedCli {
let args = ProcessedCli {
channel: None,
source_command: Some(Template::parse("fd -t f -H").unwrap()),
..Default::default()
@ -670,7 +663,7 @@ mod tests {
#[test]
fn test_apply_cli_overrides() {
let mut config = Config::default();
let args = PostProcessedCli {
let args = ProcessedCli {
tick_rate: Some(100_f64),
no_preview: true,
input_header: Some("Input Header".to_string()),
@ -753,7 +746,7 @@ mod tests {
let cable = Cable::from_prototypes(vec![channel_prototype]);
// Test CLI arguments that should override channel settings
let args = PostProcessedCli {
let args = ProcessedCli {
channel: Some("test".to_string()),
input_header: Some("CLI Input Header".to_string()),
preview_header: Some("CLI Preview Header".to_string()),
@ -817,7 +810,7 @@ mod tests {
fn test_apply_cli_overrides_ui_scale() {
// Test that the CLI ui_scale override is applied correctly
let mut config = Config::default();
let args = PostProcessedCli {
let args = ProcessedCli {
ui_scale: Some(90),
..Default::default()
};
@ -828,7 +821,7 @@ mod tests {
// Test that the config value is used when no CLI override is provided
let mut config = Config::default();
config.ui.ui_scale = 70;
let args = PostProcessedCli::default();
let args = ProcessedCli::default();
config.apply_cli_overrides(&args);
assert_eq!(config.ui.ui_scale, 70);

View File

@ -7,7 +7,7 @@ use crate::{
prototypes::ChannelPrototype,
remote_control::{CableEntry, RemoteControl},
},
cli::PostProcessedCli,
cli::ProcessedCli,
config::{Config, Theme},
draw::{ChannelState, Ctx, TvState},
errors::os_error_exit,
@ -79,7 +79,7 @@ pub struct Television {
pub colorscheme: Colorscheme,
pub ticks: u64,
pub ui_state: UiState,
pub cli_args: PostProcessedCli,
pub cli_args: ProcessedCli,
pub current_command_index: usize,
pub channel_prototype: ChannelPrototype,
}
@ -93,7 +93,7 @@ impl Television {
channel_prototype: ChannelPrototype,
base_config: Config,
cable_channels: Cable,
cli_args: PostProcessedCli,
cli_args: ProcessedCli,
) -> Self {
let mut config = Self::merge_base_config_with_prototype_specs(
&base_config,
@ -917,16 +917,16 @@ mod test {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cli_overrides() {
use crate::cli::PostProcessedCli;
use crate::cli::ProcessedCli;
let config = crate::config::Config::default();
let prototype = crate::channels::prototypes::ChannelPrototype::simple(
"test", "echo 1",
);
let cli_args = PostProcessedCli {
let cli_args = ProcessedCli {
no_remote: true,
exact: true,
..PostProcessedCli::default()
..ProcessedCli::default()
};
let tv = Television::new(
tokio::sync::mpsc::unbounded_channel().0,
@ -942,7 +942,7 @@ mod test {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_channel_keybindings_take_precedence() {
use crate::cli::PostProcessedCli;
use crate::cli::ProcessedCli;
let mut config = crate::config::Config::default();
config
@ -964,10 +964,10 @@ mod test {
)
.unwrap();
let cli_args = PostProcessedCli {
let cli_args = ProcessedCli {
no_remote: true,
exact: true,
..PostProcessedCli::default()
..ProcessedCli::default()
};
let tv = Television::new(
tokio::sync::mpsc::unbounded_channel().0,

View File

@ -9,7 +9,7 @@ use television::{
app::{App, AppOptions},
cable::Cable,
channels::prototypes::ChannelPrototype,
cli::PostProcessedCli,
cli::ProcessedCli,
config::default_config_from_file,
};
use tokio::{task::JoinHandle, time::timeout};
@ -60,10 +60,10 @@ fn setup_app(
None,
false,
);
let cli_args = PostProcessedCli {
let cli_args = ProcessedCli {
exact,
input: input.clone(),
..PostProcessedCli::default()
..ProcessedCli::default()
};
let mut app = App::new(
chan,