summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2024-07-06 19:05:22 +0200
committerCanop <cano.petrole@gmail.com>2024-07-06 19:05:22 +0200
commit849540b0c5d40db5bee802b27b0334859d89df37 (patch)
tree07c2e0d3b693a9cf3298c48ce7b11f4e2717735f
parentf0ebd2c523a6fb5d56fce5e26241d6ba3bd03346 (diff)
allow definition of transformers for preview
-rw-r--r--resources/default-conf/conf.hjson28
-rw-r--r--src/app/app_context.rs7
-rw-r--r--src/conf/conf.rs15
-rw-r--r--src/preview/mod.rs7
-rw-r--r--src/preview/preview_state.rs52
-rw-r--r--src/preview/preview_transformer.rs162
6 files changed, 251 insertions, 20 deletions
diff --git a/resources/default-conf/conf.hjson b/resources/default-conf/conf.hjson
index 29f8909..ba68194 100644
--- a/resources/default-conf/conf.hjson
+++ b/resources/default-conf/conf.hjson
@@ -217,6 +217,34 @@ lines_before_match_in_preview: 1
lines_after_match_in_preview: 1
###############################################################
+# transformations before preview
+#
+# It's possible to define transformations to apply to some files
+# before calling one of the default preview renderers in broot.
+# Below are two examples that you may uncomment and adapt:
+#
+preview_transformers: [
+ // # Use mutool to render any PDF file as an image
+ // # In this example we use placeholders for the input and output files
+ // {
+ // input_extensions: [ "pdf" ] // case doesn't matter
+ // output_extension: png
+ // mode: image
+ // command: [ "mutool", "draw", "-o", "{output-path}", "{input-path}" ]
+ // }
+
+ // # Use jq to beautify JSON
+ // # In this example, the command refers to neither the input nor the output,
+ // # so broot pipes them to the stdin and stdout of the jq process
+ // {
+ // input_extensions: [ "json" ]
+ // output_extension: json
+ // mode: text
+ // command: [ "jq" ]
+ // }
+]
+
+###############################################################
# Imports
#
# While it's possible to have all configuration in one file,
diff --git a/src/app/app_context.rs b/src/app/app_context.rs
index b772c02..b9e717d 100644
--- a/src/app/app_context.rs
+++ b/src/app/app_context.rs
@@ -11,6 +11,7 @@ use {
kitty::TransmissionMedium,
pattern::SearchModeMap,
path::SpecialPaths,
+ preview::PreviewTransformers,
skin::ExtColorMap,
syntactic::SyntaxTheme,
tree::TreeOptions,
@@ -127,6 +128,9 @@ pub struct AppContext {
/// Number of lines to display before a match in the preview
pub lines_before_match_in_preview: usize,
+
+ /// The set of transformers called before previewing a file
+ pub preview_transformers: PreviewTransformers,
}
impl AppContext {
@@ -197,6 +201,8 @@ impl AppContext {
let terminal_title_pattern = config.terminal_title.clone();
+ let preview_transformers = PreviewTransformers::new(&config.preview_transformers)?;
+
Ok(Self {
is_tty,
initial_root,
@@ -229,6 +235,7 @@ impl AppContext {
.unwrap_or_default(),
lines_after_match_in_preview: config.lines_after_match_in_preview.unwrap_or(0),
lines_before_match_in_preview: config.lines_before_match_in_preview.unwrap_or(0),
+ preview_transformers,
})
}
/// Return the --cmd argument, coming from the launch arguments (prefered)
diff --git a/src/conf/conf.rs b/src/conf/conf.rs
index 77a4d26..a1d4938 100644
--- a/src/conf/conf.rs
+++ b/src/conf/conf.rs
@@ -12,6 +12,7 @@ use {
path_from,
PathAnchor,
},
+ preview::PreviewTransformer,
skin::SkinEntry,
syntactic::SyntaxTheme,
verb::ExecPattern,
@@ -39,6 +40,14 @@ macro_rules! overwrite_map {
};
}
+macro_rules! overwrite_vec {
+ ($dst: ident, $prop: ident, $src: ident) => {
+ for v in $src.$prop {
+ $dst.$prop.push(v);
+ }
+ };
+}
+
/// The configuration read from conf.toml or conf.hjson file(s)
#[derive(Default, Clone, Debug, Deserialize)]
pub struct Conf {
@@ -86,6 +95,9 @@ pub struct Conf {
#[serde(alias="kitty-graphics-transmission")]
pub kitty_graphics_transmission: Option<TransmissionMedium>,
+ #[serde(default, alias="preview-transformers")]
+ pub preview_transformers: Vec<PreviewTransformer>,
+
#[serde(alias="lines-after-match-in-preview")]
pub lines_after_match_in_preview: Option<usize>,
@@ -221,10 +233,11 @@ impl Conf {
overwrite!(self, lines_after_match_in_preview, conf);
overwrite!(self, lines_before_match_in_preview, conf);
self.verbs.append(&mut conf.verbs);
- // the following maps are "additive": we can add entries from several
+ // the following prefs are "additive": we can add entries from several
// config files and they still make sense
overwrite_map!(self, special_paths, conf);
overwrite_map!(self, ext_colors, conf);
+ overwrite_vec!(self, preview_transformers, conf);
self.files.push(path);
// read the imports
for import in &conf.imports {
diff --git a/src/preview/mod.rs b/src/preview/mod.rs
index 6fcd812..aabccd5 100644
--- a/src/preview/mod.rs
+++ b/src/preview/mod.rs
@@ -1,23 +1,26 @@
mod dir_view;
mod preview;
+mod preview_transformer;
mod preview_state;
mod zero_len_file_view;
pub use {
dir_view::DirView,
preview::Preview,
+ preview_transformer::*,
preview_state::PreviewState,
zero_len_file_view::ZeroLenFileView,
};
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
pub enum PreviewMode {
/// image
Image,
/// show the content as text, with syntax coloring if
- /// it makes sens. Fails if the file isn't in UTF8
+ /// it makes sense. Fails if the file isn't in UTF8
Text,
/// show the content of the file as hex
diff --git a/src/preview/preview_state.rs b/src/preview/preview_state.rs
index 470169a..eb180be 100644
--- a/src/preview/preview_state.rs
+++ b/src/preview/preview_state.rs
@@ -26,7 +26,8 @@ use {
pub struct PreviewState {
pub preview_area: Area,
dirty: bool, // true when background must be cleared
- path: PathBuf, // path to the previewed file
+ source_path: PathBuf, // path to the file whose preview is requested
+ transform: Option<PreviewTransform>,
preview: Preview,
pending_pattern: InputPattern, // a pattern (or not) which has not yet be applied
filtered_preview: Option<Preview>,
@@ -38,18 +39,21 @@ pub struct PreviewState {
impl PreviewState {
pub fn new(
- path: PathBuf,
+ source_path: PathBuf,
pending_pattern: InputPattern,
preferred_mode: Option<PreviewMode>,
tree_options: TreeOptions,
con: &AppContext,
) -> PreviewState {
let preview_area = Area::uninitialized(); // will be fixed at drawing time
- let preview = Preview::new(&path, preferred_mode, con);
+ let transform = con.preview_transformers.transform(&source_path, preferred_mode);
+ let preview_path = transform.as_ref().map(|c| &c.output_path).unwrap_or(&source_path);
+ let preview = Preview::new(preview_path, preferred_mode, con);
PreviewState {
preview_area,
dirty: true,
- path,
+ source_path,
+ transform,
preview,
pending_pattern,
filtered_preview: None,
@@ -59,6 +63,9 @@ impl PreviewState {
mode: con.initial_mode(),
}
}
+ pub fn preview_path(&self) -> &Path {
+ self.transform.as_ref().map(|c| &c.output_path).unwrap_or(&self.source_path)
+ }
fn vis_preview(&self) -> &Preview {
self.filtered_preview.as_ref().unwrap_or(&self.preview)
}
@@ -73,7 +80,7 @@ impl PreviewState {
if self.preview.get_mode() == Some(mode) {
return Ok(CmdResult::Keep);
}
- Ok(match Preview::with_mode(&self.path, mode, con) {
+ Ok(match Preview::with_mode(self.preview_path(), mode, con) {
Ok(preview) => {
self.preview = preview;
self.preferred_mode = Some(mode);
@@ -88,11 +95,20 @@ impl PreviewState {
}
fn no_opt_selection(&self) -> Selection<'_> {
- Selection {
- path: &self.path,
- stype: SelectionType::File,
- is_exe: false, // not always true. It means :open_leave won't execute it
- line: self.vis_preview().get_selected_line_number().unwrap_or(0),
+ match self.transform.as_ref() {
+ // When there's a transform, we can't assume the line number makes sense
+ Some(transform) => Selection {
+ path: &transform.output_path,
+ stype: SelectionType::File,
+ is_exe: false,
+ line: 0,
+ },
+ None => Selection {
+ path: &self.source_path,
+ stype: SelectionType::File,
+ is_exe: false,
+ line: self.vis_preview().get_selected_line_number().unwrap_or(0),
+ },
}
}
@@ -159,7 +175,7 @@ impl PanelState for PreviewState {
self.filtered_preview = time!(
Info,
"preview filtering",
- self.preview.filtered(&self.path, pattern, dam, con),
+ self.preview.filtered(self.preview_path(), pattern, dam, con),
); // can be None if a cancellation was required
if let Some(ref mut filtered_preview) = self.filtered_preview {
if let Some(number) = old_selection {
@@ -171,11 +187,11 @@ impl PanelState for PreviewState {
}
fn selected_path(&self) -> Option<&Path> {
- Some(&self.path)
+ Some(&self.source_path)
}
fn set_selected_path(&mut self, path: PathBuf, con: &AppContext) {
- let selected_line_number = if self.path == path {
+ let selected_line_number = if self.preview_path() == path {
self.preview.get_selected_line_number()
} else {
None
@@ -183,11 +199,13 @@ impl PanelState for PreviewState {
if let Some(fp) = &self.filtered_preview {
self.pending_pattern = fp.pattern();
};
- self.preview = Preview::new(&path, self.preferred_mode, con);
+ self.transform = con.preview_transformers.transform(&path, self.preferred_mode);
+ let preview_path = self.transform.as_ref().map(|c| &c.output_path).unwrap_or(&path);
+ self.preview = Preview::new(preview_path, self.preferred_mode, con);
if let Some(number) = selected_line_number {
self.preview.try_select_line_number(number);
}
- self.path = path;
+ self.source_path = path;
}
fn selection(&self) -> Option<Selection<'_>> {
@@ -211,7 +229,7 @@ impl PanelState for PreviewState {
fn refresh(&mut self, _screen: Screen, con: &AppContext) -> Command {
self.dirty = true;
- self.set_selected_path(self.path.clone(), con);
+ self.set_selected_path(self.source_path.clone(), con);
Command::empty()
}
@@ -255,7 +273,7 @@ impl PanelState for PreviewState {
w.queue(cursor::MoveTo(state_area.left, 0))?;
let mut cw = CropWriter::new(w, state_area.width as usize);
let file_name = self
- .path
+ .source_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "???".to_string());
diff --git a/src/preview/preview_transformer.rs b/src/preview/preview_transformer.rs
new file mode 100644
index 0000000..1a828cc
--- /dev/null
+++ b/src/preview/preview_transformer.rs
@@ -0,0 +1,162 @@
+use {
+ crate::{
+ errors::*,
+ preview::PreviewMode,
+ },
+ serde::Deserialize,
+ std::{
+ hash::{
+ DefaultHasher,
+ Hash,
+ Hasher,
+ },
+ path::{
+ Path,
+ PathBuf,
+ },
+ process::Command,
+ },
+ tempfile::TempDir,
+};
+
+#[derive(Debug, Clone, Copy)]
+pub struct TransformerId {
+ idx: usize,
+}
+pub struct PreviewTransformers {
+ transformers: Vec<PreviewTransformer>,
+ /// Where the output files are temporarily stored
+ temp_dir: TempDir,
+}
+#[derive(Debug, Clone, Deserialize)]
+pub struct PreviewTransformer {
+ pub input_extensions: Vec<String>,
+ pub output_extension: String,
+ /// The command generating an output file from an input file
+ /// eg "mutool draw -o {output-path} {input-path}"
+ pub command: Vec<String>,
+ pub mode: PreviewMode,
+}
+pub struct PreviewTransform {
+ pub transformer_id: TransformerId,
+ pub output_path: PathBuf,
+}
+
+impl PreviewTransformers {
+ pub fn new(transformers: &[PreviewTransformer]) -> Result<Self, ConfError> {
+ let transformers = transformers.to_vec();
+ for transformer in &transformers {
+ if transformer.command.is_empty() {
+ return Err(ConfError::MissingField {
+ txt: "empty command in preview transformer".to_string(),
+ });
+ }
+ }
+ let temp_dir = tempfile::Builder::new()
+ .prefix("broot-conversions")
+ .tempdir()?;
+ Ok(Self {
+ transformers,
+ temp_dir,
+ })
+ }
+ pub fn transformer(
+ &self,
+ id: TransformerId,
+ ) -> &PreviewTransformer {
+ &self.transformers[id.idx]
+ }
+ pub fn transform(
+ &self,
+ input_path: &Path,
+ mode: Option<PreviewMode>,
+ ) -> Option<PreviewTransform> {
+ let transformer_id = self.find_transformer_for(input_path, mode)?;
+ let temp_dir = self.temp_dir.path();
+ match self.transformers[transformer_id.idx].transform(input_path, temp_dir) {
+ Ok(output_path) => Some(PreviewTransform {
+ transformer_id,
+ output_path,
+ }),
+ Err(e) => {
+ error!(
+ "conversion failed using {:?}",
+ self.transformers[transformer_id.idx].command
+ );
+ error!("conversion error: {:?}", e);
+ None
+ }
+ }
+ }
+ pub fn find_transformer_for(
+ &self,
+ path: &Path,
+ mode: Option<PreviewMode>,
+ ) -> Option<TransformerId> {
+ let extension = path.extension().and_then(|ext| ext.to_str())?;
+ for (idx, transformer) in self.transformers.iter().enumerate() {
+ if !transformer
+ .input_extensions
+ .iter()
+ .any(|ext| ext.eq_ignore_ascii_case(extension))
+ {
+ continue;
+ }
+ if let Some(mode) = mode {
+ if transformer.mode != mode {
+ continue;
+ }
+ }
+ return Some(TransformerId { idx });
+ }
+ None
+ }
+}
+
+impl PreviewTransformer {
+ pub fn transform(
+ &self,
+ input_path: &Path,
+ temp_dir: &Path,
+ ) -> Result<PathBuf, ProgramError> {
+ let hash = {
+ let mut hasher = DefaultHasher::new();
+ input_path.hash(&mut hasher);
+ hasher.finish()
+ };
+ let output_path = temp_dir.join(format!("{:x}.{}", hash, self.output_extension,));
+ if output_path.exists() {
+ return Ok(output_path);
+ }
+
+ let explicit_input = self.command.iter().any(|c| c.contains("{input-path}"));
+ let explicit_output = self.command.iter().any(|c| c.contains("{output-path}"));
+
+ let mut command = self.command.iter().map(|part| {
+ part.replace("{input-path}", &input_path.to_string_lossy())
+ .replace("{output-path}", &output_path.to_string_lossy())
+ });
+ info!("transforming {:?} to {:?}", input_path, output_path);
+ let executable = command.next().unwrap();
+ let mut process = Command::new(executable);
+ process.stderr(std::process::Stdio::null());
+ process.args(command);
+ if !explicit_input {
+ process.stdin(std::fs::File::open(input_path)?);
+ }
+ if explicit_output {
+ process.stdout(std::process::Stdio::null());
+ } else {
+ process.stdout(std::fs::File::create(&output_path)?);
+ }
+ let res = process
+ .spawn()
+ .and_then(|mut p| p.wait())
+ .map_err(|source| ProgramError::LaunchError {
+ program: self.command[0].clone(),
+ source,
+ });
+ info!("conversion result: {:?}", res);
+ Ok(output_path)
+ }
+}