mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
feat(shell): add fallback channel to the config for smart autocomplete
This commit is contained in:
parent
97314d629a
commit
f78795cbb8
@ -109,6 +109,10 @@ toggle_preview = "ctrl-o"
|
|||||||
# E.g. typing `git checkout <CTRL-T>` will open television with a list of
|
# E.g. typing `git checkout <CTRL-T>` will open television with a list of
|
||||||
# branches to choose from.
|
# branches to choose from.
|
||||||
|
|
||||||
|
[shell_integration]
|
||||||
|
# This specifies the default fallback channel if no other channel is matched.
|
||||||
|
fallback_channel = "files"
|
||||||
|
|
||||||
[shell_integration.channel_triggers]
|
[shell_integration.channel_triggers]
|
||||||
# Add your channel triggers here. Each key is a channel that will be triggered
|
# Add your channel triggers here. Each key is a channel that will be triggered
|
||||||
# by the corresponding commands.
|
# by the corresponding commands.
|
||||||
|
@ -136,6 +136,15 @@ pub enum ParsedCliChannel {
|
|||||||
Cable(CableChannelPrototype),
|
Cable(CableChannelPrototype),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ParsedCliChannel {
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Builtin(c) => c.to_string(),
|
||||||
|
Self::Cable(c) => c.name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CLI_KEYBINDINGS_DELIMITER: char = ';';
|
const CLI_KEYBINDINGS_DELIMITER: char = ';';
|
||||||
|
|
||||||
/// Parse the keybindings string into a hashmap of key -> action.
|
/// Parse the keybindings string into a hashmap of key -> action.
|
||||||
@ -154,7 +163,7 @@ fn parse_keybindings(cli_keybindings: &str) -> Result<KeyBindings> {
|
|||||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_channel(channel: &str) -> Result<ParsedCliChannel> {
|
pub 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();
|
||||||
// try to parse the channel as a cable channel
|
// try to parse the channel as a cable channel
|
||||||
cable_channels
|
cable_channels
|
||||||
@ -165,7 +174,7 @@ fn parse_channel(channel: &str) -> Result<ParsedCliChannel> {
|
|||||||
// try to parse the channel as a builtin channel
|
// try to parse the channel as a builtin channel
|
||||||
CliTvChannel::try_from(channel)
|
CliTvChannel::try_from(channel)
|
||||||
.map(ParsedCliChannel::Builtin)
|
.map(ParsedCliChannel::Builtin)
|
||||||
.map_err(|_| anyhow!("Unknown channel: {}", channel))
|
.map_err(|_| anyhow!("Unknown channel: '{}'", channel))
|
||||||
},
|
},
|
||||||
|(_, v)| Ok(ParsedCliChannel::Cable(v.clone())),
|
|(_, v)| Ok(ParsedCliChannel::Cable(v.clone())),
|
||||||
)
|
)
|
||||||
@ -223,15 +232,13 @@ pub fn list_channels() {
|
|||||||
pub fn guess_channel_from_prompt(
|
pub fn guess_channel_from_prompt(
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
command_mapping: &FxHashMap<String, String>,
|
command_mapping: &FxHashMap<String, String>,
|
||||||
|
fallback_channel: ParsedCliChannel,
|
||||||
) -> Result<ParsedCliChannel> {
|
) -> Result<ParsedCliChannel> {
|
||||||
debug!("Guessing channel from prompt: {}", prompt);
|
debug!("Guessing channel from prompt: {}", prompt);
|
||||||
// git checkout -qf
|
// git checkout -qf
|
||||||
// --- -------- --- <---------
|
// --- -------- --- <---------
|
||||||
if prompt.trim().is_empty() {
|
if prompt.trim().is_empty() {
|
||||||
return match command_mapping.get("") {
|
return Ok(fallback_channel);
|
||||||
Some(channel) => parse_channel(channel),
|
|
||||||
None => Err(anyhow!("No channel found for prompt: {}", prompt)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
let rev_prompt_words = prompt.split_whitespace().rev();
|
let rev_prompt_words = prompt.split_whitespace().rev();
|
||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
@ -259,7 +266,9 @@ pub fn guess_channel_from_prompt(
|
|||||||
// reset the stack
|
// reset the stack
|
||||||
stack.clear();
|
stack.clear();
|
||||||
}
|
}
|
||||||
Err(anyhow!("No channel found for prompt: {}", prompt))
|
|
||||||
|
debug!("No match found, falling back to default channel");
|
||||||
|
Ok(fallback_channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
|
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
|
||||||
@ -458,4 +467,61 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn guess_channel_from_prompt_setup(
|
||||||
|
) -> (FxHashMap<String, String>, ParsedCliChannel) {
|
||||||
|
let mut command_mapping = FxHashMap::default();
|
||||||
|
command_mapping.insert("vim".to_string(), "files".to_string());
|
||||||
|
command_mapping.insert("export".to_string(), "env".to_string());
|
||||||
|
|
||||||
|
(
|
||||||
|
command_mapping,
|
||||||
|
ParsedCliChannel::Builtin(CliTvChannel::Env),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_guess_channel_from_prompt_present() {
|
||||||
|
let prompt = "vim -d file1";
|
||||||
|
|
||||||
|
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||||
|
|
||||||
|
let channel =
|
||||||
|
guess_channel_from_prompt(prompt, &command_mapping, fallback)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(channel.name(), "files");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_guess_channel_from_prompt_fallback() {
|
||||||
|
let prompt = "git checkout ";
|
||||||
|
|
||||||
|
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||||
|
|
||||||
|
let channel = guess_channel_from_prompt(
|
||||||
|
prompt,
|
||||||
|
&command_mapping,
|
||||||
|
fallback.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(channel, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_guess_channel_from_prompt_empty() {
|
||||||
|
let prompt = "";
|
||||||
|
|
||||||
|
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||||
|
|
||||||
|
let channel = guess_channel_from_prompt(
|
||||||
|
prompt,
|
||||||
|
&command_mapping,
|
||||||
|
fallback.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(channel, fallback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,12 @@ impl Config {
|
|||||||
mut default: Config,
|
mut default: Config,
|
||||||
mut user: Config,
|
mut user: Config,
|
||||||
) -> Config {
|
) -> Config {
|
||||||
|
// merge shell integration fallback channel with default fallback channel
|
||||||
|
if user.shell_integration.fallback_channel.is_empty() {
|
||||||
|
user.shell_integration
|
||||||
|
.fallback_channel
|
||||||
|
.clone_from(&default.shell_integration.fallback_channel);
|
||||||
|
}
|
||||||
// merge shell integration triggers with commands
|
// merge shell integration triggers with commands
|
||||||
default.shell_integration.merge_triggers();
|
default.shell_integration.merge_triggers();
|
||||||
user.shell_integration.merge_triggers();
|
user.shell_integration.merge_triggers();
|
||||||
|
@ -15,6 +15,7 @@ pub struct ShellIntegrationConfig {
|
|||||||
pub commands: FxHashMap<String, String>,
|
pub commands: FxHashMap<String, String>,
|
||||||
/// {channel: [commands]}
|
/// {channel: [commands]}
|
||||||
pub channel_triggers: FxHashMap<String, Vec<String>>,
|
pub channel_triggers: FxHashMap<String, Vec<String>>,
|
||||||
|
pub fallback_channel: String,
|
||||||
pub keybindings: FxHashMap<String, String>,
|
pub keybindings: FxHashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,13 +149,28 @@ async fn poll_event(timeout: Duration) -> bool {
|
|||||||
PollFuture { timeout }.await
|
PollFuture { timeout }.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flush_existing_events() {
|
||||||
|
let mut counter = 0;
|
||||||
|
while let Ok(true) = crossterm::event::poll(Duration::from_millis(0)) {
|
||||||
|
if let Ok(crossterm::event::Event::Key(_)) = crossterm::event::read() {
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if counter > 0 {
|
||||||
|
debug!("Flushed {} existing events", counter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EventLoop {
|
impl EventLoop {
|
||||||
|
// FIXME: this init parameter doesn't seem to be used anymore
|
||||||
pub fn new(tick_rate: f64, init: bool) -> Self {
|
pub fn new(tick_rate: f64, init: bool) -> Self {
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
let tick_interval = Duration::from_secs_f64(1.0 / tick_rate);
|
let tick_interval = Duration::from_secs_f64(1.0 / tick_rate);
|
||||||
|
|
||||||
let (abort, mut abort_recv) = mpsc::unbounded_channel();
|
let (abort, mut abort_recv) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
flush_existing_events();
|
||||||
|
|
||||||
if init {
|
if init {
|
||||||
//let mut reader = crossterm::event::EventStream::new();
|
//let mut reader = crossterm::event::EventStream::new();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
@ -6,6 +6,7 @@ use std::process::exit;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use television::channels::cable::PreviewKind;
|
use television::channels::cable::PreviewKind;
|
||||||
|
use television::cli::parse_channel;
|
||||||
use television::utils::clipboard::CLIPBOARD;
|
use television::utils::clipboard::CLIPBOARD;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
@ -31,28 +32,37 @@ async fn main() -> Result<()> {
|
|||||||
television::errors::init()?;
|
television::errors::init()?;
|
||||||
television::logging::init()?;
|
television::logging::init()?;
|
||||||
|
|
||||||
// post-process the CLI arguments
|
debug!("\n\n==== NEW SESSION =====\n");
|
||||||
let args: PostProcessedCli = Cli::parse().into();
|
|
||||||
debug!("{:?}", args);
|
// process the CLI arguments
|
||||||
|
let cli = Cli::parse();
|
||||||
|
debug!("CLI: {:?}", cli);
|
||||||
|
let args: PostProcessedCli = cli.into();
|
||||||
|
debug!("PostProcessedCli: {:?}", args);
|
||||||
|
|
||||||
// load the configuration file
|
// load the configuration file
|
||||||
|
debug!("Loading configuration...");
|
||||||
let mut config = Config::new(&ConfigEnv::init()?)?;
|
let mut config = Config::new(&ConfigEnv::init()?)?;
|
||||||
|
|
||||||
// optionally handle subcommands
|
// optionally handle subcommands
|
||||||
|
debug!("Handling subcommands...");
|
||||||
args.command.as_ref().map(handle_subcommands);
|
args.command.as_ref().map(handle_subcommands);
|
||||||
|
|
||||||
// optionally change the working directory
|
// optionally change the working directory
|
||||||
args.working_directory.as_ref().map(set_current_dir);
|
args.working_directory.as_ref().map(set_current_dir);
|
||||||
|
|
||||||
// optionally override configuration values with CLI arguments
|
// optionally override configuration values with CLI arguments
|
||||||
|
debug!("Applying CLI overrides...");
|
||||||
apply_cli_overrides(&args, &mut config);
|
apply_cli_overrides(&args, &mut config);
|
||||||
|
|
||||||
// determine the channel to use based on the CLI arguments and configuration
|
// determine the channel to use based on the CLI arguments and configuration
|
||||||
|
debug!("Determining channel...");
|
||||||
let channel =
|
let channel =
|
||||||
determine_channel(args.clone(), &config, is_readable_stdin())?;
|
determine_channel(args.clone(), &config, is_readable_stdin())?;
|
||||||
|
|
||||||
CLIPBOARD.with(<_>::default);
|
CLIPBOARD.with(<_>::default);
|
||||||
|
|
||||||
|
debug!("Creating application...");
|
||||||
let mut app =
|
let mut app =
|
||||||
App::new(channel, config, &args.passthrough_keybindings, args.input);
|
App::new(channel, config, &args.passthrough_keybindings, args.input);
|
||||||
stdout().flush()?;
|
stdout().flush()?;
|
||||||
@ -141,9 +151,11 @@ pub fn determine_channel(
|
|||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
} else if let Some(prompt) = args.autocomplete_prompt {
|
} else if let Some(prompt) = args.autocomplete_prompt {
|
||||||
|
debug!("Using autocomplete prompt: {:?}", prompt);
|
||||||
let channel = guess_channel_from_prompt(
|
let channel = guess_channel_from_prompt(
|
||||||
&prompt,
|
&prompt,
|
||||||
&config.shell_integration.commands,
|
&config.shell_integration.commands,
|
||||||
|
parse_channel(&config.shell_integration.fallback_channel)?,
|
||||||
)?;
|
)?;
|
||||||
debug!("Using guessed channel: {:?}", channel);
|
debug!("Using guessed channel: {:?}", channel);
|
||||||
match channel {
|
match channel {
|
||||||
@ -217,6 +229,7 @@ mod tests {
|
|||||||
let mut config = Config {
|
let mut config = Config {
|
||||||
shell_integration:
|
shell_integration:
|
||||||
television::config::shell_integration::ShellIntegrationConfig {
|
television::config::shell_integration::ShellIntegrationConfig {
|
||||||
|
fallback_channel: "files".to_string(),
|
||||||
commands: FxHashMap::default(),
|
commands: FxHashMap::default(),
|
||||||
channel_triggers: {
|
channel_triggers: {
|
||||||
let mut m = FxHashMap::default();
|
let mut m = FxHashMap::default();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user