Repository: alexpasmantier/television (language: rust)

Issue:
Allow overriding or disabling Ctrl+T keybinding in shell
**Description**
When I execute in the shell `Ctrl+T`, I get this error:

```
Error:
   0: No channel found for prompt:
```

I guess this happens because the shell integration is expecting a command here. However, I use [fzf-file-widget](https://github.com/junegunn/fzf/blob/master/shell/key-bindings.zsh) as generic option for any command.

Would it be possible to override `Ctrl+T` keybinding? I don't find the way.

**Example**
Just press `Ctrl+T` in the shell.

**Expected behavior**
Television allowing changing shell keybinding, or at least disabling some particular keybindings.

**Actual behavior**
I get the described error.

**Environment**
 - OS: Ubuntu 24.04
 - Project version: 0.9.2


Discussion / hints:
Will add an option to configure the default bindings with something like:

```sh
tv init zsh --bind-history 'C-r' --bind-smart-completion 'C-t'
```
> Will add an option to configure the default bindings with something like:
> 
> ```sh
> tv init zsh --bind-history 'C-r' --bind-smart-completion 'C-t'
> ```
that would be very useful ❤️

Would be cool if we can get an option to disable the shell-history search hotkey
> Will add an option to configure the default bindings with something like:
> 
> tv init zsh --bind-history 'C-r' --bind-smart-completion 'C-t'

Is it the same thing that @logoff was asking for?

It would be nice to be able to customize the commands (like `C-r`, `C-p`...) and be able to disable the history one since I use [https://github.com/atuinsh/atuin](atuin).

I might be wrong, but I think @logoff was saying that if you hit `C-t` in a shell without a command, you get the `No channel found for prompt` error.

Current workaround is this:

```
[shell_integration.commands]
"" = "files"
```

but maybe it would be nice to have something like `"*" = "files"` or a default, so you could use tv with any command.

Like if I try with a fake `aaa` command:

```
❯ aaa Error:
   0: No channel found for prompt: aaa
```

Fix patch:
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<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,
+        }
+    }
 }
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<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> {
     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<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());
+    }
+}
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
 


Test patch: