mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
feat(cli): allow passing custom keybindings through the cli (#409)
Fixes #134
This commit is contained in:
parent
3a5b5ec0cc
commit
3222037a02
15
man/tv.1
15
man/tv.1
@ -1,10 +1,10 @@
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.TH television 1 "television 0.10.9"
|
||||
.TH television 1 "television 0.10.10"
|
||||
.SH NAME
|
||||
television \- A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||
.SH SYNOPSIS
|
||||
\fBtelevision\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-\-no\-preview\fR] [\fB\-\-delimiter\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-f\fR|\fB\-\-frame\-rate\fR] [\fB\-\-passthrough\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
|
||||
\fBtelevision\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-\-no\-preview\fR] [\fB\-\-delimiter\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-f\fR|\fB\-\-frame\-rate\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-\-passthrough\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
|
||||
.SH DESCRIPTION
|
||||
A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||
.SH OPTIONS
|
||||
@ -44,6 +44,15 @@ compromise for most users.
|
||||
|
||||
This option is deprecated and will be removed in a future release.
|
||||
.TP
|
||||
\fB\-k\fR, \fB\-\-keybindings\fR=\fISTRING\fR
|
||||
Keybindings to override the default keybindings.
|
||||
|
||||
This can be used to override the default keybindings with a custom subset
|
||||
The keybindings are specified as a semicolon separated list of keybinding
|
||||
expressions using the configuration file formalism.
|
||||
|
||||
Example: `tv \-\-keybindings=\*(Aqquit="esc";select_next_entry=["down","ctrl\-j"]\*(Aq`
|
||||
.TP
|
||||
\fB\-\-passthrough\-keybindings\fR=\fISTRING\fR
|
||||
Passthrough keybindings (comma separated, e.g. "q,ctrl\-w,ctrl\-t")
|
||||
|
||||
@ -95,6 +104,6 @@ Initializes shell completion ("tv init zsh")
|
||||
television\-help(1)
|
||||
Print this message or the help of the given subcommand(s)
|
||||
.SH VERSION
|
||||
v0.10.9
|
||||
v0.10.10
|
||||
.SH AUTHORS
|
||||
Alexandre Pasmantier <alex.pasmant@gmail.com>
|
||||
|
@ -110,7 +110,7 @@ impl App {
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
let (_, event_rx) = mpsc::unbounded_channel();
|
||||
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
||||
let tick_rate = config.config.tick_rate;
|
||||
let tick_rate = config.application.tick_rate;
|
||||
let keybindings = merge_keybindings(config.keybindings.clone(), {
|
||||
&KeyBindings::from(passthrough_keybindings.iter().filter_map(
|
||||
|s| match parse_key(s) {
|
||||
|
@ -56,6 +56,16 @@ pub struct Cli {
|
||||
#[arg(short, long, value_name = "FLOAT", verbatim_doc_comment)]
|
||||
pub frame_rate: Option<f64>,
|
||||
|
||||
/// Keybindings to override the default keybindings.
|
||||
///
|
||||
/// This can be used to override the default keybindings with a custom subset
|
||||
/// The keybindings are specified as a semicolon separated list of keybinding
|
||||
/// expressions using the configuration file formalism.
|
||||
///
|
||||
/// Example: `tv --keybindings='quit="esc";select_next_entry=["down","ctrl-j"]'`
|
||||
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub keybindings: Option<String>,
|
||||
|
||||
/// Passthrough keybindings (comma separated, e.g. "q,ctrl-w,ctrl-t")
|
||||
///
|
||||
/// These keybindings will trigger selection of the current entry and be
|
||||
|
@ -9,6 +9,7 @@ use crate::channels::{
|
||||
cable::CableChannelPrototype, entry::PreviewCommand, CliTvChannel,
|
||||
};
|
||||
use crate::cli::args::{Cli, Command};
|
||||
use crate::config::KeyBindings;
|
||||
use crate::{
|
||||
cable,
|
||||
config::{get_config_dir, get_data_dir},
|
||||
@ -28,6 +29,7 @@ pub struct PostProcessedCli {
|
||||
pub command: Option<Command>,
|
||||
pub working_directory: Option<String>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
pub keybindings: Option<KeyBindings>,
|
||||
}
|
||||
|
||||
impl Default for PostProcessedCli {
|
||||
@ -43,12 +45,21 @@ impl Default for PostProcessedCli {
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
keybindings: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cli> for PostProcessedCli {
|
||||
fn from(cli: Cli) -> Self {
|
||||
let keybindings: Option<KeyBindings> = cli.keybindings.map(|kb| {
|
||||
parse_keybindings(&kb)
|
||||
.map_err(|e| {
|
||||
cli_parsing_error_exit(&e.to_string());
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let passthrough_keybindings = cli
|
||||
.passthrough_keybindings
|
||||
.unwrap_or_default()
|
||||
@ -62,11 +73,13 @@ impl From<Cli> for PostProcessedCli {
|
||||
command: preview,
|
||||
delimiter: cli.delimiter.clone(),
|
||||
})
|
||||
.map(|preview_command| {
|
||||
.map_or(PreviewKind::None, |preview_command| {
|
||||
parse_preview_kind(&preview_command)
|
||||
.expect("Error parsing preview command")
|
||||
})
|
||||
.unwrap_or(PreviewKind::None);
|
||||
.map_err(|e| {
|
||||
cli_parsing_error_exit(&e.to_string());
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let channel: ParsedCliChannel;
|
||||
let working_directory: Option<String>;
|
||||
@ -102,10 +115,16 @@ impl From<Cli> for PostProcessedCli {
|
||||
command: cli.command,
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
keybindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
std::process::exit(1);
|
||||
@ -117,6 +136,24 @@ pub enum ParsedCliChannel {
|
||||
Cable(CableChannelPrototype),
|
||||
}
|
||||
|
||||
const CLI_KEYBINDINGS_DELIMITER: char = ';';
|
||||
|
||||
/// Parse the keybindings string into a hashmap of key -> action.
|
||||
///
|
||||
/// The formalism used is the same as the one used in the configuration file:
|
||||
/// ```ignore
|
||||
/// quit="esc";select_next_entry=["down","ctrl-j"]
|
||||
/// ```
|
||||
/// Parsing it globally consists of splitting by the delimiter, reconstructing toml key-value pairs
|
||||
/// and parsing that using logic already implemented in the configuration module.
|
||||
fn parse_keybindings(cli_keybindings: &str) -> Result<KeyBindings> {
|
||||
let toml_definition = cli_keybindings
|
||||
.split(CLI_KEYBINDINGS_DELIMITER)
|
||||
.fold(String::new(), |acc, kb| acc + kb + "\n");
|
||||
|
||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
fn parse_channel(channel: &str) -> Result<ParsedCliChannel> {
|
||||
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
||||
// try to parse the channel as a cable channel
|
||||
@ -259,7 +296,10 @@ Data directory: {data_dir_path}"
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::channels::entry::PreviewType;
|
||||
use crate::{
|
||||
action::Action, channels::entry::PreviewType, config::Binding,
|
||||
event::Key,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -273,6 +313,7 @@ mod tests {
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
keybindings: None,
|
||||
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
|
||||
input: None,
|
||||
command: None,
|
||||
@ -315,6 +356,7 @@ mod tests {
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
keybindings: None,
|
||||
passthrough_keybindings: None,
|
||||
input: None,
|
||||
command: None,
|
||||
@ -344,6 +386,7 @@ mod tests {
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
keybindings: None,
|
||||
passthrough_keybindings: None,
|
||||
input: None,
|
||||
command: None,
|
||||
@ -368,6 +411,7 @@ mod tests {
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
keybindings: None,
|
||||
passthrough_keybindings: None,
|
||||
input: None,
|
||||
command: None,
|
||||
@ -382,4 +426,36 @@ mod tests {
|
||||
PreviewKind::Builtin(PreviewType::EnvVar)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_keybindings() {
|
||||
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),
|
||||
keybindings: Some(
|
||||
"quit=\"esc\";select_next_entry=[\"down\",\"ctrl-j\"]"
|
||||
.to_string(),
|
||||
),
|
||||
passthrough_keybindings: None,
|
||||
input: None,
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
||||
let mut expected = KeyBindings::default();
|
||||
expected.insert(Action::Quit, Binding::SingleKey(Key::Esc));
|
||||
expected.insert(
|
||||
Action::SelectNextEntry,
|
||||
Binding::MultipleKeys(vec![Key::Down, Key::Ctrl('j')]),
|
||||
);
|
||||
|
||||
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ pub struct Config {
|
||||
/// General application configuration
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[serde(default, flatten)]
|
||||
pub config: AppConfig,
|
||||
pub application: AppConfig,
|
||||
/// Keybindings configuration
|
||||
#[serde(default)]
|
||||
pub keybindings: KeyBindings,
|
||||
@ -176,7 +176,7 @@ impl Config {
|
||||
user.keybindings = keybindings;
|
||||
|
||||
Config {
|
||||
config: user.config,
|
||||
application: user.application,
|
||||
keybindings: user.keybindings,
|
||||
ui: user.ui,
|
||||
previewers: user.previewers,
|
||||
@ -288,8 +288,8 @@ mod tests {
|
||||
file.write_all(DEFAULT_CONFIG.as_bytes()).unwrap();
|
||||
|
||||
let config = Config::load_user_config(config_dir).unwrap();
|
||||
assert_eq!(config.config.data_dir, get_data_dir());
|
||||
assert_eq!(config.config.config_dir, get_config_dir());
|
||||
assert_eq!(config.application.data_dir, get_data_dir());
|
||||
assert_eq!(config.application.config_dir, get_config_dir());
|
||||
assert_eq!(config, toml::from_str(DEFAULT_CONFIG).unwrap());
|
||||
}
|
||||
|
||||
@ -310,7 +310,7 @@ mod tests {
|
||||
toml::from_str(DEFAULT_CONFIG).unwrap();
|
||||
default_config.shell_integration.merge_triggers();
|
||||
|
||||
assert_eq!(config.config, default_config.config);
|
||||
assert_eq!(config.application, default_config.application);
|
||||
assert_eq!(config.keybindings, default_config.keybindings);
|
||||
assert_eq!(config.ui, default_config.ui);
|
||||
assert_eq!(config.previewers, default_config.previewers);
|
||||
@ -364,7 +364,7 @@ mod tests {
|
||||
|
||||
let mut default_config: Config =
|
||||
toml::from_str(DEFAULT_CONFIG).unwrap();
|
||||
default_config.config.frame_rate = 30.0;
|
||||
default_config.application.frame_rate = 30.0;
|
||||
default_config.ui.ui_scale = 40;
|
||||
default_config.ui.theme = "television".to_string();
|
||||
default_config.previewers.file.theme =
|
||||
@ -392,7 +392,7 @@ mod tests {
|
||||
.insert("command_history".to_string(), "ctrl-h".to_string());
|
||||
default_config.shell_integration.merge_triggers();
|
||||
|
||||
assert_eq!(config.config, default_config.config);
|
||||
assert_eq!(config.application, default_config.application);
|
||||
assert_eq!(config.keybindings, default_config.keybindings);
|
||||
assert_eq!(config.ui, default_config.ui);
|
||||
assert_eq!(config.previewers, default_config.previewers);
|
||||
|
@ -19,7 +19,7 @@ use television::cli::{
|
||||
PostProcessedCli,
|
||||
};
|
||||
|
||||
use television::config::{Config, ConfigEnv};
|
||||
use television::config::{merge_keybindings, Config, ConfigEnv};
|
||||
use television::utils::shell::render_autocomplete_script_template;
|
||||
use television::utils::{
|
||||
shell::{completion_script, Shell},
|
||||
@ -45,11 +45,7 @@ async fn main() -> Result<()> {
|
||||
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;
|
||||
}
|
||||
apply_cli_overrides(&args, &mut config);
|
||||
|
||||
// determine the channel to use based on the CLI arguments and configuration
|
||||
let channel =
|
||||
@ -76,6 +72,22 @@ async fn main() -> Result<()> {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/// Apply overrides from the CLI arguments to the configuration.
|
||||
///
|
||||
/// This function mutates the configuration in place.
|
||||
fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
||||
if let Some(tick_rate) = args.tick_rate {
|
||||
config.application.tick_rate = tick_rate;
|
||||
}
|
||||
if args.no_preview {
|
||||
config.ui.show_preview_panel = false;
|
||||
}
|
||||
if let Some(keybindings) = &args.keybindings {
|
||||
config.keybindings =
|
||||
merge_keybindings(config.keybindings.clone(), keybindings);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_dir(path: &String) -> Result<()> {
|
||||
let path = Path::new(path);
|
||||
if !path.exists() {
|
||||
@ -239,4 +251,18 @@ mod tests {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_cli_overrides() {
|
||||
let mut config = Config::default();
|
||||
let args = PostProcessedCli {
|
||||
tick_rate: Some(100_f64),
|
||||
no_preview: true,
|
||||
..Default::default()
|
||||
};
|
||||
apply_cli_overrides(&args, &mut config);
|
||||
|
||||
assert_eq!(config.application.tick_rate, 100_f64);
|
||||
assert!(!config.ui.show_preview_panel);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user