diff options
Diffstat (limited to 'src/preview.rs')
-rw-r--r-- | src/preview.rs | 329 |
1 files changed, 255 insertions, 74 deletions
diff --git a/src/preview.rs b/src/preview.rs index 8d686a6..be5f18b 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -19,7 +19,10 @@ use tuikit::attr::{Attr, Color}; use users::UsersCache; use crate::config::Colors; -use crate::constant_strings_paths::THUMBNAIL_PATH; +use crate::constant_strings_paths::{ + DIFF, FFMPEG, FONTIMAGE, ISOINFO, JUPYTER, LSBLK, LSOF, MEDIAINFO, PANDOC, RSVG_CONVERT, SS, + THUMBNAIL_PATH, UEBERZUG, +}; use crate::content_window::ContentWindow; use crate::decompress::list_files_zip; use crate::fileinfo::{FileInfo, FileKind}; @@ -27,7 +30,7 @@ use crate::filter::FilterKind; use crate::opener::execute_and_capture_output_without_check; use crate::status::Status; use crate::tree::{ColoredString, Tree}; -use crate::utils::filename_from_path; +use crate::utils::{filename_from_path, is_program_in_path}; /// Different kind of preview used to display some informaitons /// About the file. @@ -45,6 +48,9 @@ pub enum Preview { Iso(Iso), Diff(Diff), ColoredText(ColoredText), + Socket(Socket), + BlockDevice(BlockDevice), + FifoCharDevice(FifoCharDevice), #[default] Empty, } @@ -53,6 +59,7 @@ pub enum Preview { pub enum TextKind { HELP, LOG, + EPUB, #[default] TEXTFILE, } @@ -82,32 +89,69 @@ impl Preview { FileKind::NormalFile => match file_info.extension.to_lowercase().as_str() { e if is_ext_compressed(e) => Ok(Self::Archive(ZipContent::new(&file_info.path)?)), e if is_ext_pdf(e) => Ok(Self::Pdf(PdfContent::new(&file_info.path))), - e if is_ext_image(e) => Ok(Self::Ueberzug(Ueberzug::image(&file_info.path)?)), - e if is_ext_audio(e) => Ok(Self::Media(MediaContent::new(&file_info.path)?)), - e if is_ext_video(e) => { + e if is_ext_image(e) && is_program_in_path(UEBERZUG) => { + Ok(Self::Ueberzug(Ueberzug::image(&file_info.path)?)) + } + e if is_ext_audio(e) && is_program_in_path(MEDIAINFO) => { + Ok(Self::Media(MediaContent::new(&file_info.path)?)) + } + e if is_ext_video(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(FFMPEG) => + { Ok(Self::Ueberzug(Ueberzug::video_thumbnail(&file_info.path)?)) } - e if is_ext_font(e) => { + e if is_ext_font(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(FONTIMAGE) => + { 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) => { + e if is_ext_svg(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(RSVG_CONVERT) => + { + Ok(Self::Ueberzug(Ueberzug::svg_thumbnail(&file_info.path)?)) + } + e if is_ext_iso(e) && is_program_in_path(ISOINFO) => { + Ok(Self::Iso(Iso::new(&file_info.path)?)) + } + e if is_ext_notebook(e) && is_program_in_path(JUPYTER) => { Ok(Self::notebook(&file_info.path) .context("Preview: Couldn't parse notebook")?) } - e if is_ext_doc(e) => { + e if is_ext_doc(e) && is_program_in_path(PANDOC) => { Ok(Self::doc(&file_info.path).context("Preview: Couldn't parse doc")?) } + e if is_ext_epub(e) && is_program_in_path(PANDOC) => { + Ok(Self::epub(&file_info.path).context("Preview: Couldn't parse epub")?) + } e => match Self::preview_syntaxed(e, &file_info.path) { Some(syntaxed_preview) => Ok(syntaxed_preview), None => Self::preview_text_or_binary(file_info), }, }, + FileKind::Socket if is_program_in_path(SS) => Ok(Self::socket(file_info)), + FileKind::BlockDevice if is_program_in_path(LSBLK) => Ok(Self::blockdevice(file_info)), + FileKind::Fifo | FileKind::CharDevice if is_program_in_path(LSOF) => { + Ok(Self::fifo_chardevice(file_info)) + } _ => Err(anyhow!("new preview: can't preview this filekind",)), } } + fn socket(file_info: &FileInfo) -> Self { + Self::Socket(Socket::new(file_info)) + } + + fn blockdevice(file_info: &FileInfo) -> Self { + Self::BlockDevice(BlockDevice::new(file_info)) + } + + fn fifo_chardevice(file_info: &FileInfo) -> Self { + Self::FifoCharDevice(FifoCharDevice::new(file_info)) + } + /// Creates a new, static window used when we display a preview in the second pane pub fn window_for_second_pane(&self, height: usize) -> ContentWindow { ContentWindow::new(self.len(), height) @@ -129,26 +173,28 @@ impl Preview { fn notebook(path: &Path) -> Option<Self> { let path_str = path.to_str()?; + // nbconvert is bundled with jupyter, no need to check again let output = execute_and_capture_output_without_check( - "jupyter", + JUPYTER, &["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()) - }) + Self::syntaxed_from_str(output, "md") } fn doc(path: &Path) -> Option<Self> { let path_str = path.to_str()?; let output = execute_and_capture_output_without_check( - "pandoc", + PANDOC, &["-s", "-t", "markdown", "--", path_str], ) .ok()?; + Self::syntaxed_from_str(output, "md") + } + + fn syntaxed_from_str(output: String, ext: &str) -> Option<Self> { let ss = SyntaxSet::load_defaults_nonewlines(); - ss.find_syntax_by_extension("md").map(|syntax| { + ss.find_syntax_by_extension(ext).map(|syntax| { Self::Syntaxed(HLContent::from_str(&output, ss.clone(), syntax).unwrap_or_default()) }) } @@ -164,7 +210,7 @@ impl Preview { } fn is_binary(file_info: &FileInfo, file: &mut std::fs::File, buffer: &mut [u8]) -> bool { - file_info.size().unwrap_or_default() >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64 + file_info.true_size >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64 && file.read_exact(buffer).is_ok() && inspect(buffer) == ContentType::BINARY } @@ -191,6 +237,12 @@ impl Preview { Self::ColoredText(ColoredText::new(output)) } + pub fn epub(path: &Path) -> Result<Self> { + Ok(Self::Text( + TextContent::epub(path).context("Couldn't read epub")?, + )) + } + /// Empty preview, holding nothing. pub fn new_empty() -> Self { Self::Empty @@ -206,12 +258,15 @@ impl Preview { Self::Binary(binary) => binary.len(), Self::Pdf(pdf) => pdf.len(), Self::Archive(zip) => zip.len(), - Self::Ueberzug(_img) => 0, + Self::Ueberzug(_) => 0, 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(), + Self::Socket(socket) => socket.len(), + Self::BlockDevice(blockdevice) => blockdevice.len(), + Self::FifoCharDevice(fifo) => fifo.len(), } } @@ -221,6 +276,117 @@ impl Preview { } } +/// Read a number of lines from a text file. Returns a vector of strings. +fn read_nb_lines(path: &Path, size_limit: usize) -> Result<Vec<String>> { + let reader = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(reader + .lines() + .take(size_limit) + .map(|line| line.unwrap_or_else(|_| "".to_owned())) + .collect()) +} + +/// Preview a socket file with `ss -lpmepiT` +#[derive(Clone, Default)] +pub struct Socket { + content: Vec<String>, + length: usize, +} + +impl Socket { + /// New socket preview + /// See `man ss` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec<String>; + if let Ok(output) = std::process::Command::new(SS).arg("-lpmepiT").output() { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s + .lines() + .filter(|l| l.contains(&fileinfo.filename)) + .map(|s| s.to_owned()) + .collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + +/// Preview a blockdevice file with lsblk +#[derive(Clone, Default)] +pub struct BlockDevice { + content: Vec<String>, + length: usize, +} + +impl BlockDevice { + /// New socket preview + /// See `man ss` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec<String>; + if let Ok(output) = std::process::Command::new(LSBLK) + .args([ + "-lfo", + "FSTYPE,PATH,LABEL,UUID,FSVER,MOUNTPOINT,MODEL,SIZE,FSAVAIL,FSUSE%", + &fileinfo.path.display().to_string(), + ]) + .output() + { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s.lines().map(|s| s.to_owned()).collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + +/// Preview a fifo or a chardevice file with lsof +#[derive(Clone, Default)] +pub struct FifoCharDevice { + content: Vec<String>, + length: usize, +} + +impl FifoCharDevice { + /// New FIFO preview + /// See `man lsof` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec<String>; + if let Ok(output) = std::process::Command::new(LSOF) + .arg(&fileinfo.path.display().to_string()) + .output() + { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s.lines().map(|s| s.to_owned()).collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + /// Holds a preview of a text content. /// It's a boxed vector of strings (per line) #[derive(Clone, Default)] @@ -234,7 +400,7 @@ impl TextContent { const SIZE_LIMIT: usize = 1048576; fn help(help: &str) -> Self { - let content: Vec<String> = help.split('\n').map(|s| s.to_owned()).collect(); + let content: Vec<String> = help.lines().map(|line| line.to_owned()).collect(); Self { kind: TextKind::HELP, length: content.len(), @@ -250,13 +416,23 @@ impl TextContent { } } + fn epub(path: &Path) -> Option<Self> { + let path_str = path.to_str()?; + let output = execute_and_capture_output_without_check( + PANDOC, + &["-s", "-t", "plain", "--", path_str], + ) + .ok()?; + let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect(); + Some(Self { + kind: TextKind::EPUB, + length: content.len(), + content, + }) + } + fn from_file(path: &Path) -> Result<Self> { - let reader = std::io::BufReader::new(std::fs::File::open(path)?); - let content: Vec<String> = reader - .lines() - .take(Self::SIZE_LIMIT) - .map(|line| line.unwrap_or_else(|_| "".to_owned())) - .collect(); + let content = read_nb_lines(path, Self::SIZE_LIMIT)?; Ok(Self { kind: TextKind::TEXTFILE, length: content.len(), @@ -279,18 +455,12 @@ pub struct HLContent { impl HLContent { const SIZE_LIMIT: usize = 32768; - /// Creates a new displayable content of a syntect supported file. /// 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) -> Result<Self> { - let reader = std::io::BufReader::new(std::fs::File::open(path)?); - let raw_content: Vec<String> = reader - .lines() - .take(Self::SIZE_LIMIT) - .map(|line| line.unwrap_or_else(|_| "".to_owned())) - .collect(); + let raw_content = read_nb_lines(path, Self::SIZE_LIMIT)?; let highlighted_content = Self::parse_raw_content(raw_content, syntax_set, syntax_ref)?; Ok(Self { @@ -406,7 +576,7 @@ impl BinaryContent { Ok(Self { path: file_info.path.clone(), - length: file_info.size().unwrap_or_default() / Self::LINE_WIDTH as u64, + length: file_info.true_size / Self::LINE_WIDTH as u64, content, }) } @@ -530,7 +700,7 @@ pub struct MediaContent { impl MediaContent { fn new(path: &Path) -> Result<Self> { let content: Vec<String>; - if let Ok(output) = std::process::Command::new("mediainfo").arg(path).output() { + if let Ok(output) = std::process::Command::new(MEDIAINFO).arg(path).output() { let s = String::from_utf8(output.stdout).unwrap_or_default(); content = s.lines().map(|s| s.to_owned()).collect(); } else { @@ -612,7 +782,7 @@ impl Ueberzug { .to_str() .context("make_thumbnail: couldn't parse the path into a string")?; Self::make_thumbnail( - "ffmpeg", + FFMPEG, &[ "-i", path_str, @@ -630,7 +800,7 @@ impl Ueberzug { 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]) + Self::make_thumbnail(FONTIMAGE, &["-o", THUMBNAIL_PATH, path_str]) } fn make_svg_thumbnail(svg_path: &Path) -> Result<()> { @@ -638,7 +808,7 @@ impl Ueberzug { .to_str() .context("make_thumbnail: couldn't parse the path into a string")?; Self::make_thumbnail( - "rsvg-convert", + RSVG_CONVERT, &["--keep-aspect-ratio", path_str, "-o", THUMBNAIL_PATH], ) } @@ -694,7 +864,7 @@ impl ColoredText { /// if the directory has a lot of children. #[derive(Clone, Debug)] pub struct Directory { - pub content: Vec<(String, ColoredString)>, + pub content: Vec<ColoredTriplet>, pub tree: Tree, len: usize, pub selected_index: usize, @@ -777,19 +947,25 @@ 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) -> Result<()> { - if self.selected_index + 1 < self.content.len() { + if self.selected_index < self.content.len() { + self.tree.increase_required_height(); + self.unselect_children(); self.selected_index += 1; + self.update_tree_position_from_index(colors)?; } - self.update_tree_position_from_index(colors) + Ok(()) } /// Select the previous sibling if any. /// This is the element immediatly below the current one. pub fn select_prev(&mut self, colors: &Colors) -> Result<()> { if self.selected_index > 0 { + self.tree.decrease_required_height(); + self.unselect_children(); self.selected_index -= 1; + self.update_tree_position_from_index(colors)?; } - self.update_tree_position_from_index(colors) + Ok(()) } /// Move up 10 times. @@ -805,8 +981,12 @@ impl Directory { /// Move down 10 times 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; + if self.selected_index > self.content.len() { + if !self.content.is_empty() { + self.selected_index = self.content.len(); + } else { + self.selected_index = 1; + } } self.update_tree_position_from_index(colors) } @@ -848,18 +1028,19 @@ impl Directory { /// Calculates the top, bottom and lenght of the view, depending on which element /// is selected and the size of the window used to display. - pub fn calculate_tree_window(&self, height: usize) -> (usize, usize, usize) { + pub fn calculate_tree_window(&self, terminal_height: usize) -> (usize, usize, usize) { let length = self.content.len(); let top: usize; let bottom: usize; - if self.selected_index < height - 1 { + let window_height = terminal_height - ContentWindow::WINDOW_MARGIN_TOP; + if self.selected_index < terminal_height - 1 { top = 0; - bottom = height - 1; + bottom = window_height; } else { - let padding = std::cmp::max(10, height / 2); - top = self.selected_index - 1 - padding; - bottom = self.selected_index + height - 1 + padding; + let padding = std::cmp::max(10, terminal_height / 2); + top = self.selected_index - padding; + bottom = top + window_height; } (top, bottom, length) @@ -874,11 +1055,11 @@ pub struct Diff { impl Diff { pub fn new(first_path: &str, second_path: &str) -> Result<Self> { let content: Vec<String> = - execute_and_capture_output_without_check("diff", &[first_path, second_path])? + execute_and_capture_output_without_check(DIFF, &[first_path, second_path])? .lines() .map(|s| s.to_owned()) .collect(); - info!("diff:\n{content:?}"); + info!("{DIFF}:\n{content:?}"); Ok(Self { length: content.len(), @@ -900,11 +1081,11 @@ 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", &["-l", "-i", path])? + execute_and_capture_output_without_check(ISOINFO, &["-l", "-i", path])? .lines() .map(|s| s.to_owned()) .collect(); - info!("isofino:\n{content:?}"); + info!("{ISOINFO}:\n{content:?}"); Ok(Self { length: content.len(), @@ -929,21 +1110,6 @@ pub trait Window<T> { ) -> Take<Skip<Enumerate<Iter<'_, T>>>>; } -impl Window<Vec<SyntaxedString>> for HLContent { - fn window( - &self, - top: usize, - bottom: usize, - length: usize, - ) -> std::iter::Take<Skip<Enumerate<Iter<'_, Vec<SyntaxedString>>>>> { - self.content - .iter() - .enumerate() - .skip(top) - .take(min(length, bottom + 1)) - } -} - macro_rules! impl_window { ($t:ident, $u:ident) => { impl Window<$u> for $t { @@ -963,17 +1129,26 @@ macro_rules! impl_window { }; } -type ColoredPair = (String, ColoredString); +/// A tuple with `(ColoredString, String, ColoredString)`. +/// Used to iter and impl window trait in tree mode. +pub type ColoredTriplet = (ColoredString, String, ColoredString); + +/// A vector of highlighted strings +pub type VecSyntaxedString = Vec<SyntaxedString>; +impl_window!(HLContent, VecSyntaxedString); impl_window!(TextContent, String); impl_window!(BinaryContent, Line); impl_window!(PdfContent, String); impl_window!(ZipContent, String); impl_window!(MediaContent, String); -impl_window!(Directory, ColoredPair); +impl_window!(Directory, ColoredTriplet); impl_window!(Diff, String); impl_window!(Iso, String); impl_window!(ColoredText, String); +impl_window!(Socket, String); +impl_window!(BlockDevice, String); +impl_window!(FifoCharDevice, String); fn is_ext_compressed(ext: &str) -> bool { matches!( @@ -981,7 +1156,9 @@ fn is_ext_compressed(ext: &str) -> bool { "zip" | "gzip" | "bzip2" | "xz" | "lzip" | "lzma" | "tar" | "mtree" | "raw" | "7z" ) } -fn is_ext_image(ext: &str) -> bool { + +/// True iff the extension is a known (by me) image extension. +pub fn is_ext_image(ext: &str) -> bool { matches!( ext, "png" | "jpg" | "jpeg" | "tiff" | "heif" | "gif" | "raw" | "cr2" | "nef" | "orf" | "sr2" @@ -1011,11 +1188,11 @@ fn is_ext_video(ext: &str) -> bool { } fn is_ext_font(ext: &str) -> bool { - matches!(ext, "ttf") + matches!(ext, "ttf" | "otf") } fn is_ext_svg(ext: &str) -> bool { - matches!(ext, "svg") + matches!(ext, "svg" | "svgz") } fn is_ext_pdf(ext: &str) -> bool { @@ -1034,6 +1211,10 @@ fn is_ext_doc(ext: &str) -> bool { matches!(ext, "doc" | "docx" | "odt" | "sxw") } +fn is_ext_epub(ext: &str) -> bool { + ext == "epub" +} + 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(|_| {})); |