From 5271b507a04af992f49ef04871abc8edeb5e0b81 Mon Sep 17 00:00:00 2001 From: Bertrand Chardon <51328958+bertrand-chardon@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:43:22 +0100 Subject: [PATCH] chore(terminal): custom shell keybindings (#313) --- .config/config.toml | 8 +++ television/config/mod.rs | 2 +- television/config/shell_integration.rs | 33 ++++++++++++ television/main.rs | 12 ++++- television/utils/shell.rs | 75 +++++++++++++++++++++++++- television/utils/shell/completion.bash | 4 +- television/utils/shell/completion.fish | 4 +- television/utils/shell/completion.zsh | 4 +- 8 files changed, 133 insertions(+), 9 deletions(-) diff --git a/.config/config.toml b/.config/config.toml index 776b1a0..bbfd6dc 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -195,3 +195,11 @@ toggle_preview = "ctrl-o" # gitrepos channel "nvim" = "git-repos" + +[shell_integration.keybindings] +# controls which key binding should trigger tv +# for shell autocomplete +"smart_autocomplete" = "ctrl-t" +# controls which keybinding should trigger tv +# for command history +"command_history" = "ctrl-r" \ No newline at end of file diff --git a/television/config/mod.rs b/television/config/mod.rs index 3448def..67d7953 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -15,7 +15,7 @@ use ui::UiConfig; mod keybindings; mod previewers; -mod shell_integration; +pub mod shell_integration; mod themes; mod ui; 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..d7bc2e0 100644 --- a/television/main.rs +++ b/television/main.rs @@ -15,7 +15,9 @@ use television::cli::{ guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel, PostProcessedCli, }; + use television::config::Config; +use television::utils::shell::render_autocomplete_script_template; use television::utils::{ shell::{completion_script, Shell}, stdin::is_readable_stdin, @@ -38,7 +40,15 @@ async fn main() -> Result<()> { exit(0); } television::cli::Command::InitShell { shell } => { - let script = completion_script(Shell::from(shell))?; + let target_shell = 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 script = render_autocomplete_script_template( + target_shell, + completion_script(target_shell)?, + &config.shell_integration, + )?; println!("{script}"); exit(0); } diff --git a/television/utils/shell.rs b/television/utils/shell.rs index 5b546e1..41e46a5 100644 --- a/television/utils/shell.rs +++ b/television/utils/shell.rs @@ -1,5 +1,5 @@ +use crate::config::shell_integration::ShellIntegrationConfig; use anyhow::Result; - #[derive(Debug, Clone, Copy, PartialEq)] pub enum Shell { Bash, @@ -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,66 @@ pub fn completion_script(shell: Shell) -> Result<&'static str> { _ => anyhow::bail!("This shell is not yet supported: {:?}", shell), } } + +pub fn render_autocomplete_script_template( + shell: Shell, + template: &str, + config: &ShellIntegrationConfig, +) -> Result { + let script = template + .replace( + "{tv_smart_autocomplete_keybinding}", + &ctrl_keybinding( + shell, + config.get_shell_autocomplete_keybinding_character(), + )?, + ) + .replace( + "{tv_shell_history_keybinding}", + &ctrl_keybinding( + shell, + config.get_command_history_keybinding_character(), + )?, + ); + Ok(script) +} + +#[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