refactor: drop TelevisionChannel enum and all associated macros (#498)

This drops the `TelevisionChannel` enum which served as a unified
interface for builtin and cable channels as well as all related macros
and the `television-derive` package.
This simplifies the code quite a lot and will improve overall
maintainability.
This commit is contained in:
Alexandre Pasmantier 2025-05-06 11:18:32 +02:00 committed by GitHub
parent 58d73dbeba
commit 2b2654b6aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 162 additions and 570 deletions

10
Cargo.lock generated
View File

@ -1424,7 +1424,6 @@ dependencies = [
"rustc-hash",
"serde",
"signal-hook",
"television-derive",
"tempfile",
"tokio",
"toml",
@ -1434,15 +1433,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "television-derive"
version = "0.0.27"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.19.1"

View File

@ -31,8 +31,6 @@ build = "build.rs"
path = "television/lib.rs"
[dependencies]
television-derive = { path = "television-derive", version = "0.0.27" }
anyhow = "1.0"
base64 = "0.22.1"
directories = "6.0"

View File

@ -8,13 +8,12 @@ use ratatui::prelude::{Line, Style};
use ratatui::style::Color;
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
use ratatui::Terminal;
use television::channels::cable::prototypes::CableChannelPrototypes;
use television::channels::cable::prototypes::Cable;
use television::{
action::Action,
channels::{
cable::prototypes::CableChannelPrototype,
cable::prototypes::ChannelPrototype,
entry::{into_ranges, Entry},
OnAir,
},
config::{Config, ConfigEnv},
screen::{colors::ResultsColorscheme, results::build_results_list},
@ -22,6 +21,7 @@ use television::{
};
use tokio::runtime::Runtime;
#[allow(clippy::too_many_lines)]
pub fn draw_results_list(c: &mut Criterion) {
// FIXME: there's probably a way to have this as a benchmark asset
// possible as a JSON file and to load it for the benchmark using Serde
@ -458,6 +458,7 @@ pub fn draw_results_list(c: &mut Criterion) {
});
}
#[allow(clippy::missing_panics_doc)]
pub fn draw(c: &mut Criterion) {
let width = 250;
let height = 80;
@ -472,7 +473,7 @@ 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 channel = CableChannelPrototype::default();
let channel = ChannelPrototype::default();
// Wait for the channel to finish loading
let mut tv = Television::new(
tx,
@ -482,7 +483,7 @@ pub fn draw(c: &mut Criterion) {
false,
false,
false,
CableChannelPrototypes::default(),
Cable::default(),
);
tv.find("television");
for _ in 0..5 {

View File

@ -1,26 +0,0 @@
[package]
name = "television-derive"
version = "0.0.27"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"
authors = ["Alexandre Pasmantier <alex.pasmant@gmail.com>"]
repository = "https://github.com/alexpasmantier/television"
homepage = "https://github.com/alexpasmantier/television"
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
categories = [
"command-line-utilities",
"command-line-interface",
"concurrency",
"development-tools",
]
rust-version = "1.83"
[dependencies]
proc-macro2 = "1.0.93"
quote = "1.0.38"
syn = "2.0.96"
[lib]
proc-macro = true

View File

@ -1,166 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
/// This macro generates the `OnAir` trait implementation for the given enum.
///
/// The `OnAir` trait is used to interact with the different television channels
/// and forwards the method calls to the corresponding channel variants.
///
/// Example:
/// ```ignore
/// use television-derive::Broadcast;
/// use television::channels::{TelevisionChannel, OnAir};
/// use television::channels::{files, text};
///
/// #[derive(Broadcast)]
/// enum TelevisionChannel {
/// Files(files::Channel),
/// Text(text::Channel),
/// }
///
/// let mut channel = TelevisionChannel::Files(files::Channel::default());
///
/// // Use the `OnAir` trait methods directly on TelevisionChannel
/// channel.find("pattern");
/// let results = channel.results(10, 0);
/// let result = channel.get_result(0);
/// let result_count = channel.result_count();
/// let total_count = channel.total_count();
/// let running = channel.running();
/// channel.shutdown();
/// ```
#[proc_macro_derive(Broadcast)]
pub fn tv_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_tv_channel(&ast)
}
fn impl_tv_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(OnAir)] is only defined for enums");
};
// Ensure the enum has at least one variant
assert!(
!variants.is_empty(),
"#[derive(OnAir)] requires at least one variant"
);
let enum_name = &ast.ident;
let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect();
// Generate the trait implementation for the TelevisionChannel trait
let trait_impl = quote! {
impl OnAir for #enum_name {
fn find(&mut self, pattern: &str) {
match self {
#(
#enum_name::#variant_names(ref mut channel) => {
channel.find(pattern);
}
)*
}
}
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
match self {
#(
#enum_name::#variant_names(ref mut channel) => {
channel.results(num_entries, offset)
}
)*
}
}
fn get_result(&self, index: u32) -> Option<Entry> {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.get_result(index)
}
)*
}
}
fn selected_entries(&self) -> &FxHashSet<Entry> {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.selected_entries()
}
)*
}
}
fn toggle_selection(&mut self, entry: &Entry) {
match self {
#(
#enum_name::#variant_names(ref mut channel) => {
channel.toggle_selection(entry)
}
)*
}
}
fn result_count(&self) -> u32 {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.result_count()
}
)*
}
}
fn total_count(&self) -> u32 {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.total_count()
}
)*
}
}
fn running(&self) -> bool {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.running()
}
)*
}
}
fn shutdown(&self) {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.shutdown()
}
)*
}
}
fn supports_preview(&self) -> bool {
match self {
#(
#enum_name::#variant_names(ref channel) => {
channel.supports_preview()
}
)*
}
}
}
};
trait_impl.into()
}

View File

@ -4,10 +4,8 @@ use anyhow::Result;
use tokio::sync::mpsc;
use tracing::{debug, trace};
use crate::channels::cable::prototypes::{
CableChannelPrototype, CableChannelPrototypes,
};
use crate::channels::{entry::Entry, OnAir};
use crate::channels::cable::prototypes::{Cable, ChannelPrototype};
use crate::channels::entry::Entry;
use crate::config::{default_tick_rate, Config};
use crate::keymap::Keymap;
use crate::render::UiState;
@ -138,11 +136,11 @@ const ACTION_BUF_SIZE: usize = 8;
impl App {
pub fn new(
channel_prototype: CableChannelPrototype,
channel_prototype: ChannelPrototype,
config: Config,
input: Option<String>,
options: AppOptions,
cable_channels: &CableChannelPrototypes,
cable_channels: &Cable,
) -> Self {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, render_rx) = mpsc::unbounded_channel();
@ -160,7 +158,7 @@ impl App {
options.no_remote,
options.no_help,
options.exact,
CableChannelPrototypes((*cable_channels).clone()),
cable_channels.clone(),
);
Self {

View File

@ -6,17 +6,15 @@ use anyhow::Result;
use tracing::{debug, error};
use crate::{
channels::cable::prototypes::{
CableChannelPrototype, CableChannelPrototypes,
},
channels::cable::prototypes::{Cable, ChannelPrototype},
config::get_config_dir,
};
/// Just a proxy struct to deserialize prototypes
#[derive(Debug, serde::Deserialize, Default)]
pub struct SerializedChannelPrototypes {
pub struct CableSpec {
#[serde(rename = "cable_channel")]
pub prototypes: Vec<CableChannelPrototype>,
pub prototypes: Vec<ChannelPrototype>,
}
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
@ -42,7 +40,7 @@ const DEFAULT_CABLE_CHANNELS: &str =
/// ├── my_channels.toml
/// └── windows_channels.toml
/// ```
pub fn load_cable_channels() -> Result<CableChannelPrototypes> {
pub fn load_cable() -> Result<Cable> {
let config_dir = get_config_dir();
// list all files in the config directory
@ -60,13 +58,13 @@ pub fn load_cable_channels() -> Result<CableChannelPrototypes> {
}
let default_prototypes =
toml::from_str::<SerializedChannelPrototypes>(DEFAULT_CABLE_CHANNELS)
toml::from_str::<CableSpec>(DEFAULT_CABLE_CHANNELS)
.expect("Failed to parse default cable channels");
let prototypes = file_paths.iter().fold(
Vec::<CableChannelPrototype>::new(),
Vec::<ChannelPrototype>::new(),
|mut acc, p| {
match toml::from_str::<SerializedChannelPrototypes>(
match toml::from_str::<CableSpec>(
&std::fs::read_to_string(p)
.expect("Unable to read configuration file"),
) {
@ -97,7 +95,7 @@ pub fn load_cable_channels() -> Result<CableChannelPrototypes> {
{
cable_channels.insert(prototype.name.clone(), prototype);
}
Ok(CableChannelPrototypes(cable_channels))
Ok(Cable(cable_channels))
}
fn is_cable_file_format<P>(p: P) -> bool

View File

@ -2,11 +2,11 @@ use std::collections::HashSet;
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use prototypes::{CableChannelPrototype, DEFAULT_DELIMITER};
use prototypes::{ChannelPrototype, DEFAULT_DELIMITER};
use rustc_hash::{FxBuildHasher, FxHashSet};
use tracing::debug;
use crate::channels::{entry::Entry, preview::PreviewCommand, OnAir};
use crate::channels::{entry::Entry, preview::PreviewCommand};
use crate::matcher::Matcher;
use crate::matcher::{config::Config, injector::Injector};
use crate::utils::command::shell_command;
@ -34,8 +34,8 @@ impl Default for Channel {
}
}
impl From<CableChannelPrototype> for Channel {
fn from(prototype: CableChannelPrototype) -> Self {
impl From<ChannelPrototype> for Channel {
fn from(prototype: ChannelPrototype) -> Self {
Self::new(
&prototype.name,
&prototype.source_command,
@ -77,6 +77,59 @@ impl Channel {
crawl_handle,
}
}
pub fn find(&mut self, pattern: &str) {
self.matcher.find(pattern);
}
pub fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
self.matcher.tick();
self.matcher
.results(num_entries, offset)
.into_iter()
.map(|item| {
let path = item.matched_string;
Entry::new(path).with_name_match_indices(&item.match_indices)
})
.collect()
}
pub fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| {
let path = item.matched_string;
Entry::new(path)
})
}
pub fn selected_entries(&self) -> &FxHashSet<Entry> {
&self.selected_entries
}
pub 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());
}
}
pub fn result_count(&self) -> u32 {
self.matcher.matched_item_count
}
pub fn total_count(&self) -> u32 {
self.matcher.total_item_count
}
pub fn running(&self) -> bool {
self.matcher.status.running || !self.crawl_handle.is_finished()
}
pub fn shutdown(&self) {}
pub fn supports_preview(&self) -> bool {
self.preview_command.is_some()
}
}
#[allow(clippy::unused_async)]
@ -123,58 +176,3 @@ async fn load_candidates(
}
let _ = child.wait();
}
impl OnAir for Channel {
fn find(&mut self, pattern: &str) {
self.matcher.find(pattern);
}
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
self.matcher.tick();
self.matcher
.results(num_entries, offset)
.into_iter()
.map(|item| {
let path = item.matched_string;
Entry::new(path).with_name_match_indices(&item.match_indices)
})
.collect()
}
fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| {
let path = item.matched_string;
Entry::new(path)
})
}
fn selected_entries(&self) -> &FxHashSet<Entry> {
&self.selected_entries
}
fn toggle_selection(&mut self, entry: &Entry) {
if self.selected_entries.contains(entry) {
self.selected_entries.remove(entry);
} else {
self.selected_entries.insert(entry.clone());
}
}
fn result_count(&self) -> u32 {
self.matcher.matched_item_count
}
fn total_count(&self) -> u32 {
self.matcher.total_item_count
}
fn running(&self) -> bool {
self.matcher.status.running || !self.crawl_handle.is_finished()
}
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
self.preview_command.is_some()
}
}

View File

@ -4,9 +4,7 @@ use std::{
ops::Deref,
};
use crate::{
cable::SerializedChannelPrototypes, channels::preview::PreviewCommand,
};
use crate::{cable::CableSpec, channels::preview::PreviewCommand};
/// A prototype for a cable channel.
///
@ -40,7 +38,7 @@ use crate::{
/// preview_command = "cat {}"
/// ```
#[derive(Clone, Debug, serde::Deserialize, PartialEq)]
pub struct CableChannelPrototype {
pub struct ChannelPrototype {
pub name: String,
pub source_command: String,
#[serde(default)]
@ -54,7 +52,7 @@ pub struct CableChannelPrototype {
const STDIN_CHANNEL_NAME: &str = "stdin";
const STDIN_SOURCE_COMMAND: &str = "cat";
impl CableChannelPrototype {
impl ChannelPrototype {
pub fn new(
name: &str,
source_command: &str,
@ -102,9 +100,9 @@ impl CableChannelPrototype {
const DEFAULT_PROTOTYPE_NAME: &str = "files";
pub const DEFAULT_DELIMITER: &str = " ";
impl Default for CableChannelPrototype {
impl Default for ChannelPrototype {
fn default() -> Self {
CableChannelPrototypes::default()
Cable::default()
.get(DEFAULT_PROTOTYPE_NAME)
.cloned()
.unwrap()
@ -118,7 +116,7 @@ fn default_delimiter() -> Option<String> {
Some(DEFAULT_DELIMITER.to_string())
}
impl Display for CableChannelPrototype {
impl Display for ChannelPrototype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
@ -129,13 +127,11 @@ impl Display for CableChannelPrototype {
/// This is used to store cable channel prototypes throughout the application
/// in a way that facilitates answering questions like "what's the prototype
/// for `files`?" or "does this channel exist?".
#[derive(Debug, serde::Deserialize)]
pub struct CableChannelPrototypes(
pub FxHashMap<String, CableChannelPrototype>,
);
#[derive(Debug, serde::Deserialize, Clone)]
pub struct Cable(pub FxHashMap<String, ChannelPrototype>);
impl Deref for CableChannelPrototypes {
type Target = FxHashMap<String, CableChannelPrototype>;
impl Deref for Cable {
type Target = FxHashMap<String, ChannelPrototype>;
fn deref(&self) -> &Self::Target {
&self.0
@ -153,18 +149,16 @@ const DEFAULT_CABLE_CHANNELS_FILE: &str =
const DEFAULT_CABLE_CHANNELS_FILE: &str =
include_str!("../../../cable/windows-channels.toml");
impl Default for CableChannelPrototypes {
impl Default for Cable {
/// Fallback to the default cable channels specification (the template file
/// included in the repo).
fn default() -> Self {
let s = toml::from_str::<SerializedChannelPrototypes>(
DEFAULT_CABLE_CHANNELS_FILE,
)
.expect("Unable to parse default cable channels");
let s = toml::from_str::<CableSpec>(DEFAULT_CABLE_CHANNELS_FILE)
.expect("Unable to parse default cable channels");
let mut prototypes = FxHashMap::default();
for prototype in s.prototypes {
prototypes.insert(prototype.name.clone(), prototype);
}
CableChannelPrototypes(prototypes)
Cable(prototypes)
}
}

View File

@ -1,144 +1,4 @@
use crate::channels::entry::Entry;
use anyhow::Result;
use cable::prototypes::CableChannelPrototype;
use rustc_hash::FxHashSet;
use television_derive::Broadcast;
pub mod cable;
pub mod entry;
pub mod preview;
pub mod remote_control;
/// The interface that all television channels must implement.
///
/// # Note
/// The `OnAir` trait requires the `Send` trait to be implemented as well.
/// This is necessary to allow the channels to be used with the tokio
/// runtime, which requires `Send` in order to be able to send tasks between
/// worker threads safely.
///
/// # Methods
/// - `find`: Find entries that match the given pattern. This method does not
/// return anything and instead typically stores the results internally for
/// later retrieval allowing to perform the search in the background while
/// incrementally polling the results.
/// ```ignore
/// fn find(&mut self, pattern: &str);
/// ```
/// - `results`: Get the results of the search (at a given point in time, see
/// above). This method returns a specific portion of entries that match the
/// search pattern. The `num_entries` parameter specifies the number of
/// entries to return and the `offset` parameter specifies the starting index
/// of the entries to return.
/// ```ignore
/// fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry>;
/// ```
/// - `get_result`: Get a specific result by its index.
/// ```ignore
/// fn get_result(&self, index: u32) -> Option<Entry>;
/// ```
/// - `result_count`: Get the number of results currently available.
/// ```ignore
/// fn result_count(&self) -> u32;
/// ```
/// - `total_count`: Get the total number of entries currently available (e.g.
/// the haystack).
/// ```ignore
/// fn total_count(&self) -> u32;
/// ```
///
pub trait OnAir: Send {
/// Find entries that match the given pattern.
///
/// This method does not return anything and instead typically stores the
/// results internally for later retrieval allowing to perform the search
/// in the background while incrementally polling the results with
/// `results`.
fn find(&mut self, pattern: &str);
/// Get the results of the search (that are currently available).
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry>;
/// Get a specific result by its index.
fn get_result(&self, index: u32) -> Option<Entry>;
/// Get the currently selected entries.
fn selected_entries(&self) -> &FxHashSet<Entry>;
/// Toggles selection for the entry under the cursor.
fn toggle_selection(&mut self, entry: &Entry);
/// Get the number of results currently available.
fn result_count(&self) -> u32;
/// Get the total number of entries currently available.
fn total_count(&self) -> u32;
/// Check if the channel is currently running.
fn running(&self) -> bool;
/// Turn off
fn shutdown(&self);
/// Whether this channel supports previewing entries.
fn supports_preview(&self) -> bool;
}
/// The available television channels.
///
/// Each channel is represented by a variant of the enum and should implement
/// the `OnAir` trait.
///
/// # Important
/// When adding a new channel, make sure to add a new variant to this enum and
/// implement the `OnAir` trait for it.
///
/// # Derive
/// ## `CliChannel`
/// The `CliChannel` derive macro generates the necessary glue code to
/// automatically create the corresponding `CliTvChannel` enum with unit
/// variants that can be used to select the channel from the command line.
/// It also generates the necessary glue code to automatically create a channel
/// instance from the selected CLI enum variant.
///
/// ## `Broadcast`
/// The `Broadcast` derive macro generates the necessary glue code to
/// automatically forward method calls to the corresponding channel variant.
/// This allows to use the `OnAir` trait methods directly on the `TelevisionChannel`
/// enum. In a more straightforward way, it implements the `OnAir` trait for the
/// `TelevisionChannel` enum.
///
/// ## `UnitChannel`
/// This macro generates an enum with unit variants that can be used instead
/// 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(Broadcast)]
pub enum TelevisionChannel {
/// The remote control channel.
///
/// This channel allows to switch between different channels.
RemoteControl(remote_control::RemoteControl),
/// A custom channel.
///
/// This channel allows to search through custom data.
Cable(cable::Channel),
}
impl TelevisionChannel {
pub fn zap(&self, channel_name: &str) -> Result<CableChannelPrototype> {
match self {
TelevisionChannel::RemoteControl(remote_control) => {
remote_control.zap(channel_name)
}
TelevisionChannel::Cable(_) => unreachable!(),
}
}
pub fn name(&self) -> String {
match self {
TelevisionChannel::Cable(channel) => channel.name.clone(),
TelevisionChannel::RemoteControl(_) => String::from("Remote"),
}
}
}

View File

@ -1,7 +1,7 @@
use std::fmt::Display;
use super::{cable::prototypes::DEFAULT_DELIMITER, entry::Entry};
use crate::channels::cable::prototypes::CableChannelPrototype;
use crate::channels::cable::prototypes::ChannelPrototype;
use lazy_regex::{regex, Lazy, Regex};
use tracing::debug;
@ -65,8 +65,8 @@ impl PreviewCommand {
}
}
impl From<&CableChannelPrototype> for Option<PreviewCommand> {
fn from(value: &CableChannelPrototype) -> Self {
impl From<&ChannelPrototype> for Option<PreviewCommand> {
fn from(value: &ChannelPrototype) -> Self {
if let Some(command) = value.preview_command.as_ref() {
let delimiter = value
.preview_delimiter

View File

@ -1,32 +1,23 @@
use std::collections::HashSet;
use crate::channels::cable::prototypes::CableChannelPrototypes;
use crate::channels::cable::prototypes::Cable;
use crate::channels::entry::Entry;
use crate::channels::OnAir;
use crate::matcher::{config::Config, Matcher};
use anyhow::Result;
use devicons::FileIcon;
use rustc_hash::{FxBuildHasher, FxHashSet};
use super::cable::prototypes::CableChannelPrototype;
use super::cable::prototypes::ChannelPrototype;
pub struct RemoteControl {
matcher: Matcher<String>,
cable_channels: Option<CableChannelPrototypes>,
selected_entries: FxHashSet<Entry>,
cable_channels: Option<Cable>,
}
const NUM_THREADS: usize = 1;
impl RemoteControl {
pub fn new(cable_channels: Option<CableChannelPrototypes>) -> Self {
pub fn new(cable_channels: Option<Cable>) -> Self {
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
let injector = matcher.injector();
for c in cable_channels
.as_ref()
.unwrap_or(&CableChannelPrototypes::default())
.keys()
{
for c in cable_channels.as_ref().unwrap_or(&Cable::default()).keys() {
let () = injector.push(c.clone(), |e, cols| {
cols[0] = e.to_string().into();
});
@ -34,11 +25,10 @@ impl RemoteControl {
RemoteControl {
matcher,
cable_channels,
selected_entries: HashSet::with_hasher(FxBuildHasher),
}
}
pub fn zap(&self, channel_name: &str) -> Result<CableChannelPrototype> {
pub fn zap(&self, channel_name: &str) -> Result<ChannelPrototype> {
match self
.cable_channels
.as_ref()
@ -69,12 +59,12 @@ const CABLE_ICON: FileIcon = FileIcon {
color: "#000000",
};
impl OnAir for RemoteControl {
fn find(&mut self, pattern: &str) {
impl RemoteControl {
pub fn find(&mut self, pattern: &str) {
self.matcher.find(pattern);
}
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
pub fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
self.matcher.tick();
self.matcher
.results(num_entries, offset)
@ -88,35 +78,28 @@ impl OnAir for RemoteControl {
.collect()
}
fn get_result(&self, index: u32) -> Option<Entry> {
pub fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| {
let path = item.matched_string;
Entry::new(path).with_icon(TV_ICON)
})
}
fn selected_entries(&self) -> &FxHashSet<Entry> {
&self.selected_entries
}
#[allow(unused_variables)]
fn toggle_selection(&mut self, entry: &Entry) {}
fn result_count(&self) -> u32 {
pub fn result_count(&self) -> u32 {
self.matcher.matched_item_count
}
fn total_count(&self) -> u32 {
pub fn total_count(&self) -> u32 {
self.matcher.total_item_count
}
fn running(&self) -> bool {
pub fn running(&self) -> bool {
self.matcher.status.running
}
fn shutdown(&self) {}
pub fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
pub fn supports_preview(&self) -> bool {
false
}
}

View File

@ -4,9 +4,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use tracing::debug;
use crate::channels::cable::prototypes::{
CableChannelPrototype, CableChannelPrototypes,
};
use crate::channels::cable::prototypes::{Cable, ChannelPrototype};
use crate::channels::preview::PreviewCommand;
use crate::cli::args::{Cli, Command};
use crate::config::{KeyBindings, DEFAULT_CHANNEL};
@ -20,7 +18,7 @@ pub mod args;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct PostProcessedCli {
pub channel: CableChannelPrototype,
pub channel: ChannelPrototype,
pub preview_command: Option<PreviewCommand>,
pub no_preview: bool,
pub tick_rate: Option<f64>,
@ -41,7 +39,7 @@ pub struct PostProcessedCli {
impl Default for PostProcessedCli {
fn default() -> Self {
Self {
channel: CableChannelPrototype::default(),
channel: ChannelPrototype::default(),
preview_command: None,
no_preview: false,
tick_rate: None,
@ -80,10 +78,10 @@ impl From<Cli> for PostProcessedCli {
offset_expr: None,
});
let mut channel: CableChannelPrototype;
let mut channel: ChannelPrototype;
let working_directory: Option<String>;
let cable_channels = cable::load_cable_channels().unwrap_or_default();
let cable_channels = cable::load_cable().unwrap_or_default();
if cli.channel.is_none() {
channel = cable_channels
.get(DEFAULT_CHANNEL)
@ -178,8 +176,8 @@ fn parse_keybindings_literal(
pub fn parse_channel(
channel: &str,
cable_channels: &CableChannelPrototypes,
) -> Result<CableChannelPrototype> {
cable_channels: &Cable,
) -> Result<ChannelPrototype> {
// try to parse the channel as a cable channel
match cable_channels
.iter()
@ -191,7 +189,7 @@ pub fn parse_channel(
}
pub fn list_channels() {
for c in cable::load_cable_channels().unwrap_or_default().keys() {
for c in cable::load_cable().unwrap_or_default().keys() {
println!("\t{c}");
}
}
@ -223,8 +221,8 @@ pub fn guess_channel_from_prompt(
prompt: &str,
command_mapping: &FxHashMap<String, String>,
fallback_channel: &str,
cable_channels: &CableChannelPrototypes,
) -> Result<CableChannelPrototype> {
cable_channels: &Cable,
) -> Result<ChannelPrototype> {
debug!("Guessing channel from prompt: {}", prompt);
// git checkout -qf
// --- -------- --- <---------
@ -317,7 +315,7 @@ mod tests {
let post_processed_cli: PostProcessedCli = cli.into();
let expected = CableChannelPrototype {
let expected = ChannelPrototype {
preview_delimiter: Some(":".to_string()),
..Default::default()
};
@ -350,10 +348,7 @@ mod tests {
let post_processed_cli: PostProcessedCli = cli.into();
assert_eq!(
post_processed_cli.channel,
CableChannelPrototype::default(),
);
assert_eq!(post_processed_cli.channel, ChannelPrototype::default(),);
assert_eq!(
post_processed_cli.working_directory,
Some(".".to_string())
@ -388,7 +383,7 @@ mod tests {
/// Returns a tuple containing a command mapping and a fallback channel.
fn guess_channel_from_prompt_setup<'a>(
) -> (FxHashMap<String, String>, &'a str, CableChannelPrototypes) {
) -> (FxHashMap<String, String>, &'a str, Cable) {
let mut command_mapping = FxHashMap::default();
command_mapping.insert("vim".to_string(), "files".to_string());
command_mapping.insert("export".to_string(), "env".to_string());
@ -396,7 +391,7 @@ mod tests {
(
command_mapping,
"env",
cable::load_cable_channels().unwrap_or_default(),
cable::load_cable().unwrap_or_default(),
)
}

View File

@ -6,9 +6,7 @@ use std::process::exit;
use anyhow::Result;
use clap::Parser;
use television::cable;
use television::channels::cable::prototypes::{
CableChannelPrototype, CableChannelPrototypes,
};
use television::channels::cable::prototypes::{Cable, ChannelPrototype};
use television::utils::clipboard::CLIPBOARD;
use tracing::{debug, error, info};
@ -43,7 +41,7 @@ async fn main() -> Result<()> {
let mut config = Config::new(&ConfigEnv::init()?)?;
debug!("Loading cable channels...");
let cable_channels = cable::load_cable_channels().unwrap_or_default();
let cable_channels = cable::load_cable().unwrap_or_default();
// optionally handle subcommands
debug!("Handling subcommands...");
@ -154,11 +152,11 @@ pub fn determine_channel(
args: PostProcessedCli,
config: &Config,
readable_stdin: bool,
cable_channels: &CableChannelPrototypes,
) -> Result<CableChannelPrototype> {
cable_channels: &Cable,
) -> Result<ChannelPrototype> {
if readable_stdin {
debug!("Using stdin channel");
Ok(CableChannelPrototype::stdin(args.preview_command))
Ok(ChannelPrototype::stdin(args.preview_command))
} else if let Some(prompt) = args.autocomplete_prompt {
debug!("Using autocomplete prompt: {:?}", prompt);
let channel_prototype = guess_channel_from_prompt(
@ -179,8 +177,7 @@ pub fn determine_channel(
mod tests {
use rustc_hash::FxHashMap;
use television::{
cable::load_cable_channels,
channels::cable::prototypes::CableChannelPrototype,
cable::load_cable, channels::cable::prototypes::ChannelPrototype,
};
use super::*;
@ -189,11 +186,11 @@ mod tests {
args: &PostProcessedCli,
config: &Config,
readable_stdin: bool,
expected_channel: &CableChannelPrototype,
cable_channels: Option<CableChannelPrototypes>,
expected_channel: &ChannelPrototype,
cable_channels: Option<Cable>,
) {
let channels: CableChannelPrototypes = cable_channels
.unwrap_or_else(|| load_cable_channels().unwrap_or_default());
let channels: Cable =
cable_channels.unwrap_or_else(|| load_cable().unwrap_or_default());
let channel =
determine_channel(args.clone(), config, readable_stdin, &channels)
.unwrap();
@ -208,7 +205,7 @@ mod tests {
#[tokio::test]
/// Test that the channel is stdin when stdin is readable
async fn test_determine_channel_readable_stdin() {
let channel = CableChannelPrototype::default();
let channel = ChannelPrototype::default();
let args = PostProcessedCli {
channel,
..Default::default()
@ -218,9 +215,7 @@ mod tests {
&args,
&config,
true,
&CableChannelPrototype::new(
"stdin", "cat", false, None, None, None,
),
&ChannelPrototype::new("stdin", "cat", false, None, None, None),
None,
);
}
@ -228,9 +223,8 @@ mod tests {
#[tokio::test]
async fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string());
let expected_channel = CableChannelPrototype::new(
"dirs", "ls {}", false, None, None, None,
);
let expected_channel =
ChannelPrototype::new("dirs", "ls {}", false, None, None, None);
let args = PostProcessedCli {
autocomplete_prompt,
..Default::default()
@ -263,7 +257,7 @@ mod tests {
#[tokio::test]
async fn test_determine_channel_standard_case() {
let channel =
CableChannelPrototype::new("dirs", "", false, None, None, None);
ChannelPrototype::new("dirs", "", false, None, None, None);
let args = PostProcessedCli {
channel,
..Default::default()
@ -273,7 +267,7 @@ mod tests {
&args,
&config,
false,
&CableChannelPrototype::new("dirs", "", false, None, None, None),
&ChannelPrototype::new("dirs", "", false, None, None, None),
None,
);
}

View File

@ -1,4 +1,4 @@
use crate::channels::cable::prototypes::CableChannelPrototype;
use crate::channels::cable::prototypes::ChannelPrototype;
use crate::preview::{Preview, PreviewContent};
use crate::utils::command::shell_command;
use crate::{
@ -130,8 +130,8 @@ pub fn try_preview(
in_flight_previews.lock().remove(&entry.name);
}
impl From<&CableChannelPrototype> for Option<Previewer> {
fn from(value: &CableChannelPrototype) -> Self {
impl From<&ChannelPrototype> for Option<Previewer> {
fn from(value: &ChannelPrototype) -> Self {
Option::<PreviewCommand>::from(value).map(Previewer::new)
}
}

View File

@ -1,14 +1,12 @@
use crate::{
action::Action,
cable::load_cable_channels,
channels::{
cable::{
prototypes::{CableChannelPrototype, CableChannelPrototypes},
prototypes::{Cable, ChannelPrototype},
Channel as CableChannel,
},
entry::Entry,
remote_control::RemoteControl,
OnAir, TelevisionChannel,
},
config::{Config, Theme},
draw::{ChannelState, Ctx, TvState},
@ -48,7 +46,7 @@ pub struct Television {
action_tx: UnboundedSender<Action>,
pub config: Config,
pub channel: CableChannel,
pub remote_control: Option<TelevisionChannel>,
pub remote_control: Option<RemoteControl>,
pub mode: Mode,
pub current_pattern: String,
pub matching_mode: MatchingMode,
@ -70,13 +68,13 @@ impl Television {
#[must_use]
pub fn new(
action_tx: UnboundedSender<Action>,
channel_prototype: CableChannelPrototype,
channel_prototype: ChannelPrototype,
mut config: Config,
input: Option<String>,
no_remote: bool,
no_help: bool,
exact: bool,
cable_channels: CableChannelPrototypes,
cable_channels: Cable,
) -> Self {
let mut results_picker = Picker::new(input.clone());
if config.ui.input_bar_position == InputPosition::Bottom {
@ -108,9 +106,7 @@ impl Television {
let remote_control = if no_remote {
None
} else {
Some(TelevisionChannel::RemoteControl(RemoteControl::new(Some(
cable_channels,
))))
Some(RemoteControl::new(Some(cable_channels)))
};
if no_help {
@ -150,13 +146,6 @@ impl Television {
self.ui_state = ui_state;
}
pub fn init_remote_control(&mut self) {
let cable_channels = load_cable_channels().unwrap_or_default();
self.remote_control = Some(TelevisionChannel::RemoteControl(
RemoteControl::new(Some(cable_channels)),
));
}
pub fn dump_context(&self) -> Ctx {
let channel_state = ChannelState::new(
self.channel.name.clone(),
@ -189,10 +178,7 @@ impl Television {
self.channel.name.clone()
}
pub fn change_channel(
&mut self,
channel_prototype: CableChannelPrototype,
) {
pub fn change_channel(&mut self, channel_prototype: ChannelPrototype) {
self.preview_state.reset();
self.preview_state.enabled =
channel_prototype.preview_command.is_some();
@ -213,7 +199,9 @@ impl Television {
);
}
Mode::RemoteControl => {
self.remote_control.as_mut().unwrap().find(pattern);
if let Some(rc) = self.remote_control.as_mut() {
rc.find(pattern);
}
}
}
}
@ -244,11 +232,9 @@ impl Television {
}
Mode::RemoteControl => {
if let Some(i) = self.rc_picker.selected() {
return self
.remote_control
.as_ref()
.unwrap()
.get_result(i.try_into().unwrap());
if let Some(rc) = &self.remote_control {
return rc.get_result(i.try_into().unwrap());
}
}
None
}
@ -491,12 +477,10 @@ impl Television {
match self.mode {
Mode::Channel => {
self.mode = Mode::RemoteControl;
self.init_remote_control();
}
Mode::RemoteControl => {
// this resets the RC picker
self.reset_picker_input();
self.init_remote_control();
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.reset_picker_selection();
self.mode = Mode::Channel;
@ -528,7 +512,6 @@ impl Television {
.remote_control
.as_ref()
.unwrap()
// FIXME: is the TelevisionChannel enum still worth it?
.zap(entry.name.as_str())?;
// this resets the RC picker
self.reset_picker_selection();

View File

@ -3,9 +3,7 @@ use std::{collections::HashSet, path::PathBuf, time::Duration};
use television::{
action::Action,
app::{App, AppOptions},
channels::cable::prototypes::{
CableChannelPrototype, CableChannelPrototypes,
},
channels::cable::prototypes::{Cable, ChannelPrototype},
config::default_config_from_file,
};
use tokio::{task::JoinHandle, time::timeout};
@ -23,19 +21,19 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
/// The app is started in a separate task and can be interacted with by sending
/// actions to the action channel.
fn setup_app(
channel_prototype: Option<CableChannelPrototype>,
channel_prototype: Option<ChannelPrototype>,
select_1: bool,
exact: bool,
) -> (
JoinHandle<television::app::AppOutput>,
tokio::sync::mpsc::UnboundedSender<Action>,
) {
let chan: CableChannelPrototype = channel_prototype.unwrap_or_else(|| {
let chan: ChannelPrototype = channel_prototype.unwrap_or_else(|| {
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("target_dir");
std::env::set_current_dir(&target_dir).unwrap();
CableChannelPrototype::default()
ChannelPrototype::default()
});
let mut config = default_config_from_file().unwrap();
// this speeds up the tests
@ -49,13 +47,7 @@ fn setup_app(
false,
config.application.tick_rate,
);
let mut app = App::new(
chan,
config,
input,
options,
&CableChannelPrototypes::default(),
);
let mut app = App::new(chan, config, input, options, &Cable::default());
// retrieve the app's action channel handle in order to send a quit action
let tx = app.action_tx.clone();
@ -220,8 +212,8 @@ async fn test_app_exact_search_positive() {
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_exits_when_select_1_and_only_one_result() {
let prototype = CableChannelPrototype::new(
"cable",
let prototype = ChannelPrototype::new(
"some_channel",
"echo file1.txt",
false,
None,
@ -258,8 +250,8 @@ async fn test_app_exits_when_select_1_and_only_one_result() {
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
let prototype = CableChannelPrototype::new(
"cable",
let prototype = ChannelPrototype::new(
"some_channel",
"echo 'file1.txt\nfile2.txt'",
false,
None,