From 67677fb917b6d59d8217eaf6369b95f5ba940ff0 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:50:14 +0200 Subject: [PATCH] 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) --- .config/config.toml | 3 +- .github/workflows/ci.yml | 6 +- Cargo.lock | 2 +- Cargo.toml | 2 +- benches/main/ui.rs | 11 +- cable/unix-channels.toml | 37 +++ cable/windows-channels.toml | 58 ++-- television-derive/Cargo.toml | 2 +- television-derive/src/lib.rs | 215 -------------- television/cable.rs | 16 +- television/channels/alias.rs | 182 ------------ television/channels/cable.rs | 81 +++++- television/channels/dirs.rs | 186 ------------- television/channels/env.rs | 132 --------- television/channels/files.rs | 190 ------------- television/channels/git_repos.rs | 204 -------------- television/channels/mod.rs | 169 +---------- television/channels/remote_control.rs | 96 ++----- television/channels/text.rs | 387 -------------------------- television/cli/args.rs | 9 +- television/cli/mod.rs | 189 ++++++------- television/config/mod.rs | 9 + television/config/themes.rs | 22 -- television/main.rs | 80 +++--- television/preview/mod.rs | 13 +- television/screen/colors.rs | 1 - television/screen/keybindings.rs | 103 ------- television/screen/metadata.rs | 1 - television/screen/mode.rs | 1 - television/television.rs | 82 ++---- television/utils/command.rs | 8 + tests/app.rs | 6 +- 32 files changed, 386 insertions(+), 2117 deletions(-) delete mode 100644 television/channels/alias.rs delete mode 100644 television/channels/dirs.rs delete mode 100644 television/channels/env.rs delete mode 100644 television/channels/files.rs delete mode 100644 television/channels/git_repos.rs delete mode 100644 television/channels/text.rs diff --git a/.config/config.toml b/.config/config.toml index 268de0b..71e2769 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5ffa31..32fcda2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 40f138e..3bb1c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2252,7 +2252,7 @@ dependencies = [ [[package]] name = "television-derive" -version = "0.0.26" +version = "0.0.27" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 42a9a10..d7460c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/benches/main/ui.rs b/benches/main/ui.rs index efee73c..5828636 100644 --- a/benches/main/ui.rs +++ b/benches/main/ui.rs @@ -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( diff --git a/cable/unix-channels.toml b/cable/unix-channels.toml index 099fedb..c0c3a3c 100644 --- a/cable/unix-channels.toml +++ b/cable/unix-channels.toml @@ -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" diff --git a/cable/windows-channels.toml b/cable/windows-channels.toml index b511080..e76f4a2 100644 --- a/cable/windows-channels.toml +++ b/cable/windows-channels.toml @@ -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" diff --git a/television-derive/Cargo.toml b/television-derive/Cargo.toml index d6c67f9..afa6943 100644 --- a/television-derive/Cargo.toml +++ b/television-derive/Cargo.toml @@ -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" diff --git a/television-derive/src/lib.rs b/television-derive/src/lib.rs index 5c008ab..29af41f 100644 --- a/television-derive/src/lib.rs +++ b/television-derive/src/lib.rs @@ -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 { - 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 implementation - let into_impl = quote! { - impl Into 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() -} diff --git a/television/cable.rs b/television/cable.rs index ac84b2b..71d35ff 100644 --- a/television/cable.rs +++ b/television/cable.rs @@ -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, + pub prototypes: Vec, } const CABLE_FILE_NAME_SUFFIX: &str = "channels"; @@ -65,14 +65,14 @@ pub fn load_cable_channels() -> Result { file_paths.push(default_channels_path); } - let user_defined_prototypes = file_paths.iter().fold( + let prototypes = file_paths.iter().fold( Vec::::new(), |mut acc, p| { match toml::from_str::( &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 { }, ); - 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)) diff --git a/television/channels/alias.rs b/television/channels/alias.rs deleted file mode 100644 index ecf57da..0000000 --- a/television/channels/alias.rs +++ /dev/null @@ -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, - file_icon: FileIcon, - selected_entries: FxHashSet, - crawl_handle: tokio::task::JoinHandle<()>, -} - -const NUM_THREADS: usize = 1; - -const FILE_ICON_STR: &str = "nu"; - -fn get_raw_aliases(shell: Shell) -> Vec { - // 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 { - 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 { - 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 { - &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) { - 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(); - }); - }); -} diff --git a/television/channels/cable.rs b/television/channels/cable.rs index 26ebaf7..7731627 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -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 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, ) -> 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) { +async fn load_candidates( + command: String, + interactive: bool, + injector: Injector, +) { 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, #[serde(default = "default_delimiter")] pub preview_delimiter: Option, } +impl CableChannelPrototype { + pub fn new( + name: &str, + source_command: &str, + interactive: bool, + preview_command: Option, + preview_delimiter: Option, + ) -> 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); 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::(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) + } +} diff --git a/television/channels/dirs.rs b/television/channels/dirs.rs deleted file mode 100644 index 6756302..0000000 --- a/television/channels/dirs.rs +++ /dev/null @@ -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, - 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, -} - -impl Channel { - pub fn new(paths: Vec) -> 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::>() - .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 { - 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 { - 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 { - &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, injector: Injector) { - 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 - }) - }); -} diff --git a/television/channels/env.rs b/television/channels/env.rs deleted file mode 100644 index 8dcc401..0000000 --- a/television/channels/env.rs +++ /dev/null @@ -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, - file_icon: FileIcon, - selected_entries: FxHashSet, -} - -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 { - 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 { - 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 { - &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 - } -} diff --git a/television/channels/files.rs b/television/channels/files.rs deleted file mode 100644 index 7361272..0000000 --- a/television/channels/files.rs +++ /dev/null @@ -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, - 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, -} - -impl Channel { - pub fn new(paths: Vec) -> 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::>() - .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::>() - .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 { - 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 { - 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 { - &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, injector: Injector) { - 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 - }) - }); -} diff --git a/television/channels/git_repos.rs b/television/channels/git_repos.rs deleted file mode 100644 index 0511aef..0000000 --- a/television/channels/git_repos.rs +++ /dev/null @@ -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, - icon: FileIcon, - crawl_handle: JoinHandle<()>, - selected_entries: FxHashSet, - 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 { - 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 { - 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 { - &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 { - 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) { - 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 - }) - }); -} diff --git a/television/channels/mod.rs b/television/channels/mod.rs index 876f3ed..0604278 100644 --- a/television/channels/mod.rs +++ b/television/channels/mod.rs @@ -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 { 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 { -/// 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 { - 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( - ::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], -} diff --git a/television/channels/remote_control.rs b/television/channels/remote_control.rs index 8137e6e..1f85a30 100644 --- a/television/channels/remote_control.rs +++ b/television/channels/remote_control.rs @@ -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, + matcher: Matcher, cable_channels: Option, selected_entries: FxHashSet, } -#[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, - cable_channels: Option, - ) -> Self { + pub fn new(cable_channels: Option) -> 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 { 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 { - let mut value_variants = CliTvChannel::value_variants() - .iter() - .map(std::string::ToString::to_string) - .collect::>(); - - 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() } diff --git a/television/channels/text.rs b/television/channels/text.rs deleted file mode 100644 index 0159479..0000000 --- a/television/channels/text.rs +++ /dev/null @@ -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, - crawl_handle: tokio::task::JoinHandle<()>, - selected_entries: FxHashSet, -} - -impl Channel { - pub fn new(directories: Vec) -> 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) -> 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) -> 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 { - 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 { - 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 { - &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, - injector: Injector, -) { - 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, - current_dir: &PathBuf, - path: &Path, -) -> Option { - 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 - } - } -} diff --git a/television/cli/args.rs b/television/cli/args.rs index 29a2543..a319b4a 100644 --- a/television/cli/args.rs +++ b/television/cli/args.rs @@ -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, /// A preview command to use with the stdin channel. /// diff --git a/television/cli/mod.rs b/television/cli/mod.rs index 5e31731..a9f1f8b 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -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, @@ -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 for PostProcessedCli { .unwrap() }); - let channel: ParsedCliChannel; + let channel: CableChannelPrototype; let working_directory: Option; - 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 { - let cable_channels = cable::load_cable_channels().unwrap_or_default(); +pub fn parse_channel( + channel: &str, + cable_channels: &CableChannels, +) -> Result { // 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 { - cable::load_cable_channels() - .unwrap_or_default() - .iter() - .map(|(k, _)| k.clone()) - .collect() -} - -pub fn list_builtin_channels() -> Vec { - 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, - fallback_channel: ParsedCliChannel, -) -> Result { + fallback_channel: &str, + cable_channels: &CableChannels, +) -> Result { 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, ParsedCliChannel) { + /// Returns a tuple containing a command mapping and a fallback channel. + fn guess_channel_from_prompt_setup<'a>( + ) -> (FxHashMap, &'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); } } diff --git a/television/config/mod.rs b/television/config/mod.rs index 429ab90..25907ba 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -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 { diff --git a/television/config/themes.rs b/television/config/themes.rs index 8f64558..7e00041 100644 --- a/television/config/themes.rs +++ b/television/config/themes.rs @@ -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 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) - ); } } diff --git a/television/main.rs b/television/main.rs index 2883f8d..88c2ca0 100644 --- a/television/main.rs +++ b/television/main.rs @@ -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 { 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, ) { + 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, ); } diff --git a/television/preview/mod.rs b/television/preview/mod.rs index cfd6a83..c2571ff 100644 --- a/television/preview/mod.rs +++ b/television/preview/mod.rs @@ -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, @@ -115,6 +115,17 @@ pub struct PreviewState { pub target_line: Option, } +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 { diff --git a/television/screen/colors.rs b/television/screen/colors.rs index 4f9f3f1..ae5a151 100644 --- a/television/screen/colors.rs +++ b/television/screen/colors.rs @@ -51,5 +51,4 @@ pub struct InputColorscheme { pub struct ModeColorscheme { pub channel: Color, pub remote_control: Color, - pub send_to_channel: Color, } diff --git a/television/screen/keybindings.rs b/television/screen/keybindings.rs index 0b362a4..5692eb5 100644 --- a/television/screen/keybindings.rs +++ b/television/screen/keybindings.rs @@ -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, - > = 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], diff --git a/television/screen/metadata.rs b/television/screen/metadata.rs index 2ca1beb..2855ae9 100644 --- a/television/screen/metadata.rs +++ b/television/screen/metadata.rs @@ -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"), } } } diff --git a/television/screen/mode.rs b/television/screen/mode.rs index 439b287..78664f5 100644 --- a/television/screen/mode.rs +++ b/television/screen/mode.rs @@ -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, } } diff --git a/television/television.rs b/television/television.rs index 40f16a4..7aa3356 100644 --- a/television/television.rs +++ b/television/television.rs @@ -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::>(), - )); 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::>(), - )); 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(()); diff --git a/television/utils/command.rs b/television/utils/command.rs index f971ac0..5a83bce 100644 --- a/television/utils/command.rs +++ b/television/utils/command.rs @@ -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 } diff --git a/tests/app.rs b/tests/app.rs index 9aab002..c463007 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -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