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#[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 #[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#[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 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 pub fn is_empty(&self) -> bool {
199 matches!(self, Self::Empty)
200 }
201
202 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
219pub 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 pub fn empty() -> Preview {
236 clear_tmp_files();
237 Preview::Empty
238 }
239
240 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 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 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 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
455fn 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#[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 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#[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 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 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 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 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 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 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#[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 const SIZE_LIMIT: usize = 1 << 15;
782
783 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#[derive(Clone)]
855pub struct SyntaxedString {
856 pub content: String,
857 pub style: Style,
858}
859
860impl SyntaxedString {
861 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 font_style.contains(FontStyle::BOLD) {
882 modifier |= Modifier::BOLD;
883 }
884
885 if font_style.contains(FontStyle::UNDERLINE) {
887 modifier |= Modifier::UNDERLINED;
888 }
889
890 modifier
891 }
892}
893
894#[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 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#[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 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 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 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
1018pub 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}
1039pub 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
1070pub 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);