chore(terminal): custom shell keybindings (#313)

This commit is contained in:
Bertrand Chardon 2025-01-25 22:43:22 +01:00 committed by GitHub
parent eede078715
commit 5271b507a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 133 additions and 9 deletions

View File

@ -195,3 +195,11 @@ toggle_preview = "ctrl-o"
# gitrepos channel # gitrepos channel
"nvim" = "git-repos" "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"

View File

@ -15,7 +15,7 @@ use ui::UiConfig;
mod keybindings; mod keybindings;
mod previewers; mod previewers;
mod shell_integration; pub mod shell_integration;
mod themes; mod themes;
mod ui; mod ui;

View File

@ -1,7 +1,40 @@
use crate::config::parse_key;
use crate::event::Key;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
pub struct ShellIntegrationConfig { pub struct ShellIntegrationConfig {
pub commands: FxHashMap<String, String>, pub commands: FxHashMap<String, String>,
pub keybindings: FxHashMap<String, String>,
}
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,
}
}
} }

View File

@ -15,7 +15,9 @@ use television::cli::{
guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel, guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel,
PostProcessedCli, PostProcessedCli,
}; };
use television::config::Config; use television::config::Config;
use television::utils::shell::render_autocomplete_script_template;
use television::utils::{ use television::utils::{
shell::{completion_script, Shell}, shell::{completion_script, Shell},
stdin::is_readable_stdin, stdin::is_readable_stdin,
@ -38,7 +40,15 @@ async fn main() -> Result<()> {
exit(0); exit(0);
} }
television::cli::Command::InitShell { shell } => { 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}"); println!("{script}");
exit(0); exit(0);
} }

View File

@ -1,5 +1,5 @@
use crate::config::shell_integration::ShellIntegrationConfig;
use anyhow::Result; use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Shell { pub enum Shell {
Bash, 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_BASH: &str = include_str!("shell/completion.bash");
const COMPLETION_FISH: &str = include_str!("shell/completion.fish"); 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<String> {
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> { pub fn completion_script(shell: Shell) -> Result<&'static str> {
match shell { match shell {
Shell::Bash => Ok(COMPLETION_BASH), 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), _ => anyhow::bail!("This shell is not yet supported: {:?}", shell),
} }
} }
pub fn render_autocomplete_script_template(
shell: Shell,
template: &str,
config: &ShellIntegrationConfig,
) -> Result<String> {
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());
}
}

View File

@ -23,5 +23,5 @@ function tv_shell_history() {
fi fi
} }
bind -x '"\C-t": tv_smart_autocomplete' bind -x '"{tv_smart_autocomplete_keybinding}": tv_smart_autocomplete'
bind -x '"\C-r": tv_shell_history' bind -x '"{tv_shell_history_keybinding}": tv_shell_history'

View File

@ -22,5 +22,5 @@ function tv_shell_history
end end
end end
bind \ct tv_smart_autocomplete bind {tv_smart_autocomplete_keybinding} tv_smart_autocomplete
bind \cr tv_shell_history bind {tv_shell_history_keybinding} tv_shell_history

View File

@ -51,6 +51,6 @@ zle -N tv-smart-autocomplete _tv_smart_autocomplete
zle -N tv-shell-history _tv_shell_history zle -N tv-shell-history _tv_shell_history
bindkey '^T' tv-smart-autocomplete bindkey '{tv_smart_autocomplete_keybinding}' tv-smart-autocomplete
bindkey '^R' tv-shell-history bindkey '{tv_shell_history_keybinding}' tv-shell-history