diff options
author | Canop <cano.petrole@gmail.com> | 2024-07-06 19:05:22 +0200 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2024-07-06 19:05:22 +0200 |
commit | 849540b0c5d40db5bee802b27b0334859d89df37 (patch) | |
tree | 07c2e0d3b693a9cf3298c48ce7b11f4e2717735f | |
parent | f0ebd2c523a6fb5d56fce5e26241d6ba3bd03346 (diff) |
allow definition of transformers for preview
-rw-r--r-- | resources/default-conf/conf.hjson | 28 | ||||
-rw-r--r-- | src/app/app_context.rs | 7 | ||||
-rw-r--r-- | src/conf/conf.rs | 15 | ||||
-rw-r--r-- | src/preview/mod.rs | 7 | ||||
-rw-r--r-- | src/preview/preview_state.rs | 52 | ||||
-rw-r--r-- | src/preview/preview_transformer.rs | 162 |
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) + } +} |