Skip to main content

fm/modes/display/
preview.rs

1use std::cmp::min;
2use std::convert::Into;
3use std::fmt::{Display, Write as _};
4use std::fs::symlink_metadata;
5use std::io::{BufRead, BufReader, Read};
6use std::iter::{Enumerate, Skip, Take};
7use std::path::{Path, PathBuf};
8use std::slice::Iter;
9use std::sync::Arc;
10
11use anyhow::{Context, Result};
12use content_inspector::{inspect, ContentType};
13use ratatui::style::{Color, Modifier, Style};
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16use syntect::{
17    easy::HighlightLines,
18    highlighting::{FontStyle, Style as SyntectStyle},
19    parsing::{SyntaxReference, SyntaxSet},
20};
21
22use crate::app::try_build_plugin;
23use crate::common::{
24    clear_tmp_files, filename_from_path, is_in_path, path_to_string, ACTION_LOG_PATH, BSDTAR,
25    FFMPEG, FONTIMAGE, ISOINFO, JUPYTER, LIBREOFFICE, LSBLK, MEDIAINFO, PANDOC, PDFINFO, PDFTOPPM,
26    PDFTOTEXT, READELF, RSVG_CONVERT, SEVENZ, SS, TRANSMISSION_SHOW, UDEVADM,
27};
28use crate::config::{
29    get_prefered_imager, get_previewer_command, get_previewer_plugins, get_syntect_theme, Imagers,
30};
31use crate::io::execute_and_capture_output_without_check;
32use crate::log_info;
33use crate::modes::{
34    extract_extension, list_files_tar, list_files_zip, ContentWindow, DisplayedImage,
35    DisplayedImageBuilder, FileKind, FilterKind, Quote, TLine, Tree, TreeBuilder, TreeLines, Users,
36};
37
38fn images_are_enabled() -> bool {
39    let Some(prefered_imager) = get_prefered_imager() else {
40        return false;
41    };
42    !matches!(prefered_imager.imager, Imagers::Disabled)
43}
44
45/// Different kind of extension for grouped by previewers.
46/// Any extension we can preview should be matched here.
47#[derive(Default, Eq, PartialEq)]
48pub enum ExtensionKind {
49    Archive,
50    Audio,
51    Epub,
52    Font,
53    Image,
54    Iso,
55    Notebook,
56    Office,
57    Pdf,
58    Sevenz,
59    Svg,
60    Torrent,
61    Video,
62
63    #[default]
64    Default,
65}
66
67impl ExtensionKind {
68    /// Match any known extension against an extension kind.
69    #[rustfmt::skip]
70    pub fn matcher(ext: &str) -> Self {
71        match ext {
72            "zip" | "gzip" | "bzip2" | "xz" | "lzip" | "lzma" | "tar" | "mtree" | "raw" | "gz" | "zst" | "deb" | "rpm"
73            => Self::Archive,
74            "7z" | "7za"
75            => Self::Sevenz,
76            "png" | "jpg" | "jpeg" | "tiff" | "heif" | "gif" | "cr2" | "nef" | "orf" | "sr2"
77            => Self::Image,
78            "ogg" | "ogm" | "riff" | "mp2" | "mp3" | "wm" | "qt" | "ac3" | "dts" | "aac" | "mac" | "flac" | "ape"
79            => Self::Audio,
80            "mkv" | "webm" | "mpeg" | "mp4" | "avi" | "flv" | "mpg" | "wmv" | "m4v" | "mov"
81            => Self::Video,
82            "ttf" | "otf" | "woff"
83            => Self::Font,
84            "svg" | "svgz"
85            => Self::Svg,
86            "pdf"
87            => Self::Pdf,
88            "iso"
89            => Self::Iso,
90            "ipynb"
91            => Self::Notebook,
92            "doc" | "docx" | "odt" | "sxw" | "xlsx" | "xls" 
93            => Self::Office,
94            "epub"
95            => Self::Epub,
96            "torrent"
97            => Self::Torrent,
98            _
99            => Self::Default,
100        }
101    }
102
103    #[rustfmt::skip]
104    fn has_programs(&self) -> bool {
105        match self {
106            Self::Archive   => is_in_path(BSDTAR),
107            Self::Epub      => is_in_path(PANDOC),
108            Self::Iso       => is_in_path(ISOINFO),
109            Self::Notebook  => is_in_path(JUPYTER),
110            Self::Audio     => is_in_path(MEDIAINFO),
111            Self::Office    => is_in_path(LIBREOFFICE),
112            Self::Torrent   => is_in_path(TRANSMISSION_SHOW),
113            Self::Sevenz    => is_in_path(SEVENZ),
114            Self::Svg       => is_in_path(RSVG_CONVERT),
115            Self::Video     => is_in_path(FFMPEG),
116            Self::Font      => is_in_path(FONTIMAGE),
117            Self::Pdf       => {
118                               is_in_path(PDFINFO)
119                            && is_in_path(PDFTOPPM)
120            }
121
122            _           => true,
123        }
124    }
125
126    fn is_image_kind(&self) -> bool {
127        matches!(
128            &self,
129            ExtensionKind::Font
130                | ExtensionKind::Image
131                | ExtensionKind::Office
132                | ExtensionKind::Pdf
133                | ExtensionKind::Svg
134                | ExtensionKind::Video
135        )
136    }
137}
138
139impl std::fmt::Display for ExtensionKind {
140    #[rustfmt::skip]
141    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
142        let repr = match self {
143            Self::Archive   => "archive",
144            Self::Image     => "image",
145            Self::Audio     => "audio",
146            Self::Video     => "video",
147            Self::Font      => "font",
148            Self::Sevenz    => "7zip",
149            Self::Svg       => "svg",
150            Self::Pdf       => "pdf",
151            Self::Iso       => "iso",
152            Self::Notebook  => "notebook",
153            Self::Office    => "office",
154            Self::Epub      => "epub",
155            Self::Torrent   => "torrent",
156            Self::Default   => "default",
157        };
158        write!(f, "{}", repr)
159    }
160}
161
162/// Different kind of preview used to display some informaitons
163/// About the file.
164/// We check if it's an archive first, then a pdf file, an image, a media file
165#[derive(Default)]
166pub enum Preview {
167    Syntaxed(HLContent),
168    Text(Text),
169    Binary(BinaryContent),
170    Image(DisplayedImage),
171    Tree(Tree),
172    #[default]
173    Empty,
174}
175
176impl Preview {
177    /// The size (most of the time the number of lines) of the preview.
178    /// Some preview (thumbnail, empty) can't be scrolled and their size is always 0.
179    pub fn len(&self) -> usize {
180        match self {
181            Self::Empty => 0,
182            Self::Syntaxed(preview) => preview.len(),
183            Self::Text(preview) => preview.len(),
184            Self::Binary(preview) => preview.len(),
185            Self::Image(preview) => preview.len(),
186            Self::Tree(tree) => tree.displayable().lines().len(),
187        }
188    }
189
190    pub fn kind_display(&self) -> &str {
191        match self {
192            Self::Empty => "empty",
193            Self::Syntaxed(_) => "an highlighted text",
194            Self::Text(text) => text.kind.for_first_line(),
195            Self::Binary(_) => "a binary file",
196            Self::Image(image) => image.kind.for_first_line(),
197            Self::Tree(_) => "a tree",
198        }
199    }
200
201    /// True if nothing is currently previewed.
202    pub fn is_empty(&self) -> bool {
203        matches!(self, Self::Empty)
204    }
205
206    /// Creates a new, static window used when we display a preview in the second pane
207    pub fn window_for_second_pane(&self, height: usize) -> ContentWindow {
208        ContentWindow::new(self.len(), height)
209    }
210
211    pub fn filepath(&self) -> Arc<Path> {
212        match self {
213            Self::Empty => Arc::from(Path::new("")),
214            Self::Binary(preview) => preview.filepath(),
215            Self::Image(preview) => preview.filepath(),
216            Self::Syntaxed(preview) => preview.filepath(),
217            Self::Text(preview) => preview.filepath(),
218            Self::Tree(tree) => Arc::from(tree.root_path()),
219        }
220    }
221}
222
223/// Builder of previews. It just knows what file asked a preview.
224/// Using a builder is useful since there's many kind of preview which all use a different method.
225pub struct PreviewBuilder {
226    path: PathBuf,
227}
228
229impl PreviewBuilder {
230    const CONTENT_INSPECTOR_MIN_SIZE: usize = 1024;
231
232    pub fn new(path: &Path) -> Self {
233        Self {
234            path: path.to_owned(),
235        }
236    }
237
238    /// Empty preview, holding nothing.
239    pub fn empty() -> Preview {
240        clear_tmp_files();
241        Preview::Empty
242    }
243
244    /// Creates a new preview instance based on the filekind and the extension of
245    /// the file.
246    /// Sometimes it reads the content of the file, sometimes it delegates
247    /// it to the display method.
248    /// Directories aren't handled there since we need more arguments to create
249    /// their previews.
250    pub fn build(self) -> Result<Preview> {
251        if let Some(preview) = self.command_preview() {
252            return Ok(preview);
253        }
254        if let Some(preview) = self.plugin_preview() {
255            return Ok(preview);
256        }
257        self.internal_preview()
258    }
259
260    fn plugin_preview(&self) -> Option<Preview> {
261        if let Some(plugins) = get_previewer_plugins() {
262            try_build_plugin(&self.path, plugins)
263        } else {
264            None
265        }
266    }
267
268    fn internal_preview(&self) -> Result<Preview> {
269        clear_tmp_files();
270        let file_kind = FileKind::new(&symlink_metadata(&self.path)?, &self.path);
271        match file_kind {
272            FileKind::Directory => self.directory(),
273            FileKind::NormalFile => self.normal_file(),
274            FileKind::Socket if is_in_path(SS) => self.socket(),
275            FileKind::BlockDevice if is_in_path(LSBLK) => self.block_device(),
276            FileKind::Fifo | FileKind::CharDevice if is_in_path(UDEVADM) => self.fifo_chardevice(),
277            FileKind::SymbolicLink(true) => self.valid_symlink(),
278            _ => Ok(Preview::default()),
279        }
280    }
281
282    /// Creates a new `Directory` from the self.file_info
283    /// It explores recursivelly the directory and creates a tree.
284    /// The recursive exploration is limited to depth 2.
285    fn directory(&self) -> Result<Preview> {
286        let users = Users::default();
287        Ok(Preview::Tree(
288            TreeBuilder::new(std::sync::Arc::from(self.path.as_path()), &users)
289                .with_max_depth(4)
290                .with_hidden(false)
291                .with_filter_kind(&FilterKind::All)
292                .build(),
293        ))
294    }
295
296    fn valid_symlink(&self) -> Result<Preview> {
297        Self::new(&std::fs::read_link(&self.path).unwrap_or_default()).build()
298    }
299
300    fn normal_file(&self) -> Result<Preview> {
301        let extension = extract_extension(&self.path)
302            .trim_end_matches(['~', '_'])
303            .to_lowercase();
304        let kind = ExtensionKind::matcher(&extension);
305        match kind {
306            ExtensionKind::Archive if kind.has_programs() => {
307                Ok(Preview::Text(Text::archive(&self.path, &extension)?))
308            }
309            ExtensionKind::Sevenz if kind.has_programs() => {
310                Ok(Preview::Text(Text::sevenz(&self.path)?))
311            }
312            ExtensionKind::Iso if kind.has_programs() => Ok(Preview::Text(Text::iso(&self.path)?)),
313            ExtensionKind::Epub if kind.has_programs() => Ok(Preview::Text(
314                Text::epub(&self.path).context("Preview: Couldn't read epub")?,
315            )),
316            ExtensionKind::Torrent if kind.has_programs() => Ok(Preview::Text(
317                Text::torrent(&self.path).context("Preview: Couldn't read torrent")?,
318            )),
319            ExtensionKind::Notebook if kind.has_programs() => {
320                Ok(Self::notebook(&self.path).context("Preview: Couldn't parse notebook")?)
321            }
322            ExtensionKind::Audio if kind.has_programs() => {
323                Ok(Preview::Text(Text::media_content(&self.path)?))
324            }
325            _ if kind.is_image_kind() && kind.has_programs() && images_are_enabled() => {
326                Self::image(&self.path, kind)
327            }
328            _ if kind.is_image_kind() => Self::text_image(&self.path, kind),
329            _ => match self.syntaxed(&extension) {
330                Some(syntaxed_preview) => Ok(syntaxed_preview),
331                None => self.text_or_binary(),
332            },
333        }
334    }
335
336    fn image(path: &Path, kind: ExtensionKind) -> Result<Preview> {
337        let preview = DisplayedImageBuilder::new(path, kind.into()).build()?;
338        if preview.is_empty() {
339            Ok(Preview::Empty)
340        } else {
341            Ok(Preview::Image(preview))
342        }
343    }
344
345    fn text_image(path: &Path, kind: ExtensionKind) -> Result<Preview> {
346        let preview = match kind {
347            ExtensionKind::Image | ExtensionKind::Video if is_in_path(MEDIAINFO) => {
348                Preview::Text(Text::media_content(path)?)
349            }
350            ExtensionKind::Pdf if is_in_path(PDFTOTEXT) => Preview::Text(Text::pdf_text(path)?),
351            ExtensionKind::Office if is_in_path(LIBREOFFICE) => {
352                Preview::Text(Text::office_text(path)?)
353            }
354            ExtensionKind::Font | ExtensionKind::Svg => Preview::Binary(BinaryContent::new(path)?),
355            _ => Preview::Empty,
356        };
357        Ok(preview)
358    }
359
360    fn socket(&self) -> Result<Preview> {
361        Ok(Preview::Text(Text::socket(&self.path)?))
362    }
363
364    fn block_device(&self) -> Result<Preview> {
365        Ok(Preview::Text(Text::block_device(&self.path)?))
366    }
367
368    fn fifo_chardevice(&self) -> Result<Preview> {
369        Ok(Preview::Text(Text::fifo_chardevice(&self.path)?))
370    }
371
372    fn syntaxed(&self, ext: &str) -> Option<Preview> {
373        if symlink_metadata(&self.path).ok()?.len() > HLContent::SIZE_LIMIT as u64 {
374            return None;
375        };
376        let ss = SyntaxSet::load_defaults_nonewlines();
377        Some(Preview::Syntaxed(
378            HLContent::new(&self.path, ss.clone(), ss.find_syntax_by_extension(ext)?)
379                .unwrap_or_default(),
380        ))
381    }
382
383    fn notebook(path: &Path) -> Option<Preview> {
384        let path_str = path.to_str()?;
385        // nbconvert is bundled with jupyter, no need to check again
386        let output = execute_and_capture_output_without_check(
387            JUPYTER,
388            &["nbconvert", "--to", "markdown", path_str, "--stdout"],
389        )
390        .ok()?;
391        Self::syntaxed_from_str(output, "md")
392    }
393
394    fn syntaxed_from_str(output: String, ext: &str) -> Option<Preview> {
395        let ss = SyntaxSet::load_defaults_nonewlines();
396        Some(Preview::Syntaxed(
397            HLContent::from_str(
398                Path::new("command"),
399                &output,
400                ss.clone(),
401                ss.find_syntax_by_extension(ext)?,
402            )
403            .unwrap_or_default(),
404        ))
405    }
406
407    fn text_or_binary(&self) -> Result<Preview> {
408        if let Some(elf) = self.read_elf() {
409            Ok(Preview::Text(Text::from_readelf(&self.path, elf)?))
410        } else if self.is_binary()? {
411            Ok(Preview::Binary(BinaryContent::new(&self.path)?))
412        } else {
413            Ok(Preview::Text(Text::from_file(&self.path)?))
414        }
415    }
416
417    fn read_elf(&self) -> Option<String> {
418        let Ok(output) = execute_and_capture_output_without_check(
419            READELF,
420            &["-WCa", self.path.to_string_lossy().as_ref()],
421        ) else {
422            return None;
423        };
424        if output.is_empty() {
425            None
426        } else {
427            Some(output)
428        }
429    }
430
431    fn is_binary(&self) -> Result<bool> {
432        let mut file = std::fs::File::open(&self.path)?;
433        let mut buffer = [0; Self::CONTENT_INSPECTOR_MIN_SIZE];
434        let Ok(metadata) = self.path.metadata() else {
435            return Ok(false);
436        };
437
438        Ok(metadata.len() >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64
439            && file.read_exact(&mut buffer).is_ok()
440            && inspect(&buffer) == ContentType::BINARY)
441    }
442
443    /// Creates the help preview as if it was a text file.
444    pub fn help(help: &str) -> Preview {
445        Preview::Text(Text::help(help))
446    }
447
448    pub fn log(log: Vec<String>) -> Preview {
449        Preview::Text(Text::log(log))
450    }
451
452    pub fn cli_info(output: &str, command: String) -> Preview {
453        crate::log_info!("cli_info. command {command} - output\n{output}");
454        Preview::Text(Text::command_stdout(
455            output,
456            command,
457            Arc::from(Path::new("")),
458        ))
459    }
460
461    pub fn plugin_text(text: String, name: &str, path: &Path) -> Preview {
462        Preview::Text(Text::plugin(text, name, path))
463    }
464
465    fn command_preview(&self) -> Option<Preview> {
466        let extension = self.path.extension()?.to_string_lossy().to_string();
467        let commands = get_previewer_command()?;
468        log_info!("{extension} - {commands:?}");
469        for command in commands.iter() {
470            if command.extensions.contains(&extension) {
471                return command.preview(&self.path).ok();
472            }
473        }
474
475        None
476    }
477}
478
479#[derive(Debug, PartialEq, Serialize, Deserialize)]
480pub struct PreviewerCommand {
481    name: String,
482    extensions: Vec<String>,
483    command: String,
484}
485
486impl PreviewerCommand {
487    fn preview(&self, path: &Path) -> Result<Preview> {
488        let command = self
489            .command
490            .replace("%s", &path.display().to_string().quote()?);
491        let args: Vec<_> = command.split_whitespace().collect();
492        Ok(Preview::Text(Text::from_command_output(
493            TextKind::Plugin,
494            args[0],
495            &args[1..],
496            Arc::from(path),
497        )?))
498    }
499}
500
501/// Reads a number of lines from a text file, _removing all ANSI control characters_.
502/// Returns a vector of strings.
503fn read_nb_lines(path: &Path, size_limit: usize) -> Result<Vec<String>> {
504    let re = Regex::new(r"[[:cntrl:]]").unwrap();
505    let reader = std::io::BufReader::new(std::fs::File::open(path)?);
506    Ok(reader
507        .lines()
508        .take(size_limit)
509        .map(|line| line.unwrap_or_default())
510        .map(|s| re.replace_all(&s, "").to_string())
511        .collect())
512}
513
514/// Different kind of text previewed.
515/// Wether it's a text file or the output of a command.
516#[derive(Clone, Default, Debug)]
517pub enum TextKind {
518    #[default]
519    TEXTFILE,
520
521    Archive,
522    Blockdevice,
523    CommandStdout,
524    Elf,
525    Epub,
526    FifoChardevice,
527    Help,
528    Iso,
529    Log,
530    Mediacontent,
531    Office,
532    Pdf,
533    Plugin,
534    Sevenz,
535    Socket,
536    Torrent,
537}
538
539impl TextKind {
540    /// Used to display the kind of content in this file.
541    pub fn for_first_line(&self) -> &'static str {
542        match self {
543            Self::TEXTFILE => "a textfile",
544            Self::Archive => "an archive",
545            Self::Blockdevice => "a Blockdevice file",
546            Self::CommandStdout => "a command stdout",
547            Self::Elf => "an elf file",
548            Self::Epub => "an epub",
549            Self::FifoChardevice => "a Fifo or Chardevice file",
550            Self::Help => "Help",
551            Self::Iso => "Iso",
552            Self::Log => "Log",
553            Self::Plugin => "a text",
554            Self::Office => "a doc",
555            Self::Mediacontent => "a media content",
556            Self::Pdf => "a pdf",
557            Self::Sevenz => "a 7z archive",
558            Self::Socket => "a Socket file",
559            Self::Torrent => "a torrent",
560        }
561    }
562}
563
564impl Display for TextKind {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        writeln!(f, "{kind_str}", kind_str = self.for_first_line())
567    }
568}
569
570/// Holds a preview of a text content.
571/// It's a vector of strings (per line)
572#[derive(Clone, Debug)]
573pub struct Text {
574    pub kind: TextKind,
575    pub title: String,
576    filepath: Arc<Path>,
577    content: Vec<String>,
578    length: usize,
579}
580
581impl Default for Text {
582    fn default() -> Self {
583        Self {
584            kind: TextKind::default(),
585            title: String::default(),
586            filepath: Arc::from(Path::new("")),
587            content: vec![],
588            length: 0,
589        }
590    }
591}
592
593impl Text {
594    /// Only files with less than 1MiB will be read
595    const SIZE_LIMIT: usize = 1 << 20;
596
597    fn help(help: &str) -> Self {
598        let content: Vec<String> = help.lines().map(|line| line.to_owned()).collect();
599        Self {
600            title: "Help".to_string(),
601            kind: TextKind::Help,
602            filepath: Arc::from(Path::new("")),
603            length: content.len(),
604            content,
605        }
606    }
607
608    fn plugin(text: String, name: &str, path: &Path) -> Self {
609        let content: Vec<String> = text.lines().map(|line| line.to_owned()).collect();
610        Self {
611            title: name.to_string(),
612            kind: TextKind::Plugin,
613            length: content.len(),
614            filepath: Arc::from(path),
615            content,
616        }
617    }
618
619    fn log(content: Vec<String>) -> Self {
620        Self {
621            title: "Logs".to_string(),
622            kind: TextKind::Log,
623            length: content.len(),
624            filepath: Arc::from(Path::new(ACTION_LOG_PATH)),
625            content,
626        }
627    }
628
629    fn epub(path: &Path) -> Option<Self> {
630        let path_str = path.to_str()?;
631        let output = execute_and_capture_output_without_check(
632            PANDOC,
633            &["-s", "-t", "plain", "--", path_str],
634        )
635        .ok()?;
636        let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect();
637        Some(Self {
638            title: "Epub".to_string(),
639            kind: TextKind::Epub,
640            length: content.len(),
641            filepath: Arc::from(path),
642            content,
643        })
644    }
645
646    fn from_file(path: &Path) -> Result<Self> {
647        let content = read_nb_lines(path, Self::SIZE_LIMIT)?;
648        Ok(Self {
649            title: filename_from_path(path).context("")?.to_owned(),
650            kind: TextKind::TEXTFILE,
651            filepath: Arc::from(path),
652            length: content.len(),
653            content,
654        })
655    }
656
657    fn from_readelf(path: &Path, elf: String) -> Result<Self> {
658        Ok(Self {
659            title: filename_from_path(path).context("")?.to_owned(),
660            kind: TextKind::Elf,
661            length: elf.len(),
662            filepath: Arc::from(path),
663            content: elf.lines().map(|line| line.to_owned()).collect(),
664        })
665    }
666
667    fn from_command_output(
668        kind: TextKind,
669        command: &str,
670        args: &[&str],
671        filepath: Arc<Path>,
672    ) -> Result<Self> {
673        let content: Vec<String> = execute_and_capture_output_without_check(command, args)?
674            .lines()
675            .map(|s| s.to_owned())
676            .collect();
677        Ok(Self {
678            title: command.to_owned(),
679            kind,
680            length: content.len(),
681            filepath,
682            content,
683        })
684    }
685
686    fn media_content(path: &Path) -> Result<Self> {
687        Self::from_command_output(
688            TextKind::Mediacontent,
689            MEDIAINFO,
690            &[path_to_string(&path).as_str()],
691            Arc::from(path),
692        )
693    }
694
695    fn pdf_text(path: &Path) -> Result<Self> {
696        Self::from_command_output(
697            TextKind::Pdf,
698            PDFTOTEXT,
699            &[path_to_string(&path).as_str()],
700            Arc::from(path),
701        )
702    }
703
704    fn office_text(path: &Path) -> Result<Self> {
705        Self::from_command_output(
706            TextKind::Office,
707            LIBREOFFICE,
708            &["--cat", path_to_string(&path).as_str()],
709            Arc::from(path),
710        )
711    }
712
713    /// Holds a list of file of an archive as returned by
714    /// `ZipArchive::file_names` or from  a `tar tvf` command.
715    /// A generic error message prevent it from returning an error.
716    fn archive(path: &Path, ext: &str) -> Result<Self> {
717        let content = match ext {
718            "zip" => list_files_zip(path).unwrap_or(vec!["Invalid Zip content".to_owned()]),
719            "zst" | "gz" | "bz" | "xz" | "gzip" | "bzip2" | "deb" | "rpm" => {
720                list_files_tar(path).unwrap_or(vec!["Invalid Tar content".to_owned()])
721            }
722            _ => vec![format!("Unsupported format: {ext}")],
723        };
724
725        Ok(Self {
726            title: filename_from_path(path).context("")?.to_owned(),
727            kind: TextKind::Archive,
728            filepath: Arc::from(path),
729            length: content.len(),
730            content,
731        })
732    }
733
734    fn sevenz(path: &Path) -> Result<Self> {
735        Self::from_command_output(
736            TextKind::Sevenz,
737            SEVENZ,
738            &["l", &path_to_string(&path)],
739            Arc::from(path),
740        )
741    }
742
743    fn iso(path: &Path) -> Result<Self> {
744        Self::from_command_output(
745            TextKind::Iso,
746            ISOINFO,
747            &["-l", "-i", &path_to_string(&path)],
748            Arc::from(path),
749        )
750    }
751
752    fn torrent(path: &Path) -> Result<Self> {
753        Self::from_command_output(
754            TextKind::Torrent,
755            TRANSMISSION_SHOW,
756            &[&path_to_string(&path)],
757            Arc::from(path),
758        )
759    }
760
761    /// New socket preview
762    /// See `man ss` for a description of the arguments.
763    fn socket(path: &Path) -> Result<Self> {
764        let mut preview =
765            Self::from_command_output(TextKind::Socket, SS, &["-lpmepiT"], Arc::from(path))?;
766        preview.content = preview
767            .content
768            .iter()
769            .filter(|l| l.contains(path.file_name().unwrap().to_string_lossy().as_ref()))
770            .map(|s| s.to_owned())
771            .collect();
772        Ok(preview)
773    }
774
775    /// New blockdevice preview
776    /// See `man lsblk` for a description of the arguments.
777    fn block_device(path: &Path) -> Result<Self> {
778        Self::from_command_output(
779            TextKind::Blockdevice,
780            LSBLK,
781            &[
782                "-lfo",
783                "FSTYPE,PATH,LABEL,UUID,FSVER,MOUNTPOINT,MODEL,SIZE,FSAVAIL,FSUSE%",
784                &path_to_string(&path),
785            ],
786            Arc::from(path),
787        )
788    }
789
790    /// New FIFO preview
791    /// See `man udevadm` for a description of the arguments.
792    fn fifo_chardevice(path: &Path) -> Result<Self> {
793        Self::from_command_output(
794            TextKind::FifoChardevice,
795            UDEVADM,
796            &[
797                "info",
798                "-a",
799                "-n",
800                path_to_string(&path).as_str(),
801                "--no-pager",
802            ],
803            Arc::from(path),
804        )
805    }
806    /// Make a new previewed colored text.
807    pub fn command_stdout(output: &str, title: String, filepath: Arc<Path>) -> Self {
808        let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect();
809        let length = content.len();
810        Self {
811            title,
812            kind: TextKind::CommandStdout,
813            content,
814            filepath,
815            length,
816        }
817    }
818
819    fn len(&self) -> usize {
820        self.length
821    }
822
823    fn filepath(&self) -> Arc<Path> {
824        self.filepath.clone()
825    }
826}
827
828/// Holds a preview of a code text file whose language is supported by `Syntect`.
829/// The file is colored propery and line numbers are shown.
830#[derive(Clone)]
831pub struct HLContent {
832    path: Arc<Path>,
833    content: Vec<Vec<SyntaxedString>>,
834    length: usize,
835}
836
837impl Default for HLContent {
838    fn default() -> Self {
839        Self {
840            path: Arc::from(Path::new("")),
841            content: vec![],
842            length: 0,
843        }
844    }
845}
846
847impl HLContent {
848    /// Only files with less than 32kiB will be read
849    const SIZE_LIMIT: usize = 1 << 15;
850
851    /// Creates a new displayable content of a syntect supported file.
852    /// It may fail if the file isn't properly formatted or the extension
853    /// is wrong (ie. python content with .c extension).
854    /// ATM only Monokaï (dark) theme is supported.
855    fn new(path: &Path, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> Result<Self> {
856        let raw_content = read_nb_lines(path, Self::SIZE_LIMIT)?;
857        Self::build(path, raw_content, syntax_set, syntax_ref)
858    }
859
860    fn from_str(
861        name: &Path,
862        text: &str,
863        syntax_set: SyntaxSet,
864        syntax_ref: &SyntaxReference,
865    ) -> Result<Self> {
866        let raw_content = text
867            .lines()
868            .take(Self::SIZE_LIMIT)
869            .map(|s| s.to_owned())
870            .collect();
871        Self::build(name, raw_content, syntax_set, syntax_ref)
872    }
873
874    fn build(
875        path: &Path,
876        raw_content: Vec<String>,
877        syntax_set: SyntaxSet,
878        syntax_ref: &SyntaxReference,
879    ) -> Result<Self> {
880        let highlighted_content = Self::parse_raw_content(raw_content, syntax_set, syntax_ref)?;
881        Ok(Self {
882            path: path.into(),
883            length: highlighted_content.len(),
884            content: highlighted_content,
885        })
886    }
887
888    fn len(&self) -> usize {
889        self.length
890    }
891
892    fn filepath(&self) -> Arc<Path> {
893        self.path.clone()
894    }
895
896    fn parse_raw_content(
897        raw_content: Vec<String>,
898        syntax_set: SyntaxSet,
899        syntax_ref: &SyntaxReference,
900    ) -> Result<Vec<Vec<SyntaxedString>>> {
901        let mut highlighted_content = vec![];
902        let syntect_theme = get_syntect_theme().context("Syntect set should be set")?;
903        let mut highlighter = HighlightLines::new(syntax_ref, syntect_theme);
904
905        for line in raw_content.iter() {
906            let mut v_line = vec![];
907            if let Ok(v) = highlighter.highlight_line(line, &syntax_set) {
908                for (style, token) in v.iter() {
909                    v_line.push(SyntaxedString::from_syntect(token, *style));
910                }
911            }
912            highlighted_content.push(v_line)
913        }
914
915        Ok(highlighted_content)
916    }
917}
918
919/// Holds a string to be displayed with given .
920/// We have to read the  from Syntect and parse it into ratatui attr
921/// This struct does the parsing.
922#[derive(Clone)]
923pub struct SyntaxedString {
924    pub content: String,
925    pub style: Style,
926}
927
928impl SyntaxedString {
929    /// Parse a content and style into a `SyntaxedString`
930    /// Only the foreground color is read, we don't the background nor
931    /// the style (bold, italic, underline) defined in Syntect.
932    fn from_syntect(content: &str, style: SyntectStyle) -> Self {
933        let content = content.to_owned();
934        let fg = style.foreground;
935        let style = Style {
936            fg: Some(Color::Rgb(fg.r, fg.g, fg.b)),
937            bg: None,
938            add_modifier: Self::font_style_to_effect(&style.font_style),
939            sub_modifier: Modifier::empty(),
940            underline_color: None,
941        };
942        Self { content, style }
943    }
944
945    fn font_style_to_effect(font_style: &FontStyle) -> Modifier {
946        let mut modifier = Modifier::empty();
947
948        // If the FontStyle has the bold bit set, add bold to the Effect
949        if font_style.contains(FontStyle::BOLD) {
950            modifier |= Modifier::BOLD;
951        }
952
953        // If the FontStyle has the underline bit set, add underline to the Modifier
954        if font_style.contains(FontStyle::UNDERLINE) {
955            modifier |= Modifier::UNDERLINED;
956        }
957
958        modifier
959    }
960}
961
962/// Holds a preview of a binary content.
963/// It doesn't try to respect endianness.
964/// The lines are formatted to display 16 bytes.
965/// The number of lines is truncated to $2^20 = 1048576$.
966#[derive(Clone)]
967pub struct BinaryContent {
968    pub path: Arc<Path>,
969    length: u64,
970    content: Vec<Line>,
971}
972
973impl Default for BinaryContent {
974    fn default() -> Self {
975        Self {
976            path: Arc::from(Path::new("")),
977            length: 0,
978            content: vec![],
979        }
980    }
981}
982
983impl BinaryContent {
984    const LINE_WIDTH: usize = 16;
985    const SIZE_LIMIT: usize = 1048576;
986
987    fn new(path: &Path) -> Result<Self> {
988        let Ok(metadata) = path.metadata() else {
989            return Ok(Self::default());
990        };
991        let length = metadata.len() / Self::LINE_WIDTH as u64;
992        let content = Self::read_content(path)?;
993
994        Ok(Self {
995            path: path.into(),
996            length,
997            content,
998        })
999    }
1000
1001    fn read_content(path: &Path) -> Result<Vec<Line>> {
1002        let mut reader = BufReader::new(std::fs::File::open(path)?);
1003        let mut buffer = [0; Self::LINE_WIDTH];
1004        let mut content = vec![];
1005        while let Ok(nb_bytes_read) = reader.read(&mut buffer[..]) {
1006            if nb_bytes_read != Self::LINE_WIDTH {
1007                content.push(Line::new((&buffer[0..nb_bytes_read]).into()));
1008                break;
1009            }
1010            content.push(Line::new(buffer.into()));
1011            if content.len() >= Self::SIZE_LIMIT {
1012                break;
1013            }
1014        }
1015        Ok(content)
1016    }
1017
1018    /// WATCHOUT !
1019    /// Doesn't return the size of the file, like similar methods in other variants.
1020    /// It returns the number of **lines**.
1021    /// It's the size of the file divided by `BinaryContent::LINE_WIDTH` which is 16.
1022    pub fn len(&self) -> usize {
1023        self.length as usize
1024    }
1025
1026    pub fn is_empty(&self) -> bool {
1027        self.length == 0
1028    }
1029
1030    pub fn filepath(&self) -> Arc<Path> {
1031        self.path.clone()
1032    }
1033
1034    pub fn number_width_hex(&self) -> usize {
1035        format!("{:x}", self.len() * 16).len()
1036    }
1037}
1038
1039/// Holds a `Vec` of "bytes" (`u8`).
1040/// It's mostly used to implement a `print` method.
1041#[derive(Clone)]
1042pub struct Line {
1043    line: Vec<u8>,
1044}
1045
1046impl Line {
1047    fn new(line: Vec<u8>) -> Self {
1048        Self { line }
1049    }
1050
1051    /// Format a line of 16 bytes as BigEndian, separated by spaces.
1052    /// Every byte is zero filled if necessary.
1053    pub fn format_hex(&self) -> String {
1054        let mut hex_repr = String::new();
1055        for (i, byte) in self.line.iter().enumerate() {
1056            let _ = write!(hex_repr, "{byte:02x}");
1057            if i % 2 == 1 {
1058                hex_repr.push(' ');
1059            }
1060        }
1061        hex_repr
1062    }
1063
1064    /// Converts a byte into '.' if it represent a non ASCII printable char
1065    /// or it's corresponding char.
1066    fn byte_to_char(byte: &u8) -> char {
1067        let ch = *byte as char;
1068        if ch.is_ascii_graphic() {
1069            ch
1070        } else {
1071            '.'
1072        }
1073    }
1074
1075    /// Format a line of 16 bytes as an ASCII string.
1076    /// Non ASCII printable bytes are replaced by dots.
1077    pub fn format_as_ascii(&self) -> String {
1078        self.line.iter().map(Self::byte_to_char).collect()
1079    }
1080
1081    pub fn format_line_nr_hex(line_nr: usize, width: usize) -> String {
1082        format!("{line_nr:0width$x}  ")
1083    }
1084}
1085
1086/// Common trait for many preview methods which are just a bunch of lines with
1087/// no specific formatting.
1088/// Some previewing (thumbnail and syntaxed text) needs more details.
1089pub trait TakeSkip<T> {
1090    fn take_skip(&self, top: usize, bottom: usize, length: usize) -> Take<Skip<Iter<'_, T>>>;
1091}
1092
1093macro_rules! impl_take_skip {
1094    ($t:ident, $u:ident) => {
1095        impl TakeSkip<$u> for $t {
1096            fn take_skip(
1097                &self,
1098                top: usize,
1099                bottom: usize,
1100                length: usize,
1101            ) -> Take<Skip<Iter<'_, $u>>> {
1102                self.content.iter().skip(top).take(min(length, bottom + 1))
1103            }
1104        }
1105    };
1106}
1107/// Common trait for many preview methods which are just a bunch of lines with
1108/// no specific formatting.
1109/// Some previewing (thumbnail and syntaxed text) needs more details.
1110pub trait TakeSkipEnum<T> {
1111    fn take_skip_enum(
1112        &self,
1113        top: usize,
1114        bottom: usize,
1115        length: usize,
1116    ) -> Take<Skip<Enumerate<Iter<'_, T>>>>;
1117}
1118
1119macro_rules! impl_take_skip_enum {
1120    ($t:ident, $u:ident) => {
1121        impl TakeSkipEnum<$u> for $t {
1122            fn take_skip_enum(
1123                &self,
1124                top: usize,
1125                bottom: usize,
1126                length: usize,
1127            ) -> Take<Skip<Enumerate<Iter<'_, $u>>>> {
1128                self.content
1129                    .iter()
1130                    .enumerate()
1131                    .skip(top)
1132                    .take(min(length, bottom + 1))
1133            }
1134        }
1135    };
1136}
1137
1138/// A vector of highlighted strings
1139pub type VecSyntaxedString = Vec<SyntaxedString>;
1140
1141impl_take_skip_enum!(HLContent, VecSyntaxedString);
1142impl_take_skip_enum!(Text, String);
1143impl_take_skip_enum!(BinaryContent, Line);
1144impl_take_skip_enum!(TreeLines, TLine);
1145
1146impl_take_skip!(HLContent, VecSyntaxedString);
1147impl_take_skip!(Text, String);
1148impl_take_skip!(BinaryContent, Line);
1149impl_take_skip!(TreeLines, TLine);