diff options
Diffstat (limited to 'src/preview.rs')
-rw-r--r-- | src/preview.rs | 270 |
1 files changed, 219 insertions, 51 deletions
diff --git a/src/preview.rs b/src/preview.rs index 40b1793..7fa0721 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -8,6 +8,7 @@ use std::panic; use std::path::{Path, PathBuf}; use std::slice::Iter; +use anyhow::{anyhow, Context, Result}; use content_inspector::{inspect, ContentType}; use log::info; use pdf_extract; @@ -23,7 +24,6 @@ use crate::content_window::ContentWindow; use crate::decompress::list_files_zip; use crate::fileinfo::{FileInfo, FileKind}; use crate::filter::FilterKind; -use crate::fm_error::{FmError, FmResult}; use crate::opener::execute_and_capture_output_without_check; use crate::status::Status; use crate::tree::{ColoredString, Tree}; @@ -42,7 +42,9 @@ pub enum Preview { Ueberzug(Ueberzug), Media(MediaContent), Directory(Directory), + Iso(Iso), Diff(Diff), + ColoredText(ColoredText), #[default] Empty, } @@ -67,7 +69,7 @@ impl Preview { users_cache: &UsersCache, status: &Status, colors: &Colors, - ) -> FmResult<Self> { + ) -> Result<Self> { match file_info.file_kind { FileKind::Directory => Ok(Self::Directory(Directory::new( &file_info.path, @@ -85,15 +87,24 @@ impl Preview { e if is_ext_video(e) => { Ok(Self::Ueberzug(Ueberzug::video_thumbnail(&file_info.path)?)) } + e if is_ext_font(e) => { + Ok(Self::Ueberzug(Ueberzug::font_thumbnail(&file_info.path)?)) + } + e if is_ext_svg(e) => Ok(Self::Ueberzug(Ueberzug::svg_thumbnail(&file_info.path)?)), + e if is_ext_iso(e) => Ok(Self::Iso(Iso::new(&file_info.path)?)), + e if is_ext_notebook(e) => { + Ok(Self::notebook(&file_info.path) + .context("Preview: Couldn't parse notebook")?) + } + e if is_ext_doc(e) => { + Ok(Self::doc(&file_info.path).context("Preview: Couldn't parse doc")?) + } e => match Self::preview_syntaxed(e, &file_info.path) { Some(syntaxed_preview) => Ok(syntaxed_preview), None => Self::preview_text_or_binary(file_info), }, }, - _ => Err(FmError::custom( - "new preview", - "Can't preview this filekind", - )), + _ => Err(anyhow!("new preview: can't preview this filekind",)), } } @@ -116,7 +127,33 @@ impl Preview { }) } - fn preview_text_or_binary(file_info: &FileInfo) -> FmResult<Self> { + fn notebook(path: &Path) -> Option<Self> { + let path_str = path.to_str()?; + let output = execute_and_capture_output_without_check( + "jupyter", + &vec!["nbconvert", "--to", "markdown", path_str, "--stdout"], + ) + .ok()?; + let ss = SyntaxSet::load_defaults_nonewlines(); + ss.find_syntax_by_extension("md").map(|syntax| { + Self::Syntaxed(HLContent::from_str(&output, ss.clone(), syntax).unwrap_or_default()) + }) + } + + fn doc(path: &Path) -> Option<Self> { + let path_str = path.to_str()?; + let output = execute_and_capture_output_without_check( + "pandoc", + &vec!["-s", "-t", "markdown", "--", path_str], + ) + .ok()?; + let ss = SyntaxSet::load_defaults_nonewlines(); + ss.find_syntax_by_extension("md").map(|syntax| { + Self::Syntaxed(HLContent::from_str(&output, ss.clone(), syntax).unwrap_or_default()) + }) + } + + fn preview_text_or_binary(file_info: &FileInfo) -> Result<Self> { let mut file = std::fs::File::open(file_info.path.clone())?; let mut buffer = vec![0; Self::CONTENT_INSPECTOR_MIN_SIZE]; if Self::is_binary(file_info, &mut file, &mut buffer) { @@ -133,11 +170,11 @@ impl Preview { } /// Returns mediainfo of a media file. - pub fn mediainfo(path: &Path) -> FmResult<Self> { + pub fn mediainfo(path: &Path) -> Result<Self> { Ok(Self::Media(MediaContent::new(path)?)) } - pub fn diff(first_path: &str, second_path: &str) -> FmResult<Self> { + pub fn diff(first_path: &str, second_path: &str) -> Result<Self> { Ok(Self::Diff(Diff::new(first_path, second_path)?)) } @@ -150,6 +187,10 @@ impl Preview { Self::Text(TextContent::log(log)) } + pub fn cli_info(output: &str) -> Self { + Self::ColoredText(ColoredText::new(output)) + } + /// Empty preview, holding nothing. pub fn new_empty() -> Self { Self::Empty @@ -169,6 +210,8 @@ impl Preview { Self::Media(media) => media.len(), Self::Directory(directory) => directory.len(), Self::Diff(diff) => diff.len(), + Self::Iso(iso) => iso.len(), + Self::ColoredText(text) => text.len(), } } @@ -207,7 +250,7 @@ impl TextContent { } } - fn from_file(path: &Path) -> FmResult<Self> { + fn from_file(path: &Path) -> Result<Self> { let reader = std::io::BufReader::new(std::fs::File::open(path)?); let content: Vec<String> = reader .lines() @@ -241,7 +284,7 @@ impl HLContent { /// It may file if the file isn't properly formatted or the extension /// is wrong (ie. python content with .c extension). /// ATM only Solarized (dark) theme is supported. - fn new(path: &Path, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> FmResult<Self> { + fn new(path: &Path, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> Result<Self> { let reader = std::io::BufReader::new(std::fs::File::open(path)?); let raw_content: Vec<String> = reader .lines() @@ -256,6 +299,15 @@ impl HLContent { }) } + fn from_str(text: &str, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> Result<Self> { + let raw_content = text.lines().map(|s| s.to_owned()).collect(); + let highlighted_content = Self::parse_raw_content(raw_content, syntax_set, syntax_ref)?; + Ok(Self { + length: highlighted_content.len(), + content: highlighted_content, + }) + } + fn len(&self) -> usize { self.length } @@ -264,7 +316,7 @@ impl HLContent { raw_content: Vec<String>, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference, - ) -> FmResult<Vec<Vec<SyntaxedString>>> { + ) -> Result<Vec<Vec<SyntaxedString>>> { let mut monokai = BufReader::new(Cursor::new(include_bytes!( "../assets/themes/Monokai_Extended.tmTheme" ))); @@ -315,7 +367,7 @@ impl SyntaxedString { canvas: &mut dyn tuikit::canvas::Canvas, row: usize, offset: usize, - ) -> FmResult<()> { + ) -> Result<()> { canvas.print_with_attr(row, self.col + offset + 2, &self.content, self.attr)?; Ok(()) } @@ -336,7 +388,7 @@ impl BinaryContent { const LINE_WIDTH: usize = 16; const SIZE_LIMIT: usize = 1048576; - fn new(file_info: &FileInfo) -> FmResult<Self> { + fn new(file_info: &FileInfo) -> Result<Self> { let mut reader = BufReader::new(std::fs::File::open(file_info.path.clone())?); let mut buffer = [0; Self::LINE_WIDTH]; let mut content: Vec<Line> = vec![]; @@ -452,7 +504,7 @@ pub struct ZipContent { } impl ZipContent { - fn new(path: &Path) -> FmResult<Self> { + fn new(path: &Path) -> Result<Self> { let content = list_files_zip(path).unwrap_or(vec!["Invalid Zip content".to_owned()]); Ok(Self { @@ -476,7 +528,7 @@ pub struct MediaContent { } impl MediaContent { - fn new(path: &Path) -> FmResult<Self> { + fn new(path: &Path) -> Result<Self> { let content: Vec<String>; if let Ok(output) = std::process::Command::new("mediainfo").arg(path).output() { let s = String::from_utf8(output.stdout).unwrap_or_default(); @@ -507,11 +559,11 @@ pub struct Ueberzug { } impl Ueberzug { - fn image(img_path: &Path) -> FmResult<Self> { + fn image(img_path: &Path) -> Result<Self> { let filename = filename_from_path(img_path)?.to_owned(); let path = img_path .to_str() - .ok_or_else(|| FmError::custom("ueberzug", "Couldn't parse the path into a string"))? + .context("ueberzug: couldn't parse the path into a string")? .to_owned(); Ok(Self { path, @@ -520,21 +572,48 @@ impl Ueberzug { }) } - fn video_thumbnail(video_path: &Path) -> FmResult<Self> { - Self::make_thumbnail(video_path)?; - Ok(Self { + fn thumbnail() -> Self { + Self { path: THUMBNAIL_PATH.to_owned(), filename: "thumbnail".to_owned(), ueberzug: ueberzug::Ueberzug::new(), - }) + } + } + + fn video_thumbnail(video_path: &Path) -> Result<Self> { + Self::make_video_thumbnail(video_path)?; + Ok(Self::thumbnail()) + } + + fn font_thumbnail(font_path: &Path) -> Result<Self> { + Self::make_font_thumbnail(font_path)?; + Ok(Self::thumbnail()) + } + + fn svg_thumbnail(svg_path: &Path) -> Result<Self> { + Self::make_svg_thumbnail(svg_path)?; + Ok(Self::thumbnail()) + } + + fn make_thumbnail(exe: &str, args: &[&str]) -> Result<()> { + let output = std::process::Command::new(exe).args(args).output()?; + if !output.stderr.is_empty() { + info!( + "make thumbnail output: {} {}", + String::from_utf8(output.stdout).unwrap_or_default(), + String::from_utf8(output.stderr).unwrap_or_default() + ); + } + Ok(()) } - fn make_thumbnail(video_path: &Path) -> FmResult<()> { - let path_str = video_path.to_str().ok_or_else(|| { - FmError::custom("make_thumbnail", "Couldn't parse the path into a string") - })?; - let output = std::process::Command::new("ffmpeg") - .args([ + fn make_video_thumbnail(video_path: &Path) -> Result<()> { + let path_str = video_path + .to_str() + .context("make_thumbnail: couldn't parse the path into a string")?; + Self::make_thumbnail( + "ffmpeg", + &[ "-i", path_str, "-vf", @@ -543,16 +622,25 @@ impl Ueberzug { "1", THUMBNAIL_PATH, "-y", - ]) - .output()?; - if !output.stderr.is_empty() { - info!( - "ffmpeg thumbnail output: {} {}", - String::from_utf8(output.stdout).unwrap_or_default(), - String::from_utf8(output.stderr).unwrap_or_default() - ); - } - Ok(()) + ], + ) + } + + fn make_font_thumbnail(font_path: &Path) -> Result<()> { + let path_str = font_path + .to_str() + .context("make_thumbnail: couldn't parse the path into a string")?; + Self::make_thumbnail("fontimage", &["-o", THUMBNAIL_PATH, path_str]) + } + + fn make_svg_thumbnail(svg_path: &Path) -> Result<()> { + let path_str = svg_path + .to_str() + .context("make_thumbnail: couldn't parse the path into a string")?; + Self::make_thumbnail( + "rsvg-convert", + &["--keep-aspect-ratio", path_str, "-o", THUMBNAIL_PATH], + ) } /// Draw the image with ueberzug in the current window. @@ -572,6 +660,35 @@ impl Ueberzug { } } +#[derive(Clone, Debug)] +pub struct ColoredText { + pub content: Vec<String>, + len: usize, + pub selected_index: usize, +} + +impl ColoredText { + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Make a new previewed colored text. + pub fn new(output: &str) -> Self { + let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect(); + let len = content.len(); + let selected_index = 0; + Self { + content, + len, + selected_index, + } + } +} + /// Display a tree view of a directory. /// The "tree view" is calculated recursively. It may take some time /// if the directory has a lot of children. @@ -593,7 +710,7 @@ impl Directory { filter_kind: &FilterKind, show_hidden: bool, max_depth: Option<usize>, - ) -> FmResult<Self> { + ) -> Result<Self> { let max_depth = match max_depth { Some(max_depth) => max_depth, None => Tree::MAX_DEPTH, @@ -618,7 +735,7 @@ impl Directory { } /// Creates an empty directory preview. - pub fn empty(path: &Path, users_cache: &UsersCache) -> FmResult<Self> { + pub fn empty(path: &Path, users_cache: &UsersCache) -> Result<Self> { Ok(Self { tree: Tree::empty(path, users_cache)?, len: 0, @@ -646,7 +763,7 @@ impl Directory { } /// Select the root node and reset the view. - pub fn select_root(&mut self, colors: &Colors) -> FmResult<()> { + pub fn select_root(&mut self, colors: &Colors) -> Result<()> { self.tree.select_root(); (self.selected_index, self.content) = self.tree.into_navigable_content(colors); Ok(()) @@ -659,7 +776,7 @@ impl Directory { /// Select the "next" element of the tree if any. /// This is the element immediatly below the current one. - pub fn select_next(&mut self, colors: &Colors) -> FmResult<()> { + pub fn select_next(&mut self, colors: &Colors) -> Result<()> { if self.selected_index + 1 < self.content.len() { self.selected_index += 1; } @@ -668,7 +785,7 @@ impl Directory { /// Select the previous sibling if any. /// This is the element immediatly below the current one. - pub fn select_prev(&mut self, colors: &Colors) -> FmResult<()> { + pub fn select_prev(&mut self, colors: &Colors) -> Result<()> { if self.selected_index > 0 { self.selected_index -= 1; } @@ -676,7 +793,7 @@ impl Directory { } /// Move up 10 times. - pub fn page_up(&mut self, colors: &Colors) -> FmResult<()> { + pub fn page_up(&mut self, colors: &Colors) -> Result<()> { if self.selected_index > 10 { self.selected_index -= 10; } else { @@ -686,7 +803,7 @@ impl Directory { } /// Move down 10 times - pub fn page_down(&mut self, colors: &Colors) -> FmResult<()> { + pub fn page_down(&mut self, colors: &Colors) -> Result<()> { self.selected_index += 10; if self.selected_index >= self.content.len() { self.selected_index = self.content.len() - 1; @@ -695,7 +812,7 @@ impl Directory { } /// Update the position of the selected element from its index. - pub fn update_tree_position_from_index(&mut self, colors: &Colors) -> FmResult<()> { + pub fn update_tree_position_from_index(&mut self, colors: &Colors) -> Result<()> { self.tree.position = self.tree.position_from_index(self.selected_index); let (_, _, node) = self.tree.select_from_position()?; self.tree.current_node = node; @@ -704,21 +821,21 @@ impl Directory { } /// Select the first child, if any. - pub fn select_first_child(&mut self, colors: &Colors) -> FmResult<()> { + pub fn select_first_child(&mut self, colors: &Colors) -> Result<()> { self.tree.select_first_child()?; (self.selected_index, self.content) = self.tree.into_navigable_content(colors); Ok(()) } /// Select the parent of current node. - pub fn select_parent(&mut self, colors: &Colors) -> FmResult<()> { + pub fn select_parent(&mut self, colors: &Colors) -> Result<()> { self.tree.select_parent()?; (self.selected_index, self.content) = self.tree.into_navigable_content(colors); Ok(()) } /// Select the last leaf of the tree (ie the last line.) - pub fn go_to_bottom_leaf(&mut self, colors: &Colors) -> FmResult<()> { + pub fn go_to_bottom_leaf(&mut self, colors: &Colors) -> Result<()> { self.tree.go_to_bottom_leaf()?; (self.selected_index, self.content) = self.tree.into_navigable_content(colors); Ok(()) @@ -755,7 +872,7 @@ pub struct Diff { } impl Diff { - pub fn new(first_path: &str, second_path: &str) -> FmResult<Self> { + pub fn new(first_path: &str, second_path: &str) -> Result<Self> { let content: Vec<String> = execute_and_capture_output_without_check("diff", &vec![first_path, second_path])? .lines() @@ -774,6 +891,32 @@ impl Diff { } } +pub struct Iso { + pub content: Vec<String>, + length: usize, +} + +impl Iso { + fn new(path: &Path) -> Result<Self> { + let path = path.to_str().context("couldn't parse the path")?; + let content: Vec<String> = + execute_and_capture_output_without_check("isoinfo", &vec!["-l", "-i", path])? + .lines() + .map(|s| s.to_owned()) + .collect(); + info!("isofino:\n{content:?}"); + + Ok(Self { + length: content.len(), + content, + }) + } + + fn len(&self) -> usize { + self.length + } +} + /// Common trait for many preview methods which are just a bunch of lines with /// no specific formatting. /// Some previewing (thumbnail and syntaxed text) needs more details. @@ -829,6 +972,8 @@ impl_window!(ZipContent, String); impl_window!(MediaContent, String); impl_window!(Directory, ColoredPair); impl_window!(Diff, String); +impl_window!(Iso, String); +impl_window!(ColoredText, String); fn is_ext_compressed(ext: &str) -> bool { matches!( @@ -837,7 +982,10 @@ fn is_ext_compressed(ext: &str) -> bool { ) } fn is_ext_image(ext: &str) -> bool { - matches!(ext, "png" | "jpg" | "jpeg" | "tiff" | "heif") + matches!( + ext, + "png" | "jpg" | "jpeg" | "tiff" | "heif" | "gif" | "raw" | "cr2" | "nef" | "orf" | "sr2" + ) } fn is_ext_audio(ext: &str) -> bool { @@ -862,10 +1010,30 @@ fn is_ext_video(ext: &str) -> bool { matches!(ext, "mkv" | "webm" | "mpeg" | "mp4" | "avi" | "flv" | "mpg") } +fn is_ext_font(ext: &str) -> bool { + matches!(ext, "ttf") +} + +fn is_ext_svg(ext: &str) -> bool { + matches!(ext, "svg") +} + fn is_ext_pdf(ext: &str) -> bool { ext == "pdf" } +fn is_ext_iso(ext: &str) -> bool { + ext == "iso" +} + +fn is_ext_notebook(ext: &str) -> bool { + ext == "ipynb" +} + +fn is_ext_doc(ext: &str) -> bool { + matches!(ext, "doc" | "docx" | "odt" | "sxw") +} + fn catch_unwind_silent<F: FnOnce() -> R + panic::UnwindSafe, R>(f: F) -> std::thread::Result<R> { let prev_hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); |