mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 03:55:23 +00:00
feat(shell): autocompletion plugin for zsh (#145)
This commit is contained in:
parent
22f1b4dc33
commit
68d118986c
@ -12,6 +12,12 @@
|
|||||||
# In that case, television will expect the configuration file to be in:
|
# In that case, television will expect the configuration file to be in:
|
||||||
# `$XDG_CONFIG_HOME/television/config.toml`
|
# `$XDG_CONFIG_HOME/television/config.toml`
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# General settings
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
frame_rate = 60
|
||||||
|
tick_rate = 50
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
# Whether to use nerd font icons in the UI
|
# Whether to use nerd font icons in the UI
|
||||||
# This option requires a font patched with Nerd Font in order to properly
|
# This option requires a font patched with Nerd Font in order to properly
|
||||||
@ -118,3 +124,55 @@ select_entry = "enter"
|
|||||||
toggle_send_to_channel = "ctrl-s"
|
toggle_send_to_channel = "ctrl-s"
|
||||||
# Toggle the help bar
|
# Toggle the help bar
|
||||||
toggle_help = "ctrl-g"
|
toggle_help = "ctrl-g"
|
||||||
|
|
||||||
|
|
||||||
|
# Shell integration
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# The shell integration feature allows you to use television as a picker for
|
||||||
|
# your shell commands. E.g. typing `git checkout <TAB>` will open television
|
||||||
|
# with a list of branches to checkout.
|
||||||
|
|
||||||
|
[shell_integration.commands]
|
||||||
|
# Which commands should trigger which channels
|
||||||
|
# The keys are the commands that should trigger the channels, and the values
|
||||||
|
# are the channels that should be triggered.
|
||||||
|
# Example: `git checkout` should trigger the `git-branches` channel
|
||||||
|
# `ls` should trigger the `dirs` channel
|
||||||
|
# `cat` should trigger the `files` channel
|
||||||
|
#
|
||||||
|
# Would be written as:
|
||||||
|
# ```
|
||||||
|
# [shell_integration.commands]
|
||||||
|
# "git checkout" = "git-branches"
|
||||||
|
# "ls" = "dirs"
|
||||||
|
# "cat" = "files"
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# The following are an example, you can remove/modify them and add your own.
|
||||||
|
|
||||||
|
# shell history (according to your shell)
|
||||||
|
"" = "zsh-history"
|
||||||
|
|
||||||
|
# dirs channel
|
||||||
|
"cd" = "dirs"
|
||||||
|
"ls" = "dirs"
|
||||||
|
"rmdir" = "dirs"
|
||||||
|
|
||||||
|
# files channel
|
||||||
|
"cat" = "files"
|
||||||
|
"less" = "files"
|
||||||
|
"head" = "files"
|
||||||
|
"tail" = "files"
|
||||||
|
"vim" = "files"
|
||||||
|
"bat" = "files"
|
||||||
|
|
||||||
|
# git-branch channel
|
||||||
|
"git checkout" = "git-branch"
|
||||||
|
"git branch -d" = "git-branch"
|
||||||
|
|
||||||
|
# docker-images channel
|
||||||
|
"docker run" = "docker-images"
|
||||||
|
|
||||||
|
# gitrepos channel
|
||||||
|
"nvim" = "gitrepos"
|
||||||
|
@ -86,6 +86,14 @@ It is inspired by the neovim [telescope](https://github.com/nvim-telescope/teles
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Shell integration (currently only zsh)
|
||||||
|
To enable shell integration, run:
|
||||||
|
```bash
|
||||||
|
echo 'eval "$(tv init zsh)"' >> ~/.zshrc
|
||||||
|
```
|
||||||
|
And then restart your shell.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
```bash
|
```bash
|
||||||
tv [channel] #[default: files] [possible values: env, files, gitrepos, text, alias]
|
tv [channel] #[default: files] [possible values: env, files, gitrepos, text, alias]
|
||||||
|
44
cable/unix-channels.toml
Normal file
44
cable/unix-channels.toml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# GIT
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-reflog"
|
||||||
|
source_command = 'git reflog'
|
||||||
|
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'
|
||||||
|
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-log"
|
||||||
|
source_command = "git log --oneline --date=short --pretty=\"format:%h %s %an %cd\" \"$@\""
|
||||||
|
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
|
||||||
|
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-branch"
|
||||||
|
source_command = "git --no-pager branch --all --format=\"%(refname:short)\""
|
||||||
|
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "docker-images"
|
||||||
|
source_command = "docker image list --format \"{{.ID}}\""
|
||||||
|
preview_command = "docker image inspect {0} | jq -C"
|
||||||
|
|
||||||
|
# S3
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "s3-buckets"
|
||||||
|
source_command = "aws s3 ls | cut -d \" \" -f 3"
|
||||||
|
preview_command = "aws s3 ls s3://{0}"
|
||||||
|
|
||||||
|
# Dotfiles
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "my-dotfiles"
|
||||||
|
source_command = "fd -t f . $HOME/.config"
|
||||||
|
preview_command = "bat -n --color=always {0}"
|
||||||
|
|
||||||
|
# Shell history
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "zsh-history"
|
||||||
|
source_command = "tac $HOME/.zsh_history | cut -d\";\" -f 2-"
|
||||||
|
preview_command = "echo '{}'"
|
||||||
|
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "bash-history"
|
||||||
|
source_command = "tac $HOME/.bash_history"
|
||||||
|
preview_command = "echo '{}'"
|
21
cable/windows-channels.toml
Normal file
21
cable/windows-channels.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# GIT
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-reflog"
|
||||||
|
source_command = 'git reflog'
|
||||||
|
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'
|
||||||
|
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-log"
|
||||||
|
source_command = "git log --oneline --date=short --pretty=\"format:%h %s %an %cd\" \"$@\""
|
||||||
|
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
|
||||||
|
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "git-branch"
|
||||||
|
source_command = "git --no-pager branch --all --format=\"%(refname:short)\""
|
||||||
|
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
[[cable_channel]]
|
||||||
|
name = "docker-images"
|
||||||
|
source_command = "docker image list --format \"{{.ID}}\""
|
||||||
|
preview_command = "docker image inspect {0} | jq -C"
|
@ -63,12 +63,14 @@ async fn load_candidates(command: String, injector: Injector<String>) {
|
|||||||
.output()
|
.output()
|
||||||
.expect("failed to execute process");
|
.expect("failed to execute process");
|
||||||
|
|
||||||
let branches = String::from_utf8(output.stdout).unwrap();
|
let decoded_output = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
for line in branches.lines() {
|
for line in decoded_output.lines() {
|
||||||
let () = injector.push(line.to_string(), |e, cols| {
|
if !line.trim().is_empty() {
|
||||||
cols[0] = e.clone().into();
|
let () = injector.push(line.to_string(), |e, cols| {
|
||||||
});
|
cols[0] = e.clone().into();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ pub mod files;
|
|||||||
pub mod indices;
|
pub mod indices;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod shell;
|
||||||
pub mod stdin;
|
pub mod stdin;
|
||||||
pub mod strings;
|
pub mod strings;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
22
crates/television-utils/src/shell.rs
Normal file
22
crates/television-utils/src/shell.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Shell {
|
||||||
|
Bash,
|
||||||
|
Zsh,
|
||||||
|
Fish,
|
||||||
|
PowerShell,
|
||||||
|
Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMPLETION_ZSH: &str = include_str!("../../../shell/completion.zsh");
|
||||||
|
|
||||||
|
pub fn completion_script(shell: Shell) -> Result<&'static str> {
|
||||||
|
match shell {
|
||||||
|
Shell::Zsh => Ok(COMPLETION_ZSH),
|
||||||
|
_ => color_eyre::eyre::bail!(
|
||||||
|
"This shell is not yet supported: {:?}",
|
||||||
|
shell
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
@ -78,15 +78,15 @@ impl From<ActionOutcome> for AppOutput {
|
|||||||
impl App {
|
impl App {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
channel: TelevisionChannel,
|
channel: TelevisionChannel,
|
||||||
tick_rate: f64,
|
config: Config,
|
||||||
frame_rate: f64,
|
|
||||||
passthrough_keybindings: &[String],
|
passthrough_keybindings: &[String],
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||||
let (render_tx, _) = mpsc::unbounded_channel();
|
let (render_tx, _) = mpsc::unbounded_channel();
|
||||||
let (_, event_rx) = mpsc::unbounded_channel();
|
let (_, event_rx) = mpsc::unbounded_channel();
|
||||||
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
||||||
let config = Config::new()?;
|
let frame_rate = config.config.frame_rate;
|
||||||
|
let tick_rate = config.config.tick_rate;
|
||||||
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
|
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
|
||||||
Mode::Channel,
|
Mode::Channel,
|
||||||
passthrough_keybindings
|
passthrough_keybindings
|
||||||
|
@ -16,6 +16,14 @@ struct ChannelPrototypes {
|
|||||||
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
||||||
const CABLE_FILE_FORMAT: &str = "toml";
|
const CABLE_FILE_FORMAT: &str = "toml";
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
const DEFAULT_CABLE_CHANNELS: &str =
|
||||||
|
include_str!("../../cable/unix-channels.toml");
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
const DEFAULT_CABLE_CHANNELS: &str =
|
||||||
|
include_str!("../../cable/windows-channels.toml");
|
||||||
|
|
||||||
/// Load the cable configuration from the config directory.
|
/// Load the cable configuration from the config directory.
|
||||||
///
|
///
|
||||||
/// Cable is loaded by compiling all files that match the following
|
/// Cable is loaded by compiling all files that match the following
|
||||||
@ -40,7 +48,7 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
|||||||
.filter(|p| {
|
.filter(|p| {
|
||||||
p.extension()
|
p.extension()
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| e.to_str())
|
||||||
.map_or(false, |e| e == CABLE_FILE_FORMAT)
|
.map_or(false, |e| e.to_lowercase() == CABLE_FILE_FORMAT)
|
||||||
})
|
})
|
||||||
.filter(|p| {
|
.filter(|p| {
|
||||||
p.file_stem()
|
p.file_stem()
|
||||||
@ -48,7 +56,7 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
|||||||
.map_or(false, |s| s.ends_with(CABLE_FILE_NAME_SUFFIX))
|
.map_or(false, |s| s.ends_with(CABLE_FILE_NAME_SUFFIX))
|
||||||
});
|
});
|
||||||
|
|
||||||
let all_prototypes = file_paths.fold(Vec::new(), |mut acc, p| {
|
let user_defined_prototypes = file_paths.fold(Vec::new(), |mut acc, p| {
|
||||||
let r: ChannelPrototypes = toml::from_str(
|
let r: ChannelPrototypes = toml::from_str(
|
||||||
&std::fs::read_to_string(p)
|
&std::fs::read_to_string(p)
|
||||||
.expect("Unable to read configuration file"),
|
.expect("Unable to read configuration file"),
|
||||||
@ -58,10 +66,20 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
|||||||
acc
|
acc
|
||||||
});
|
});
|
||||||
|
|
||||||
debug!("Loaded cable channels: {:?}", all_prototypes);
|
debug!("Loaded cable channels: {:?}", user_defined_prototypes);
|
||||||
|
|
||||||
|
let default_prototypes: ChannelPrototypes =
|
||||||
|
toml::from_str(DEFAULT_CABLE_CHANNELS)
|
||||||
|
.expect("Unable to parse default cable channels");
|
||||||
|
|
||||||
let mut cable_channels = HashMap::new();
|
let mut cable_channels = HashMap::new();
|
||||||
for prototype in all_prototypes {
|
// chaining default with user defined prototypes so that users may override the
|
||||||
|
// default prototypes
|
||||||
|
for prototype in default_prototypes
|
||||||
|
.prototypes
|
||||||
|
.into_iter()
|
||||||
|
.chain(user_defined_prototypes)
|
||||||
|
{
|
||||||
cable_channels.insert(prototype.name.clone(), prototype);
|
cable_channels.insert(prototype.name.clone(), prototype);
|
||||||
}
|
}
|
||||||
Ok(CableChannels(cable_channels))
|
Ok(CableChannels(cable_channels))
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use std::path::Path;
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use color_eyre::{eyre::eyre, Result};
|
use color_eyre::{eyre::eyre, Result};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cable,
|
cable,
|
||||||
@ -11,6 +12,7 @@ use television_channels::{
|
|||||||
cable::CableChannelPrototype, channels::CliTvChannel,
|
cable::CableChannelPrototype, channels::CliTvChannel,
|
||||||
entry::PreviewCommand,
|
entry::PreviewCommand,
|
||||||
};
|
};
|
||||||
|
use television_utils::shell::Shell as UtilShell;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version = version(), about)]
|
#[command(author, version = version(), about)]
|
||||||
@ -29,12 +31,12 @@ pub struct Cli {
|
|||||||
pub delimiter: String,
|
pub delimiter: String,
|
||||||
|
|
||||||
/// Tick rate, i.e. number of ticks per second
|
/// Tick rate, i.e. number of ticks per second
|
||||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 50.0)]
|
#[arg(short, long, value_name = "FLOAT")]
|
||||||
pub tick_rate: f64,
|
pub tick_rate: Option<f64>,
|
||||||
|
|
||||||
/// Frame rate, i.e. number of frames per second
|
/// Frame rate, i.e. number of frames per second
|
||||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
|
#[arg(short, long, value_name = "FLOAT")]
|
||||||
pub frame_rate: f64,
|
pub frame_rate: Option<f64>,
|
||||||
|
|
||||||
/// Passthrough keybindings (comma separated, e.g. "q,ctrl-w,ctrl-t") These keybindings will
|
/// Passthrough keybindings (comma separated, e.g. "q,ctrl-w,ctrl-t") These keybindings will
|
||||||
/// trigger selection of the current entry and be passed through to stdout along with the entry
|
/// trigger selection of the current entry and be passed through to stdout along with the entry
|
||||||
@ -46,6 +48,10 @@ pub struct Cli {
|
|||||||
#[arg(value_name = "PATH", index = 2)]
|
#[arg(value_name = "PATH", index = 2)]
|
||||||
pub working_directory: Option<String>,
|
pub working_directory: Option<String>,
|
||||||
|
|
||||||
|
/// Try to guess the channel from the provided input prompt
|
||||||
|
#[arg(long, value_name = "STRING")]
|
||||||
|
pub autocomplete_prompt: Option<String>,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
}
|
}
|
||||||
@ -54,17 +60,46 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Lists available channels
|
/// Lists available channels
|
||||||
ListChannels,
|
ListChannels,
|
||||||
|
/// Initializes shell completion ("tv init zsh")
|
||||||
|
#[clap(name = "init")]
|
||||||
|
InitShell {
|
||||||
|
/// The shell for which to generate the autocompletion script
|
||||||
|
#[arg(value_enum)]
|
||||||
|
shell: Shell,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
|
||||||
|
pub enum Shell {
|
||||||
|
Bash,
|
||||||
|
Zsh,
|
||||||
|
Fish,
|
||||||
|
PowerShell,
|
||||||
|
Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Shell> for UtilShell {
|
||||||
|
fn from(val: Shell) -> Self {
|
||||||
|
match val {
|
||||||
|
Shell::Bash => UtilShell::Bash,
|
||||||
|
Shell::Zsh => UtilShell::Zsh,
|
||||||
|
Shell::Fish => UtilShell::Fish,
|
||||||
|
Shell::PowerShell => UtilShell::PowerShell,
|
||||||
|
Shell::Cmd => UtilShell::Cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PostProcessedCli {
|
pub struct PostProcessedCli {
|
||||||
pub channel: ParsedCliChannel,
|
pub channel: ParsedCliChannel,
|
||||||
pub preview_command: Option<PreviewCommand>,
|
pub preview_command: Option<PreviewCommand>,
|
||||||
pub tick_rate: f64,
|
pub tick_rate: Option<f64>,
|
||||||
pub frame_rate: f64,
|
pub frame_rate: Option<f64>,
|
||||||
pub passthrough_keybindings: Vec<String>,
|
pub passthrough_keybindings: Vec<String>,
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
pub working_directory: Option<String>,
|
pub working_directory: Option<String>,
|
||||||
|
pub autocomplete_prompt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Cli> for PostProcessedCli {
|
impl From<Cli> for PostProcessedCli {
|
||||||
@ -84,7 +119,7 @@ impl From<Cli> for PostProcessedCli {
|
|||||||
let channel: ParsedCliChannel;
|
let channel: ParsedCliChannel;
|
||||||
let working_directory: Option<String>;
|
let working_directory: Option<String>;
|
||||||
|
|
||||||
match channel_parser(&cli.channel) {
|
match parse_channel(&cli.channel) {
|
||||||
Ok(p) => {
|
Ok(p) => {
|
||||||
channel = p;
|
channel = p;
|
||||||
working_directory = cli.working_directory;
|
working_directory = cli.working_directory;
|
||||||
@ -112,6 +147,7 @@ impl From<Cli> for PostProcessedCli {
|
|||||||
passthrough_keybindings,
|
passthrough_keybindings,
|
||||||
command: cli.command,
|
command: cli.command,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
autocomplete_prompt: cli.autocomplete_prompt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +165,7 @@ pub enum ParsedCliChannel {
|
|||||||
Cable(CableChannelPrototype),
|
Cable(CableChannelPrototype),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channel_parser(channel: &str) -> Result<ParsedCliChannel> {
|
fn parse_channel(channel: &str) -> Result<ParsedCliChannel> {
|
||||||
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
||||||
CliTvChannel::try_from(channel)
|
CliTvChannel::try_from(channel)
|
||||||
.map(ParsedCliChannel::Builtin)
|
.map(ParsedCliChannel::Builtin)
|
||||||
@ -170,6 +206,69 @@ pub fn list_channels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backtrack from the end of the prompt and try to match each word to a known command
|
||||||
|
/// if a match is found, return the corresponding channel
|
||||||
|
/// if no match is found, throw an error
|
||||||
|
///
|
||||||
|
/// ## Example:
|
||||||
|
/// ```
|
||||||
|
/// let prompt = "ls -l";
|
||||||
|
/// let command_mapping = hashmap! {
|
||||||
|
/// "ls".to_string() => "files".to_string(),
|
||||||
|
/// "cat".to_string() => "files".to_string(),
|
||||||
|
/// };
|
||||||
|
/// let channel = guess_channel_from_prompt(prompt, &command_mapping).unwrap();
|
||||||
|
///
|
||||||
|
/// assert_eq!(channel, ParsedCliChannel::Builtin(CliTvChannel::Files));
|
||||||
|
/// ```
|
||||||
|
/// NOTE: this is a very naive implementation and needs to be improved
|
||||||
|
/// - it should be able to handle prompts containing multiple commands
|
||||||
|
/// e.g. "ls -l && cat <CTRL+T>"
|
||||||
|
/// - it should be able to handle commands within delimiters (quotes, brackets, etc.)
|
||||||
|
pub fn guess_channel_from_prompt(
|
||||||
|
prompt: &str,
|
||||||
|
command_mapping: &HashMap<String, String>,
|
||||||
|
) -> Result<ParsedCliChannel> {
|
||||||
|
debug!("Guessing channel from prompt: {}", prompt);
|
||||||
|
// git checkout -qf
|
||||||
|
// --- -------- --- <---------
|
||||||
|
if prompt.trim().is_empty() {
|
||||||
|
match command_mapping.get("") {
|
||||||
|
Some(channel) => return parse_channel(channel),
|
||||||
|
None => {
|
||||||
|
return Err(eyre!("No channel found for prompt: {}", prompt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rev_prompt_words = prompt.split_whitespace().rev();
|
||||||
|
let mut stack = Vec::new();
|
||||||
|
// for each patern
|
||||||
|
for (pattern, channel) in command_mapping {
|
||||||
|
if pattern.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// push every word of the pattern onto the stack
|
||||||
|
stack.extend(pattern.split_whitespace());
|
||||||
|
for word in rev_prompt_words.clone() {
|
||||||
|
// if the stack is empty, we have a match
|
||||||
|
if stack.is_empty() {
|
||||||
|
return parse_channel(channel);
|
||||||
|
}
|
||||||
|
// if the word matches the top of the stack, pop it
|
||||||
|
if stack.last() == Some(&word) {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the stack is empty, we have a match
|
||||||
|
if stack.is_empty() {
|
||||||
|
return parse_channel(channel);
|
||||||
|
}
|
||||||
|
// reset the stack
|
||||||
|
stack.clear();
|
||||||
|
}
|
||||||
|
Err(eyre!("No channel found for prompt: {}", prompt))
|
||||||
|
}
|
||||||
|
|
||||||
fn delimiter_parser(s: &str) -> Result<String, String> {
|
fn delimiter_parser(s: &str) -> Result<String, String> {
|
||||||
Ok(match s {
|
Ok(match s {
|
||||||
"" => ":".to_string(),
|
"" => ":".to_string(),
|
||||||
@ -230,11 +329,12 @@ mod tests {
|
|||||||
channel: "files".to_string(),
|
channel: "files".to_string(),
|
||||||
preview: Some("bat -n --color=always {}".to_string()),
|
preview: Some("bat -n --color=always {}".to_string()),
|
||||||
delimiter: ":".to_string(),
|
delimiter: ":".to_string(),
|
||||||
tick_rate: 50.0,
|
tick_rate: Some(50.0),
|
||||||
frame_rate: 60.0,
|
frame_rate: Some(60.0),
|
||||||
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
|
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
|
||||||
command: None,
|
command: None,
|
||||||
working_directory: Some("/home/user".to_string()),
|
working_directory: Some("/home/user".to_string()),
|
||||||
|
autocomplete_prompt: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
@ -250,8 +350,8 @@ mod tests {
|
|||||||
delimiter: ":".to_string()
|
delimiter: ":".to_string()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(post_processed_cli.tick_rate, 50.0);
|
assert_eq!(post_processed_cli.tick_rate, Some(50.0));
|
||||||
assert_eq!(post_processed_cli.frame_rate, 60.0);
|
assert_eq!(post_processed_cli.frame_rate, Some(60.0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_processed_cli.passthrough_keybindings,
|
post_processed_cli.passthrough_keybindings,
|
||||||
vec!["q".to_string(), "ctrl-w".to_string(), "ctrl-t".to_string()]
|
vec!["q".to_string(), "ctrl-w".to_string(), "ctrl-t".to_string()]
|
||||||
@ -269,11 +369,12 @@ mod tests {
|
|||||||
channel: ".".to_string(),
|
channel: ".".to_string(),
|
||||||
preview: None,
|
preview: None,
|
||||||
delimiter: ":".to_string(),
|
delimiter: ":".to_string(),
|
||||||
tick_rate: 50.0,
|
tick_rate: Some(50.0),
|
||||||
frame_rate: 60.0,
|
frame_rate: Some(60.0),
|
||||||
passthrough_keybindings: None,
|
passthrough_keybindings: None,
|
||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
|
autocomplete_prompt: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
|
@ -8,6 +8,7 @@ pub use keybindings::KeyBindings;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use previewers::PreviewersConfig;
|
use previewers::PreviewersConfig;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use shell_integration::ShellIntegrationConfig;
|
||||||
use styles::Styles;
|
use styles::Styles;
|
||||||
pub use themes::Theme;
|
pub use themes::Theme;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@ -15,6 +16,7 @@ use ui::UiConfig;
|
|||||||
|
|
||||||
mod keybindings;
|
mod keybindings;
|
||||||
mod previewers;
|
mod previewers;
|
||||||
|
mod shell_integration;
|
||||||
mod styles;
|
mod styles;
|
||||||
mod themes;
|
mod themes;
|
||||||
mod ui;
|
mod ui;
|
||||||
@ -28,6 +30,10 @@ pub struct AppConfig {
|
|||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub config_dir: PathBuf,
|
pub config_dir: PathBuf,
|
||||||
|
#[serde(default = "default_frame_rate")]
|
||||||
|
pub frame_rate: f64,
|
||||||
|
#[serde(default = "default_tick_rate")]
|
||||||
|
pub tick_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -44,6 +50,8 @@ pub struct Config {
|
|||||||
pub ui: UiConfig,
|
pub ui: UiConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub previewers: PreviewersConfig,
|
pub previewers: PreviewersConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shell_integration: ShellIntegrationConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@ -79,9 +87,15 @@ impl Config {
|
|||||||
let mut builder = config::Config::builder()
|
let mut builder = config::Config::builder()
|
||||||
.set_default("data_dir", data_dir.to_str().unwrap())?
|
.set_default("data_dir", data_dir.to_str().unwrap())?
|
||||||
.set_default("config_dir", config_dir.to_str().unwrap())?
|
.set_default("config_dir", config_dir.to_str().unwrap())?
|
||||||
|
.set_default("frame_rate", default_config.config.frame_rate)?
|
||||||
|
.set_default("tick_rate", default_config.config.tick_rate)?
|
||||||
.set_default("ui", default_config.ui.clone())?
|
.set_default("ui", default_config.ui.clone())?
|
||||||
.set_default("previewers", default_config.previewers.clone())?
|
.set_default("previewers", default_config.previewers.clone())?
|
||||||
.set_default("theme", default_config.ui.theme.clone())?;
|
.set_default("theme", default_config.ui.theme.clone())?
|
||||||
|
.set_default(
|
||||||
|
"shell_integration",
|
||||||
|
default_config.shell_integration.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Load the user's config file
|
// Load the user's config file
|
||||||
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
|
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
|
||||||
@ -163,3 +177,11 @@ pub fn get_config_dir() -> PathBuf {
|
|||||||
fn project_directory() -> Option<ProjectDirs> {
|
fn project_directory() -> Option<ProjectDirs> {
|
||||||
ProjectDirs::from("com", "", env!("CARGO_PKG_NAME"))
|
ProjectDirs::from("com", "", env!("CARGO_PKG_NAME"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_frame_rate() -> f64 {
|
||||||
|
60.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_tick_rate() -> f64 {
|
||||||
|
50.0
|
||||||
|
}
|
||||||
|
24
crates/television/config/shell_integration.rs
Normal file
24
crates/television/config/shell_integration.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
pub struct ShellIntegrationConfig {
|
||||||
|
pub commands: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShellIntegrationConfig> for config::ValueKind {
|
||||||
|
fn from(val: ShellIntegrationConfig) -> Self {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
m.insert(
|
||||||
|
String::from("commands"),
|
||||||
|
config::ValueKind::Table(
|
||||||
|
val.commands
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, config::ValueKind::String(v).into()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
config::ValueKind::Table(m)
|
||||||
|
}
|
||||||
|
}
|
@ -4,16 +4,23 @@ use std::path::Path;
|
|||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{list_channels, ParsedCliChannel, PostProcessedCli};
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use television_channels::channels::TelevisionChannel;
|
|
||||||
use television_channels::entry::PreviewType;
|
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::cli::Cli;
|
use crate::cli::{
|
||||||
use television_channels::channels::stdin::Channel as StdinChannel;
|
guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel,
|
||||||
use television_utils::stdin::is_readable_stdin;
|
PostProcessedCli,
|
||||||
|
};
|
||||||
|
use crate::config::Config;
|
||||||
|
use television_channels::{
|
||||||
|
channels::{stdin::Channel as StdinChannel, TelevisionChannel},
|
||||||
|
entry::PreviewType,
|
||||||
|
};
|
||||||
|
use television_utils::{
|
||||||
|
shell::{completion_script, Shell},
|
||||||
|
stdin::is_readable_stdin,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
@ -44,9 +51,20 @@ async fn main() -> Result<()> {
|
|||||||
list_channels();
|
list_channels();
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
cli::Command::InitShell { shell } => {
|
||||||
|
let script = completion_script(Shell::from(shell))?;
|
||||||
|
println!("{script}");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut config = Config::new()?;
|
||||||
|
config.config.tick_rate =
|
||||||
|
args.tick_rate.unwrap_or(config.config.tick_rate);
|
||||||
|
config.config.frame_rate =
|
||||||
|
args.frame_rate.unwrap_or(config.config.frame_rate);
|
||||||
|
|
||||||
if let Some(working_directory) = args.working_directory {
|
if let Some(working_directory) = args.working_directory {
|
||||||
let path = Path::new(&working_directory);
|
let path = Path::new(&working_directory);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@ -70,6 +88,18 @@ async fn main() -> Result<()> {
|
|||||||
TelevisionChannel::Stdin(StdinChannel::new(
|
TelevisionChannel::Stdin(StdinChannel::new(
|
||||||
args.preview_command.map(PreviewType::Command),
|
args.preview_command.map(PreviewType::Command),
|
||||||
))
|
))
|
||||||
|
} else if let Some(prompt) = args.autocomplete_prompt {
|
||||||
|
let channel = guess_channel_from_prompt(
|
||||||
|
&prompt,
|
||||||
|
&config.shell_integration.commands,
|
||||||
|
)?;
|
||||||
|
debug!("Using guessed channel: {:?}", channel);
|
||||||
|
match channel {
|
||||||
|
ParsedCliChannel::Builtin(c) => c.to_channel(),
|
||||||
|
ParsedCliChannel::Cable(c) => {
|
||||||
|
TelevisionChannel::Cable(c.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!("Using {:?} channel", args.channel);
|
debug!("Using {:?} channel", args.channel);
|
||||||
match args.channel {
|
match args.channel {
|
||||||
@ -80,8 +110,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
args.tick_rate,
|
config,
|
||||||
args.frame_rate,
|
|
||||||
&args.passthrough_keybindings,
|
&args.passthrough_keybindings,
|
||||||
) {
|
) {
|
||||||
Ok(mut app) => {
|
Ok(mut app) => {
|
||||||
|
29
shell/completion.zsh
Normal file
29
shell/completion.zsh
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
_tv_search() {
|
||||||
|
emulate -L zsh
|
||||||
|
zle -I
|
||||||
|
|
||||||
|
local current_prompt
|
||||||
|
current_prompt=$LBUFFER
|
||||||
|
|
||||||
|
local output
|
||||||
|
|
||||||
|
output=$(tv --autocomplete-prompt "$current_prompt" $*)
|
||||||
|
|
||||||
|
zle reset-prompt
|
||||||
|
|
||||||
|
if [[ -n $output ]]; then
|
||||||
|
RBUFFER=""
|
||||||
|
LBUFFER=$current_prompt$output
|
||||||
|
|
||||||
|
# uncomment this to automatically accept the line
|
||||||
|
# (i.e. run the command without having to press enter twice)
|
||||||
|
# zle accept-line
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
zle -N tv-search _tv_search
|
||||||
|
|
||||||
|
|
||||||
|
bindkey '^T' tv-search
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user