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:
|
||||
# `$XDG_CONFIG_HOME/television/config.toml`
|
||||
#
|
||||
|
||||
# General settings
|
||||
# ----------------------------------------------------------------------------
|
||||
frame_rate = 60
|
||||
tick_rate = 50
|
||||
|
||||
[ui]
|
||||
# Whether to use nerd font icons in the UI
|
||||
# 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 the help bar
|
||||
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>
|
||||
|
||||
### Shell integration (currently only zsh)
|
||||
To enable shell integration, run:
|
||||
```bash
|
||||
echo 'eval "$(tv init zsh)"' >> ~/.zshrc
|
||||
```
|
||||
And then restart your shell.
|
||||
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
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()
|
||||
.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() {
|
||||
let () = injector.push(line.to_string(), |e, cols| {
|
||||
cols[0] = e.clone().into();
|
||||
});
|
||||
for line in decoded_output.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
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 input;
|
||||
pub mod metadata;
|
||||
pub mod shell;
|
||||
pub mod stdin;
|
||||
pub mod strings;
|
||||
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 {
|
||||
pub fn new(
|
||||
channel: TelevisionChannel,
|
||||
tick_rate: f64,
|
||||
frame_rate: f64,
|
||||
config: Config,
|
||||
passthrough_keybindings: &[String],
|
||||
) -> Result<Self> {
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
let (render_tx, _) = mpsc::unbounded_channel();
|
||||
let (_, event_rx) = 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(
|
||||
Mode::Channel,
|
||||
passthrough_keybindings
|
||||
|
@ -16,6 +16,14 @@ struct ChannelPrototypes {
|
||||
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
||||
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.
|
||||
///
|
||||
/// Cable is loaded by compiling all files that match the following
|
||||
@ -40,7 +48,7 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.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| {
|
||||
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))
|
||||
});
|
||||
|
||||
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(
|
||||
&std::fs::read_to_string(p)
|
||||
.expect("Unable to read configuration file"),
|
||||
@ -58,10 +66,20 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
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 tracing::debug;
|
||||
|
||||
use crate::{
|
||||
cable,
|
||||
@ -11,6 +12,7 @@ use television_channels::{
|
||||
cable::CableChannelPrototype, channels::CliTvChannel,
|
||||
entry::PreviewCommand,
|
||||
};
|
||||
use television_utils::shell::Shell as UtilShell;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = version(), about)]
|
||||
@ -29,12 +31,12 @@ pub struct Cli {
|
||||
pub delimiter: String,
|
||||
|
||||
/// Tick rate, i.e. number of ticks per second
|
||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 50.0)]
|
||||
pub tick_rate: f64,
|
||||
#[arg(short, long, value_name = "FLOAT")]
|
||||
pub tick_rate: Option<f64>,
|
||||
|
||||
/// Frame rate, i.e. number of frames per second
|
||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
|
||||
pub frame_rate: f64,
|
||||
#[arg(short, long, value_name = "FLOAT")]
|
||||
pub frame_rate: Option<f64>,
|
||||
|
||||
/// 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
|
||||
@ -46,6 +48,10 @@ pub struct Cli {
|
||||
#[arg(value_name = "PATH", index = 2)]
|
||||
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: Option<Command>,
|
||||
}
|
||||
@ -54,17 +60,46 @@ pub struct Cli {
|
||||
pub enum Command {
|
||||
/// Lists available channels
|
||||
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)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: ParsedCliChannel,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
pub tick_rate: f64,
|
||||
pub frame_rate: f64,
|
||||
pub tick_rate: Option<f64>,
|
||||
pub frame_rate: Option<f64>,
|
||||
pub passthrough_keybindings: Vec<String>,
|
||||
pub command: Option<Command>,
|
||||
pub working_directory: Option<String>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Cli> for PostProcessedCli {
|
||||
@ -84,7 +119,7 @@ impl From<Cli> for PostProcessedCli {
|
||||
let channel: ParsedCliChannel;
|
||||
let working_directory: Option<String>;
|
||||
|
||||
match channel_parser(&cli.channel) {
|
||||
match parse_channel(&cli.channel) {
|
||||
Ok(p) => {
|
||||
channel = p;
|
||||
working_directory = cli.working_directory;
|
||||
@ -112,6 +147,7 @@ impl From<Cli> for PostProcessedCli {
|
||||
passthrough_keybindings,
|
||||
command: cli.command,
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,7 +165,7 @@ pub enum ParsedCliChannel {
|
||||
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();
|
||||
CliTvChannel::try_from(channel)
|
||||
.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> {
|
||||
Ok(match s {
|
||||
"" => ":".to_string(),
|
||||
@ -230,11 +329,12 @@ mod tests {
|
||||
channel: "files".to_string(),
|
||||
preview: Some("bat -n --color=always {}".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: 50.0,
|
||||
frame_rate: 60.0,
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
|
||||
command: None,
|
||||
working_directory: Some("/home/user".to_string()),
|
||||
autocomplete_prompt: None,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
@ -250,8 +350,8 @@ mod tests {
|
||||
delimiter: ":".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(post_processed_cli.tick_rate, 50.0);
|
||||
assert_eq!(post_processed_cli.frame_rate, 60.0);
|
||||
assert_eq!(post_processed_cli.tick_rate, Some(50.0));
|
||||
assert_eq!(post_processed_cli.frame_rate, Some(60.0));
|
||||
assert_eq!(
|
||||
post_processed_cli.passthrough_keybindings,
|
||||
vec!["q".to_string(), "ctrl-w".to_string(), "ctrl-t".to_string()]
|
||||
@ -269,11 +369,12 @@ mod tests {
|
||||
channel: ".".to_string(),
|
||||
preview: None,
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: 50.0,
|
||||
frame_rate: 60.0,
|
||||
tick_rate: Some(50.0),
|
||||
frame_rate: Some(60.0),
|
||||
passthrough_keybindings: None,
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
@ -8,6 +8,7 @@ pub use keybindings::KeyBindings;
|
||||
use lazy_static::lazy_static;
|
||||
use previewers::PreviewersConfig;
|
||||
use serde::Deserialize;
|
||||
use shell_integration::ShellIntegrationConfig;
|
||||
use styles::Styles;
|
||||
pub use themes::Theme;
|
||||
use tracing::{debug, warn};
|
||||
@ -15,6 +16,7 @@ use ui::UiConfig;
|
||||
|
||||
mod keybindings;
|
||||
mod previewers;
|
||||
mod shell_integration;
|
||||
mod styles;
|
||||
mod themes;
|
||||
mod ui;
|
||||
@ -28,6 +30,10 @@ pub struct AppConfig {
|
||||
pub data_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
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)]
|
||||
@ -44,6 +50,8 @@ pub struct Config {
|
||||
pub ui: UiConfig,
|
||||
#[serde(default)]
|
||||
pub previewers: PreviewersConfig,
|
||||
#[serde(default)]
|
||||
pub shell_integration: ShellIntegrationConfig,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
@ -79,9 +87,15 @@ impl Config {
|
||||
let mut builder = config::Config::builder()
|
||||
.set_default("data_dir", data_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("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
|
||||
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> {
|
||||
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 clap::Parser;
|
||||
use cli::{list_channels, ParsedCliChannel, PostProcessedCli};
|
||||
use color_eyre::Result;
|
||||
use television_channels::channels::TelevisionChannel;
|
||||
use television_channels::entry::PreviewType;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::cli::Cli;
|
||||
use television_channels::channels::stdin::Channel as StdinChannel;
|
||||
use television_utils::stdin::is_readable_stdin;
|
||||
use crate::cli::{
|
||||
guess_channel_from_prompt, list_channels, Cli, ParsedCliChannel,
|
||||
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 app;
|
||||
@ -44,9 +51,20 @@ async fn main() -> Result<()> {
|
||||
list_channels();
|
||||
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 {
|
||||
let path = Path::new(&working_directory);
|
||||
if !path.exists() {
|
||||
@ -70,6 +88,18 @@ async fn main() -> Result<()> {
|
||||
TelevisionChannel::Stdin(StdinChannel::new(
|
||||
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 {
|
||||
debug!("Using {:?} channel", args.channel);
|
||||
match args.channel {
|
||||
@ -80,8 +110,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
},
|
||||
args.tick_rate,
|
||||
args.frame_rate,
|
||||
config,
|
||||
&args.passthrough_keybindings,
|
||||
) {
|
||||
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