From ae1dd99118b78b3f5e2301c8367ef32d5e6d3c01 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Wed, 23 Apr 2025 21:33:00 +0200 Subject: [PATCH] refactor: television is going the cable-only route --- benches/main/ui.rs | 11 +- cable/unix-channels.toml | 30 ++ cable/windows-channels.toml | 66 +++-- television-derive/src/lib.rs | 215 -------------- television/cable.rs | 8 +- television/channels/alias.rs | 182 ------------ television/channels/cable.rs | 48 +++- 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/mod.rs | 81 ++---- television/config/themes.rs | 22 -- television/main.rs | 43 ++- 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 ++---- tests/app.rs | 6 +- 24 files changed, 223 insertions(+), 2054 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/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..75c008b 100644 --- a/cable/unix-channels.toml +++ b/cable/unix-channels.toml @@ -1,4 +1,34 @@ +# Files +[[cable_channel]] +name = "files" +source_command = "fd -t f" +preview_command = ":files:" + +# 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..6d39f84 100644 --- a/cable/windows-channels.toml +++ b/cable/windows-channels.toml @@ -1,41 +1,73 @@ +# Files +[[cable_channel]] +name = "files" +source_command = "Get-ChildItem -Recurse -File" +preview_command = ":files:" + +# Directories +[[cable_channel]] +name = "dirs" +source_command = "Get-ChildItem -Recurse -Directory" +preview_command = "Get-ChildItem -Force -LiteralPath '{}' | Format-List" + +# Environment variables +[[cable_channel]] +name = "env" +source_command = "Get-ChildItem Env:" +preview_command = "$env:{0} -split ';'" + +# Aliases +[[cable_channel]] +name = "aliases" +source_command = "Get-Alias" +interactive = true + # GIT +[[cable_channel]] +name = "git-repos" +source_command = "Get-ChildItem -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" +# S3 +[[cable_channel]] +name = "s3-buckets" +source_command = "aws s3 ls | ForEach-Object { ($_ -split '\s+')[-1] }" +preview_command = "aws s3 ls s3://{0}" + +# 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/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..fcb4a28 100644 --- a/television/cable.rs +++ b/television/cable.rs @@ -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,10 @@ pub fn load_cable_channels() -> Result { }, ); - debug!("Loaded cable channels: {:?}", user_defined_prototypes); + debug!("Loaded cable channels: {:?}", prototypes); 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..423ea52 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -41,6 +41,7 @@ impl Default for Channel { Self::new( "Files", "find . -type f", + false, Some(PreviewCommand::new("bat -n --color=always {}", ":")), ) } @@ -51,6 +52,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 +81,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 +112,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 +236,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)] 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/mod.rs b/television/cli/mod.rs index 5e31731..6a31276 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -5,9 +5,7 @@ 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::CableChannelPrototype, entry::PreviewCommand}; use crate::cli::args::{Cli, Command}; use crate::config::KeyBindings; use crate::{ @@ -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,7 +83,7 @@ impl From for PostProcessedCli { .unwrap() }); - let channel: ParsedCliChannel; + let channel: CableChannelPrototype; let working_directory: Option; match parse_channel(&cli.channel) { @@ -99,7 +97,7 @@ impl From for PostProcessedCli { if cli.working_directory.is_none() && Path::new(&cli.channel).exists() { - channel = ParsedCliChannel::Builtin(CliTvChannel::Files); + channel = CableChannelPrototype::default(); working_directory = Some(cli.channel.clone()); } else { unknown_channel_exit(&cli.channel); @@ -138,21 +136,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 +157,20 @@ fn parse_keybindings_literal( toml::from_str(&toml_definition).map_err(|e| anyhow!(e)) } -pub fn parse_channel(channel: &str) -> Result { +pub fn parse_channel(channel: &str) -> Result { let cable_channels = cable::load_cable_channels().unwrap_or_default(); // 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!("Unknown channel: {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,8 +201,8 @@ pub fn list_channels() { pub fn guess_channel_from_prompt( prompt: &str, command_mapping: &FxHashMap, - fallback_channel: ParsedCliChannel, -) -> Result { + fallback_channel: CableChannelPrototype, +) -> Result { debug!("Guessing channel from prompt: {}", prompt); // git checkout -qf // --- -------- --- <--------- @@ -338,7 +296,7 @@ mod tests { assert_eq!( post_processed_cli.channel, - ParsedCliChannel::Builtin(CliTvChannel::Files) + CableChannelPrototype::default(), ); assert_eq!( post_processed_cli.preview_kind, @@ -368,7 +326,7 @@ mod tests { assert_eq!( post_processed_cli.channel, - ParsedCliChannel::Builtin(CliTvChannel::Files) + CableChannelPrototype::default(), ); assert_eq!( post_processed_cli.working_directory, @@ -436,15 +394,16 @@ mod tests { assert_eq!(post_processed_cli.keybindings, Some(expected)); } + /// Returns a tuple containing a command mapping and a fallback channel. fn guess_channel_from_prompt_setup( - ) -> (FxHashMap, ParsedCliChannel) { + ) -> (FxHashMap, CableChannelPrototype) { 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), + CableChannelPrototype::new("env", "", false, None, None), ) } @@ -458,7 +417,7 @@ mod tests { guess_channel_from_prompt(prompt, &command_mapping, fallback) .unwrap(); - assert_eq!(channel.name(), "files"); + assert_eq!(channel.name, "files"); } #[test] 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..79a7ed2 100644 --- a/television/main.rs +++ b/television/main.rs @@ -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}; @@ -167,26 +166,17 @@ pub fn determine_channel( parse_channel(&config.shell_integration.fallback_channel)?, )?; 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::channels::cable::CableChannelPrototype; use super::*; @@ -199,8 +189,9 @@ mod tests { let channel = determine_channel(args.clone(), config, readable_stdin).unwrap(); - assert!( - channel.name() == expected_channel.name(), + assert_eq!( + channel.name(), + expected_channel.name(), "Expected {:?} but got {:?}", expected_channel.name(), channel.name() @@ -208,10 +199,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() @@ -228,8 +218,9 @@ mod tests { #[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, @@ -256,9 +247,8 @@ mod tests { #[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,8 +258,9 @@ mod tests { &args, &config, false, - &TelevisionChannel::Dirs( - television::channels::dirs::Channel::default(), + &TelevisionChannel::Cable( + CableChannelPrototype::new("dirs", "", false, None, None) + .into(), ), ); } 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/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