From 810e32716e893b62e7a8341c88e11e38e14fe7c6 Mon Sep 17 00:00:00 2001 From: Bertrand Chardon Date: Sat, 25 Jan 2025 21:20:49 +0100 Subject: [PATCH] chore(terminal): custom shell keybindings - allow custom keybindings through configuration - this only works for ctrl-xxx keybindings for now --- television/config/shell_integration.rs | 33 +++++++++++++++++ television/main.rs | 28 +++++++++++++-- television/utils/shell.rs | 50 ++++++++++++++++++++++++++ television/utils/shell/completion.bash | 4 +-- television/utils/shell/completion.fish | 4 +-- television/utils/shell/completion.zsh | 4 +-- 6 files changed, 115 insertions(+), 8 deletions(-) diff --git a/television/config/shell_integration.rs b/television/config/shell_integration.rs index 4cf11c6..b68c24e 100644 --- a/television/config/shell_integration.rs +++ b/television/config/shell_integration.rs @@ -1,7 +1,40 @@ +use crate::config::parse_key; +use crate::event::Key; use rustc_hash::FxHashMap; use serde::Deserialize; #[derive(Clone, Debug, Deserialize, Default)] pub struct ShellIntegrationConfig { pub commands: FxHashMap, + pub keybindings: FxHashMap, +} +const SMART_AUTOCOMPLETE_CONFIGURATION_KEY: &str = "smart_autocomplete"; +const COMMAND_HISTORY_CONFIGURATION_KEY: &str = "command_history"; +const DEFAULT_SHELL_AUTOCOMPLETE_KEY: char = 'T'; +const DEFAULT_COMMAND_HISTORY_KEY: char = 'R'; + +impl ShellIntegrationConfig { + // based on the keybindings configuration provided in the configuration file + // (if any), extract the character triggers shell autocomplete + pub fn get_shell_autocomplete_keybinding_character(&self) -> char { + match self.keybindings.get(SMART_AUTOCOMPLETE_CONFIGURATION_KEY) { + Some(s) => match parse_key(s) { + Ok(Key::Ctrl(c)) => c.to_uppercase().next().unwrap(), + _ => DEFAULT_SHELL_AUTOCOMPLETE_KEY, + }, + None => DEFAULT_SHELL_AUTOCOMPLETE_KEY, + } + } + // based on the keybindings configuration provided in the configuration file + // (if any), extract the character triggers command history management + // through tv + pub fn get_command_history_keybinding_character(&self) -> char { + match self.keybindings.get(COMMAND_HISTORY_CONFIGURATION_KEY) { + Some(s) => match parse_key(s) { + Ok(Key::Ctrl(c)) => c.to_uppercase().next().unwrap(), + _ => DEFAULT_COMMAND_HISTORY_KEY, + }, + None => DEFAULT_COMMAND_HISTORY_KEY, + } + } } diff --git a/television/main.rs b/television/main.rs index 0bec8f2..5753c7d 100644 --- a/television/main.rs +++ b/television/main.rs @@ -15,9 +15,10 @@ use television::cli::{ guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel, PostProcessedCli, }; + use television::config::Config; use television::utils::{ - shell::{completion_script, Shell}, + shell::{completion_script, ctrl_keybinding, Shell}, stdin::is_readable_stdin, }; @@ -38,7 +39,30 @@ async fn main() -> Result<()> { exit(0); } television::cli::Command::InitShell { shell } => { - let script = completion_script(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 templated_script = completion_script(Shell::from(shell))?; + let script = templated_script + .replace( + "{tv_smart_autocomplete_keybinding}", + &ctrl_keybinding( + Shell::from(shell), + config + .shell_integration + .get_shell_autocomplete_keybinding_character(), + )?, + ) + .replace( + "{tv_shell_history_keybinding}", + &ctrl_keybinding( + Shell::from(shell), + config + .shell_integration + .get_command_history_keybinding_character(), + )?, + ); + println!("{script}"); exit(0); } diff --git a/television/utils/shell.rs b/television/utils/shell.rs index 5b546e1..6c47bc4 100644 --- a/television/utils/shell.rs +++ b/television/utils/shell.rs @@ -13,6 +13,16 @@ 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"); +// create the appropriate key binding for each supported shell +pub fn ctrl_keybinding(shell: Shell, character: char) -> Result { + match shell { + Shell::Bash => Ok(format!(r"\C-{}", character)), + Shell::Zsh => Ok(format!(r"^{}", character)), + Shell::Fish => Ok(format!(r"\c{}", character)), + _ => anyhow::bail!("This shell is not yet supported: {:?}", shell), + } +} + pub fn completion_script(shell: Shell) -> Result<&'static str> { match shell { Shell::Bash => Ok(COMPLETION_BASH), @@ -21,3 +31,43 @@ pub fn completion_script(shell: Shell) -> Result<&'static str> { _ => anyhow::bail!("This shell is not yet supported: {:?}", shell), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bash_ctrl_keybinding() { + let character = 's'; + let shell = Shell::Bash; + let result = ctrl_keybinding(shell, character); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "\\C-s"); + } + + #[test] + fn test_zsh_ctrl_keybinding() { + let character = 's'; + let shell = Shell::Zsh; + let result = ctrl_keybinding(shell, character); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "^s"); + } + + #[test] + fn test_fish_ctrl_keybinding() { + let character = 's'; + let shell = Shell::Fish; + let result = ctrl_keybinding(shell, character); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "\\cs"); + } + + #[test] + fn test_powershell_ctrl_keybinding() { + let character = 's'; + let shell = Shell::PowerShell; + let result = ctrl_keybinding(shell, character); + assert!(result.is_err()); + } +} diff --git a/television/utils/shell/completion.bash b/television/utils/shell/completion.bash index e570403..e352efe 100644 --- a/television/utils/shell/completion.bash +++ b/television/utils/shell/completion.bash @@ -23,5 +23,5 @@ function tv_shell_history() { fi } -bind -x '"\C-t": tv_smart_autocomplete' -bind -x '"\C-r": tv_shell_history' +bind -x '"{tv_smart_autocomplete_keybinding}": tv_smart_autocomplete' +bind -x '"{tv_shell_history_keybinding}": tv_shell_history' diff --git a/television/utils/shell/completion.fish b/television/utils/shell/completion.fish index 9c38bfc..dee2851 100644 --- a/television/utils/shell/completion.fish +++ b/television/utils/shell/completion.fish @@ -22,5 +22,5 @@ function tv_shell_history end end -bind \ct tv_smart_autocomplete -bind \cr tv_shell_history +bind {tv_smart_autocomplete_keybinding} tv_smart_autocomplete +bind {tv_shell_history_keybinding} tv_shell_history diff --git a/television/utils/shell/completion.zsh b/television/utils/shell/completion.zsh index 2624e44..885b503 100644 --- a/television/utils/shell/completion.zsh +++ b/television/utils/shell/completion.zsh @@ -51,6 +51,6 @@ zle -N tv-smart-autocomplete _tv_smart_autocomplete zle -N tv-shell-history _tv_shell_history -bindkey '^T' tv-smart-autocomplete -bindkey '^R' tv-shell-history +bindkey '{tv_smart_autocomplete_keybinding}' tv-smart-autocomplete +bindkey '{tv_shell_history_keybinding}' tv-shell-history