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