refactor(preview): simplify channel previews code and remove intermediate PreviewKind struct (#490)

This commit is contained in:
Alexandre Pasmantier 2025-05-01 17:42:01 +02:00 committed by GitHub
parent dbff3a330b
commit b9f42e8c29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 151 additions and 162 deletions

View File

@ -8,16 +8,18 @@ use ratatui::prelude::{Line, Style};
use ratatui::style::Color;
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
use ratatui::Terminal;
use television::action::Action;
use television::channels::cable::prototypes::CableChannelPrototype;
use television::channels::entry::into_ranges;
use television::channels::entry::{Entry, PreviewType};
use television::channels::OnAir;
use television::channels::TelevisionChannel;
use television::config::{Config, ConfigEnv};
use television::screen::colors::ResultsColorscheme;
use television::screen::results::build_results_list;
use television::television::Television;
use television::{
action::Action,
channels::{
cable::prototypes::CableChannelPrototype,
entry::{into_ranges, Entry},
preview::PreviewType,
OnAir, TelevisionChannel,
},
config::{Config, ConfigEnv},
screen::{colors::ResultsColorscheme, results::build_results_list},
television::Television,
};
use tokio::runtime::Runtime;
pub fn draw_results_list(c: &mut Criterion) {

View File

@ -4,8 +4,9 @@ use anyhow::Result;
use tokio::sync::mpsc;
use tracing::{debug, trace};
use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{OnAir, TelevisionChannel};
use crate::channels::{
entry::Entry, preview::PreviewType, OnAir, TelevisionChannel,
};
use crate::config::{default_tick_rate, Config};
use crate::keymap::Keymap;
use crate::render::UiState;

View File

@ -2,18 +2,20 @@ use std::collections::HashSet;
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use preview::{parse_preview_kind, PreviewKind};
use prototypes::{CableChannelPrototype, DEFAULT_DELIMITER};
use rustc_hash::{FxBuildHasher, FxHashSet};
use tracing::debug;
use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
use crate::channels::OnAir;
use crate::channels::preview::parse_preview_type;
use crate::channels::{
entry::Entry,
preview::{PreviewCommand, PreviewType},
OnAir,
};
use crate::matcher::Matcher;
use crate::matcher::{config::Config, injector::Injector};
use crate::utils::command::shell_command;
pub mod preview;
pub mod prototypes;
#[allow(dead_code)]
@ -21,7 +23,7 @@ pub struct Channel {
pub name: String,
matcher: Matcher<String>,
entries_command: String,
preview_kind: PreviewKind,
preview_type: PreviewType,
selected_entries: FxHashSet<Entry>,
crawl_handle: tokio::task::JoinHandle<()>,
}
@ -72,17 +74,17 @@ impl Channel {
));
let preview_kind = match preview_command {
Some(command) => {
parse_preview_kind(&command).unwrap_or_else(|_| {
parse_preview_type(&command).unwrap_or_else(|_| {
panic!("Invalid preview command: {command}")
})
}
None => PreviewKind::None,
None => PreviewType::None,
};
debug!("Preview kind: {:?}", preview_kind);
Self {
matcher,
entries_command: entries_command.to_string(),
preview_kind,
preview_type: preview_kind,
name: name.to_string(),
selected_entries: HashSet::with_hasher(FxBuildHasher),
crawl_handle,
@ -147,19 +149,8 @@ impl OnAir for Channel {
.into_iter()
.map(|item| {
let path = item.matched_string;
Entry::new(
path,
match &self.preview_kind {
PreviewKind::Command(ref preview_command) => {
PreviewType::Command(preview_command.clone())
}
PreviewKind::Builtin(preview_type) => {
preview_type.clone()
}
PreviewKind::None => PreviewType::None,
},
)
.with_name_match_indices(&item.match_indices)
Entry::new(path, self.preview_type.clone())
.with_name_match_indices(&item.match_indices)
})
.collect()
}
@ -167,16 +158,7 @@ impl OnAir for Channel {
fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| {
let path = item.matched_string;
Entry::new(
path,
match &self.preview_kind {
PreviewKind::Command(ref preview_command) => {
PreviewType::Command(preview_command.clone())
}
PreviewKind::Builtin(preview_type) => preview_type.clone(),
PreviewKind::None => PreviewType::None,
},
)
Entry::new(path, self.preview_type.clone())
})
}
@ -207,6 +189,6 @@ impl OnAir for Channel {
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
self.preview_kind != PreviewKind::None
self.preview_type != PreviewType::None
}
}

View File

@ -1,34 +0,0 @@
use anyhow::Result;
use regex::Regex;
use tracing::debug;
use crate::channels::entry::{PreviewCommand, PreviewType};
#[derive(Debug, Clone, PartialEq)]
pub enum PreviewKind {
Command(PreviewCommand),
Builtin(PreviewType),
None,
}
/// Parses the preview command to determine if it is a built-in (i.e. ":files:") or custom command.
///
/// # Example:
/// ```
/// use television::channels::entry::{PreviewCommand, PreviewType};
/// use television::channels::cable::preview::{parse_preview_kind, PreviewKind};
///
/// let command = PreviewCommand::new("cat {0}", ":");
/// let preview_kind = parse_preview_kind(&command).unwrap();
/// assert_eq!(preview_kind, PreviewKind::Command(command));
/// ```
pub fn parse_preview_kind(command: &PreviewCommand) -> Result<PreviewKind> {
debug!("Parsing preview kind for command: {:?}", command);
let re = Regex::new(r"^\:(\w+)\:$").unwrap();
if let Some(captures) = re.captures(&command.command) {
let preview_type = PreviewType::try_from(&captures[1])?;
Ok(PreviewKind::Builtin(preview_type))
} else {
Ok(PreviewKind::Command(command.clone()))
}
}

View File

@ -25,6 +25,9 @@ use crate::cable::SerializedChannelPrototypes;
/// - `preview_delimiter`: The delimiter to use to split an entry into
/// multiple parts that can then be referenced in the preview command (e.g.
/// `{1} + {2}`).
/// - `preview_offset`: a litteral expression that will be interpreted later on
/// in order to determine the vertical offset at which the preview should be
/// displayed.
///
/// # Example
/// The default files channel might look something like this:
@ -43,6 +46,7 @@ pub struct CableChannelPrototype {
pub preview_command: Option<String>,
#[serde(default = "default_delimiter")]
pub preview_delimiter: Option<String>,
pub preview_offset: Option<String>,
}
impl CableChannelPrototype {
@ -52,6 +56,7 @@ impl CableChannelPrototype {
interactive: bool,
preview_command: Option<String>,
preview_delimiter: Option<String>,
preview_offset: Option<String>,
) -> Self {
Self {
name: name.to_string(),
@ -59,6 +64,7 @@ impl CableChannelPrototype {
interactive,
preview_command,
preview_delimiter,
preview_offset,
}
}
}
@ -76,10 +82,13 @@ impl Default for CableChannelPrototype {
interactive: false,
preview_command: Some(DEFAULT_PREVIEW_COMMAND.to_string()),
preview_delimiter: Some(DEFAULT_DELIMITER.to_string()),
preview_offset: None,
}
}
}
/// The default delimiter to use for the preview command to use to split
/// entries into multiple referenceable parts.
#[allow(clippy::unnecessary_wraps)]
fn default_delimiter() -> Option<String> {
Some(DEFAULT_DELIMITER.to_string())
@ -107,9 +116,13 @@ impl Deref for CableChannels {
}
}
/// A default cable channels specification that is compiled into the
/// application.
#[cfg(unix)]
const DEFAULT_CABLE_CHANNELS_FILE: &str =
include_str!("../../../cable/unix-channels.toml");
/// A default cable channels specification that is compiled into the
/// application.
#[cfg(not(unix))]
const DEFAULT_CABLE_CHANNELS_FILE: &str =
include_str!("../../cable/windows-channels.toml");

View File

@ -1,10 +1,8 @@
use std::{
fmt::Display,
hash::{Hash, Hasher},
};
use std::hash::{Hash, Hasher};
use devicons::FileIcon;
use strum::EnumString;
use crate::channels::preview::PreviewType;
// NOTE: having an enum for entry types would be nice since it would allow
// having a nicer implementation for transitions between channels. This would
@ -87,7 +85,7 @@ impl Entry {
///
/// Additional fields can be set using the builder pattern.
/// ```
/// use television::channels::entry::{Entry, PreviewType};
/// use television::channels::{entry::Entry, preview::PreviewType};
/// use devicons::FileIcon;
///
/// let entry = Entry::new("name".to_string(), PreviewType::EnvVar)
@ -161,39 +159,6 @@ pub const ENTRY_PLACEHOLDER: Entry = Entry {
preview_type: PreviewType::EnvVar,
};
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct PreviewCommand {
pub command: String,
pub delimiter: String,
}
impl PreviewCommand {
pub fn new(command: &str, delimiter: &str) -> Self {
Self {
command: command.to_string(),
delimiter: delimiter.to_string(),
}
}
}
impl Display for PreviewCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum PreviewType {
Basic,
EnvVar,
Files,
#[strum(disabled)]
Command(PreviewCommand),
#[default]
None,
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -5,6 +5,7 @@ use television_derive::Broadcast;
pub mod cable;
pub mod entry;
pub mod preview;
pub mod remote_control;
pub mod stdin;

View File

@ -0,0 +1,63 @@
use std::fmt::Display;
use anyhow::Result;
use regex::Regex;
use strum::EnumString;
use tracing::debug;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct PreviewCommand {
pub command: String,
pub delimiter: String,
}
impl PreviewCommand {
pub fn new(command: &str, delimiter: &str) -> Self {
Self {
command: command.to_string(),
delimiter: delimiter.to_string(),
}
}
}
impl Display for PreviewCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum PreviewType {
Basic,
EnvVar,
Files,
#[strum(disabled)]
Command(PreviewCommand),
#[default]
None,
}
/// Parses the preview command to determine the preview type.
///
/// This checks if the command matches the builtin pattern `:{preview_type}:`
/// and then falls back to the command type if it doesn't.
///
/// # Example:
/// ```
/// use television::channels::preview::{parse_preview_type, PreviewCommand, PreviewType};
///
/// let command = PreviewCommand::new("cat {0}", ":");
/// let preview_type = parse_preview_type(&command).unwrap();
/// assert_eq!(preview_type, PreviewType::Command(command));
/// ```
pub fn parse_preview_type(command: &PreviewCommand) -> Result<PreviewType> {
debug!("Parsing preview kind for command: {:?}", command);
let re = Regex::new(r"^\:(\w+)\:$").unwrap();
if let Some(captures) = re.captures(&command.command) {
let preview_type = PreviewType::try_from(&captures[1])?;
Ok(preview_type)
} else {
Ok(PreviewType::Command(command.clone()))
}
}

View File

@ -1,7 +1,7 @@
use std::collections::HashSet;
use crate::channels::cable::prototypes::CableChannels;
use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{entry::Entry, preview::PreviewType};
use crate::channels::{OnAir, TelevisionChannel};
use crate::matcher::{config::Config, Matcher};
use anyhow::Result;

View File

@ -8,7 +8,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use tracing::debug;
use super::OnAir;
use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{entry::Entry, preview::PreviewType};
use crate::matcher::{config::Config, injector::Injector, Matcher};
pub struct Channel {

View File

@ -4,12 +4,11 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use tracing::debug;
use crate::channels::cable::{
preview::parse_preview_kind, preview::PreviewKind,
prototypes::CableChannels,
use crate::channels::cable::prototypes::{
CableChannelPrototype, CableChannels,
};
use crate::channels::{
cable::prototypes::CableChannelPrototype, entry::PreviewCommand,
use crate::channels::preview::{
parse_preview_type, PreviewCommand, PreviewType,
};
use crate::cli::args::{Cli, Command};
use crate::config::{KeyBindings, DEFAULT_CHANNEL};
@ -24,7 +23,7 @@ pub mod args;
#[derive(Debug, Clone)]
pub struct PostProcessedCli {
pub channel: CableChannelPrototype,
pub preview_kind: PreviewKind,
pub preview_kind: PreviewType,
pub no_preview: bool,
pub tick_rate: Option<f64>,
pub frame_rate: Option<f64>,
@ -44,7 +43,7 @@ impl Default for PostProcessedCli {
fn default() -> Self {
Self {
channel: CableChannelPrototype::default(),
preview_kind: PreviewKind::None,
preview_kind: PreviewType::None,
no_preview: false,
tick_rate: None,
frame_rate: None,
@ -80,8 +79,8 @@ impl From<Cli> for PostProcessedCli {
command: preview,
delimiter: cli.delimiter.clone(),
})
.map_or(PreviewKind::None, |preview_command| {
parse_preview_kind(&preview_command)
.map_or(PreviewType::None, |preview_command| {
parse_preview_type(&preview_command)
.map_err(|e| {
cli_parsing_error_exit(&e.to_string());
})
@ -302,7 +301,7 @@ Data directory: {data_dir_path}"
#[cfg(test)]
mod tests {
use crate::{
action::Action, channels::entry::PreviewType, config::Binding,
action::Action, channels::preview::PreviewType, config::Binding,
event::Key,
};
@ -327,7 +326,7 @@ mod tests {
);
assert_eq!(
post_processed_cli.preview_kind,
PreviewKind::Command(PreviewCommand {
PreviewType::Command(PreviewCommand {
command: "bat -n --color=always {}".to_string(),
delimiter: ":".to_string()
})
@ -373,10 +372,7 @@ mod tests {
let post_processed_cli: PostProcessedCli = cli.into();
assert_eq!(
post_processed_cli.preview_kind,
PreviewKind::Builtin(PreviewType::Files)
);
assert_eq!(post_processed_cli.preview_kind, PreviewType::Files);
}
#[test]
@ -390,10 +386,7 @@ mod tests {
let post_processed_cli: PostProcessedCli = cli.into();
assert_eq!(
post_processed_cli.preview_kind,
PreviewKind::Builtin(PreviewType::EnvVar)
);
assert_eq!(post_processed_cli.preview_kind, PreviewType::EnvVar);
}
#[test]

View File

@ -7,15 +7,13 @@ use anyhow::Result;
use clap::Parser;
use crossterm::terminal::enable_raw_mode;
use television::cable;
use television::channels::cable::{
preview::PreviewKind, prototypes::CableChannels,
};
use television::channels::cable::prototypes::CableChannels;
use television::utils::clipboard::CLIPBOARD;
use tracing::{debug, error, info};
use television::app::{App, AppOptions};
use television::channels::{
entry::PreviewType, stdin::Channel as StdinChannel, TelevisionChannel,
stdin::Channel as StdinChannel, TelevisionChannel,
};
use television::cli::{
args::{Cli, Command},
@ -163,13 +161,7 @@ pub fn determine_channel(
if readable_stdin {
debug!("Using stdin channel");
Ok(TelevisionChannel::Stdin(StdinChannel::new(
match &args.preview_kind {
PreviewKind::Command(ref preview_command) => {
PreviewType::Command(preview_command.clone())
}
PreviewKind::Builtin(preview_type) => preview_type.clone(),
PreviewKind::None => PreviewType::None,
},
args.preview_kind,
)))
} else if let Some(prompt) = args.autocomplete_prompt {
debug!("Using autocomplete prompt: {:?}", prompt);
@ -192,7 +184,9 @@ mod tests {
use rustc_hash::FxHashMap;
use television::{
cable::load_cable_channels,
channels::cable::prototypes::CableChannelPrototype,
channels::{
cable::prototypes::CableChannelPrototype, preview::PreviewType,
},
};
use super::*;
@ -241,8 +235,10 @@ mod tests {
async fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string());
let expected_channel = TelevisionChannel::Cable(
CableChannelPrototype::new("dirs", "ls {}", false, None, None)
.into(),
CableChannelPrototype::new(
"dirs", "ls {}", false, None, None, None,
)
.into(),
);
let args = PostProcessedCli {
autocomplete_prompt,
@ -276,7 +272,7 @@ mod tests {
#[tokio::test]
async fn test_determine_channel_standard_case() {
let channel =
CableChannelPrototype::new("dirs", "", false, None, None);
CableChannelPrototype::new("dirs", "", false, None, None, None);
let args = PostProcessedCli {
channel,
..Default::default()
@ -287,8 +283,10 @@ mod tests {
&config,
false,
&TelevisionChannel::Cable(
CableChannelPrototype::new("dirs", "", false, None, None)
.into(),
CableChannelPrototype::new(
"dirs", "", false, None, None, None,
)
.into(),
),
None,
);

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{entry::Entry, preview::PreviewType};
use devicons::FileIcon;
use ratatui::layout::Rect;

View File

@ -1,7 +1,9 @@
use crate::channels::entry::{Entry, PreviewCommand};
use crate::preview::cache::PreviewCache;
use crate::preview::{Preview, PreviewContent};
use crate::utils::command::shell_command;
use crate::{
channels::{entry::Entry, preview::PreviewCommand},
preview::cache::PreviewCache,
};
use parking_lot::Mutex;
use regex::Regex;
use rustc_hash::FxHashSet;
@ -127,7 +129,7 @@ impl CommandPreviewer {
///
/// # Example
/// ```
/// use television::channels::entry::{PreviewCommand, PreviewType, Entry};
/// use television::channels::{preview::{PreviewCommand, PreviewType}, entry::Entry};
/// use television::preview::previewers::command::format_command;
///
/// let command = PreviewCommand {
@ -209,7 +211,7 @@ pub fn try_preview(
#[cfg(test)]
mod tests {
use super::*;
use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{entry::Entry, preview::PreviewType};
#[test]
fn test_format_command() {

View File

@ -294,7 +294,7 @@ pub fn draw_results_list(
#[cfg(test)]
mod tests {
use crate::channels::entry::PreviewType;
use crate::channels::preview::PreviewType;
use super::*;

View File

@ -1,6 +1,9 @@
use crate::action::Action;
use crate::cable::load_cable_channels;
use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER};
use crate::channels::{
entry::{Entry, ENTRY_PLACEHOLDER},
preview::PreviewType,
};
use crate::channels::{
remote_control::RemoteControl, OnAir, TelevisionChannel,
};