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#[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 #[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#[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 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 pub fn is_empty(&self) -> bool {
203 matches!(self, Self::Empty)
204 }
205
206 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
223pub 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 pub fn empty() -> Preview {
240 clear_tmp_files();
241 Preview::Empty
242 }
243
244 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 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 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 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
501fn 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#[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 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#[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 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 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 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 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 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 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#[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 const SIZE_LIMIT: usize = 1 << 15;
850
851 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#[derive(Clone)]
923pub struct SyntaxedString {
924 pub content: String,
925 pub style: Style,
926}
927
928impl SyntaxedString {
929 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 font_style.contains(FontStyle::BOLD) {
950 modifier |= Modifier::BOLD;
951 }
952
953 if font_style.contains(FontStyle::UNDERLINE) {
955 modifier |= Modifier::UNDERLINED;
956 }
957
958 modifier
959 }
960}
961
962#[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 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#[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 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 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 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
1086pub 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}
1107pub 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
1138pub 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);