summaryrefslogtreecommitdiffstats
path: root/src/preview.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/preview.rs')
-rw-r--r--src/preview.rs270
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(|_| {}));