mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-03 01:50:12 +00:00
refactor!: all channels are now cable channels (#479)
- tv's default channel (when lauching `tv`) is now configurable via the `default_channel` configuration option - add `RUST_BACKTRACE=1` and `--nocapture` to ci testing for better debugging - remove all builtin channels and associated glue code as well as the `ToCliChannel` and `ToUnitChannel` derive macros - recode all builtin channels using shell commands (along with `fd`, `bat`, and `rg`) - add support for interactive shell commands inside cable channels - drop the `send_to_channel` feature until further notice (will be reimplemented later on in a more generic and customizable way)
This commit is contained in:
parent
1f0c178a2d
commit
67677fb917
@ -15,8 +15,9 @@
|
||||
|
||||
# General settings
|
||||
# ----------------------------------------------------------------------------
|
||||
frame_rate = 60 # DEPRECATED: this option is no longer used
|
||||
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
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -11,14 +11,18 @@ jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install fd
|
||||
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
|
||||
run: cargo test --locked --all-features --workspace -- --nocapture
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2252,7 +2252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television-derive"
|
||||
version = "0.0.26"
|
||||
version = "0.0.27"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -31,7 +31,7 @@ build = "build.rs"
|
||||
path = "television/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
television-derive = { path = "television-derive", version = "0.0.26" }
|
||||
television-derive = { path = "television-derive", version = "0.0.27" }
|
||||
|
||||
anyhow = "1.0"
|
||||
base64 = "0.22.1"
|
||||
|
@ -8,12 +8,12 @@ use ratatui::prelude::{Line, Style};
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
|
||||
use ratatui::Terminal;
|
||||
use std::path::PathBuf;
|
||||
use television::action::Action;
|
||||
use television::channels::cable::CableChannelPrototype;
|
||||
use television::channels::entry::into_ranges;
|
||||
use television::channels::entry::{Entry, PreviewType};
|
||||
use television::channels::OnAir;
|
||||
use television::channels::{files::Channel, TelevisionChannel};
|
||||
use television::channels::TelevisionChannel;
|
||||
use television::config::{Config, ConfigEnv};
|
||||
use television::screen::colors::ResultsColorscheme;
|
||||
use television::screen::results::build_results_list;
|
||||
@ -506,10 +506,9 @@ pub fn draw(c: &mut Criterion) {
|
||||
let backend = TestBackend::new(width, height);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
let (tx, _) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut channel =
|
||||
TelevisionChannel::Files(Channel::new(vec![
|
||||
PathBuf::from("."),
|
||||
]));
|
||||
let mut channel = TelevisionChannel::Cable(
|
||||
CableChannelPrototype::default().into(),
|
||||
);
|
||||
channel.find("television");
|
||||
// Wait for the channel to finish loading
|
||||
let mut tv = Television::new(
|
||||
|
@ -1,4 +1,41 @@
|
||||
# Files
|
||||
[[cable_channel]]
|
||||
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"
|
||||
source_command = "fd -t d"
|
||||
preview_command = "ls -la --color=always {}"
|
||||
|
||||
# Environment variables
|
||||
[[cable_channel]]
|
||||
name = "env"
|
||||
source_command = "printenv"
|
||||
preview_command = "cut -d= -f2 <<< ${0} | cut -d\" \" -f2- | sed 's/:/\\n/g'"
|
||||
|
||||
# Aliases
|
||||
[[cable_channel]]
|
||||
name = "aliases"
|
||||
source_command = "alias"
|
||||
interactive = true
|
||||
|
||||
# GIT
|
||||
[[cable_channel]]
|
||||
name = "git-repos"
|
||||
# this is a MacOS version but feel free to change it to fit your needs
|
||||
source_command = "fd -g .git -HL -t d -d 10 --prune ~ -E 'Library' -E 'Application Support' --exec dirname {}"
|
||||
preview_command = "cd {} && git log -n 200 --pretty=medium --all --graph --color"
|
||||
|
||||
[[cable_channel]]
|
||||
name = "git-diff"
|
||||
source_command = "git diff --name-only"
|
||||
|
@ -1,41 +1,65 @@
|
||||
# Files
|
||||
[[cable_channel]]
|
||||
name = "files"
|
||||
source_command = "Get-ChildItem -Recurse -File | Select-Object -ExpandProperty FullName"
|
||||
preview_command = ":files:"
|
||||
|
||||
# Directories
|
||||
[[cable_channel]]
|
||||
name = "dirs"
|
||||
source_command = "Get-ChildItem -Recurse -Directory | Select-Object -ExpandProperty FullName"
|
||||
preview_command = "ls -l {0}"
|
||||
|
||||
# Environment variables
|
||||
[[cable_channel]]
|
||||
name = "env"
|
||||
source_command = "Get-ChildItem Env:"
|
||||
|
||||
# Aliases
|
||||
[[cable_channel]]
|
||||
name = "aliases"
|
||||
source_command = "Get-Alias"
|
||||
|
||||
# GIT
|
||||
[[cable_channel]]
|
||||
name = "git-repos"
|
||||
source_command = "Get-ChildItem -Path 'C:\\Users' -Recurse -Directory -Force -ErrorAction SilentlyContinue | Where-Object { Test-Path \"$($_.FullName)\\.git\" } | Select-Object -ExpandProperty FullName"
|
||||
preview_command = "cd '{}' ; git log -n 200 --pretty=medium --all --graph --color"
|
||||
|
||||
[[cable_channel]]
|
||||
name = "git-diff"
|
||||
source_command = "git diff --name-only"
|
||||
preview_command = "git diff --color=always -- {0}"
|
||||
preview_command = "git diff --color=always {0}"
|
||||
|
||||
[[cable_channel]]
|
||||
name = "git-reflog"
|
||||
source_command = 'git reflog'
|
||||
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'
|
||||
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\" \"$@\""
|
||||
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)\""
|
||||
source_command = "git 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}}\""
|
||||
source_command = "docker image ls --format '{{.ID}}'"
|
||||
preview_command = "docker image inspect {0} | jq -C"
|
||||
|
||||
# Dotfiles (adapted to common Windows dotfile locations)
|
||||
[[cable_channel]]
|
||||
name = "my-dotfiles"
|
||||
source_command = "Get-ChildItem -Recurse -File -Path \"$env:USERPROFILE\\AppData\\Roaming\\\""
|
||||
preview_command = ":files:"
|
||||
|
||||
# Shell history
|
||||
[[cable_channel]]
|
||||
name = "zsh-history"
|
||||
source_command = "tail -r $HISTFILE | cut -d\";\" -f 2-"
|
||||
|
||||
[[cable_channel]]
|
||||
name = "bash-history"
|
||||
source_command = "tail -r $HISTFILE"
|
||||
|
||||
[[cable_channel]]
|
||||
name = "fish-history"
|
||||
source_command = "fish -c 'history'"
|
||||
|
||||
name = "powershell-history"
|
||||
source_command = "Get-Content (Get-PSReadLineOption).HistorySavePath | Select-Object -Last 500"
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-derive"
|
||||
version = "0.0.26"
|
||||
version = "0.0.27"
|
||||
edition = "2021"
|
||||
description = "The revolution will be televised."
|
||||
license = "MIT"
|
||||
|
@ -1,129 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
/// This macro generates a `CliChannel` enum and the necessary glue code
|
||||
/// to convert into a `TelevisionChannel` member:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use television::channels::{TelevisionChannel, OnAir};
|
||||
/// use television-derive::ToCliChannel;
|
||||
/// use television::channels::{files, text};
|
||||
///
|
||||
/// #[derive(ToCliChannel)]
|
||||
/// enum TelevisionChannel {
|
||||
/// Files(files::Channel),
|
||||
/// Text(text::Channel),
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let television_channel: TelevisionChannel = CliTvChannel::Files.to_channel();
|
||||
///
|
||||
/// assert!(matches!(television_channel, TelevisionChannel::Files(_)));
|
||||
/// ```
|
||||
///
|
||||
/// The `CliChannel` enum is used to select channels from the command line.
|
||||
///
|
||||
/// Any variant that should not be included in the CLI should be annotated with
|
||||
/// `#[exclude_from_cli]`.
|
||||
#[proc_macro_derive(ToCliChannel, attributes(exclude_from_cli))]
|
||||
pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
|
||||
// Construct a representation of Rust code as a syntax tree
|
||||
// that we can manipulate
|
||||
let ast = syn::parse(input).unwrap();
|
||||
|
||||
// Build the trait implementation
|
||||
impl_cli_channel(&ast)
|
||||
}
|
||||
|
||||
fn has_attribute(attrs: &[syn::Attribute], attribute: &str) -> bool {
|
||||
attrs.iter().any(|attr| attr.path().is_ident(attribute))
|
||||
}
|
||||
|
||||
const EXCLUDE_FROM_CLI: &str = "exclude_from_cli";
|
||||
|
||||
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||
// check that the struct is an enum
|
||||
let variants = if let syn::Data::Enum(data_enum) = &ast.data {
|
||||
&data_enum.variants
|
||||
} else {
|
||||
panic!("#[derive(CliChannel)] is only defined for enums");
|
||||
};
|
||||
|
||||
// check that the enum has at least one variant
|
||||
assert!(
|
||||
!variants.is_empty(),
|
||||
"#[derive(CliChannel)] requires at least one variant"
|
||||
);
|
||||
|
||||
// create the CliTvChannel enum
|
||||
let cli_enum_variants: Vec<_> = variants
|
||||
.iter()
|
||||
.filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI))
|
||||
.map(|variant| {
|
||||
let variant_name = &variant.ident;
|
||||
quote! {
|
||||
#variant_name
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let cli_enum = quote! {
|
||||
use clap::ValueEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
use std::default::Default;
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum, EnumIter, EnumString, Default, Copy, PartialEq, Eq, Serialize, Deserialize, Display)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
pub enum CliTvChannel {
|
||||
#[default]
|
||||
#(#cli_enum_variants),*
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the match arms for the `to_channel` method
|
||||
let arms = variants.iter().filter(
|
||||
|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI),
|
||||
).map(|variant| {
|
||||
let variant_name = &variant.ident;
|
||||
|
||||
// Get the inner type of the variant, assuming it is the first field of the variant
|
||||
if let syn::Fields::Unnamed(fields) = &variant.fields {
|
||||
if fields.unnamed.len() == 1 {
|
||||
// Get the inner type of the variant (e.g., EnvChannel)
|
||||
let inner_type = &fields.unnamed[0].ty;
|
||||
|
||||
quote! {
|
||||
CliTvChannel::#variant_name => TelevisionChannel::#variant_name(#inner_type::default())
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants should have exactly one unnamed field.");
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants expected to only have unnamed fields.");
|
||||
}
|
||||
});
|
||||
|
||||
let gen = quote! {
|
||||
#cli_enum
|
||||
|
||||
impl CliTvChannel {
|
||||
pub fn to_channel(self) -> TelevisionChannel {
|
||||
match self {
|
||||
#(#arms),*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_channels() -> Vec<String> {
|
||||
use strum::IntoEnumIterator;
|
||||
Self::iter().map(|v| v.to_string()).collect()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
||||
|
||||
/// This macro generates the `OnAir` trait implementation for the given enum.
|
||||
///
|
||||
/// The `OnAir` trait is used to interact with the different television channels
|
||||
@ -287,95 +164,3 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||
|
||||
trait_impl.into()
|
||||
}
|
||||
|
||||
/// This macro generates a `UnitChannel` enum and the necessary glue code
|
||||
/// to convert from and to a `TelevisionChannel` member.
|
||||
///
|
||||
/// The `UnitChannel` enum is used as a unit variant of the `TelevisionChannel`
|
||||
/// enum.
|
||||
#[proc_macro_derive(ToUnitChannel, attributes(exclude_from_unit))]
|
||||
pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
|
||||
// Construct a representation of Rust code as a syntax tree
|
||||
// that we can manipulate
|
||||
let ast = syn::parse(input).unwrap();
|
||||
|
||||
// Build the trait implementation
|
||||
impl_unit_channel(&ast)
|
||||
}
|
||||
|
||||
const EXCLUDE_FROM_UNIT: &str = "exclude_from_unit";
|
||||
|
||||
fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||
// Ensure the struct is an enum
|
||||
let variants = if let syn::Data::Enum(data_enum) = &ast.data {
|
||||
&data_enum.variants
|
||||
} else {
|
||||
panic!("#[derive(UnitChannel)] is only defined for enums");
|
||||
};
|
||||
|
||||
// Ensure the enum has at least one variant
|
||||
assert!(
|
||||
!variants.is_empty(),
|
||||
"#[derive(UnitChannel)] requires at least one variant"
|
||||
);
|
||||
|
||||
let variant_names: Vec<_> = variants
|
||||
.iter()
|
||||
.filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_UNIT))
|
||||
.map(|v| &v.ident)
|
||||
.collect();
|
||||
|
||||
let excluded_variants: Vec<_> = variants
|
||||
.iter()
|
||||
.filter(|variant| has_attribute(&variant.attrs, EXCLUDE_FROM_UNIT))
|
||||
.map(|v| &v.ident)
|
||||
.collect();
|
||||
|
||||
// Generate a unit enum from the given enum
|
||||
let unit_enum = quote! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Hash)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
pub enum UnitChannel {
|
||||
#(
|
||||
#variant_names,
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
// Generate Into<TelevisionChannel> implementation
|
||||
let into_impl = quote! {
|
||||
impl Into<TelevisionChannel> for UnitChannel {
|
||||
fn into(self) -> TelevisionChannel {
|
||||
match self {
|
||||
#(
|
||||
UnitChannel::#variant_names => TelevisionChannel::#variant_names(Default::default()),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate From<&TelevisionChannel> implementation
|
||||
let from_impl = quote! {
|
||||
impl From<&TelevisionChannel> for UnitChannel {
|
||||
fn from(channel: &TelevisionChannel) -> Self {
|
||||
match channel {
|
||||
#(
|
||||
TelevisionChannel::#variant_names(_) => Self::#variant_names,
|
||||
)*
|
||||
#(
|
||||
TelevisionChannel::#excluded_variants(_) => panic!("Cannot convert excluded variant to unit channel."),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let gen = quote! {
|
||||
#unit_enum
|
||||
#into_impl
|
||||
#from_impl
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
||||
|
@ -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";
|
||||
@ -65,14 +65,14 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
file_paths.push(default_channels_path);
|
||||
}
|
||||
|
||||
let user_defined_prototypes = file_paths.iter().fold(
|
||||
let prototypes = file_paths.iter().fold(
|
||||
Vec::<CableChannelPrototype>::new(),
|
||||
|mut acc, p| {
|
||||
match toml::from_str::<ChannelPrototypes>(
|
||||
&std::fs::read_to_string(p)
|
||||
.expect("Unable to read configuration file"),
|
||||
) {
|
||||
Ok(prototypes) => acc.extend(prototypes.prototypes),
|
||||
Ok(pts) => acc.extend(pts.prototypes),
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to parse cable channel file {:?}: {}",
|
||||
@ -84,10 +84,14 @@ pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
},
|
||||
);
|
||||
|
||||
debug!("Loaded cable channels: {:?}", user_defined_prototypes);
|
||||
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 user_defined_prototypes {
|
||||
for prototype in prototypes {
|
||||
cable_channels.insert(prototype.name.clone(), prototype);
|
||||
}
|
||||
Ok(CableChannels(cable_channels))
|
||||
|
@ -1,182 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::channels::entry::Entry;
|
||||
use crate::channels::entry::PreviewType;
|
||||
use crate::channels::OnAir;
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use crate::utils::command::shell_command;
|
||||
use crate::utils::indices::sep_name_and_value_indices;
|
||||
use crate::utils::shell::Shell;
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::FxBuildHasher;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Alias {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Alias {
|
||||
fn new(name: String, value: String) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<Alias>,
|
||||
file_icon: FileIcon,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
|
||||
const FILE_ICON_STR: &str = "nu";
|
||||
|
||||
fn get_raw_aliases(shell: Shell) -> Vec<String> {
|
||||
// this needs to be run in an interactive shell in order to get the aliases
|
||||
let mut command = shell_command(true);
|
||||
|
||||
let output = match shell {
|
||||
Shell::PowerShell => {
|
||||
command.arg("Get-Alias | Format-List -Property Name, Definition")
|
||||
}
|
||||
Shell::Cmd => command.arg("doskey /macros"),
|
||||
_ => command.arg("-i").arg("alias").arg("2>/dev/null"),
|
||||
}
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
let aliases = String::from_utf8_lossy(&output.stdout);
|
||||
aliases.lines().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new() -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
let crawl_handle = tokio::spawn(load_aliases(injector));
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
crawl_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let (
|
||||
name_indices,
|
||||
value_indices,
|
||||
should_add_name_indices,
|
||||
should_add_value_indices,
|
||||
) = sep_name_and_value_indices(
|
||||
item.match_indices,
|
||||
u32::try_from(item.inner.name.len()).unwrap(),
|
||||
);
|
||||
|
||||
let mut entry =
|
||||
Entry::new(item.inner.name.clone(), PreviewType::EnvVar)
|
||||
.with_value(item.inner.value)
|
||||
.with_icon(self.file_icon);
|
||||
|
||||
if should_add_name_indices {
|
||||
entry = entry.with_name_match_indices(&name_indices);
|
||||
}
|
||||
|
||||
if should_add_value_indices {
|
||||
entry = entry.with_value_match_indices(&value_indices);
|
||||
}
|
||||
|
||||
entry
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
Entry::new(item.inner.name.clone(), PreviewType::EnvVar)
|
||||
.with_value(item.inner.value)
|
||||
.with_icon(self.file_icon)
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_aliases(injector: Injector<Alias>) {
|
||||
let shell = Shell::from_env().unwrap_or_default();
|
||||
debug!("Current shell: {}", shell);
|
||||
let raw_aliases = get_raw_aliases(shell);
|
||||
|
||||
raw_aliases
|
||||
.iter()
|
||||
.filter_map(|alias| {
|
||||
let mut parts = alias.split('=');
|
||||
if let Some(name) = parts.next() {
|
||||
if let Some(value) = parts.next() {
|
||||
return Some(Alias::new(
|
||||
name.to_string(),
|
||||
value.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
debug!("Invalid alias format: {}", alias);
|
||||
}
|
||||
None
|
||||
})
|
||||
.for_each(|alias| {
|
||||
let () = injector.push(alias, |e, cols| {
|
||||
cols[0] = (e.name.clone() + &e.value).into();
|
||||
});
|
||||
});
|
||||
}
|
@ -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,9 +42,10 @@ pub struct Channel {
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
"Files",
|
||||
"files",
|
||||
"find . -type f",
|
||||
Some(PreviewCommand::new("bat -n --color=always {}", ":")),
|
||||
false,
|
||||
Some(PreviewCommand::new("cat {}", ":")),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -51,6 +55,7 @@ impl From<CableChannelPrototype> for Channel {
|
||||
Self::new(
|
||||
&prototype.name,
|
||||
&prototype.source_command,
|
||||
prototype.interactive,
|
||||
match prototype.preview_command {
|
||||
Some(command) => Some(PreviewCommand::new(
|
||||
&command,
|
||||
@ -79,12 +84,14 @@ impl Channel {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
entries_command: &str,
|
||||
interactive: bool,
|
||||
preview_command: Option<PreviewCommand>,
|
||||
) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
let crawl_handle = tokio::spawn(load_candidates(
|
||||
entries_command.to_string(),
|
||||
interactive,
|
||||
injector,
|
||||
));
|
||||
let preview_kind = match preview_command {
|
||||
@ -108,9 +115,13 @@ impl Channel {
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_candidates(command: String, injector: Injector<String>) {
|
||||
async fn load_candidates(
|
||||
command: String,
|
||||
interactive: bool,
|
||||
injector: Injector<String>,
|
||||
) {
|
||||
debug!("Loading candidates from command: {:?}", command);
|
||||
let mut child = shell_command(false)
|
||||
let mut child = shell_command(interactive)
|
||||
.arg(command)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@ -228,11 +239,47 @@ impl OnAir for Channel {
|
||||
pub struct CableChannelPrototype {
|
||||
pub name: String,
|
||||
pub source_command: String,
|
||||
#[serde(default)]
|
||||
pub interactive: bool,
|
||||
pub preview_command: Option<String>,
|
||||
#[serde(default = "default_delimiter")]
|
||||
pub preview_delimiter: Option<String>,
|
||||
}
|
||||
|
||||
impl CableChannelPrototype {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
source_command: &str,
|
||||
interactive: bool,
|
||||
preview_command: Option<String>,
|
||||
preview_delimiter: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
source_command: source_command.to_string(),
|
||||
interactive,
|
||||
preview_command,
|
||||
preview_delimiter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
||||
const DEFAULT_SOURCE_COMMAND: &str = "fd -t f";
|
||||
const DEFAULT_PREVIEW_COMMAND: &str = ":files:";
|
||||
|
||||
impl Default for CableChannelPrototype {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: DEFAULT_PROTOTYPE_NAME.to_string(),
|
||||
source_command: DEFAULT_SOURCE_COMMAND.to_string(),
|
||||
interactive: false,
|
||||
preview_command: Some(DEFAULT_PREVIEW_COMMAND.to_string()),
|
||||
preview_delimiter: Some(DEFAULT_DELIMITER.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_DELIMITER: &str = " ";
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
@ -246,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 {
|
||||
@ -256,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)
|
||||
}
|
||||
}
|
||||
|
@ -1,186 +0,0 @@
|
||||
use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
|
||||
use crate::channels::{OnAir, TelevisionChannel};
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use crate::utils::files::{get_default_num_threads, walk_builder};
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
// PERF: cache results (to make deleting characters smoother) with
|
||||
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(paths: Vec<PathBuf>) -> Self {
|
||||
let matcher = Matcher::new(Config::default().match_paths(true));
|
||||
// start loading files in the background
|
||||
let crawl_handle = tokio::spawn(load_dirs(paths, matcher.injector()));
|
||||
Channel {
|
||||
matcher,
|
||||
crawl_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(vec![std::env::current_dir().unwrap()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mut TelevisionChannel> for Channel {
|
||||
fn from(value: &mut TelevisionChannel) -> Self {
|
||||
match value {
|
||||
c @ TelevisionChannel::GitRepos(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(entry.name.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::Dirs(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(&entry.name))
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
const PREVIEW_COMMAND: &str = "ls -la --color=always {}";
|
||||
|
||||
#[cfg(windows)]
|
||||
const PREVIEW_COMMAND: &str = "dir /Q {}";
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(PreviewCommand::new(
|
||||
PREVIEW_COMMAND,
|
||||
" ",
|
||||
)),
|
||||
)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_icon(FileIcon::from(&path))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(PreviewCommand::new(
|
||||
PREVIEW_COMMAND,
|
||||
" ",
|
||||
)),
|
||||
)
|
||||
.with_icon(FileIcon::from(&path))
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
self.crawl_handle.abort();
|
||||
}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_dirs(paths: Vec<PathBuf>, injector: Injector<String>) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let mut builder =
|
||||
walk_builder(&paths[0], get_default_num_threads(), None, None);
|
||||
paths[1..].iter().for_each(|path| {
|
||||
builder.add(path);
|
||||
});
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
walker.run(|| {
|
||||
let injector = injector.clone();
|
||||
let current_dir = current_dir.clone();
|
||||
Box::new(move |result| {
|
||||
if let Ok(entry) = result {
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
let dir_path = &entry
|
||||
.path()
|
||||
.strip_prefix(¤t_dir)
|
||||
.unwrap_or(entry.path())
|
||||
.to_string_lossy();
|
||||
if dir_path == "" {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
let () = injector.push(dir_path.to_string(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
});
|
||||
}
|
||||
}
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use super::OnAir;
|
||||
use crate::channels::entry::{Entry, PreviewType};
|
||||
use crate::matcher::{config::Config, Matcher};
|
||||
use crate::utils::indices::sep_name_and_value_indices;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EnvVar {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct Channel {
|
||||
matcher: Matcher<EnvVar>,
|
||||
file_icon: FileIcon,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
const FILE_ICON_STR: &str = "config";
|
||||
|
||||
impl Channel {
|
||||
pub fn new() -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
for (name, value) in std::env::vars() {
|
||||
let () = injector.push(
|
||||
EnvVar {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
},
|
||||
|e, cols| {
|
||||
cols[0] = (e.name.clone() + &e.value).into();
|
||||
},
|
||||
);
|
||||
}
|
||||
Channel {
|
||||
matcher,
|
||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let (
|
||||
name_indices,
|
||||
value_indices,
|
||||
should_add_name_indices,
|
||||
should_add_value_indices,
|
||||
) = sep_name_and_value_indices(
|
||||
item.match_indices,
|
||||
u32::try_from(item.inner.name.len()).unwrap(),
|
||||
);
|
||||
|
||||
let mut entry =
|
||||
Entry::new(item.inner.name.clone(), PreviewType::EnvVar)
|
||||
.with_value(item.inner.value)
|
||||
.with_icon(self.file_icon);
|
||||
|
||||
if should_add_name_indices {
|
||||
entry = entry.with_name_match_indices(&name_indices);
|
||||
}
|
||||
|
||||
if should_add_value_indices {
|
||||
entry = entry.with_value_match_indices(&value_indices);
|
||||
}
|
||||
|
||||
entry
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
Entry::new(item.inner.name.clone(), PreviewType::EnvVar)
|
||||
.with_value(item.inner.value)
|
||||
.with_icon(self.file_icon)
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
use crate::channels::entry::{Entry, PreviewType};
|
||||
use crate::channels::{OnAir, TelevisionChannel};
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use crate::utils::files::{get_default_num_threads, walk_builder};
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
// PERF: cache results (to make deleting characters smoother) with
|
||||
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(paths: Vec<PathBuf>) -> Self {
|
||||
let matcher = Matcher::new(Config::default().match_paths(true));
|
||||
// start loading files in the background
|
||||
let crawl_handle = tokio::spawn(load_files(paths, matcher.injector()));
|
||||
Channel {
|
||||
matcher,
|
||||
crawl_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(vec![std::env::current_dir().unwrap()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mut TelevisionChannel> for Channel {
|
||||
fn from(value: &mut TelevisionChannel) -> Self {
|
||||
match value {
|
||||
c @ TelevisionChannel::GitRepos(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(entry.name.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::Files(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(entry.name.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::Text(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(&entry.name))
|
||||
.collect::<FxHashSet<_>>()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::Dirs(_) => {
|
||||
let entries = c.results(c.result_count(), 0);
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(&entry.name))
|
||||
.collect::<FxHashSet<_>>()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path.clone(), PreviewType::Files)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_icon(FileIcon::from(&path))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path.clone(), PreviewType::Files)
|
||||
.with_icon(FileIcon::from(&path))
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
self.crawl_handle.abort();
|
||||
}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_files(paths: Vec<PathBuf>, injector: Injector<String>) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let mut builder =
|
||||
walk_builder(&paths[0], get_default_num_threads(), None, None);
|
||||
paths[1..].iter().for_each(|path| {
|
||||
builder.add(path);
|
||||
});
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
walker.run(|| {
|
||||
let injector = injector.clone();
|
||||
let current_dir = current_dir.clone();
|
||||
Box::new(move |result| {
|
||||
if let Ok(entry) = result {
|
||||
if entry.file_type().unwrap().is_file() {
|
||||
let file_path = &entry
|
||||
.path()
|
||||
.strip_prefix(¤t_dir)
|
||||
.unwrap_or(entry.path())
|
||||
.to_string_lossy();
|
||||
let () =
|
||||
injector.push(file_path.to_string(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
});
|
||||
}
|
||||
}
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
use devicons::FileIcon;
|
||||
use directories::BaseDirs;
|
||||
use ignore::overrides::OverrideBuilder;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
|
||||
use crate::channels::OnAir;
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use crate::utils::files::{get_default_num_threads, walk_builder};
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
icon: FileIcon,
|
||||
crawl_handle: JoinHandle<()>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
preview_command: PreviewCommand,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new() -> Self {
|
||||
let matcher = Matcher::new(Config::default().match_paths(true));
|
||||
let base_dirs = BaseDirs::new().unwrap();
|
||||
let crawl_handle = tokio::spawn(crawl_for_repos(
|
||||
base_dirs.home_dir().to_path_buf(),
|
||||
matcher.injector(),
|
||||
));
|
||||
|
||||
let preview_command = PreviewCommand {
|
||||
command: String::from(
|
||||
"cd {} && git log -n 200 --pretty=medium --all --graph --color",
|
||||
),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
|
||||
Channel {
|
||||
matcher,
|
||||
icon: FileIcon::from("git"),
|
||||
crawl_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
preview_command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path,
|
||||
PreviewType::Command(self.preview_command.clone()),
|
||||
)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_icon(self.icon)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path,
|
||||
PreviewType::Command(self.preview_command.clone()),
|
||||
)
|
||||
.with_icon(self.icon)
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
debug!("Shutting down git repos channel");
|
||||
self.crawl_handle.abort();
|
||||
}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ignored_paths() -> Vec<PathBuf> {
|
||||
let mut ignored_paths = Vec::new();
|
||||
|
||||
if let Some(base_dirs) = BaseDirs::new() {
|
||||
let home = base_dirs.home_dir();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ignored_paths.push(home.join("Library"));
|
||||
ignored_paths.push(home.join("Applications"));
|
||||
ignored_paths.push(home.join("Music"));
|
||||
ignored_paths.push(home.join("Pictures"));
|
||||
ignored_paths.push(home.join("Movies"));
|
||||
ignored_paths.push(home.join("Downloads"));
|
||||
ignored_paths.push(home.join("Public"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
ignored_paths.push(home.join(".cache"));
|
||||
ignored_paths.push(home.join(".config"));
|
||||
ignored_paths.push(home.join(".local"));
|
||||
ignored_paths.push(home.join(".thumbnails"));
|
||||
ignored_paths.push(home.join("Downloads"));
|
||||
ignored_paths.push(home.join("Public"));
|
||||
ignored_paths.push(home.join("snap"));
|
||||
ignored_paths.push(home.join(".snap"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
ignored_paths.push(home.join("AppData"));
|
||||
ignored_paths.push(home.join("Downloads"));
|
||||
ignored_paths.push(home.join("Documents"));
|
||||
ignored_paths.push(home.join("Music"));
|
||||
ignored_paths.push(home.join("Pictures"));
|
||||
ignored_paths.push(home.join("Videos"));
|
||||
}
|
||||
|
||||
// Common paths to ignore for all platforms
|
||||
ignored_paths.push(home.join("node_modules"));
|
||||
ignored_paths.push(home.join("venv"));
|
||||
ignored_paths.push(PathBuf::from("/tmp"));
|
||||
}
|
||||
|
||||
ignored_paths
|
||||
}
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn crawl_for_repos(starting_point: PathBuf, injector: Injector<String>) {
|
||||
let mut walker_overrides_builder = OverrideBuilder::new(&starting_point);
|
||||
walker_overrides_builder.add(".git").unwrap();
|
||||
let walker = walk_builder(
|
||||
&starting_point,
|
||||
get_default_num_threads(),
|
||||
Some(walker_overrides_builder.build().unwrap()),
|
||||
Some(get_ignored_paths()),
|
||||
)
|
||||
.build_parallel();
|
||||
|
||||
walker.run(|| {
|
||||
let injector = injector.clone();
|
||||
Box::new(move |result| {
|
||||
if let Ok(entry) = result {
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
// if the entry is a .git directory, add its parent to the list of git repos
|
||||
if entry.path().ends_with(".git") {
|
||||
let parent_path =
|
||||
&entry.path().parent().unwrap().to_string_lossy();
|
||||
debug!("Found git repo: {:?}", parent_path);
|
||||
let () = injector.push(
|
||||
parent_path.to_string(),
|
||||
|e, cols| {
|
||||
cols[0] = e.clone().into();
|
||||
},
|
||||
);
|
||||
return ignore::WalkState::Skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
}
|
@ -1,18 +1,12 @@
|
||||
use crate::channels::entry::Entry;
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashSet;
|
||||
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
|
||||
use television_derive::Broadcast;
|
||||
|
||||
pub mod alias;
|
||||
pub mod cable;
|
||||
pub mod dirs;
|
||||
pub mod entry;
|
||||
pub mod env;
|
||||
pub mod files;
|
||||
pub mod git_repos;
|
||||
pub mod remote_control;
|
||||
pub mod stdin;
|
||||
pub mod text;
|
||||
|
||||
/// The interface that all television channels must implement.
|
||||
///
|
||||
@ -118,56 +112,22 @@ pub trait OnAir: Send {
|
||||
/// of carrying the actual channel instances around. It also generates the necessary
|
||||
/// glue code to automatically create a channel instance from the selected enum variant.
|
||||
#[allow(dead_code, clippy::module_name_repetitions)]
|
||||
#[derive(ToUnitChannel, ToCliChannel, Broadcast)]
|
||||
#[derive(Broadcast)]
|
||||
pub enum TelevisionChannel {
|
||||
/// The environment variables channel.
|
||||
///
|
||||
/// This channel allows to search through environment variables.
|
||||
Env(env::Channel),
|
||||
/// The files channel.
|
||||
///
|
||||
/// This channel allows to search through files.
|
||||
Files(files::Channel),
|
||||
/// The git repositories channel.
|
||||
///
|
||||
/// This channel allows to search through git repositories.
|
||||
GitRepos(git_repos::Channel),
|
||||
/// The dirs channel.
|
||||
///
|
||||
/// This channel allows to search through directories.
|
||||
Dirs(dirs::Channel),
|
||||
/// The text channel.
|
||||
///
|
||||
/// This channel allows to search through the contents of text files.
|
||||
Text(text::Channel),
|
||||
/// The standard input channel.
|
||||
///
|
||||
/// This channel allows to search through whatever is passed through stdin.
|
||||
#[exclude_from_cli]
|
||||
Stdin(stdin::Channel),
|
||||
/// The alias channel.
|
||||
///
|
||||
/// This channel allows to search through aliases.
|
||||
Alias(alias::Channel),
|
||||
/// The remote control channel.
|
||||
///
|
||||
/// This channel allows to switch between different channels.
|
||||
#[exclude_from_unit]
|
||||
#[exclude_from_cli]
|
||||
RemoteControl(remote_control::RemoteControl),
|
||||
/// A custom channel.
|
||||
///
|
||||
/// This channel allows to search through custom data.
|
||||
#[exclude_from_cli]
|
||||
Cable(cable::Channel),
|
||||
}
|
||||
|
||||
impl From<&Entry> for TelevisionChannel {
|
||||
fn from(entry: &Entry) -> Self {
|
||||
UnitChannel::try_from(entry.name.as_str()).unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TelevisionChannel {
|
||||
pub fn zap(&self, channel_name: &str) -> Result<TelevisionChannel> {
|
||||
match self {
|
||||
@ -182,130 +142,7 @@ impl TelevisionChannel {
|
||||
match self {
|
||||
TelevisionChannel::Cable(channel) => channel.name.clone(),
|
||||
TelevisionChannel::Stdin(_) => String::from("Stdin"),
|
||||
_ => UnitChannel::from(self).to_string(),
|
||||
TelevisionChannel::RemoteControl(_) => String::from("Remote"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! variant_to_module {
|
||||
(Files) => {
|
||||
files::Channel
|
||||
};
|
||||
(Text) => {
|
||||
text::Channel
|
||||
};
|
||||
(Dirs) => {
|
||||
dirs::Channel
|
||||
};
|
||||
(GitRepos) => {
|
||||
git_repos::Channel
|
||||
};
|
||||
(Env) => {
|
||||
env::Channel
|
||||
};
|
||||
(Stdin) => {
|
||||
stdin::Channel
|
||||
};
|
||||
(Alias) => {
|
||||
alias::Channel
|
||||
};
|
||||
(RemoteControl) => {
|
||||
remote_control::RemoteControl
|
||||
};
|
||||
}
|
||||
|
||||
/// A macro that generates two methods for the `TelevisionChannel` enum based on
|
||||
/// the transitions defined in the macro call.
|
||||
///
|
||||
/// The first method `available_transitions` returns a list of possible transitions
|
||||
/// from the current channel.
|
||||
///
|
||||
/// The second method `transition_to` transitions from the current channel to the
|
||||
/// target channel.
|
||||
///
|
||||
/// # Example
|
||||
/// The following example defines transitions from the `Files` channel to the `Text`
|
||||
/// channel and from the `GitRepos` channel to the `Files` and `Text` channels.
|
||||
/// ```ignore
|
||||
/// define_transitions! {
|
||||
/// // The `Files` channel can transition to the `Text` channel.
|
||||
/// Files => [Text],
|
||||
/// // The `GitRepos` channel can transition to the `Files` and `Text` channels.
|
||||
/// GitRepos => [Files, Text],
|
||||
/// }
|
||||
/// ```
|
||||
/// This will generate the following methods for the `TelevisionChannel` enum:
|
||||
/// ```ignore
|
||||
/// impl TelevisionChannel {
|
||||
/// pub fn available_transitions(&self) -> Vec<UnitChannel> {
|
||||
/// match self {
|
||||
/// TelevisionChannel::Files(_) => vec![UnitChannel::Text],
|
||||
/// TelevisionChannel::GitRepos(_) => vec![UnitChannel::Files, UnitChannel::Text],
|
||||
/// _ => Vec::new(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub fn transition_to(self, target: UnitChannel) -> TelevisionChannel {
|
||||
/// match (self, target) {
|
||||
/// (tv_channel @ TelevisionChannel::Files(_), UnitChannel::Text) => {
|
||||
/// TelevisionChannel::Text(text::Channel::from(tv_channel))
|
||||
/// },
|
||||
/// (tv_channel @ TelevisionChannel::GitRepos(_), UnitChannel::Files) => {
|
||||
/// TelevisionChannel::Files(files::Channel::from(tv_channel))
|
||||
/// },
|
||||
/// (tv_channel @ TelevisionChannel::GitRepos(_), UnitChannel::Text) => {
|
||||
/// TelevisionChannel::Text(text::Channel::from(tv_channel))
|
||||
/// },
|
||||
/// _ => unreachable!(),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
///
|
||||
macro_rules! define_transitions {
|
||||
(
|
||||
$(
|
||||
$from_variant:ident => [ $($to_variant:ident),* $(,)? ],
|
||||
)*
|
||||
) => {
|
||||
impl TelevisionChannel {
|
||||
pub fn available_transitions(&self) -> Vec<UnitChannel> {
|
||||
match self {
|
||||
$(
|
||||
TelevisionChannel::$from_variant(_) => vec![
|
||||
$( UnitChannel::$to_variant ),*
|
||||
],
|
||||
)*
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_to(&mut self, target: UnitChannel) -> TelevisionChannel {
|
||||
match (self, target) {
|
||||
$(
|
||||
$(
|
||||
(tv_channel @ TelevisionChannel::$from_variant(_), UnitChannel::$to_variant) => {
|
||||
TelevisionChannel::$to_variant(
|
||||
<variant_to_module!($to_variant)>::from(tv_channel)
|
||||
)
|
||||
},
|
||||
)*
|
||||
)*
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the transitions between the different channels.
|
||||
//
|
||||
// This is where the transitions between the different channels are defined.
|
||||
// The transitions are defined as a list of tuples where the first element
|
||||
// is the source channel and the second element is a list of potential target channels.
|
||||
define_transitions! {
|
||||
Text => [Files, Text],
|
||||
Files => [Files, Text],
|
||||
Dirs => [Files, Text, Dirs],
|
||||
GitRepos => [Files, Text, Dirs],
|
||||
}
|
||||
|
@ -1,61 +1,33 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::channels::cable::{CableChannelPrototype, CableChannels};
|
||||
use crate::channels::cable::CableChannels;
|
||||
use crate::channels::entry::{Entry, PreviewType};
|
||||
use crate::channels::{CliTvChannel, OnAir, TelevisionChannel, UnitChannel};
|
||||
use crate::channels::{OnAir, TelevisionChannel};
|
||||
use crate::matcher::{config::Config, Matcher};
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use super::cable;
|
||||
|
||||
pub struct RemoteControl {
|
||||
matcher: Matcher<RCButton>,
|
||||
matcher: Matcher<String>,
|
||||
cable_channels: Option<CableChannels>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RCButton {
|
||||
Channel(UnitChannel),
|
||||
CableChannel(CableChannelPrototype),
|
||||
}
|
||||
|
||||
impl Display for RCButton {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RCButton::Channel(channel) => write!(f, "{channel}"),
|
||||
RCButton::CableChannel(prototype) => write!(f, "{prototype}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
|
||||
impl RemoteControl {
|
||||
pub fn new(
|
||||
builtin_channels: Vec<UnitChannel>,
|
||||
cable_channels: Option<CableChannels>,
|
||||
) -> Self {
|
||||
pub fn new(cable_channels: Option<CableChannels>) -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
let buttons =
|
||||
builtin_channels.into_iter().map(RCButton::Channel).chain(
|
||||
cable_channels
|
||||
.as_ref()
|
||||
.map(|channels| {
|
||||
channels.iter().map(|(_, prototype)| {
|
||||
RCButton::CableChannel(prototype.clone())
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
for button in buttons {
|
||||
let () = injector.push(button.clone(), |e, cols| {
|
||||
for c in cable_channels
|
||||
.as_ref()
|
||||
.unwrap_or(&CableChannels::default())
|
||||
.keys()
|
||||
{
|
||||
let () = injector.push(c.clone(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
});
|
||||
}
|
||||
@ -66,12 +38,6 @@ impl RemoteControl {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_transitions_from(
|
||||
television_channel: &TelevisionChannel,
|
||||
) -> Self {
|
||||
Self::new(television_channel.available_transitions(), None)
|
||||
}
|
||||
|
||||
pub fn zap(&self, channel_name: &str) -> Result<TelevisionChannel> {
|
||||
match self
|
||||
.cable_channels
|
||||
@ -81,47 +47,20 @@ impl RemoteControl {
|
||||
Some(prototype) => {
|
||||
Ok(TelevisionChannel::Cable(cable::Channel::from(prototype)))
|
||||
}
|
||||
None => match UnitChannel::try_from(channel_name) {
|
||||
Ok(channel) => Ok(channel.into()),
|
||||
Err(_) => Err(anyhow::anyhow!(
|
||||
"No channel or cable channel prototype found for {}",
|
||||
channel_name
|
||||
)),
|
||||
},
|
||||
None => Err(anyhow::anyhow!(
|
||||
"No channel or cable channel prototype found for {}",
|
||||
channel_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteControl {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
CliTvChannel::value_variants()
|
||||
.iter()
|
||||
.flat_map(|v| UnitChannel::try_from(v.to_string().as_str()))
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_builtin_channels(
|
||||
filter_out_cable_names: Option<&[&String]>,
|
||||
) -> Vec<UnitChannel> {
|
||||
let mut value_variants = CliTvChannel::value_variants()
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(f) = filter_out_cable_names {
|
||||
value_variants.retain(|v| !f.iter().any(|c| *c == v));
|
||||
}
|
||||
|
||||
value_variants
|
||||
.iter()
|
||||
.flat_map(|v| UnitChannel::try_from(v.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
const TV_ICON: FileIcon = FileIcon {
|
||||
icon: '📺',
|
||||
color: "#000000",
|
||||
@ -146,10 +85,7 @@ impl OnAir for RemoteControl {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, PreviewType::Basic)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_icon(match item.inner {
|
||||
RCButton::Channel(_) => TV_ICON,
|
||||
RCButton::CableChannel(_) => CABLE_ICON,
|
||||
})
|
||||
.with_icon(CABLE_ICON)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -1,387 +0,0 @@
|
||||
use super::{OnAir, TelevisionChannel};
|
||||
use crate::channels::entry::{Entry, PreviewType};
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use crate::utils::files::{get_default_num_threads, walk_builder};
|
||||
use crate::utils::strings::{
|
||||
proportion_of_printable_ascii_characters, PRINTABLE_ASCII_THRESHOLD,
|
||||
};
|
||||
use devicons::FileIcon;
|
||||
use ignore::WalkState;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, Read, Seek},
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CandidateLine {
|
||||
path: PathBuf,
|
||||
line: String,
|
||||
line_number: usize,
|
||||
}
|
||||
|
||||
impl CandidateLine {
|
||||
fn new(path: PathBuf, line: String, line_number: usize) -> Self {
|
||||
CandidateLine {
|
||||
path,
|
||||
line,
|
||||
line_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct Channel {
|
||||
matcher: Matcher<CandidateLine>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(directories: Vec<PathBuf>) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
// start loading files in the background
|
||||
let crawl_handle = tokio::spawn(crawl_for_candidates(
|
||||
directories,
|
||||
matcher.injector(),
|
||||
));
|
||||
Channel {
|
||||
matcher,
|
||||
crawl_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_file_paths(file_paths: Vec<PathBuf>) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let crawl_handle = tokio::spawn(async move {
|
||||
let mut lines_in_mem = 0;
|
||||
for path in file_paths {
|
||||
if lines_in_mem > MAX_LINES_IN_MEM {
|
||||
break;
|
||||
}
|
||||
if let Some(injected_lines) =
|
||||
try_inject_lines(&injector, ¤t_dir, &path)
|
||||
{
|
||||
lines_in_mem += injected_lines;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Channel {
|
||||
matcher,
|
||||
crawl_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_text_entries(entries: Vec<Entry>) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
let load_handle = tokio::spawn(async move {
|
||||
for entry in entries.into_iter().take(MAX_LINES_IN_MEM) {
|
||||
let v = entry.value.unwrap();
|
||||
injector.push(
|
||||
CandidateLine::new(
|
||||
entry.name.into(),
|
||||
v,
|
||||
entry.line_number.unwrap(),
|
||||
),
|
||||
|e, cols| {
|
||||
cols[0] = e.line.clone().into();
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Channel {
|
||||
matcher,
|
||||
crawl_handle: load_handle,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(vec![std::env::current_dir().unwrap()])
|
||||
}
|
||||
}
|
||||
|
||||
/// Since we're limiting the number of lines in memory, it makes sense to also limit the number of files
|
||||
/// we're willing to search in when piping from the `Files` channel.
|
||||
/// This prevents blocking the UI for too long when piping from a channel with a lot of files.
|
||||
///
|
||||
/// This should be calculated based on the number of lines we're willing to keep in memory:
|
||||
/// `MAX_LINES_IN_MEM / 100` (assuming 100 lines per file on average).
|
||||
const MAX_PIPED_FILES: usize = MAX_LINES_IN_MEM / 200;
|
||||
|
||||
impl From<&mut TelevisionChannel> for Channel {
|
||||
fn from(value: &mut TelevisionChannel) -> Self {
|
||||
match value {
|
||||
c @ TelevisionChannel::Files(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(
|
||||
c.result_count().min(
|
||||
u32::try_from(MAX_PIPED_FILES).unwrap_or(u32::MAX),
|
||||
),
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::from_file_paths(
|
||||
entries
|
||||
.iter()
|
||||
.flat_map(|entry| {
|
||||
PathBuf::from(entry.name.clone()).canonicalize()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::GitRepos(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.flat_map(|entry| {
|
||||
PathBuf::from(entry.name.clone()).canonicalize()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
c @ TelevisionChannel::Text(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::from_text_entries(entries)
|
||||
}
|
||||
c @ TelevisionChannel::Dirs(_) => {
|
||||
let entries = if c.selected_entries().is_empty() {
|
||||
c.results(c.result_count(), 0)
|
||||
} else {
|
||||
c.selected_entries().iter().cloned().collect()
|
||||
};
|
||||
Self::new(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| PathBuf::from(&entry.name))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let line = item.matched_string;
|
||||
let display_path =
|
||||
item.inner.path.to_string_lossy().to_string();
|
||||
Entry::new(display_path, PreviewType::Files)
|
||||
.with_value(line)
|
||||
.with_value_match_indices(&item.match_indices)
|
||||
.with_icon(FileIcon::from(item.inner.path.as_path()))
|
||||
.with_line_number(item.inner.line_number)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let display_path = item.inner.path.to_string_lossy().to_string();
|
||||
Entry::new(display_path, PreviewType::Files)
|
||||
.with_icon(FileIcon::from(item.inner.path.as_path()))
|
||||
.with_line_number(item.inner.line_number)
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
&self.selected_entries
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, entry: &Entry) {
|
||||
if self.selected_entries.contains(entry) {
|
||||
self.selected_entries.remove(entry);
|
||||
} else {
|
||||
self.selected_entries.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
self.crawl_handle.abort();
|
||||
}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum file size we're willing to search in.
|
||||
///
|
||||
/// This is to prevent taking humongous amounts of memory when searching in
|
||||
/// a lot of files (e.g. starting tv in $HOME).
|
||||
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
|
||||
|
||||
/// The maximum number of lines we're willing to keep in memory.
|
||||
///
|
||||
/// TODO: this should be configurable by the user depending on the amount of
|
||||
/// memory they have/are willing to use.
|
||||
///
|
||||
/// This is to prevent taking humongous amounts of memory when searching in
|
||||
/// a lot of files (e.g. starting tv in $HOME).
|
||||
///
|
||||
/// This is a soft limit, we might go over it a bit.
|
||||
///
|
||||
/// A typical line should take somewhere around 100 bytes in memory (for utf8 english text),
|
||||
/// so this should take around 100 x `10_000_000` = 1GB of memory.
|
||||
const MAX_LINES_IN_MEM: usize = 10_000_000;
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn crawl_for_candidates(
|
||||
directories: Vec<PathBuf>,
|
||||
injector: Injector<CandidateLine>,
|
||||
) {
|
||||
if directories.is_empty() {
|
||||
return;
|
||||
}
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let mut walker =
|
||||
walk_builder(&directories[0], get_default_num_threads(), None, None);
|
||||
directories[1..].iter().for_each(|path| {
|
||||
walker.add(path);
|
||||
});
|
||||
|
||||
let lines_in_mem = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
walker.build_parallel().run(|| {
|
||||
let injector = injector.clone();
|
||||
let current_dir = current_dir.clone();
|
||||
let lines_in_mem = lines_in_mem.clone();
|
||||
Box::new(move |result| {
|
||||
if lines_in_mem.load(std::sync::atomic::Ordering::Relaxed)
|
||||
> MAX_LINES_IN_MEM
|
||||
{
|
||||
return WalkState::Quit;
|
||||
}
|
||||
if let Ok(entry) = result {
|
||||
if entry.file_type().unwrap().is_file() {
|
||||
if let Ok(m) = entry.metadata() {
|
||||
if m.len() > MAX_FILE_SIZE {
|
||||
return WalkState::Continue;
|
||||
}
|
||||
}
|
||||
// try to inject the lines of the file
|
||||
if let Some(injected_lines) =
|
||||
try_inject_lines(&injector, ¤t_dir, entry.path())
|
||||
{
|
||||
lines_in_mem.fetch_add(
|
||||
injected_lines,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn try_inject_lines(
|
||||
injector: &Injector<CandidateLine>,
|
||||
current_dir: &PathBuf,
|
||||
path: &Path,
|
||||
) -> Option<usize> {
|
||||
match File::open(path) {
|
||||
Ok(file) => {
|
||||
// is the file a text-based file?
|
||||
let mut reader = std::io::BufReader::new(&file);
|
||||
let mut buffer = [0u8; 128];
|
||||
match reader.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
if bytes_read == 0
|
||||
|| proportion_of_printable_ascii_characters(
|
||||
&buffer[..bytes_read],
|
||||
) < PRINTABLE_ASCII_THRESHOLD
|
||||
{
|
||||
debug!("Skipping non-text file {:?}", path);
|
||||
return None;
|
||||
}
|
||||
reader.seek(std::io::SeekFrom::Start(0)).unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error reading file {:?}: {:?}", path, e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// read the lines of the file
|
||||
let mut line_number = 0;
|
||||
let mut injected_lines = 0;
|
||||
for maybe_line in reader.lines() {
|
||||
match maybe_line {
|
||||
Ok(l) => {
|
||||
line_number += 1;
|
||||
if l.is_empty() {
|
||||
trace!("Empty line");
|
||||
continue;
|
||||
}
|
||||
let candidate = CandidateLine::new(
|
||||
path.strip_prefix(current_dir)
|
||||
.unwrap_or(path)
|
||||
.to_path_buf(),
|
||||
l.clone(),
|
||||
line_number,
|
||||
);
|
||||
let () = injector.push(candidate, |e, cols| {
|
||||
cols[0] = e.line.clone().into();
|
||||
});
|
||||
injected_lines += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error reading line: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(injected_lines)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error opening file {:?}: {:?}", path, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -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,12 +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::CableChannelPrototype, entry::PreviewCommand, CliTvChannel,
|
||||
};
|
||||
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},
|
||||
@ -20,7 +18,7 @@ pub mod args;
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: ParsedCliChannel,
|
||||
pub channel: CableChannelPrototype,
|
||||
pub preview_kind: PreviewKind,
|
||||
pub no_preview: bool,
|
||||
pub tick_rate: Option<f64>,
|
||||
@ -40,7 +38,7 @@ pub struct PostProcessedCli {
|
||||
impl Default for PostProcessedCli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
channel: ParsedCliChannel::Builtin(CliTvChannel::Files),
|
||||
channel: CableChannelPrototype::default(),
|
||||
preview_kind: PreviewKind::None,
|
||||
no_preview: false,
|
||||
tick_rate: None,
|
||||
@ -85,25 +83,40 @@ impl From<Cli> for PostProcessedCli {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let channel: ParsedCliChannel;
|
||||
let channel: CableChannelPrototype;
|
||||
let working_directory: Option<String>;
|
||||
|
||||
match parse_channel(&cli.channel) {
|
||||
Ok(p) => {
|
||||
channel = p;
|
||||
working_directory = cli.working_directory;
|
||||
}
|
||||
Err(_) => {
|
||||
// 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()
|
||||
{
|
||||
channel = ParsedCliChannel::Builtin(CliTvChannel::Files);
|
||||
working_directory = Some(cli.channel.clone());
|
||||
} else {
|
||||
unknown_channel_exit(&cli.channel);
|
||||
unreachable!();
|
||||
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;
|
||||
}
|
||||
Err(_) => {
|
||||
// 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()
|
||||
{
|
||||
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.unwrap());
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,21 +151,6 @@ fn unknown_channel_exit(channel: &str) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ParsedCliChannel {
|
||||
Builtin(CliTvChannel),
|
||||
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 = ';';
|
||||
|
||||
/// Parse a keybindings literal into a `KeyBindings` struct.
|
||||
@ -174,45 +172,22 @@ fn parse_keybindings_literal(
|
||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub fn parse_channel(channel: &str) -> Result<ParsedCliChannel> {
|
||||
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
|
||||
cable_channels
|
||||
match cable_channels
|
||||
.iter()
|
||||
.find(|(k, _)| k.to_lowercase() == channel)
|
||||
.map_or_else(
|
||||
|| {
|
||||
// try to parse the channel as a builtin channel
|
||||
CliTvChannel::try_from(channel)
|
||||
.map(ParsedCliChannel::Builtin)
|
||||
.map_err(|_| anyhow!("Unknown channel: '{}'", channel))
|
||||
},
|
||||
|(_, v)| Ok(ParsedCliChannel::Cable(v.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list_cable_channels() -> Vec<String> {
|
||||
cable::load_cable_channels()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn list_builtin_channels() -> Vec<String> {
|
||||
CliTvChannel::all_channels()
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect()
|
||||
{
|
||||
Some((_, v)) => Ok(v.clone()),
|
||||
None => Err(anyhow!("The following channel wasn't found among cable channels: {channel}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_channels() {
|
||||
println!("\x1b[4mBuiltin channels:\x1b[0m");
|
||||
for c in list_builtin_channels() {
|
||||
println!("\t{c}");
|
||||
}
|
||||
println!("\n\x1b[4mCustom channels:\x1b[0m");
|
||||
for c in list_cable_channels().iter().map(|c| c.to_lowercase()) {
|
||||
for c in cable::load_cable_channels().unwrap_or_default().keys() {
|
||||
println!("\t{c}");
|
||||
}
|
||||
}
|
||||
@ -243,13 +218,18 @@ pub fn list_channels() {
|
||||
pub fn guess_channel_from_prompt(
|
||||
prompt: &str,
|
||||
command_mapping: &FxHashMap<String, String>,
|
||||
fallback_channel: ParsedCliChannel,
|
||||
) -> Result<ParsedCliChannel> {
|
||||
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();
|
||||
@ -263,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) {
|
||||
@ -272,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");
|
||||
@ -327,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()),
|
||||
@ -338,7 +318,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
post_processed_cli.channel,
|
||||
ParsedCliChannel::Builtin(CliTvChannel::Files)
|
||||
CableChannelPrototype::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
post_processed_cli.preview_kind,
|
||||
@ -359,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()
|
||||
};
|
||||
@ -368,7 +348,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
post_processed_cli.channel,
|
||||
ParsedCliChannel::Builtin(CliTvChannel::Files)
|
||||
CableChannelPrototype::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
post_processed_cli.working_directory,
|
||||
@ -380,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()
|
||||
@ -397,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()
|
||||
@ -414,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(
|
||||
@ -436,15 +416,17 @@ mod tests {
|
||||
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
||||
}
|
||||
|
||||
fn guess_channel_from_prompt_setup(
|
||||
) -> (FxHashMap<String, String>, ParsedCliChannel) {
|
||||
/// Returns a tuple containing a command mapping and a fallback channel.
|
||||
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,
|
||||
ParsedCliChannel::Builtin(CliTvChannel::Env),
|
||||
"env",
|
||||
cable::load_cable_channels().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -452,44 +434,53 @@ 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)
|
||||
.unwrap();
|
||||
let channel = guess_channel_from_prompt(
|
||||
prompt,
|
||||
&command_mapping,
|
||||
fallback,
|
||||
&channels,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(channel.name(), "files");
|
||||
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 (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 {
|
||||
|
@ -109,7 +109,6 @@ pub struct Theme {
|
||||
// modes
|
||||
pub channel_mode_fg: Color,
|
||||
pub remote_control_mode_fg: Color,
|
||||
pub send_to_channel_mode_fg: Color,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
@ -180,7 +179,6 @@ struct Inner {
|
||||
//modes
|
||||
channel_mode_fg: String,
|
||||
remote_control_mode_fg: String,
|
||||
send_to_channel_mode_fg: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Theme {
|
||||
@ -308,15 +306,6 @@ impl<'de> Deserialize<'de> for Theme {
|
||||
&inner.remote_control_mode_fg
|
||||
))
|
||||
})?,
|
||||
send_to_channel_mode_fg: Color::from_str(
|
||||
&inner.send_to_channel_mode_fg,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid color {}",
|
||||
&inner.send_to_channel_mode_fg
|
||||
))
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -439,7 +428,6 @@ impl Into<ModeColorscheme> for &Theme {
|
||||
ModeColorscheme {
|
||||
channel: (&self.channel_mode_fg).into(),
|
||||
remote_control: (&self.remote_control_mode_fg).into(),
|
||||
send_to_channel: (&self.send_to_channel_mode_fg).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -466,7 +454,6 @@ mod tests {
|
||||
preview_title_fg = "bright-white"
|
||||
channel_mode_fg = "bright-white"
|
||||
remote_control_mode_fg = "bright-white"
|
||||
send_to_channel_mode_fg = "bright-white"
|
||||
"##;
|
||||
let theme: Theme = toml::from_str(theme_content).unwrap();
|
||||
assert_eq!(
|
||||
@ -496,10 +483,6 @@ mod tests {
|
||||
theme.remote_control_mode_fg,
|
||||
Color::Ansi(ANSIColor::BrightWhite)
|
||||
);
|
||||
assert_eq!(
|
||||
theme.send_to_channel_mode_fg,
|
||||
Color::Ansi(ANSIColor::BrightWhite)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -519,7 +502,6 @@ mod tests {
|
||||
preview_title_fg = "bright-white"
|
||||
channel_mode_fg = "bright-white"
|
||||
remote_control_mode_fg = "bright-white"
|
||||
send_to_channel_mode_fg = "bright-white"
|
||||
"##;
|
||||
let theme: Theme = toml::from_str(theme_content).unwrap();
|
||||
assert_eq!(theme.background, None);
|
||||
@ -549,9 +531,5 @@ mod tests {
|
||||
theme.remote_control_mode_fg,
|
||||
Color::Ansi(ANSIColor::BrightWhite)
|
||||
);
|
||||
assert_eq!(
|
||||
theme.send_to_channel_mode_fg,
|
||||
Color::Ansi(ANSIColor::BrightWhite)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
@ -16,8 +16,7 @@ use television::channels::{
|
||||
};
|
||||
use television::cli::{
|
||||
args::{Cli, Command},
|
||||
guess_channel_from_prompt, list_channels, ParsedCliChannel,
|
||||
PostProcessedCli,
|
||||
guess_channel_from_prompt, list_channels, PostProcessedCli,
|
||||
};
|
||||
|
||||
use television::config::{merge_keybindings, Config, ConfigEnv};
|
||||
@ -44,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
|
||||
@ -59,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);
|
||||
|
||||
@ -147,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");
|
||||
@ -164,29 +171,23 @@ 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);
|
||||
match channel {
|
||||
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
Ok(TelevisionChannel::Cable(c.into()))
|
||||
}
|
||||
}
|
||||
Ok(TelevisionChannel::Cable(channel.into()))
|
||||
} else {
|
||||
debug!("Using {:?} channel", args.channel);
|
||||
match args.channel {
|
||||
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
Ok(TelevisionChannel::Cable(c.into()))
|
||||
}
|
||||
}
|
||||
Ok(TelevisionChannel::Cable(args.channel.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
use television::{
|
||||
cable::load_cable_channels, channels::cable::CableChannelPrototype,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -195,12 +196,17 @@ 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!(
|
||||
channel.name() == expected_channel.name(),
|
||||
assert_eq!(
|
||||
channel.name(),
|
||||
expected_channel.name(),
|
||||
"Expected {:?} but got {:?}",
|
||||
expected_channel.name(),
|
||||
channel.name()
|
||||
@ -208,10 +214,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
/// Test that the channel is stdin when stdin is readable
|
||||
async fn test_determine_channel_readable_stdin() {
|
||||
let channel = television::cli::ParsedCliChannel::Builtin(
|
||||
television::channels::CliTvChannel::Env,
|
||||
);
|
||||
let channel = CableChannelPrototype::default();
|
||||
let args = PostProcessedCli {
|
||||
channel,
|
||||
..Default::default()
|
||||
@ -222,14 +227,16 @@ mod tests {
|
||||
&config,
|
||||
true,
|
||||
&TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_determine_channel_autocomplete_prompt() {
|
||||
let autocomplete_prompt = Some("cd".to_string());
|
||||
let expected_channel = television::channels::TelevisionChannel::Dirs(
|
||||
television::channels::dirs::Channel::default(),
|
||||
let expected_channel = TelevisionChannel::Cable(
|
||||
CableChannelPrototype::new("dirs", "ls {}", false, None, None)
|
||||
.into(),
|
||||
);
|
||||
let args = PostProcessedCli {
|
||||
autocomplete_prompt,
|
||||
@ -251,14 +258,19 @@ 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]
|
||||
async fn test_determine_channel_standard_case() {
|
||||
let channel = television::cli::ParsedCliChannel::Builtin(
|
||||
television::channels::CliTvChannel::Dirs,
|
||||
);
|
||||
let channel =
|
||||
CableChannelPrototype::new("dirs", "", false, None, None);
|
||||
let args = PostProcessedCli {
|
||||
channel,
|
||||
..Default::default()
|
||||
@ -268,9 +280,11 @@ mod tests {
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&TelevisionChannel::Dirs(
|
||||
television::channels::dirs::Channel::default(),
|
||||
&TelevisionChannel::Cable(
|
||||
CableChannelPrototype::new("dirs", "", false, None, None)
|
||||
.into(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ impl Preview {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct PreviewState {
|
||||
pub enabled: bool,
|
||||
pub preview: Arc<Preview>,
|
||||
@ -115,6 +115,17 @@ pub struct PreviewState {
|
||||
pub target_line: Option<u16>,
|
||||
}
|
||||
|
||||
impl Default for PreviewState {
|
||||
fn default() -> Self {
|
||||
PreviewState {
|
||||
enabled: false,
|
||||
preview: Arc::new(Preview::default()),
|
||||
scroll: 0,
|
||||
target_line: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PREVIEW_MIN_SCROLL_LINES: u16 = 3;
|
||||
|
||||
impl PreviewState {
|
||||
|
@ -51,5 +51,4 @@ pub struct InputColorscheme {
|
||||
pub struct ModeColorscheme {
|
||||
pub channel: Color,
|
||||
pub remote_control: Color,
|
||||
pub send_to_channel: Color,
|
||||
}
|
||||
|
@ -56,13 +56,6 @@ impl KeyBindings {
|
||||
&[Action::CopyEntryToClipboard],
|
||||
),
|
||||
),
|
||||
(
|
||||
DisplayableAction::SendToChannel,
|
||||
serialized_keys_for_actions(
|
||||
self,
|
||||
&[Action::ToggleSendToChannel],
|
||||
),
|
||||
),
|
||||
(
|
||||
DisplayableAction::ToggleRemoteControl,
|
||||
serialized_keys_for_actions(
|
||||
@ -101,41 +94,12 @@ impl KeyBindings {
|
||||
),
|
||||
]);
|
||||
|
||||
// send to channel mode keybindings
|
||||
let send_to_channel_bindings: FxHashMap<
|
||||
DisplayableAction,
|
||||
Vec<String>,
|
||||
> = FxHashMap::from_iter(vec![
|
||||
(
|
||||
DisplayableAction::ResultsNavigation,
|
||||
serialized_keys_for_actions(
|
||||
self,
|
||||
&[Action::SelectPrevEntry, Action::SelectNextEntry],
|
||||
),
|
||||
),
|
||||
(
|
||||
DisplayableAction::SelectEntry,
|
||||
serialized_keys_for_actions(self, &[Action::ConfirmSelection]),
|
||||
),
|
||||
(
|
||||
DisplayableAction::Cancel,
|
||||
serialized_keys_for_actions(
|
||||
self,
|
||||
&[Action::ToggleSendToChannel],
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
FxHashMap::from_iter(vec![
|
||||
(Mode::Channel, DisplayableKeybindings::new(channel_bindings)),
|
||||
(
|
||||
Mode::RemoteControl,
|
||||
DisplayableKeybindings::new(remote_control_bindings),
|
||||
),
|
||||
(
|
||||
Mode::SendToChannel,
|
||||
DisplayableKeybindings::new(send_to_channel_bindings),
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
@ -167,7 +131,6 @@ pub enum DisplayableAction {
|
||||
PreviewNavigation,
|
||||
SelectEntry,
|
||||
CopyEntryToClipboard,
|
||||
SendToChannel,
|
||||
ToggleRemoteControl,
|
||||
Cancel,
|
||||
Quit,
|
||||
@ -183,7 +146,6 @@ impl Display for DisplayableAction {
|
||||
DisplayableAction::CopyEntryToClipboard => {
|
||||
"Copy entry to clipboard"
|
||||
}
|
||||
DisplayableAction::SendToChannel => "Send to channel",
|
||||
DisplayableAction::ToggleRemoteControl => "Toggle Remote control",
|
||||
DisplayableAction::Cancel => "Cancel",
|
||||
DisplayableAction::Quit => "Quit",
|
||||
@ -207,12 +169,6 @@ pub fn build_keybindings_table<'a>(
|
||||
&keybindings[&mode],
|
||||
colorscheme,
|
||||
),
|
||||
Mode::SendToChannel => {
|
||||
build_keybindings_table_for_channel_transitions(
|
||||
&keybindings[&mode],
|
||||
colorscheme,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,18 +224,6 @@ fn build_keybindings_table_for_channel<'a>(
|
||||
colorscheme.mode.channel,
|
||||
));
|
||||
|
||||
// Send to channel
|
||||
let send_to_channel_keys = keybindings
|
||||
.bindings
|
||||
.get(&DisplayableAction::SendToChannel)
|
||||
.unwrap();
|
||||
let send_to_channel_row = Row::new(build_cells_for_group(
|
||||
"Send results to",
|
||||
send_to_channel_keys,
|
||||
colorscheme.help.metadata_field_name_fg,
|
||||
colorscheme.mode.channel,
|
||||
));
|
||||
|
||||
// Switch channels
|
||||
let switch_channels_keys = keybindings
|
||||
.bindings
|
||||
@ -300,7 +244,6 @@ fn build_keybindings_table_for_channel<'a>(
|
||||
preview_row,
|
||||
select_entry_row,
|
||||
copy_entry_row,
|
||||
send_to_channel_row,
|
||||
switch_channels_row,
|
||||
],
|
||||
widths,
|
||||
@ -353,52 +296,6 @@ fn build_keybindings_table_for_channel_selection<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
fn build_keybindings_table_for_channel_transitions<'a>(
|
||||
keybindings: &'a DisplayableKeybindings,
|
||||
colorscheme: &'a Colorscheme,
|
||||
) -> Table<'a> {
|
||||
// Results navigation
|
||||
let results_navigation_keys = keybindings
|
||||
.bindings
|
||||
.get(&DisplayableAction::ResultsNavigation)
|
||||
.unwrap();
|
||||
let results_row = Row::new(build_cells_for_group(
|
||||
"Browse channels",
|
||||
results_navigation_keys,
|
||||
colorscheme.help.metadata_field_name_fg,
|
||||
colorscheme.mode.send_to_channel,
|
||||
));
|
||||
|
||||
// Select entry
|
||||
let select_entry_keys = keybindings
|
||||
.bindings
|
||||
.get(&DisplayableAction::SelectEntry)
|
||||
.unwrap();
|
||||
let select_entry_row = Row::new(build_cells_for_group(
|
||||
"Send to channel",
|
||||
select_entry_keys,
|
||||
colorscheme.help.metadata_field_name_fg,
|
||||
colorscheme.mode.send_to_channel,
|
||||
));
|
||||
|
||||
// Cancel
|
||||
let cancel_keys = keybindings
|
||||
.bindings
|
||||
.get(&DisplayableAction::Cancel)
|
||||
.unwrap();
|
||||
let cancel_row = Row::new(build_cells_for_group(
|
||||
"Cancel",
|
||||
cancel_keys,
|
||||
colorscheme.help.metadata_field_name_fg,
|
||||
colorscheme.mode.send_to_channel,
|
||||
));
|
||||
|
||||
Table::new(
|
||||
vec![results_row, select_entry_row, cancel_row],
|
||||
vec![Constraint::Fill(1), Constraint::Fill(2)],
|
||||
)
|
||||
}
|
||||
|
||||
fn build_cells_for_group<'a>(
|
||||
group_name: &str,
|
||||
keys: &'a [String],
|
||||
|
@ -15,7 +15,6 @@ impl Display for Mode {
|
||||
match self {
|
||||
Mode::Channel => write!(f, "Channel"),
|
||||
Mode::RemoteControl => write!(f, "Remote Control"),
|
||||
Mode::SendToChannel => write!(f, "Send to Channel"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,5 @@ pub fn mode_color(mode: Mode, colorscheme: &ModeColorscheme) -> Color {
|
||||
match mode {
|
||||
Mode::Channel => colorscheme.channel,
|
||||
Mode::RemoteControl => colorscheme.remote_control,
|
||||
Mode::SendToChannel => colorscheme.send_to_channel,
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
use crate::action::Action;
|
||||
use crate::cable::load_cable_channels;
|
||||
use crate::channels::entry::{Entry, ENTRY_PLACEHOLDER};
|
||||
use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER};
|
||||
use crate::channels::{
|
||||
remote_control::{load_builtin_channels, RemoteControl},
|
||||
OnAir, TelevisionChannel, UnitChannel,
|
||||
remote_control::RemoteControl, OnAir, TelevisionChannel,
|
||||
};
|
||||
use crate::config::{Config, Theme};
|
||||
use crate::draw::{ChannelState, Ctx, TvState};
|
||||
@ -28,7 +27,6 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
pub enum Mode {
|
||||
Channel,
|
||||
RemoteControl,
|
||||
SendToChannel,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||
@ -75,9 +73,6 @@ impl Television {
|
||||
}
|
||||
let previewer = Previewer::new(Some(config.previewers.clone().into()));
|
||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||
let builtin_channels = load_builtin_channels(Some(
|
||||
&cable_channels.keys().collect::<Vec<_>>(),
|
||||
));
|
||||
|
||||
let app_metadata = AppMetadata::new(
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
@ -101,10 +96,9 @@ impl Television {
|
||||
let remote_control = if no_remote {
|
||||
None
|
||||
} else {
|
||||
Some(TelevisionChannel::RemoteControl(RemoteControl::new(
|
||||
builtin_channels,
|
||||
Some(cable_channels),
|
||||
)))
|
||||
Some(TelevisionChannel::RemoteControl(RemoteControl::new(Some(
|
||||
cable_channels,
|
||||
))))
|
||||
};
|
||||
|
||||
if no_help {
|
||||
@ -146,11 +140,8 @@ impl Television {
|
||||
|
||||
pub fn init_remote_control(&mut self) {
|
||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||
let builtin_channels = load_builtin_channels(Some(
|
||||
&cable_channels.keys().collect::<Vec<_>>(),
|
||||
));
|
||||
self.remote_control = Some(TelevisionChannel::RemoteControl(
|
||||
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
||||
RemoteControl::new(Some(cable_channels)),
|
||||
));
|
||||
}
|
||||
|
||||
@ -182,12 +173,13 @@ impl Television {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn current_channel(&self) -> UnitChannel {
|
||||
UnitChannel::from(&self.channel)
|
||||
pub fn current_channel(&self) -> String {
|
||||
self.channel.name()
|
||||
}
|
||||
|
||||
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
||||
self.preview_state.reset();
|
||||
self.preview_state.enabled = channel.supports_preview();
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.current_pattern = EMPTY_STRING.to_string();
|
||||
@ -203,7 +195,7 @@ impl Television {
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
Mode::RemoteControl => {
|
||||
self.remote_control.as_mut().unwrap().find(pattern);
|
||||
}
|
||||
}
|
||||
@ -233,7 +225,7 @@ impl Television {
|
||||
}
|
||||
None
|
||||
}
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
Mode::RemoteControl => {
|
||||
if let Some(i) = self.rc_picker.selected() {
|
||||
return self
|
||||
.remote_control
|
||||
@ -268,7 +260,7 @@ impl Television {
|
||||
Mode::Channel => {
|
||||
(self.channel.result_count(), &mut self.results_picker)
|
||||
}
|
||||
Mode::RemoteControl | Mode::SendToChannel => (
|
||||
Mode::RemoteControl => (
|
||||
self.remote_control.as_ref().unwrap().total_count(),
|
||||
&mut self.rc_picker,
|
||||
),
|
||||
@ -288,7 +280,7 @@ impl Television {
|
||||
Mode::Channel => {
|
||||
(self.channel.result_count(), &mut self.results_picker)
|
||||
}
|
||||
Mode::RemoteControl | Mode::SendToChannel => (
|
||||
Mode::RemoteControl => (
|
||||
self.remote_control.as_ref().unwrap().total_count(),
|
||||
&mut self.rc_picker,
|
||||
),
|
||||
@ -306,7 +298,7 @@ impl Television {
|
||||
fn reset_picker_selection(&mut self) {
|
||||
match self.mode {
|
||||
Mode::Channel => self.results_picker.reset_selection(),
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
Mode::RemoteControl => {
|
||||
self.rc_picker.reset_selection();
|
||||
}
|
||||
}
|
||||
@ -315,7 +307,7 @@ impl Television {
|
||||
fn reset_picker_input(&mut self) {
|
||||
match self.mode {
|
||||
Mode::Channel => self.results_picker.reset_input(),
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
Mode::RemoteControl => {
|
||||
self.rc_picker.reset_input();
|
||||
}
|
||||
}
|
||||
@ -376,7 +368,9 @@ impl Television {
|
||||
&mut self,
|
||||
selected_entry: &Entry,
|
||||
) -> Result<()> {
|
||||
if self.config.ui.show_preview_panel && self.channel.supports_preview()
|
||||
if self.config.ui.show_preview_panel
|
||||
&& self.channel.supports_preview()
|
||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||
{
|
||||
// preview content
|
||||
if let Some(preview) = self
|
||||
@ -453,9 +447,7 @@ impl Television {
|
||||
pub fn handle_input_action(&mut self, action: &Action) {
|
||||
let input = match self.mode {
|
||||
Mode::Channel => &mut self.results_picker.input,
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
&mut self.rc_picker.input
|
||||
}
|
||||
Mode::RemoteControl => &mut self.rc_picker.input,
|
||||
};
|
||||
input.handle(convert_action_to_input_request(action).unwrap());
|
||||
match action {
|
||||
@ -493,27 +485,6 @@ impl Television {
|
||||
self.reset_picker_selection();
|
||||
self.mode = Mode::Channel;
|
||||
}
|
||||
Mode::SendToChannel => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_toggle_send_to_channel(&mut self) {
|
||||
if self.remote_control.is_none() {
|
||||
return;
|
||||
}
|
||||
match self.mode {
|
||||
Mode::Channel | Mode::RemoteControl => {
|
||||
self.mode = Mode::SendToChannel;
|
||||
self.remote_control = Some(TelevisionChannel::RemoteControl(
|
||||
RemoteControl::with_transitions_from(&self.channel),
|
||||
));
|
||||
}
|
||||
Mode::SendToChannel => {
|
||||
self.reset_picker_input();
|
||||
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
|
||||
self.reset_picker_selection();
|
||||
self.mode = Mode::Channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,18 +521,6 @@ impl Television {
|
||||
self.change_channel(new_channel);
|
||||
}
|
||||
}
|
||||
Mode::SendToChannel => {
|
||||
if let Some(entry) = self.get_selected_entry(None) {
|
||||
let new_channel = self
|
||||
.channel
|
||||
.transition_to(entry.name.as_str().try_into()?);
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
|
||||
self.mode = Mode::Channel;
|
||||
self.change_channel(new_channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -644,9 +603,6 @@ impl Television {
|
||||
Action::CopyEntryToClipboard => {
|
||||
self.handle_copy_entry_to_clipboard();
|
||||
}
|
||||
Action::ToggleSendToChannel => {
|
||||
self.handle_toggle_send_to_channel();
|
||||
}
|
||||
Action::ToggleHelp => {
|
||||
if self.no_help {
|
||||
return Ok(());
|
||||
|
@ -1,5 +1,8 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(not(unix))]
|
||||
use tracing::warn;
|
||||
|
||||
use super::shell::Shell;
|
||||
|
||||
pub fn shell_command(interactive: bool) -> Command {
|
||||
@ -17,5 +20,10 @@ pub fn shell_command(interactive: bool) -> Command {
|
||||
cmd.arg("-i");
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
if interactive {
|
||||
warn!("Interactive mode is not supported on Windows.");
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::{collections::HashSet, path::PathBuf, time::Duration};
|
||||
use television::{
|
||||
action::Action,
|
||||
app::{App, AppOptions},
|
||||
channels::TelevisionChannel,
|
||||
channels::{cable::CableChannelPrototype, TelevisionChannel},
|
||||
config::default_config_from_file,
|
||||
};
|
||||
use tokio::{task::JoinHandle, time::timeout};
|
||||
@ -33,9 +33,7 @@ fn setup_app(
|
||||
.join("tests")
|
||||
.join("target_dir");
|
||||
std::env::set_current_dir(&target_dir).unwrap();
|
||||
TelevisionChannel::Files(television::channels::files::Channel::new(
|
||||
vec![target_dir],
|
||||
))
|
||||
TelevisionChannel::Cable(CableChannelPrototype::default().into())
|
||||
});
|
||||
let mut config = default_config_from_file().unwrap();
|
||||
// this speeds up the tests
|
||||
|
Loading…
x
Reference in New Issue
Block a user