wip: dropping builtin previewers

This commit is contained in:
Alexandre Pasmantier 2025-05-01 23:58:04 +02:00
parent 1086899ba7
commit 59bba56ab1
26 changed files with 355 additions and 2852 deletions

1085
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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]

1
TODO.md Normal file
View File

@ -0,0 +1 @@
- checkout the `which` crate for searching binaries

View File

@ -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
)]))
);
}

View File

@ -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()
}
}

View File

@ -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");
}

View File

@ -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: &regex::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'");
}
}

View File

@ -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)
})
}

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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"))]

View File

@ -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 {}

View File

@ -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,
))
}

View File

@ -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));
}
}

View 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);
}

View File

@ -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,
})
}
}

View File

@ -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, &regex::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: &regex::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'");
}
}

View File

@ -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()
}

View File

@ -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,
))
}

View File

@ -1,5 +0,0 @@
pub mod basic;
pub mod command;
pub mod env;
pub mod files;
pub mod meta;

View File

@ -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)
}

View File

@ -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,

View File

@ -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(

View File

@ -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])
}
}

View File

@ -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;

View File

@ -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
}
}