mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 20:15:23 +00:00
wip: dropping builtin previewers
This commit is contained in:
parent
1086899ba7
commit
59bba56ab1
1085
Cargo.lock
generated
1085
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -48,19 +48,18 @@ ratatui = { version = "0.29", features = ["serde", "macros"] }
|
||||
better-panic = "0.3"
|
||||
signal-hook = "0.3"
|
||||
human-panic = "2.0"
|
||||
# FIXME: we probably don't need strum anymore
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
regex = "1.11"
|
||||
parking_lot = "0.12"
|
||||
nom = "7.1"
|
||||
thiserror = "2.0"
|
||||
simdutf8 = { version = "0.1", optional = true }
|
||||
smallvec = { version = "1.13", features = ["const_generics"] }
|
||||
gag = "1.0"
|
||||
nucleo = "0.5"
|
||||
toml = "0.8"
|
||||
image = "0.25"
|
||||
syntect = { package = "syntect", version = "5.2", default-features = false }
|
||||
bat = { package = "bat", version = "0.25", default-features = false }
|
||||
lazy-regex = { version = "3.4.1", features = [
|
||||
"lite",
|
||||
], default-features = false }
|
||||
|
||||
|
||||
# target specific dependencies
|
||||
@ -82,11 +81,7 @@ tempfile = "3.16.0"
|
||||
[features]
|
||||
simd = ["dep:simdutf8"]
|
||||
zero-copy = []
|
||||
# Use fancy-regex for aarch64 Linux
|
||||
fancy = ["syntect/regex-fancy", "bat/regex-fancy"]
|
||||
# Use oniguruma for other platforms
|
||||
onig = ["syntect/regex-onig", "bat/regex-onig"]
|
||||
default = ["zero-copy", "simd", "onig"]
|
||||
default = ["zero-copy", "simd"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
|
@ -4,9 +4,7 @@ use anyhow::Result;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::channels::{
|
||||
entry::Entry, preview::PreviewType, OnAir, TelevisionChannel,
|
||||
};
|
||||
use crate::channels::{entry::Entry, OnAir, TelevisionChannel};
|
||||
use crate::config::{default_tick_rate, Config};
|
||||
use crate::keymap::Keymap;
|
||||
use crate::render::UiState;
|
||||
@ -123,7 +121,6 @@ impl From<ActionOutcome> for AppOutput {
|
||||
ActionOutcome::Input(input) => Self {
|
||||
selected_entries: Some(FxHashSet::from_iter([Entry::new(
|
||||
input,
|
||||
PreviewType::None,
|
||||
)])),
|
||||
},
|
||||
ActionOutcome::None => Self {
|
||||
@ -445,7 +442,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_maybe_select_1() {
|
||||
let mut app = App::new(
|
||||
TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
|
||||
TelevisionChannel::Stdin(StdinChannel::new(None)),
|
||||
Config::default(),
|
||||
None,
|
||||
AppOptions::default(),
|
||||
@ -453,14 +450,13 @@ mod test {
|
||||
app.television
|
||||
.results_picker
|
||||
.entries
|
||||
.push(Entry::new("test".to_string(), PreviewType::None));
|
||||
.push(Entry::new("test".to_string()));
|
||||
let outcome = app.maybe_select_1();
|
||||
assert!(outcome.is_some());
|
||||
assert_eq!(
|
||||
outcome.unwrap(),
|
||||
ActionOutcome::Entries(FxHashSet::from_iter([Entry::new(
|
||||
"test".to_string(),
|
||||
PreviewType::None
|
||||
)]))
|
||||
);
|
||||
}
|
||||
|
@ -6,12 +6,7 @@ use prototypes::{CableChannelPrototype, DEFAULT_DELIMITER};
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::preview::parse_preview_type;
|
||||
use crate::channels::{
|
||||
entry::Entry,
|
||||
preview::{PreviewCommand, PreviewType},
|
||||
OnAir,
|
||||
};
|
||||
use crate::channels::{entry::Entry, preview::PreviewCommand, OnAir};
|
||||
use crate::matcher::Matcher;
|
||||
use crate::matcher::{config::Config, injector::Injector};
|
||||
use crate::utils::command::shell_command;
|
||||
@ -23,7 +18,7 @@ pub struct Channel {
|
||||
pub name: String,
|
||||
matcher: Matcher<String>,
|
||||
entries_command: String,
|
||||
preview_type: PreviewType,
|
||||
preview_command: Option<PreviewCommand>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
@ -72,19 +67,10 @@ impl Channel {
|
||||
interactive,
|
||||
injector,
|
||||
));
|
||||
let preview_kind = match preview_command {
|
||||
Some(command) => {
|
||||
parse_preview_type(&command).unwrap_or_else(|_| {
|
||||
panic!("Invalid preview command: {command}")
|
||||
})
|
||||
}
|
||||
None => PreviewType::None,
|
||||
};
|
||||
debug!("Preview kind: {:?}", preview_kind);
|
||||
Self {
|
||||
matcher,
|
||||
entries_command: entries_command.to_string(),
|
||||
preview_type: preview_kind,
|
||||
preview_command,
|
||||
name: name.to_string(),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
crawl_handle,
|
||||
@ -149,8 +135,7 @@ impl OnAir for Channel {
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, self.preview_type.clone())
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
Entry::new(path).with_name_match_indices(&item.match_indices)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -158,7 +143,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, self.preview_type.clone())
|
||||
Entry::new(path)
|
||||
})
|
||||
}
|
||||
|
||||
@ -189,6 +174,6 @@ impl OnAir for Channel {
|
||||
fn shutdown(&self) {}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
self.preview_type != PreviewType::None
|
||||
self.preview_command.is_some()
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ use std::hash::{Hash, Hasher};
|
||||
|
||||
use devicons::FileIcon;
|
||||
|
||||
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
|
||||
// permit implementing `From<EntryType>` for channels which would make the
|
||||
@ -24,8 +22,6 @@ pub struct Entry {
|
||||
pub icon: Option<FileIcon>,
|
||||
/// The optional line number associated with the entry.
|
||||
pub line_number: Option<usize>,
|
||||
/// The type of preview associated with the entry.
|
||||
pub preview_type: PreviewType,
|
||||
}
|
||||
|
||||
impl Hash for Entry {
|
||||
@ -85,10 +81,10 @@ impl Entry {
|
||||
///
|
||||
/// Additional fields can be set using the builder pattern.
|
||||
/// ```
|
||||
/// use television::channels::{entry::Entry, preview::PreviewType};
|
||||
/// use television::channels::entry::Entry;
|
||||
/// use devicons::FileIcon;
|
||||
///
|
||||
/// let entry = Entry::new("name".to_string(), PreviewType::EnvVar)
|
||||
/// let entry = Entry::new("name".to_string())
|
||||
/// .with_value("value".to_string())
|
||||
/// .with_name_match_indices(&vec![0])
|
||||
/// .with_value_match_indices(&vec![0])
|
||||
@ -98,12 +94,11 @@ impl Entry {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - The name of the entry.
|
||||
/// * `preview_type` - The type of preview associated with the entry.
|
||||
///
|
||||
/// # Returns
|
||||
/// A new entry with the given name and preview type.
|
||||
/// The other fields are set to `None` by default.
|
||||
pub fn new(name: String, preview_type: PreviewType) -> Self {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
value: None,
|
||||
@ -111,7 +106,6 @@ impl Entry {
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: None,
|
||||
preview_type,
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +150,6 @@ pub const ENTRY_PLACEHOLDER: Entry = Entry {
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: None,
|
||||
preview_type: PreviewType::EnvVar,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@ -196,7 +189,6 @@ mod tests {
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: None,
|
||||
preview_type: PreviewType::Basic,
|
||||
};
|
||||
assert_eq!(entry.stdout_repr(), "test name with spaces");
|
||||
}
|
||||
@ -210,7 +202,6 @@ mod tests {
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: Some(a),
|
||||
preview_type: PreviewType::Basic,
|
||||
};
|
||||
assert_eq!(entry.stdout_repr(), "test_file_name.rs:10");
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use strum::EnumString;
|
||||
use tracing::debug;
|
||||
use lazy_regex::{regex, Lazy, Regex};
|
||||
|
||||
use super::entry::Entry;
|
||||
|
||||
static CMD_RE: &Lazy<Regex> = regex!(r"\{(\d+)\}");
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct PreviewCommand {
|
||||
@ -18,6 +19,40 @@ impl PreviewCommand {
|
||||
delimiter: delimiter.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the command with the entry name and provided placeholders.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use television::channels::{preview::PreviewCommand, entry::Entry};
|
||||
///
|
||||
/// let command = PreviewCommand {
|
||||
/// command: "something {} {2} {0}".to_string(),
|
||||
/// delimiter: ":".to_string(),
|
||||
/// };
|
||||
/// let entry = Entry::new("a:given:entry:to:preview".to_string());
|
||||
///
|
||||
/// let formatted_command = command.format_with(&entry);
|
||||
///
|
||||
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'");
|
||||
/// ```
|
||||
pub fn format_with(&self, entry: &Entry) -> String {
|
||||
let parts = entry.name.split(&self.delimiter).collect::<Vec<&str>>();
|
||||
|
||||
let mut formatted_command = self
|
||||
.command
|
||||
.replace("{}", format!("'{}'", entry.name).as_str());
|
||||
|
||||
formatted_command = CMD_RE
|
||||
.replace_all(&formatted_command, |caps: ®ex::Captures| {
|
||||
let index =
|
||||
caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
|
||||
format!("'{}'", parts[index])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
formatted_command
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreviewCommand {
|
||||
@ -26,38 +61,59 @@ impl Display for PreviewCommand {
|
||||
}
|
||||
}
|
||||
|
||||
#[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::*;
|
||||
use crate::channels::entry::Entry;
|
||||
|
||||
/// 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()))
|
||||
#[test]
|
||||
fn test_format_command() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {} {2} {0}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&entry);
|
||||
|
||||
assert_eq!(
|
||||
formatted_command,
|
||||
"something 'an:entry:to:preview' 'to' 'an'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_no_placeholders() {
|
||||
let command = PreviewCommand {
|
||||
command: "something".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&entry);
|
||||
|
||||
assert_eq!(formatted_command, "something");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_global_placeholder_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&entry);
|
||||
|
||||
assert_eq!(formatted_command, "something 'an:entry:to:preview'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_positional_placeholders_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {0} -t {2}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&entry);
|
||||
|
||||
assert_eq!(formatted_command, "something 'an' -t 'to'");
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::channels::cable::prototypes::CableChannels;
|
||||
use crate::channels::{entry::Entry, preview::PreviewType};
|
||||
use crate::channels::entry::Entry;
|
||||
use crate::channels::{OnAir, TelevisionChannel};
|
||||
use crate::matcher::{config::Config, Matcher};
|
||||
use anyhow::Result;
|
||||
@ -83,7 +83,7 @@ impl OnAir for RemoteControl {
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, PreviewType::Basic)
|
||||
Entry::new(path)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_icon(CABLE_ICON)
|
||||
})
|
||||
@ -93,7 +93,7 @@ impl OnAir for RemoteControl {
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, PreviewType::Basic).with_icon(TV_ICON)
|
||||
Entry::new(path).with_icon(TV_ICON)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -7,19 +7,19 @@ use std::{
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use tracing::debug;
|
||||
|
||||
use super::OnAir;
|
||||
use crate::channels::{entry::Entry, preview::PreviewType};
|
||||
use super::{preview::PreviewCommand, OnAir};
|
||||
use crate::channels::entry::Entry;
|
||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
preview_type: PreviewType,
|
||||
preview_command: Option<PreviewCommand>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
instream_handle: std::thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(preview_type: PreviewType) -> Self {
|
||||
pub fn new(preview_command: Option<PreviewCommand>) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
|
||||
@ -27,7 +27,7 @@ impl Channel {
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
preview_type,
|
||||
preview_command,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
instream_handle,
|
||||
}
|
||||
@ -36,7 +36,7 @@ impl Channel {
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(PreviewType::default())
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ where
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
preview_type: PreviewType::default(),
|
||||
preview_command: None,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
instream_handle,
|
||||
}
|
||||
@ -112,16 +112,16 @@ impl OnAir for Channel {
|
||||
.map(|item| {
|
||||
// NOTE: we're passing `PreviewType::Basic` here just as a placeholder
|
||||
// to avoid storing the preview command multiple times for each item.
|
||||
Entry::new(item.matched_string, PreviewType::Basic)
|
||||
Entry::new(item.matched_string)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
Entry::new(item.matched_string, self.preview_type.clone())
|
||||
})
|
||||
self.matcher
|
||||
.get_result(index)
|
||||
.map(|item| Entry::new(item.matched_string))
|
||||
}
|
||||
|
||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
||||
@ -151,6 +151,6 @@ impl OnAir for Channel {
|
||||
fn shutdown(&self) {}
|
||||
|
||||
fn supports_preview(&self) -> bool {
|
||||
self.preview_type != PreviewType::None
|
||||
self.preview_command.is_some()
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,7 @@ use tracing::debug;
|
||||
use crate::channels::cable::prototypes::{
|
||||
CableChannelPrototype, CableChannels,
|
||||
};
|
||||
use crate::channels::preview::{
|
||||
parse_preview_type, PreviewCommand, PreviewType,
|
||||
};
|
||||
use crate::channels::preview::PreviewCommand;
|
||||
use crate::cli::args::{Cli, Command};
|
||||
use crate::config::{KeyBindings, DEFAULT_CHANNEL};
|
||||
use crate::{
|
||||
@ -23,7 +21,7 @@ pub mod args;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: CableChannelPrototype,
|
||||
pub preview_kind: PreviewType,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
pub no_preview: bool,
|
||||
pub tick_rate: Option<f64>,
|
||||
pub frame_rate: Option<f64>,
|
||||
@ -44,7 +42,7 @@ impl Default for PostProcessedCli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
channel: CableChannelPrototype::default(),
|
||||
preview_kind: PreviewType::None,
|
||||
preview_command: None,
|
||||
no_preview: false,
|
||||
tick_rate: None,
|
||||
frame_rate: None,
|
||||
@ -75,18 +73,9 @@ impl From<Cli> for PostProcessedCli {
|
||||
});
|
||||
|
||||
// parse the preview command if provided
|
||||
let preview_kind = cli
|
||||
.preview
|
||||
.map(|preview| PreviewCommand {
|
||||
let preview_command = cli.preview.map(|preview| PreviewCommand {
|
||||
command: preview,
|
||||
delimiter: cli.delimiter.clone(),
|
||||
})
|
||||
.map_or(PreviewType::None, |preview_command| {
|
||||
parse_preview_type(&preview_command)
|
||||
.map_err(|e| {
|
||||
cli_parsing_error_exit(&e.to_string());
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let channel: CableChannelPrototype;
|
||||
@ -129,7 +118,7 @@ impl From<Cli> for PostProcessedCli {
|
||||
|
||||
Self {
|
||||
channel,
|
||||
preview_kind,
|
||||
preview_command,
|
||||
no_preview: cli.no_preview,
|
||||
tick_rate: cli.tick_rate,
|
||||
frame_rate: cli.frame_rate,
|
||||
@ -303,10 +292,7 @@ Data directory: {data_dir_path}"
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
action::Action, channels::preview::PreviewType, config::Binding,
|
||||
event::Key,
|
||||
};
|
||||
use crate::{action::Action, config::Binding, event::Key};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -328,8 +314,8 @@ mod tests {
|
||||
CableChannelPrototype::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
post_processed_cli.preview_kind,
|
||||
PreviewType::Command(PreviewCommand {
|
||||
post_processed_cli.preview_command,
|
||||
Some(PreviewCommand {
|
||||
command: "bat -n --color=always {}".to_string(),
|
||||
delimiter: ":".to_string()
|
||||
})
|
||||
@ -364,34 +350,6 @@ mod tests {
|
||||
assert_eq!(post_processed_cli.command, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builtin_previewer_files() {
|
||||
let cli = Cli {
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":files:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
||||
assert_eq!(post_processed_cli.preview_kind, PreviewType::Files);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builtin_previewer_env() {
|
||||
let cli = Cli {
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":env_var:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
||||
assert_eq!(post_processed_cli.preview_kind, PreviewType::EnvVar);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_keybindings() {
|
||||
let cli = Cli {
|
||||
|
@ -9,7 +9,6 @@ use anyhow::{Context, Result};
|
||||
use directories::ProjectDirs;
|
||||
pub use keybindings::merge_keybindings;
|
||||
pub use keybindings::{parse_key, Binding, KeyBindings};
|
||||
use previewers::PreviewersConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shell_integration::ShellIntegrationConfig;
|
||||
pub use themes::Theme;
|
||||
@ -17,7 +16,6 @@ use tracing::{debug, warn};
|
||||
pub use ui::UiConfig;
|
||||
|
||||
mod keybindings;
|
||||
mod previewers;
|
||||
pub mod shell_integration;
|
||||
mod themes;
|
||||
mod ui;
|
||||
@ -70,9 +68,6 @@ pub struct Config {
|
||||
/// UI configuration
|
||||
#[serde(default)]
|
||||
pub ui: UiConfig,
|
||||
/// Previewers configuration
|
||||
#[serde(default)]
|
||||
pub previewers: PreviewersConfig,
|
||||
/// Shell integration configuration
|
||||
#[serde(default)]
|
||||
pub shell_integration: ShellIntegrationConfig,
|
||||
@ -201,7 +196,6 @@ impl Config {
|
||||
application: user.application,
|
||||
keybindings: user.keybindings,
|
||||
ui: user.ui,
|
||||
previewers: user.previewers,
|
||||
shell_integration: user.shell_integration,
|
||||
}
|
||||
}
|
||||
@ -330,7 +324,6 @@ mod tests {
|
||||
assert_eq!(config.application, default_config.application);
|
||||
assert_eq!(config.keybindings, default_config.keybindings);
|
||||
assert_eq!(config.ui, default_config.ui);
|
||||
assert_eq!(config.previewers, default_config.previewers);
|
||||
// backwards compatibility
|
||||
assert_eq!(
|
||||
config.shell_integration.commands,
|
||||
@ -384,8 +377,6 @@ mod tests {
|
||||
default_config.application.frame_rate = 30.0;
|
||||
default_config.ui.ui_scale = 40;
|
||||
default_config.ui.theme = "television".to_string();
|
||||
default_config.previewers.file.theme =
|
||||
"Visual Studio Dark".to_string();
|
||||
default_config.keybindings.extend({
|
||||
let mut map = FxHashMap::default();
|
||||
map.insert(
|
||||
@ -408,7 +399,6 @@ mod tests {
|
||||
assert_eq!(config.application, default_config.application);
|
||||
assert_eq!(config.keybindings, default_config.keybindings);
|
||||
assert_eq!(config.ui, default_config.ui);
|
||||
assert_eq!(config.previewers, default_config.previewers);
|
||||
assert_eq!(
|
||||
config.shell_integration.commands,
|
||||
[(&String::from("git add"), &String::from("git-diff"))]
|
||||
|
@ -1,40 +0,0 @@
|
||||
use crate::preview::{previewers, PreviewerConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Hash)]
|
||||
pub struct PreviewersConfig {
|
||||
#[serde(default)]
|
||||
pub basic: BasicPreviewerConfig,
|
||||
pub file: FilePreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub env_var: EnvVarPreviewerConfig,
|
||||
}
|
||||
|
||||
impl From<PreviewersConfig> for PreviewerConfig {
|
||||
fn from(val: PreviewersConfig) -> Self {
|
||||
PreviewerConfig::default()
|
||||
.file(previewers::files::FilePreviewerConfig::new(val.file.theme))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Hash)]
|
||||
pub struct BasicPreviewerConfig {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
//pub max_file_size: u64,
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl Default for FilePreviewerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
//max_file_size: 1024 * 1024,
|
||||
theme: String::from("TwoDark"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Hash)]
|
||||
pub struct EnvVarPreviewerConfig {}
|
@ -1,33 +1,12 @@
|
||||
use crate::preview::{Preview, PreviewContent};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn not_supported(title: &str) -> Arc<Preview> {
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::NotSupported,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn file_too_large(title: &str) -> Arc<Preview> {
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::FileTooLarge,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn loading(title: &str) -> Arc<Preview> {
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::Loading,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
@ -37,7 +16,6 @@ pub fn timeout(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::Timeout,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
@ -1,55 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::channels::{entry::Entry, preview::PreviewType};
|
||||
use devicons::FileIcon;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
pub mod ansi;
|
||||
pub mod cache;
|
||||
pub mod previewers;
|
||||
|
||||
// previewer types
|
||||
use crate::utils::cache::RingSet;
|
||||
use crate::utils::image::ImagePreviewWidget;
|
||||
use crate::utils::syntax::HighlightedLines;
|
||||
pub use previewers::basic::BasicPreviewer;
|
||||
pub use previewers::basic::BasicPreviewerConfig;
|
||||
pub use previewers::command::CommandPreviewer;
|
||||
pub use previewers::command::CommandPreviewerConfig;
|
||||
pub use previewers::env::EnvVarPreviewer;
|
||||
pub use previewers::env::EnvVarPreviewerConfig;
|
||||
pub use previewers::files::FilePreviewer;
|
||||
pub use previewers::files::FilePreviewerConfig;
|
||||
pub mod meta;
|
||||
pub mod previewer;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub enum PreviewContent {
|
||||
Empty,
|
||||
FileTooLarge,
|
||||
SyntectHighlightedText(HighlightedLines),
|
||||
Loading,
|
||||
Timeout,
|
||||
NotSupported,
|
||||
PlainText(Vec<String>),
|
||||
PlainTextWrapped(String),
|
||||
AnsiText(String),
|
||||
Image(ImagePreviewWidget),
|
||||
}
|
||||
|
||||
impl PreviewContent {
|
||||
pub fn total_lines(&self) -> u16 {
|
||||
match self {
|
||||
PreviewContent::SyntectHighlightedText(hl_lines) => {
|
||||
hl_lines.lines.len().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
PreviewContent::PlainText(lines) => {
|
||||
lines.len().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
PreviewContent::AnsiText(text) => {
|
||||
text.lines().count().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
PreviewContent::Image(image) => {
|
||||
image.height().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@ -71,9 +42,6 @@ pub struct Preview {
|
||||
pub title: String,
|
||||
pub content: PreviewContent,
|
||||
pub icon: Option<FileIcon>,
|
||||
/// If the preview is partial, this field contains the byte offset
|
||||
/// up to which the preview holds.
|
||||
pub partial_offset: Option<usize>,
|
||||
pub total_lines: u16,
|
||||
}
|
||||
|
||||
@ -83,7 +51,6 @@ impl Default for Preview {
|
||||
title: String::new(),
|
||||
content: PreviewContent::Empty,
|
||||
icon: None,
|
||||
partial_offset: None,
|
||||
total_lines: 0,
|
||||
}
|
||||
}
|
||||
@ -94,14 +61,12 @@ impl Preview {
|
||||
title: String,
|
||||
content: PreviewContent,
|
||||
icon: Option<FileIcon>,
|
||||
partial_offset: Option<usize>,
|
||||
total_lines: u16,
|
||||
) -> Self {
|
||||
Preview {
|
||||
title,
|
||||
content,
|
||||
icon,
|
||||
partial_offset,
|
||||
total_lines,
|
||||
}
|
||||
}
|
||||
@ -174,110 +139,3 @@ impl PreviewState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Previewer {
|
||||
basic: BasicPreviewer,
|
||||
file: FilePreviewer,
|
||||
env_var: EnvVarPreviewer,
|
||||
command: CommandPreviewer,
|
||||
requests: RingSet<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PreviewerConfig {
|
||||
basic: BasicPreviewerConfig,
|
||||
file: FilePreviewerConfig,
|
||||
env_var: EnvVarPreviewerConfig,
|
||||
command: CommandPreviewerConfig,
|
||||
}
|
||||
|
||||
impl PreviewerConfig {
|
||||
pub fn basic(mut self, config: BasicPreviewerConfig) -> Self {
|
||||
self.basic = config;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file(mut self, config: FilePreviewerConfig) -> Self {
|
||||
self.file = config;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env_var(mut self, config: EnvVarPreviewerConfig) -> Self {
|
||||
self.env_var = config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
const REQUEST_STACK_SIZE: usize = 10;
|
||||
|
||||
impl Previewer {
|
||||
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
||||
let config = config.unwrap_or_default();
|
||||
Previewer {
|
||||
basic: BasicPreviewer::new(Some(config.basic)),
|
||||
file: FilePreviewer::new(Some(config.file)),
|
||||
env_var: EnvVarPreviewer::new(Some(config.env_var)),
|
||||
command: CommandPreviewer::new(Some(config.command)),
|
||||
requests: RingSet::with_capacity(REQUEST_STACK_SIZE),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_request(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
preview_window: Option<Rect>,
|
||||
) -> Option<Arc<Preview>> {
|
||||
match &entry.preview_type {
|
||||
PreviewType::Basic => Some(self.basic.preview(entry)),
|
||||
PreviewType::EnvVar => Some(self.env_var.preview(entry)),
|
||||
PreviewType::Files => self.file.preview(entry, preview_window),
|
||||
PreviewType::Command(cmd) => self.command.preview(entry, cmd),
|
||||
PreviewType::None => Some(Arc::new(Preview::default())),
|
||||
}
|
||||
}
|
||||
|
||||
fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
match &entry.preview_type {
|
||||
PreviewType::Basic => Some(self.basic.preview(entry)),
|
||||
PreviewType::EnvVar => Some(self.env_var.preview(entry)),
|
||||
PreviewType::Files => self.file.cached(entry),
|
||||
PreviewType::Command(_) => self.command.cached(entry),
|
||||
PreviewType::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
// we could use a target scroll here to make the previewer
|
||||
// faster, but since it's already running in the background and quite
|
||||
// fast for most standard file sizes, plus we're caching the previews,
|
||||
// I'm not sure the extra complexity is worth it.
|
||||
pub fn preview(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
preview_window: Option<Rect>,
|
||||
) -> Option<Arc<Preview>> {
|
||||
// check if we have a preview for the current request
|
||||
if let Some(preview) = self.cached(entry) {
|
||||
return Some(preview);
|
||||
}
|
||||
|
||||
// otherwise, if we haven't acknowledged the request yet, acknowledge it
|
||||
self.requests.push(entry.clone());
|
||||
|
||||
// lookup request stack and return the most recent preview available
|
||||
for request in self.requests.back_to_front() {
|
||||
if let Some(preview) =
|
||||
self.dispatch_request(&request, preview_window)
|
||||
{
|
||||
return Some(preview);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_config(&mut self, config: PreviewerConfig) {
|
||||
self.basic = BasicPreviewer::new(Some(config.basic));
|
||||
self.file = FilePreviewer::new(Some(config.file));
|
||||
self.env_var = EnvVarPreviewer::new(Some(config.env_var));
|
||||
}
|
||||
}
|
||||
|
152
television/preview/previewer.rs
Normal file
152
television/preview/previewer.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use crate::preview::{Preview, PreviewContent};
|
||||
use crate::utils::cache::RingSet;
|
||||
use crate::utils::command::shell_command;
|
||||
use crate::{
|
||||
channels::{entry::Entry, preview::PreviewCommand},
|
||||
preview::cache::PreviewCache,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Previewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
requests: RingSet<Entry>,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||
command: PreviewCommand,
|
||||
}
|
||||
|
||||
const REQUEST_STACK_SIZE: usize = 10;
|
||||
|
||||
impl Previewer {
|
||||
// we could use a target scroll here to make the previewer
|
||||
// faster, but since it's already running in the background and quite
|
||||
// fast for most standard file sizes, plus we're caching the previews,
|
||||
// I'm not sure the extra complexity is worth it.
|
||||
pub fn handle_request(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
// check if we have a preview for the current request
|
||||
if let Some(preview) = self.preview(entry) {
|
||||
return Some(preview);
|
||||
}
|
||||
|
||||
// otherwise, if we haven't acknowledged the request yet, acknowledge it
|
||||
self.requests.push(entry.clone());
|
||||
|
||||
// lookup request stack and return the most recent preview available
|
||||
for entry in self.requests.back_to_front() {
|
||||
if let Some(preview) = self.preview(&entry) {
|
||||
return Some(preview);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
|
||||
|
||||
impl Previewer {
|
||||
pub fn new(command: PreviewCommand) -> Self {
|
||||
Previewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
requests: RingSet::with_capacity(REQUEST_STACK_SIZE),
|
||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
||||
in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())),
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
self.cache.lock().get(&entry.name)
|
||||
}
|
||||
|
||||
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
if let Some(preview) = self.cached(entry) {
|
||||
Some(preview)
|
||||
} else {
|
||||
// preview is not in cache, spawn a task to compute the preview
|
||||
debug!("Preview cache miss for {:?}", entry.name);
|
||||
self.handle_preview_request(entry);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_preview_request(&mut self, entry: &Entry) {
|
||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.in_flight_previews.lock().insert(entry.name.clone());
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||
let command = self.command.clone();
|
||||
let in_flight_previews = self.in_flight_previews.clone();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&command,
|
||||
&entry_c,
|
||||
&cache,
|
||||
&concurrent_tasks,
|
||||
&in_flight_previews,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
"Too many concurrent preview tasks, skipping {:?}",
|
||||
entry.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_preview(
|
||||
command: &PreviewCommand,
|
||||
entry: &Entry,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let command = command.format_with(entry);
|
||||
debug!("Formatted preview command: {:?}", command);
|
||||
|
||||
let child = shell_command(false)
|
||||
.arg(&command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if child.status.success() {
|
||||
let content = String::from_utf8_lossy(&child.stdout);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
} else {
|
||||
let content = String::from_utf8_lossy(&child.stderr);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::channels::entry::Entry;
|
||||
use crate::preview::{Preview, PreviewContent};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BasicPreviewer {
|
||||
_config: BasicPreviewerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BasicPreviewerConfig {}
|
||||
|
||||
impl BasicPreviewer {
|
||||
pub fn new(config: Option<BasicPreviewerConfig>) -> Self {
|
||||
BasicPreviewer {
|
||||
_config: config.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview(&self, entry: &Entry) -> Arc<Preview> {
|
||||
Arc::new(Preview {
|
||||
title: entry.name.clone(),
|
||||
content: PreviewContent::PlainTextWrapped(entry.name.clone()),
|
||||
icon: entry.icon,
|
||||
partial_offset: None,
|
||||
total_lines: 1,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,294 +0,0 @@
|
||||
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;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct CommandPreviewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
config: CommandPreviewerConfig,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||
command_re: Regex,
|
||||
}
|
||||
|
||||
impl Default for CommandPreviewer {
|
||||
fn default() -> Self {
|
||||
CommandPreviewer::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandPreviewerConfig {
|
||||
delimiter: String,
|
||||
}
|
||||
|
||||
const DEFAULT_DELIMITER: &str = " ";
|
||||
|
||||
impl Default for CommandPreviewerConfig {
|
||||
fn default() -> Self {
|
||||
CommandPreviewerConfig {
|
||||
delimiter: String::from(DEFAULT_DELIMITER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandPreviewerConfig {
|
||||
pub fn new(delimiter: &str) -> Self {
|
||||
CommandPreviewerConfig {
|
||||
delimiter: String::from(delimiter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
|
||||
|
||||
impl CommandPreviewer {
|
||||
pub fn new(config: Option<CommandPreviewerConfig>) -> Self {
|
||||
let config = config.unwrap_or_default();
|
||||
CommandPreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
config,
|
||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
||||
in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())),
|
||||
command_re: Regex::new(r"\{(\d+)\}").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
self.cache.lock().get(&entry.name)
|
||||
}
|
||||
|
||||
pub fn preview(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
command: &PreviewCommand,
|
||||
) -> Option<Arc<Preview>> {
|
||||
if let Some(preview) = self.cached(entry) {
|
||||
Some(preview)
|
||||
} else {
|
||||
// preview is not in cache, spawn a task to compute the preview
|
||||
debug!("Preview cache miss for {:?}", entry.name);
|
||||
self.handle_preview_request(entry, command);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_preview_request(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
command: &PreviewCommand,
|
||||
) {
|
||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.in_flight_previews.lock().insert(entry.name.clone());
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||
let command = command.clone();
|
||||
let in_flight_previews = self.in_flight_previews.clone();
|
||||
let command_re = self.command_re.clone();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&command,
|
||||
&entry_c,
|
||||
&cache,
|
||||
&concurrent_tasks,
|
||||
&in_flight_previews,
|
||||
&command_re,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
"Too many concurrent preview tasks, skipping {:?}",
|
||||
entry.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the command with the entry name and provided placeholders
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use television::channels::{preview::{PreviewCommand, PreviewType}, entry::Entry};
|
||||
/// use television::preview::previewers::command::format_command;
|
||||
///
|
||||
/// let command = PreviewCommand {
|
||||
/// command: "something {} {2} {0}".to_string(),
|
||||
/// delimiter: ":".to_string(),
|
||||
/// };
|
||||
/// let entry = Entry::new("a:given:entry:to:preview".to_string(), PreviewType::Command(command.clone()));
|
||||
/// let formatted_command = format_command(&command, &entry, ®ex::Regex::new(r"\{(\d+)\}").unwrap());
|
||||
///
|
||||
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'");
|
||||
/// ```
|
||||
pub fn format_command(
|
||||
command: &PreviewCommand,
|
||||
entry: &Entry,
|
||||
command_re: &Regex,
|
||||
) -> String {
|
||||
let parts = entry.name.split(&command.delimiter).collect::<Vec<&str>>();
|
||||
debug!("Parts: {:?}", parts);
|
||||
|
||||
let mut formatted_command = command
|
||||
.command
|
||||
.replace("{}", format!("'{}'", entry.name).as_str());
|
||||
|
||||
formatted_command = command_re
|
||||
.replace_all(&formatted_command, |caps: ®ex::Captures| {
|
||||
let index =
|
||||
caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
|
||||
format!("'{}'", parts[index])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
formatted_command
|
||||
}
|
||||
|
||||
pub fn try_preview(
|
||||
command: &PreviewCommand,
|
||||
entry: &Entry,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||
command_re: &Regex,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let command = format_command(command, entry, command_re);
|
||||
debug!("Formatted preview command: {:?}", command);
|
||||
|
||||
let child = shell_command(false)
|
||||
.arg(&command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if child.status.success() {
|
||||
let content = String::from_utf8_lossy(&child.stdout);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
} else {
|
||||
let content = String::from_utf8_lossy(&child.stderr);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::channels::{entry::Entry, preview::PreviewType};
|
||||
|
||||
#[test]
|
||||
fn test_format_command() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {} {2} {0}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(
|
||||
&command,
|
||||
&entry,
|
||||
&Regex::new(r"\{(\d+)\}").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
formatted_command,
|
||||
"something 'an:entry:to:preview' 'to' 'an'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_no_placeholders() {
|
||||
let command = PreviewCommand {
|
||||
command: "something".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(
|
||||
&command,
|
||||
&entry,
|
||||
&Regex::new(r"\{(\d+)\}").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(formatted_command, "something");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_global_placeholder_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(
|
||||
&command,
|
||||
&entry,
|
||||
&Regex::new(r"\{(\d+)\}").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(formatted_command, "something 'an:entry:to:preview'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_positional_placeholders_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {0} -t {2}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(
|
||||
&command,
|
||||
&entry,
|
||||
&Regex::new(r"\{(\d+)\}").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(formatted_command, "something 'an' -t 'to'");
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::channels::entry;
|
||||
use crate::preview::{Preview, PreviewContent};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvVarPreviewer {
|
||||
_config: EnvVarPreviewerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvVarPreviewerConfig {}
|
||||
|
||||
impl EnvVarPreviewer {
|
||||
pub fn new(config: Option<EnvVarPreviewerConfig>) -> Self {
|
||||
EnvVarPreviewer {
|
||||
_config: config.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview(&self, entry: &entry::Entry) -> Arc<Preview> {
|
||||
let content = entry.value.as_ref().map(|preview| {
|
||||
maybe_add_newline_after_colon(preview, &entry.name)
|
||||
});
|
||||
let total_lines = content.as_ref().map_or_else(
|
||||
|| 1,
|
||||
|c| u16::try_from(c.lines().count()).unwrap_or(u16::MAX),
|
||||
);
|
||||
|
||||
Arc::new(Preview {
|
||||
title: entry.name.clone(),
|
||||
content: match content {
|
||||
Some(content) => PreviewContent::PlainTextWrapped(content),
|
||||
None => PreviewContent::Empty,
|
||||
},
|
||||
icon: entry.icon,
|
||||
partial_offset: None,
|
||||
total_lines,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const PATH: &str = "PATH";
|
||||
|
||||
fn maybe_add_newline_after_colon(s: &str, name: &str) -> String {
|
||||
if name.contains(PATH) {
|
||||
return s.replace(':', "\n");
|
||||
}
|
||||
s.to_string()
|
||||
}
|
@ -1,379 +0,0 @@
|
||||
use crate::utils::files::{read_into_lines_capped, ReadResult};
|
||||
use crate::utils::syntax::HighlightedLines;
|
||||
use image::ImageReader;
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::layout::Rect;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Seek};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use syntect::{highlighting::Theme, parsing::SyntaxSet};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::channels::entry;
|
||||
use crate::preview::cache::PreviewCache;
|
||||
use crate::preview::{previewers::meta, Preview, PreviewContent};
|
||||
use crate::utils::image::ImagePreviewWidget;
|
||||
use crate::utils::{
|
||||
files::FileType,
|
||||
strings::preprocess_line,
|
||||
syntax::{self, load_highlighting_assets, HighlightingAssetsExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FilePreviewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
pub syntax_set: Arc<SyntaxSet>,
|
||||
pub syntax_theme: Arc<Theme>,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl FilePreviewerConfig {
|
||||
pub fn new(theme: String) -> Self {
|
||||
FilePreviewerConfig { theme }
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
|
||||
|
||||
const BAT_THEME_ENV_VAR: &str = "BAT_THEME";
|
||||
|
||||
impl FilePreviewer {
|
||||
pub fn new(config: Option<FilePreviewerConfig>) -> Self {
|
||||
let hl_assets = load_highlighting_assets();
|
||||
let syntax_set = hl_assets.get_syntax_set().unwrap().clone();
|
||||
|
||||
let theme_name = match std::env::var(BAT_THEME_ENV_VAR) {
|
||||
Ok(t) => t,
|
||||
Err(_) => match config {
|
||||
Some(c) => c.theme,
|
||||
// this will error and default back nicely
|
||||
None => "unknown".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let theme = hl_assets.get_theme_no_output(&theme_name).clone();
|
||||
|
||||
FilePreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
syntax_set: Arc::new(syntax_set),
|
||||
syntax_theme: Arc::new(theme),
|
||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
||||
in_flight_previews: Arc::new(Mutex::new(HashSet::with_hasher(
|
||||
FxBuildHasher,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached(&self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
||||
self.cache.lock().get(&entry.name)
|
||||
}
|
||||
|
||||
pub fn preview(
|
||||
&mut self,
|
||||
entry: &entry::Entry,
|
||||
preview_window: Option<Rect>,
|
||||
) -> Option<Arc<Preview>> {
|
||||
if let Some(preview) = self.cached(entry) {
|
||||
trace!("Preview cache hit for {:?}", entry.name);
|
||||
if preview.partial_offset.is_some() {
|
||||
// preview is partial, spawn a task to compute the next chunk
|
||||
// and return the partial preview
|
||||
debug!("Spawning partial preview task for {:?}", entry.name);
|
||||
self.handle_preview_request(
|
||||
entry,
|
||||
Some(preview.clone()),
|
||||
preview_window,
|
||||
);
|
||||
}
|
||||
Some(preview)
|
||||
} else {
|
||||
// preview is not in cache, spawn a task to compute the preview
|
||||
trace!("Preview cache miss for {:?}", entry.name);
|
||||
self.handle_preview_request(entry, None, preview_window);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_preview_request(
|
||||
&mut self,
|
||||
entry: &entry::Entry,
|
||||
partial_preview: Option<Arc<Preview>>,
|
||||
preview_window: Option<Rect>,
|
||||
) {
|
||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||
trace!("Preview already in flight for {:?}", entry.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.in_flight_previews.lock().insert(entry.name.clone());
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let syntax_set = self.syntax_set.clone();
|
||||
let syntax_theme = self.syntax_theme.clone();
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||
let in_flight_previews = self.in_flight_previews.clone();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&entry_c,
|
||||
partial_preview,
|
||||
&cache,
|
||||
&syntax_set,
|
||||
&syntax_theme,
|
||||
&concurrent_tasks,
|
||||
&in_flight_previews,
|
||||
preview_window,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn cache_preview(&mut self, key: String, preview: &Arc<Preview>) {
|
||||
self.cache.lock().insert(key, preview);
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the buffer used to read the file in bytes.
|
||||
/// This ends up being the max size of partial previews.
|
||||
const PARTIAL_BUFREAD_SIZE: usize = 5 * 1024 * 1024;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn try_preview(
|
||||
entry: &entry::Entry,
|
||||
partial_preview: Option<Arc<Preview>>,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
syntax_set: &Arc<SyntaxSet>,
|
||||
syntax_theme: &Arc<Theme>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||
preview_window: Option<Rect>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let path = PathBuf::from(&entry.name);
|
||||
|
||||
// if we're dealing with a partial preview, no need to re-check for textual content
|
||||
if partial_preview.is_some()
|
||||
|| matches!(FileType::from(&path), FileType::Text)
|
||||
{
|
||||
debug!("File is text-based: {:?}", entry.name);
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
// if we're dealing with a partial preview, seek to the provided offset
|
||||
// and use the previous state to compute the next chunk of the preview
|
||||
let cached_lines = if let Some(p) = partial_preview {
|
||||
if let PreviewContent::SyntectHighlightedText(hl) =
|
||||
&p.content
|
||||
{
|
||||
let _ = file.seek(std::io::SeekFrom::Start(
|
||||
// this is always Some in this case
|
||||
p.partial_offset.unwrap() as u64,
|
||||
));
|
||||
Some(hl.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// compute the highlighted version in the background
|
||||
match read_into_lines_capped(file, PARTIAL_BUFREAD_SIZE) {
|
||||
ReadResult::Full(lines) => {
|
||||
if let Some(content) = compute_highlighted_text_preview(
|
||||
entry,
|
||||
&lines
|
||||
.iter()
|
||||
.map(|l| preprocess_line(l).0 + "\n")
|
||||
.collect::<Vec<_>>(),
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
cached_lines.as_ref(),
|
||||
) {
|
||||
let total_lines = content.total_lines();
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
content,
|
||||
entry.icon,
|
||||
None,
|
||||
total_lines,
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
}
|
||||
ReadResult::Partial(p) => {
|
||||
if let Some(content) = compute_highlighted_text_preview(
|
||||
entry,
|
||||
&p.lines
|
||||
.iter()
|
||||
.map(|l| preprocess_line(l).0 + "\n")
|
||||
.collect::<Vec<_>>(),
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
cached_lines.as_ref(),
|
||||
) {
|
||||
let total_lines = content.total_lines();
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
content,
|
||||
entry.icon,
|
||||
Some(p.bytes_read),
|
||||
total_lines,
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
}
|
||||
ReadResult::Error(e) => {
|
||||
warn!("Error reading file: {:?}", e);
|
||||
let p = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), &p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error opening file: {:?}", e);
|
||||
let p = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), &p);
|
||||
}
|
||||
}
|
||||
} else if matches!(FileType::from(&path), FileType::Image) {
|
||||
cache.lock().insert(
|
||||
entry.name.clone(),
|
||||
&meta::loading(&format!("Loading {}", entry.name)),
|
||||
);
|
||||
|
||||
debug!("File {:?} is an image", entry.name);
|
||||
let option_image = match ImageReader::open(path) {
|
||||
Ok(reader) => match reader.with_guessed_format() {
|
||||
Ok(reader) => match reader.decode() {
|
||||
Ok(image) => Some(image),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Error impossible to decode {}: {:?}",
|
||||
entry.name, e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Error impossible to guess the format of {}: {:?}",
|
||||
entry.name, e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Error opening image {}: {:?}", entry.name, e);
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(image) = option_image {
|
||||
let preview_window_dimension = preview_window.map(|rect| {
|
||||
(
|
||||
u32::from(rect.width.saturating_sub(2)),
|
||||
u32::from(rect.height.saturating_sub(2)),
|
||||
) // - 2 for the margin
|
||||
});
|
||||
let image_preview_widget = ImagePreviewWidget::from_dynamic_image(
|
||||
image,
|
||||
preview_window_dimension,
|
||||
);
|
||||
let total_lines =
|
||||
image_preview_widget.height().try_into().unwrap_or(u16::MAX);
|
||||
let content = PreviewContent::Image(image_preview_widget);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
content,
|
||||
entry.icon,
|
||||
None,
|
||||
total_lines,
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
} else {
|
||||
let p = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), &p);
|
||||
}
|
||||
} else {
|
||||
debug!("File format isn't supported for preview: {:?}", entry.name);
|
||||
let preview = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
}
|
||||
|
||||
fn compute_highlighted_text_preview(
|
||||
entry: &entry::Entry,
|
||||
lines: &[String],
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
previous_lines: Option<&HighlightedLines>,
|
||||
) -> Option<PreviewContent> {
|
||||
debug!(
|
||||
"Computing highlights in the background for {:?}",
|
||||
entry.name
|
||||
);
|
||||
|
||||
match syntax::compute_highlights_incremental(
|
||||
&PathBuf::from(&entry.name),
|
||||
lines,
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
previous_lines,
|
||||
) {
|
||||
Ok(highlighted_lines) => {
|
||||
Some(PreviewContent::SyntectHighlightedText(highlighted_lines))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error computing highlights: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This should be enough for most terminal sizes
|
||||
const TEMP_PLAIN_TEXT_PREVIEW_HEIGHT: usize = 200;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc<Preview> {
|
||||
debug!("Creating plain text preview for {:?}", title);
|
||||
let mut lines = Vec::with_capacity(TEMP_PLAIN_TEXT_PREVIEW_HEIGHT);
|
||||
// PERF: instead of using lines(), maybe check for the length of the first line instead and
|
||||
// truncate accordingly (since this is just a temp preview)
|
||||
for maybe_line in reader.lines() {
|
||||
match maybe_line {
|
||||
Ok(line) => lines.push(preprocess_line(&line).0),
|
||||
Err(e) => {
|
||||
warn!("Error reading file: {:?}", e);
|
||||
return meta::not_supported(title);
|
||||
}
|
||||
}
|
||||
if lines.len() >= TEMP_PLAIN_TEXT_PREVIEW_HEIGHT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let total_lines = u16::try_from(lines.len()).unwrap_or(u16::MAX);
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::PlainText(lines),
|
||||
None,
|
||||
None,
|
||||
total_lines,
|
||||
))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod basic;
|
||||
pub mod command;
|
||||
pub mod env;
|
||||
pub mod files;
|
||||
pub mod meta;
|
@ -1,20 +1,15 @@
|
||||
use crate::preview::PreviewState;
|
||||
use crate::preview::{
|
||||
ansi::IntoText, PreviewContent, FILE_TOO_LARGE_MSG, LOADING_MSG,
|
||||
PREVIEW_NOT_SUPPORTED_MSG, TIMEOUT_MSG,
|
||||
ansi::IntoText, PreviewContent, LOADING_MSG, TIMEOUT_MSG,
|
||||
};
|
||||
use crate::screen::colors::{Colorscheme, PreviewColorscheme};
|
||||
use crate::utils::image::ImagePreviewWidget;
|
||||
use crate::utils::strings::{
|
||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||
EMPTY_STRING,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use devicons::FileIcon;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::widgets::{
|
||||
Block, BorderType, Borders, Padding, Paragraph, Widget, Wrap,
|
||||
};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
@ -26,22 +21,6 @@ use std::str::FromStr;
|
||||
const FILL_CHAR_SLANTED: char = '╱';
|
||||
const FILL_CHAR_EMPTY: char = ' ';
|
||||
|
||||
pub enum PreviewWidget<'a> {
|
||||
Paragraph(Paragraph<'a>),
|
||||
Image(ImagePreviewWidget),
|
||||
}
|
||||
impl Widget for PreviewWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
PreviewWidget::Paragraph(p) => p.render(area, buf),
|
||||
PreviewWidget::Image(image) => image.render(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw_preview_content_block(
|
||||
f: &mut Frame,
|
||||
@ -59,25 +38,23 @@ pub fn draw_preview_content_block(
|
||||
use_nerd_font_icons,
|
||||
)?;
|
||||
// render the preview content
|
||||
let rp = build_preview_widget(
|
||||
let rp = build_preview_paragraph(
|
||||
inner,
|
||||
&preview_state.preview.content,
|
||||
preview_state.target_line,
|
||||
preview_state.scroll,
|
||||
colorscheme,
|
||||
);
|
||||
f.render_widget(rp, inner);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_preview_widget<'a>(
|
||||
pub fn build_preview_paragraph<'a>(
|
||||
inner: Rect,
|
||||
preview_content: &'a PreviewContent,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: &'a Colorscheme,
|
||||
) -> PreviewWidget<'a> {
|
||||
) -> Paragraph<'a> {
|
||||
let preview_block =
|
||||
Block::default().style(Style::default()).padding(Padding {
|
||||
top: 0,
|
||||
@ -87,75 +64,23 @@ pub fn build_preview_widget<'a>(
|
||||
});
|
||||
|
||||
match preview_content {
|
||||
PreviewContent::AnsiText(text) => PreviewWidget::Paragraph(
|
||||
build_ansi_text_paragraph(text, preview_block, preview_scroll),
|
||||
),
|
||||
PreviewContent::PlainText(content) => {
|
||||
PreviewWidget::Paragraph(build_plain_text_paragraph(
|
||||
content,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
colorscheme.preview,
|
||||
))
|
||||
PreviewContent::AnsiText(text) => {
|
||||
build_ansi_text_paragraph(text, preview_block, preview_scroll)
|
||||
}
|
||||
PreviewContent::PlainTextWrapped(content) => PreviewWidget::Paragraph(
|
||||
build_plain_text_wrapped_paragraph(
|
||||
content,
|
||||
preview_block,
|
||||
colorscheme.preview,
|
||||
)
|
||||
.scroll((preview_scroll, 0)),
|
||||
),
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||
PreviewWidget::Paragraph(build_syntect_highlighted_paragraph(
|
||||
&highlighted_lines.lines,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
colorscheme.preview,
|
||||
inner.height,
|
||||
))
|
||||
}
|
||||
PreviewContent::Image(image) => PreviewWidget::Image(image.clone()),
|
||||
|
||||
// meta
|
||||
PreviewContent::Loading => PreviewWidget::Paragraph(
|
||||
PreviewContent::Loading => {
|
||||
build_meta_preview_paragraph(inner, LOADING_MSG, FILL_CHAR_EMPTY)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
),
|
||||
PreviewContent::NotSupported => PreviewWidget::Paragraph(
|
||||
build_meta_preview_paragraph(
|
||||
inner,
|
||||
PREVIEW_NOT_SUPPORTED_MSG,
|
||||
FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
),
|
||||
PreviewContent::FileTooLarge => PreviewWidget::Paragraph(
|
||||
build_meta_preview_paragraph(
|
||||
inner,
|
||||
FILE_TOO_LARGE_MSG,
|
||||
FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
),
|
||||
|
||||
PreviewContent::Timeout => PreviewWidget::Paragraph(
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
}
|
||||
PreviewContent::Timeout => {
|
||||
build_meta_preview_paragraph(inner, TIMEOUT_MSG, FILL_CHAR_EMPTY)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
),
|
||||
PreviewContent::Empty => {
|
||||
PreviewWidget::Paragraph(Paragraph::new(Text::raw(EMPTY_STRING)))
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
}
|
||||
PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,26 +178,6 @@ fn build_plain_text_wrapped_paragraph<'a>(
|
||||
.wrap(Wrap { trim: true })
|
||||
}
|
||||
|
||||
fn build_syntect_highlighted_paragraph<'a>(
|
||||
highlighted_lines: &'a [Vec<(syntect::highlighting::Style, String)>],
|
||||
preview_block: Block<'a>,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: PreviewColorscheme,
|
||||
height: u16,
|
||||
) -> Paragraph<'a> {
|
||||
compute_paragraph_from_highlighted_lines(
|
||||
highlighted_lines,
|
||||
target_line.map(|l| l as usize),
|
||||
preview_scroll,
|
||||
colorscheme,
|
||||
height,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
//.scroll((preview_scroll, 0))
|
||||
}
|
||||
|
||||
pub fn build_meta_preview_paragraph<'a>(
|
||||
inner: Rect,
|
||||
message: &str,
|
||||
@ -385,77 +290,3 @@ fn draw_content_outer_block(
|
||||
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
||||
Span::from(format!("{line_number:5} "))
|
||||
}
|
||||
|
||||
fn compute_paragraph_from_highlighted_lines(
|
||||
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
||||
line_specifier: Option<usize>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: PreviewColorscheme,
|
||||
height: u16,
|
||||
) -> Paragraph<'static> {
|
||||
let preview_lines: Vec<Line> = highlighted_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(preview_scroll.saturating_sub(1).into())
|
||||
.take(height.into())
|
||||
.map(|(i, l)| {
|
||||
let line_number =
|
||||
build_line_number_span(i + 1).style(Style::default().fg(
|
||||
if line_specifier.is_some()
|
||||
&& i == line_specifier.unwrap().saturating_sub(1)
|
||||
{
|
||||
colorscheme.gutter_selected_fg
|
||||
} else {
|
||||
colorscheme.gutter_fg
|
||||
},
|
||||
));
|
||||
Line::from_iter(
|
||||
std::iter::once(line_number)
|
||||
.chain(std::iter::once(Span::styled(
|
||||
" │ ",
|
||||
Style::default().fg(colorscheme.gutter_fg).dim(),
|
||||
)))
|
||||
.chain(l.iter().cloned().map(|sr| {
|
||||
convert_syn_region_to_span(
|
||||
&(sr.0, sr.1),
|
||||
if line_specifier.is_some()
|
||||
&& i == line_specifier
|
||||
.unwrap()
|
||||
.saturating_sub(1)
|
||||
{
|
||||
Some(colorscheme.highlight_bg)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Paragraph::new(preview_lines)
|
||||
}
|
||||
|
||||
pub fn convert_syn_region_to_span<'a>(
|
||||
syn_region: &(syntect::highlighting::Style, String),
|
||||
background: Option<Color>,
|
||||
) -> Span<'a> {
|
||||
let mut style = Style::default()
|
||||
.fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground));
|
||||
if let Some(background) = background {
|
||||
style = style.bg(background);
|
||||
}
|
||||
style = match syn_region.0.font_style {
|
||||
syntect::highlighting::FontStyle::BOLD => style.bold(),
|
||||
syntect::highlighting::FontStyle::ITALIC => style.italic(),
|
||||
syntect::highlighting::FontStyle::UNDERLINE => style.underlined(),
|
||||
_ => style,
|
||||
};
|
||||
Span::styled(syn_region.1.clone(), style)
|
||||
}
|
||||
|
||||
fn convert_syn_color_to_ratatui_color(
|
||||
color: syntect::highlighting::Color,
|
||||
) -> Color {
|
||||
Color::Rgb(color.r, color.g, color.b)
|
||||
}
|
||||
|
@ -294,14 +294,11 @@ pub fn draw_results_list(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::channels::preview::PreviewType;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_result_line() {
|
||||
let entry =
|
||||
Entry::new(String::from("something nice"), PreviewType::None)
|
||||
let entry = Entry::new(String::from("something nice"))
|
||||
.with_name_match_indices(
|
||||
// something nice
|
||||
// 012345678901234
|
||||
@ -331,7 +328,7 @@ mod tests {
|
||||
fn test_build_result_line_multibyte_chars() {
|
||||
let entry =
|
||||
// See https://github.com/alexpasmantier/television/issues/439
|
||||
Entry::new(String::from("ジェイムス下地 - REDLINE Original Soundtrack - 06 - ROBOWORLD TV.mp3"), PreviewType::None)
|
||||
Entry::new(String::from("ジェイムス下地 - REDLINE Original Soundtrack - 06 - ROBOWORLD TV.mp3"))
|
||||
.with_name_match_indices(&[27, 28, 29, 30, 31]);
|
||||
let result_line = build_result_line(
|
||||
&entry,
|
||||
|
@ -1,9 +1,6 @@
|
||||
use crate::action::Action;
|
||||
use crate::cable::load_cable_channels;
|
||||
use crate::channels::{
|
||||
entry::{Entry, ENTRY_PLACEHOLDER},
|
||||
preview::PreviewType,
|
||||
};
|
||||
use crate::channels::entry::{Entry, ENTRY_PLACEHOLDER};
|
||||
use crate::channels::{
|
||||
remote_control::RemoteControl, OnAir, TelevisionChannel,
|
||||
};
|
||||
@ -11,7 +8,7 @@ use crate::config::{Config, Theme};
|
||||
use crate::draw::{ChannelState, Ctx, TvState};
|
||||
use crate::input::convert_action_to_input_request;
|
||||
use crate::picker::Picker;
|
||||
use crate::preview::{Preview, PreviewState, Previewer};
|
||||
use crate::preview::{previewer::Previewer, Preview, PreviewState};
|
||||
use crate::render::UiState;
|
||||
use crate::screen::colors::Colorscheme;
|
||||
use crate::screen::layout::InputPosition;
|
||||
@ -74,7 +71,8 @@ impl Television {
|
||||
if config.ui.input_bar_position == InputPosition::Bottom {
|
||||
results_picker = results_picker.inverted();
|
||||
}
|
||||
let previewer = Previewer::new(Some(config.previewers.clone().into()));
|
||||
// FIXME: fix this
|
||||
let previewer = Previewer::new();
|
||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||
|
||||
let app_metadata = AppMetadata::new(
|
||||
@ -371,15 +369,10 @@ impl Television {
|
||||
&mut self,
|
||||
selected_entry: &Entry,
|
||||
) -> Result<()> {
|
||||
if self.config.ui.show_preview_panel
|
||||
&& self.channel.supports_preview()
|
||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||
if self.config.ui.show_preview_panel && self.channel.supports_preview()
|
||||
{
|
||||
// preview content
|
||||
if let Some(preview) = self
|
||||
.previewer
|
||||
.preview(selected_entry, self.ui_state.layout.preview_window)
|
||||
{
|
||||
if let Some(preview) = self.previewer.preview(selected_entry) {
|
||||
// only update if the preview content has changed
|
||||
if self.preview_state.preview.title != preview.title {
|
||||
self.preview_state.update(
|
||||
|
@ -1,185 +0,0 @@
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, Pixel, Rgba};
|
||||
use ratatui::buffer::{Buffer, Cell};
|
||||
use ratatui::layout::{Position, Rect};
|
||||
use ratatui::prelude::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
|
||||
static PIXEL_STRING: &str = "▀";
|
||||
const FILTER_TYPE: FilterType = FilterType::Lanczos3;
|
||||
|
||||
// use to reduce the size of the image before storing it
|
||||
const DEFAULT_CACHED_WIDTH: u32 = 50;
|
||||
const DEFAULT_CACHED_HEIGHT: u32 = 100;
|
||||
|
||||
const GRAY: Rgba<u8> = Rgba([242, 242, 242, 255]);
|
||||
const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]);
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||
pub struct ImagePreviewWidget {
|
||||
cells: Vec<Vec<Cell>>,
|
||||
}
|
||||
|
||||
impl Widget for &ImagePreviewWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let height = self.height();
|
||||
let width = self.width();
|
||||
// offset of the left top corner where the image is centered
|
||||
let total_width = usize::from(area.width) + 2 * usize::from(area.x);
|
||||
let x_offset = total_width.saturating_sub(width) / 2 + 1;
|
||||
let total_height = usize::from(area.height) + 2 * usize::from(area.y);
|
||||
let y_offset = total_height.saturating_sub(height) / 2;
|
||||
|
||||
let (area_border_up, area_border_down) =
|
||||
(area.y, area.y + area.height);
|
||||
let (area_border_left, area_border_right) =
|
||||
(area.x, area.x + area.width);
|
||||
for (y, row) in self.cells.iter().enumerate() {
|
||||
let pos_y = u16::try_from(y_offset + y).unwrap_or(u16::MAX);
|
||||
if pos_y >= area_border_up && pos_y < area_border_down {
|
||||
for (x, cell) in row.iter().enumerate() {
|
||||
let pos_x =
|
||||
u16::try_from(x_offset + x).unwrap_or(u16::MAX);
|
||||
if pos_x >= area_border_left && pos_x <= area_border_right
|
||||
{
|
||||
if let Some(buf_cell) =
|
||||
buf.cell_mut(Position::new(pos_x, pos_y))
|
||||
{
|
||||
*buf_cell = cell.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ImagePreviewWidget {
|
||||
pub fn new(cells: Vec<Vec<Cell>>) -> ImagePreviewWidget {
|
||||
ImagePreviewWidget { cells }
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.cells.len()
|
||||
}
|
||||
pub fn width(&self) -> usize {
|
||||
if self.height() > 0 {
|
||||
self.cells[0].len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dynamic_image(
|
||||
dynamic_image: DynamicImage,
|
||||
dimension: Option<(u32, u32)>,
|
||||
) -> Self {
|
||||
let (window_width, window_height) =
|
||||
dimension.unwrap_or((DEFAULT_CACHED_WIDTH, DEFAULT_CACHED_HEIGHT));
|
||||
let (max_width, max_height) = (window_width, window_height * 2 - 2); // -2 to have some space with the title
|
||||
|
||||
// first quick resize
|
||||
let big_resized_image = if dynamic_image.width() > max_width * 4
|
||||
|| dynamic_image.height() > max_height * 4
|
||||
{
|
||||
dynamic_image.resize(
|
||||
max_width * 4,
|
||||
max_height * 4,
|
||||
FilterType::Nearest,
|
||||
)
|
||||
} else {
|
||||
dynamic_image
|
||||
};
|
||||
|
||||
// this time resize with the filter
|
||||
let resized_image = if big_resized_image.width() > max_width
|
||||
|| big_resized_image.height() > max_height
|
||||
{
|
||||
big_resized_image.resize(max_width, max_height, FILTER_TYPE)
|
||||
} else {
|
||||
big_resized_image
|
||||
};
|
||||
|
||||
let cells = Self::cells_from_dynamic_image(resized_image);
|
||||
ImagePreviewWidget::new(cells)
|
||||
}
|
||||
|
||||
fn cells_from_dynamic_image(image: DynamicImage) -> Vec<Vec<Cell>> {
|
||||
let image_rgba = image.into_rgba8();
|
||||
|
||||
//creation of the grid of cell
|
||||
image_rgba
|
||||
// iter over pair of rows
|
||||
.rows()
|
||||
.step_by(2)
|
||||
.zip(image_rgba.rows().skip(1).step_by(2))
|
||||
.enumerate()
|
||||
.map(|(double_row_y, (row_1, row_2))| {
|
||||
// create rows of cells
|
||||
row_1
|
||||
.into_iter()
|
||||
.zip(row_2)
|
||||
.enumerate()
|
||||
.map(|(x, (color_up, color_down))| {
|
||||
let position = (x, double_row_y);
|
||||
DoublePixel::new(*color_up, *color_down)
|
||||
.add_grid_background(position)
|
||||
.into_cell()
|
||||
})
|
||||
.collect::<Vec<Cell>>()
|
||||
})
|
||||
.collect::<Vec<Vec<Cell>>>()
|
||||
}
|
||||
}
|
||||
|
||||
// util to convert Rgba into ratatui's Cell
|
||||
struct DoublePixel {
|
||||
color_up: Rgba<u8>,
|
||||
color_down: Rgba<u8>,
|
||||
}
|
||||
impl DoublePixel {
|
||||
pub fn new(color_up: Rgba<u8>, color_down: Rgba<u8>) -> Self {
|
||||
Self {
|
||||
color_up,
|
||||
color_down,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_grid_background(mut self, position: (usize, usize)) -> Self {
|
||||
let color_up = self.color_up.0;
|
||||
let color_down = self.color_down.0;
|
||||
self.color_up = Self::blend_with_background(color_up, position, 0);
|
||||
self.color_down = Self::blend_with_background(color_down, position, 1);
|
||||
self
|
||||
}
|
||||
|
||||
fn blend_with_background(
|
||||
color: impl Into<Rgba<u8>>,
|
||||
position: (usize, usize),
|
||||
offset: usize,
|
||||
) -> Rgba<u8> {
|
||||
let color = color.into();
|
||||
if color[3] == 255 {
|
||||
color
|
||||
} else {
|
||||
let is_white = (position.0 + position.1 * 2 + offset) % 2 == 0;
|
||||
let mut base = if is_white { WHITE } else { GRAY };
|
||||
base.blend(&color);
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_cell(self) -> Cell {
|
||||
let mut cell = Cell::new(PIXEL_STRING);
|
||||
cell.set_bg(Self::convert_image_color_to_ratatui_color(
|
||||
self.color_down,
|
||||
))
|
||||
.set_fg(Self::convert_image_color_to_ratatui_color(self.color_up));
|
||||
cell
|
||||
}
|
||||
|
||||
fn convert_image_color_to_ratatui_color(color: Rgba<u8>) -> Color {
|
||||
Color::Rgb(color[0], color[1], color[2])
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ pub mod clipboard;
|
||||
pub mod command;
|
||||
pub mod files;
|
||||
pub mod hashmaps;
|
||||
pub mod image;
|
||||
pub mod indices;
|
||||
pub mod input;
|
||||
pub mod metadata;
|
||||
@ -11,5 +10,4 @@ pub mod rocell;
|
||||
pub mod shell;
|
||||
pub mod stdin;
|
||||
pub mod strings;
|
||||
pub mod syntax;
|
||||
pub mod threads;
|
||||
|
@ -1,284 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use bat::assets::HighlightingAssets;
|
||||
use gag::Gag;
|
||||
use std::path::{Path, PathBuf};
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{
|
||||
HighlightIterator, HighlightState, Highlighter, Style, Theme,
|
||||
};
|
||||
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
||||
use tracing::warn;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HighlightingState {
|
||||
parse_state: ParseState,
|
||||
highlight_state: HighlightState,
|
||||
}
|
||||
|
||||
impl HighlightingState {
|
||||
pub fn new(
|
||||
parse_state: ParseState,
|
||||
highlight_state: HighlightState,
|
||||
) -> Self {
|
||||
Self {
|
||||
parse_state,
|
||||
highlight_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LineHighlighter<'a> {
|
||||
highlighter: Highlighter<'a>,
|
||||
pub parse_state: ParseState,
|
||||
pub highlight_state: HighlightState,
|
||||
}
|
||||
|
||||
impl<'a> LineHighlighter<'a> {
|
||||
pub fn new(
|
||||
syntax: &SyntaxReference,
|
||||
theme: &'a Theme,
|
||||
) -> LineHighlighter<'a> {
|
||||
let highlighter = Highlighter::new(theme);
|
||||
let highlight_state =
|
||||
HighlightState::new(&highlighter, ScopeStack::new());
|
||||
Self {
|
||||
highlighter,
|
||||
parse_state: ParseState::new(syntax),
|
||||
highlight_state,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_state(
|
||||
state: HighlightingState,
|
||||
theme: &'a Theme,
|
||||
) -> LineHighlighter<'a> {
|
||||
Self {
|
||||
highlighter: Highlighter::new(theme),
|
||||
parse_state: state.parse_state,
|
||||
highlight_state: state.highlight_state,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlights a line of a file
|
||||
pub fn highlight_line<'b>(
|
||||
&mut self,
|
||||
line: &'b str,
|
||||
syntax_set: &SyntaxSet,
|
||||
) -> Result<Vec<(Style, &'b str)>, syntect::Error> {
|
||||
let ops = self.parse_state.parse_line(line, syntax_set)?;
|
||||
let iter = HighlightIterator::new(
|
||||
&mut self.highlight_state,
|
||||
&ops[..],
|
||||
line,
|
||||
&self.highlighter,
|
||||
);
|
||||
Ok(iter.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
note = "Use `compute_highlights_incremental` instead, which also returns the state"
|
||||
)]
|
||||
pub fn compute_highlights_for_path(
|
||||
file_path: &Path,
|
||||
lines: &[String],
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
) -> Result<Vec<Vec<(Style, String)>>> {
|
||||
let syntax = set_syntax_set(syntax_set, file_path);
|
||||
let mut highlighter = HighlightLines::new(syntax, syntax_theme);
|
||||
let mut highlighted_lines = Vec::new();
|
||||
for line in lines {
|
||||
let hl_regions = highlighter.highlight_line(line, syntax_set)?;
|
||||
highlighted_lines.push(
|
||||
hl_regions
|
||||
.iter()
|
||||
.map(|(style, text)| (*style, (*text).to_string()))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Ok(highlighted_lines)
|
||||
}
|
||||
|
||||
fn set_syntax_set<'a>(
|
||||
syntax_set: &'a SyntaxSet,
|
||||
file_path: &Path,
|
||||
) -> &'a SyntaxReference {
|
||||
syntax_set
|
||||
.find_syntax_for_file(file_path)
|
||||
.unwrap_or(None)
|
||||
.unwrap_or_else(|| {
|
||||
warn!(
|
||||
"No syntax found for {:?}, defaulting to plain text",
|
||||
file_path
|
||||
);
|
||||
syntax_set.find_syntax_plain_text()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct HighlightedLines {
|
||||
pub lines: Vec<Vec<(Style, String)>>,
|
||||
//pub state: Option<HighlightingState>,
|
||||
}
|
||||
|
||||
impl HighlightedLines {
|
||||
pub fn new(
|
||||
lines: Vec<Vec<(Style, String)>>,
|
||||
_state: &Option<HighlightingState>,
|
||||
) -> Self {
|
||||
Self { lines, /*state*/ }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_highlights_incremental(
|
||||
file_path: &Path,
|
||||
lines: &[String],
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
_cached_lines: Option<&HighlightedLines>,
|
||||
) -> Result<HighlightedLines> {
|
||||
let mut highlighted_lines: Vec<_>;
|
||||
let mut highlighter: LineHighlighter;
|
||||
|
||||
//if let Some(HighlightedLines {
|
||||
// lines: c_lines,
|
||||
// state: Some(s),
|
||||
//}) = cached_lines
|
||||
//{
|
||||
// highlighter = LineHighlighter::from_state(s, syntax_theme);
|
||||
// highlighted_lines = c_lines;
|
||||
//} else {
|
||||
// let syntax = set_syntax_set(syntax_set, file_path);
|
||||
// highlighter = LineHighlighter::new(syntax, syntax_theme);
|
||||
// highlighted_lines = Vec::new();
|
||||
//};
|
||||
let syntax = set_syntax_set(syntax_set, file_path);
|
||||
highlighter = LineHighlighter::new(syntax, syntax_theme);
|
||||
highlighted_lines = Vec::with_capacity(lines.len());
|
||||
|
||||
for line in lines {
|
||||
let hl_regions = highlighter.highlight_line(line, syntax_set)?;
|
||||
highlighted_lines.push(
|
||||
hl_regions
|
||||
.iter()
|
||||
.map(|(style, text)| (*style, (*text).to_string()))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(HighlightedLines::new(
|
||||
highlighted_lines,
|
||||
&Some(HighlightingState::new(
|
||||
highlighter.parse_state.clone(),
|
||||
highlighter.highlight_state.clone(),
|
||||
)),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn compute_highlights_for_line<'a>(
|
||||
line: &'a str,
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
file_path: &str,
|
||||
) -> Result<Vec<(Style, &'a str)>> {
|
||||
let syntax = syntax_set.find_syntax_for_file(file_path)?;
|
||||
match syntax {
|
||||
None => {
|
||||
warn!(
|
||||
"No syntax found for path {:?}, defaulting to plain text",
|
||||
file_path
|
||||
);
|
||||
Ok(vec![(Style::default(), line)])
|
||||
}
|
||||
Some(syntax) => {
|
||||
let mut highlighter = HighlightLines::new(syntax, syntax_theme);
|
||||
Ok(highlighter.highlight_line(line, syntax_set)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28
|
||||
//
|
||||
// Copyright (c) 2018-2023 bat-developers (https://github.com/sharkdp/bat).
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
use directories::BaseDirs;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::env;
|
||||
|
||||
/// Wrapper for 'dirs' that treats `MacOS` more like `Linux`, by following the XDG specification.
|
||||
///
|
||||
/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are
|
||||
/// checked first. The fallback directories are `~/.cache/bat` and `~/.config/bat`, respectively.
|
||||
pub struct BatProjectDirs {
|
||||
cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl BatProjectDirs {
|
||||
fn new() -> Option<BatProjectDirs> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let cache_dir_op = env::var_os("XDG_CACHE_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|p| p.is_absolute())
|
||||
.or_else(|| BaseDirs::new().map(|d| d.home_dir().join(".cache")));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let cache_dir_op = BaseDirs::new().map(|d| d.cache_dir().to_owned());
|
||||
|
||||
let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
|
||||
|
||||
Some(BatProjectDirs { cache_dir })
|
||||
}
|
||||
|
||||
pub fn cache_dir(&self) -> &Path {
|
||||
&self.cache_dir
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_highlighting_assets() -> HighlightingAssets {
|
||||
let project_dirs = BatProjectDirs::new()
|
||||
.unwrap_or_else(|| panic!("Could not get home directory"));
|
||||
|
||||
HighlightingAssets::from_cache(project_dirs.cache_dir())
|
||||
.unwrap_or_else(|_| HighlightingAssets::from_binary())
|
||||
}
|
||||
|
||||
pub trait HighlightingAssetsExt {
|
||||
fn get_theme_no_output(&self, theme_name: &str) -> &Theme;
|
||||
}
|
||||
|
||||
impl HighlightingAssetsExt for HighlightingAssets {
|
||||
/// Get a theme by name. If the theme is not found, the default theme is returned.
|
||||
///
|
||||
/// This is an ugly hack to work around the fact that bat actually prints a warning
|
||||
/// to stderr when a theme is not found which might mess up the TUI. This function
|
||||
/// suppresses that warning by temporarily redirecting stderr and stdout.
|
||||
fn get_theme_no_output(&self, theme_name: &str) -> &Theme {
|
||||
let _e = Gag::stderr();
|
||||
let _o = Gag::stdout();
|
||||
let theme = self.get_theme(theme_name);
|
||||
theme
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user