mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
refactor: avoid multiple cable channel parsing passes
This commit is contained in:
parent
ba1c3fe8ee
commit
9b56e6687b
@ -17,6 +17,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
frame_rate = 60 # DEPRECATED: this option is no longer used
|
||||
tick_rate = 50
|
||||
default_channel = "files"
|
||||
|
||||
[ui]
|
||||
# Whether to use nerd font icons in the UI
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -11,6 +11,8 @@ jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@ -18,7 +20,7 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install fd
|
||||
run: sudo apt-get install -y fd-find
|
||||
run: sudo apt install -y fd-find && sudo ln -s $(which fdfind) /usr/bin/fd
|
||||
- name: Run tests
|
||||
run: cargo test --locked --all-features --workspace -- --nocapture
|
||||
|
||||
|
@ -4,6 +4,13 @@ name = "files"
|
||||
source_command = "fd -t f"
|
||||
preview_command = ":files:"
|
||||
|
||||
# Text
|
||||
[[cable_channel]]
|
||||
name = "text"
|
||||
source_command = "rg . --no-heading --line-number"
|
||||
preview_command = "bat -n --color=always {0} -H {1}"
|
||||
preview_delimiter = ":"
|
||||
|
||||
# Directories
|
||||
[[cable_channel]]
|
||||
name = "dirs"
|
||||
|
@ -10,9 +10,9 @@ use crate::config::get_config_dir;
|
||||
|
||||
/// Just a proxy struct to deserialize prototypes
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
struct ChannelPrototypes {
|
||||
pub struct ChannelPrototypes {
|
||||
#[serde(rename = "cable_channel")]
|
||||
prototypes: Vec<CableChannelPrototype>,
|
||||
pub prototypes: Vec<CableChannelPrototype>,
|
||||
}
|
||||
|
||||
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
||||
@ -85,6 +85,10 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
);
|
||||
|
||||
debug!("Loaded cable channels: {:?}", prototypes);
|
||||
if prototypes.is_empty() {
|
||||
error!("No cable channels found");
|
||||
return Err(anyhow::anyhow!("No cable channels found"));
|
||||
}
|
||||
|
||||
let mut cable_channels = FxHashMap::default();
|
||||
for prototype in prototypes {
|
||||
|
@ -13,11 +13,14 @@ use regex::Regex;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
|
||||
use crate::channels::OnAir;
|
||||
use crate::matcher::Matcher;
|
||||
use crate::matcher::{config::Config, injector::Injector};
|
||||
use crate::utils::command::shell_command;
|
||||
use crate::{
|
||||
cable::ChannelPrototypes,
|
||||
channels::entry::{Entry, PreviewCommand, PreviewType},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PreviewKind {
|
||||
@ -39,10 +42,10 @@ pub struct Channel {
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
"Files",
|
||||
"files",
|
||||
"find . -type f",
|
||||
false,
|
||||
Some(PreviewCommand::new("bat -n --color=always {}", ":")),
|
||||
Some(PreviewCommand::new("cat {}", ":")),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -290,7 +293,7 @@ impl Display for CableChannelPrototype {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct CableChannels(pub FxHashMap<String, CableChannelPrototype>);
|
||||
|
||||
impl Deref for CableChannels {
|
||||
@ -300,3 +303,25 @@ impl Deref for CableChannels {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_CABLE_CHANNELS_FILE: &str =
|
||||
include_str!("../../cable/unix-channels.toml");
|
||||
#[cfg(not(unix))]
|
||||
const DEFAULT_CABLE_CHANNELS_FILE: &str =
|
||||
include_str!("../../cable/windows-channels.toml");
|
||||
|
||||
impl Default for CableChannels {
|
||||
/// Fallback to the default cable channels specification (the template file
|
||||
/// included in the repo).
|
||||
fn default() -> Self {
|
||||
let pts =
|
||||
toml::from_str::<ChannelPrototypes>(DEFAULT_CABLE_CHANNELS_FILE)
|
||||
.expect("Unable to parse default cable channels");
|
||||
let mut channels = FxHashMap::default();
|
||||
for prototype in pts.prototypes {
|
||||
channels.insert(prototype.name.clone(), prototype);
|
||||
}
|
||||
CableChannels(channels)
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,8 @@ pub struct Cli {
|
||||
/// A list of the available channels can be displayed using the
|
||||
/// `list-channels` command. The channel can also be changed from within
|
||||
/// the application.
|
||||
#[arg(
|
||||
value_enum,
|
||||
default_value = "files",
|
||||
index = 1,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub channel: String,
|
||||
#[arg(value_enum, index = 1, verbatim_doc_comment)]
|
||||
pub channel: Option<String>,
|
||||
|
||||
/// A preview command to use with the stdin channel.
|
||||
///
|
||||
|
@ -4,10 +4,10 @@ use std::path::Path;
|
||||
use anyhow::{anyhow, Result};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::cable::{parse_preview_kind, PreviewKind};
|
||||
use crate::channels::cable::{parse_preview_kind, CableChannels, PreviewKind};
|
||||
use crate::channels::{cable::CableChannelPrototype, entry::PreviewCommand};
|
||||
use crate::cli::args::{Cli, Command};
|
||||
use crate::config::KeyBindings;
|
||||
use crate::config::{KeyBindings, DEFAULT_CHANNEL};
|
||||
use crate::{
|
||||
cable,
|
||||
config::{get_config_dir, get_data_dir},
|
||||
@ -86,7 +86,16 @@ impl From<Cli> for PostProcessedCli {
|
||||
let channel: CableChannelPrototype;
|
||||
let working_directory: Option<String>;
|
||||
|
||||
match parse_channel(&cli.channel) {
|
||||
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
||||
if cli.channel.is_none() {
|
||||
channel = cable_channels
|
||||
.get(DEFAULT_CHANNEL)
|
||||
.expect("Default channel not found in cable channels")
|
||||
.clone();
|
||||
working_directory = cli.working_directory;
|
||||
} else {
|
||||
let cli_channel = cli.channel.as_ref().unwrap().to_owned();
|
||||
match parse_channel(&cli_channel, &cable_channels) {
|
||||
Ok(p) => {
|
||||
channel = p;
|
||||
working_directory = cli.working_directory;
|
||||
@ -95,16 +104,22 @@ impl From<Cli> for PostProcessedCli {
|
||||
// if the path is provided as first argument and it exists, use it as the working
|
||||
// directory and default to the files channel
|
||||
if cli.working_directory.is_none()
|
||||
&& Path::new(&cli.channel).exists()
|
||||
&& Path::new(&cli_channel).exists()
|
||||
{
|
||||
channel = CableChannelPrototype::default();
|
||||
working_directory = Some(cli.channel.clone());
|
||||
channel = cable_channels
|
||||
.get(DEFAULT_CHANNEL)
|
||||
.expect(
|
||||
"Default channel not found in cable channels",
|
||||
)
|
||||
.clone();
|
||||
working_directory = Some(cli.channel.unwrap().clone());
|
||||
} else {
|
||||
unknown_channel_exit(&cli.channel);
|
||||
unknown_channel_exit(&cli.channel.unwrap());
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
channel,
|
||||
@ -157,15 +172,17 @@ fn parse_keybindings_literal(
|
||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub fn parse_channel(channel: &str) -> Result<CableChannelPrototype> {
|
||||
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
||||
pub fn parse_channel(
|
||||
channel: &str,
|
||||
cable_channels: &CableChannels,
|
||||
) -> Result<CableChannelPrototype> {
|
||||
// try to parse the channel as a cable channel
|
||||
match cable_channels
|
||||
.iter()
|
||||
.find(|(k, _)| k.to_lowercase() == channel)
|
||||
{
|
||||
Some((_, v)) => Ok(v.clone()),
|
||||
None => Err(anyhow!("Unknown channel: {channel}")),
|
||||
None => Err(anyhow!("The following channel wasn't found among cable channels: {channel}")),
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,13 +218,18 @@ pub fn list_channels() {
|
||||
pub fn guess_channel_from_prompt(
|
||||
prompt: &str,
|
||||
command_mapping: &FxHashMap<String, String>,
|
||||
fallback_channel: CableChannelPrototype,
|
||||
fallback_channel: &str,
|
||||
cable_channels: &CableChannels,
|
||||
) -> Result<CableChannelPrototype> {
|
||||
debug!("Guessing channel from prompt: {}", prompt);
|
||||
// git checkout -qf
|
||||
// --- -------- --- <---------
|
||||
let fallback = cable_channels
|
||||
.get(fallback_channel)
|
||||
.expect("Fallback channel not found in cable channels")
|
||||
.clone();
|
||||
if prompt.trim().is_empty() {
|
||||
return Ok(fallback_channel);
|
||||
return Ok(fallback);
|
||||
}
|
||||
let rev_prompt_words = prompt.split_whitespace().rev();
|
||||
let mut stack = Vec::new();
|
||||
@ -221,7 +243,7 @@ pub fn guess_channel_from_prompt(
|
||||
for word in rev_prompt_words.clone() {
|
||||
// if the stack is empty, we have a match
|
||||
if stack.is_empty() {
|
||||
return parse_channel(channel);
|
||||
return parse_channel(channel, cable_channels);
|
||||
}
|
||||
// if the word matches the top of the stack, pop it
|
||||
if stack.last() == Some(&word) {
|
||||
@ -230,14 +252,14 @@ pub fn guess_channel_from_prompt(
|
||||
}
|
||||
// if the stack is empty, we have a match
|
||||
if stack.is_empty() {
|
||||
return parse_channel(channel);
|
||||
return parse_channel(channel, cable_channels);
|
||||
}
|
||||
// reset the stack
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
debug!("No match found, falling back to default channel");
|
||||
Ok(fallback_channel)
|
||||
Ok(fallback)
|
||||
}
|
||||
|
||||
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
|
||||
@ -285,7 +307,7 @@ mod tests {
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn test_from_cli() {
|
||||
let cli = Cli {
|
||||
channel: "files".to_string(),
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some("bat -n --color=always {}".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
working_directory: Some("/home/user".to_string()),
|
||||
@ -317,7 +339,7 @@ mod tests {
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn test_from_cli_no_args() {
|
||||
let cli = Cli {
|
||||
channel: ".".to_string(),
|
||||
channel: Some(".".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
@ -338,7 +360,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_builtin_previewer_files() {
|
||||
let cli = Cli {
|
||||
channel: "files".to_string(),
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":files:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
@ -355,7 +377,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_builtin_previewer_env() {
|
||||
let cli = Cli {
|
||||
channel: "files".to_string(),
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":env_var:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
@ -372,7 +394,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_custom_keybindings() {
|
||||
let cli = Cli {
|
||||
channel: "files".to_string(),
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":env_var:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
keybindings: Some(
|
||||
@ -395,15 +417,16 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Returns a tuple containing a command mapping and a fallback channel.
|
||||
fn guess_channel_from_prompt_setup(
|
||||
) -> (FxHashMap<String, String>, CableChannelPrototype) {
|
||||
fn guess_channel_from_prompt_setup<'a>(
|
||||
) -> (FxHashMap<String, String>, &'a str, CableChannels) {
|
||||
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,
|
||||
CableChannelPrototype::new("env", "", false, None, None),
|
||||
"env",
|
||||
cable::load_cable_channels().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -411,10 +434,15 @@ mod tests {
|
||||
fn test_guess_channel_from_prompt_present() {
|
||||
let prompt = "vim -d file1";
|
||||
|
||||
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||
let (command_mapping, fallback, channels) =
|
||||
guess_channel_from_prompt_setup();
|
||||
|
||||
let channel =
|
||||
guess_channel_from_prompt(prompt, &command_mapping, fallback)
|
||||
let channel = guess_channel_from_prompt(
|
||||
prompt,
|
||||
&command_mapping,
|
||||
fallback,
|
||||
&channels,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(channel.name, "files");
|
||||
@ -424,31 +452,35 @@ mod tests {
|
||||
fn test_guess_channel_from_prompt_fallback() {
|
||||
let prompt = "git checkout ";
|
||||
|
||||
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||
let (command_mapping, fallback, channels) =
|
||||
guess_channel_from_prompt_setup();
|
||||
|
||||
let channel = guess_channel_from_prompt(
|
||||
prompt,
|
||||
&command_mapping,
|
||||
fallback.clone(),
|
||||
fallback,
|
||||
&channels,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(channel, fallback);
|
||||
assert_eq!(channel.name, fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guess_channel_from_prompt_empty() {
|
||||
let prompt = "";
|
||||
|
||||
let (command_mapping, fallback) = guess_channel_from_prompt_setup();
|
||||
let (command_mapping, fallback, channels) =
|
||||
guess_channel_from_prompt_setup();
|
||||
|
||||
let channel = guess_channel_from_prompt(
|
||||
prompt,
|
||||
&command_mapping,
|
||||
fallback.clone(),
|
||||
fallback,
|
||||
&channels,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(channel, fallback);
|
||||
assert_eq!(channel.name, fallback);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,15 @@ pub struct AppConfig {
|
||||
pub frame_rate: f64,
|
||||
#[serde(default = "default_tick_rate")]
|
||||
pub tick_rate: f64,
|
||||
/// The default channel to use when no channel is specified
|
||||
#[serde(default = "default_channel")]
|
||||
pub default_channel: String,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CHANNEL: &str = "files";
|
||||
|
||||
fn default_channel() -> String {
|
||||
DEFAULT_CHANNEL.to_string()
|
||||
}
|
||||
|
||||
impl Hash for AppConfig {
|
||||
|
@ -5,8 +5,8 @@ use std::process::exit;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use television::channels::cable::PreviewKind;
|
||||
use television::cli::parse_channel;
|
||||
use television::cable;
|
||||
use television::channels::cable::{CableChannels, PreviewKind};
|
||||
use television::utils::clipboard::CLIPBOARD;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@ -43,6 +43,9 @@ async fn main() -> Result<()> {
|
||||
debug!("Loading configuration...");
|
||||
let mut config = Config::new(&ConfigEnv::init()?)?;
|
||||
|
||||
debug!("Loading cable channels...");
|
||||
let cable_channels = cable::load_cable_channels().unwrap_or_default();
|
||||
|
||||
// optionally handle subcommands
|
||||
debug!("Handling subcommands...");
|
||||
args.command
|
||||
@ -58,8 +61,12 @@ async fn main() -> Result<()> {
|
||||
|
||||
// determine the channel to use based on the CLI arguments and configuration
|
||||
debug!("Determining channel...");
|
||||
let channel =
|
||||
determine_channel(args.clone(), &config, is_readable_stdin())?;
|
||||
let channel = determine_channel(
|
||||
args.clone(),
|
||||
&config,
|
||||
is_readable_stdin(),
|
||||
&cable_channels,
|
||||
)?;
|
||||
|
||||
CLIPBOARD.with(<_>::default);
|
||||
|
||||
@ -146,6 +153,7 @@ pub fn determine_channel(
|
||||
args: PostProcessedCli,
|
||||
config: &Config,
|
||||
readable_stdin: bool,
|
||||
cable_channels: &CableChannels,
|
||||
) -> Result<TelevisionChannel> {
|
||||
if readable_stdin {
|
||||
debug!("Using stdin channel");
|
||||
@ -163,7 +171,8 @@ pub fn determine_channel(
|
||||
let channel = guess_channel_from_prompt(
|
||||
&prompt,
|
||||
&config.shell_integration.commands,
|
||||
parse_channel(&config.shell_integration.fallback_channel)?,
|
||||
&config.shell_integration.fallback_channel,
|
||||
cable_channels,
|
||||
)?;
|
||||
debug!("Using guessed channel: {:?}", channel);
|
||||
Ok(TelevisionChannel::Cable(channel.into()))
|
||||
@ -176,7 +185,9 @@ pub fn determine_channel(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
use television::channels::cable::CableChannelPrototype;
|
||||
use television::{
|
||||
cable::load_cable_channels, channels::cable::CableChannelPrototype,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -185,9 +196,13 @@ mod tests {
|
||||
config: &Config,
|
||||
readable_stdin: bool,
|
||||
expected_channel: &TelevisionChannel,
|
||||
cable_channels: Option<CableChannels>,
|
||||
) {
|
||||
let channels: CableChannels = cable_channels
|
||||
.unwrap_or_else(|| load_cable_channels().unwrap_or_default());
|
||||
let channel =
|
||||
determine_channel(args.clone(), config, readable_stdin).unwrap();
|
||||
determine_channel(args.clone(), config, readable_stdin, &channels)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
channel.name(),
|
||||
@ -212,6 +227,7 @@ mod tests {
|
||||
&config,
|
||||
true,
|
||||
&TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -242,7 +258,13 @@ mod tests {
|
||||
};
|
||||
config.shell_integration.merge_triggers();
|
||||
|
||||
assert_is_correct_channel(&args, &config, false, &expected_channel);
|
||||
assert_is_correct_channel(
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&expected_channel,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -262,6 +284,7 @@ mod tests {
|
||||
CableChannelPrototype::new("dirs", "", false, None, None)
|
||||
.into(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user