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