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;
9
10use anyhow::{Context, Result};
11use content_inspector::{inspect, ContentType};
12use ratatui::style::{Color, Modifier, Style};
13use syntect::{
14 easy::HighlightLines,
15 highlighting::{FontStyle, Style as SyntectStyle},
16 parsing::{SyntaxReference, SyntaxSet},
17};
18
19use crate::common::{
20 clear_tmp_files, filename_from_path, is_in_path, path_to_string, BSDTAR, FFMPEG, FONTIMAGE,
21 ISOINFO, JUPYTER, LIBREOFFICE, LSBLK, MEDIAINFO, PANDOC, PDFINFO, PDFTOPPM, READELF,
22 RSVG_CONVERT, SEVENZ, SS, TRANSMISSION_SHOW, UDEVADM,
23};
24use crate::config::get_syntect_theme;
25use crate::io::execute_and_capture_output_without_check;
26use crate::modes::{
27 extract_extension, list_files_tar, list_files_zip, ContentWindow, DisplayedImage,
28 DisplayedImageBuilder, FileKind, FilterKind, TLine, Tree, TreeBuilder, TreeLines, Users,
29};
30
31#[derive(Default, Eq, PartialEq)]
34pub enum ExtensionKind {
35 Archive,
36 Audio,
37 Epub,
38 Font,
39 Image,
40 Iso,
41 Notebook,
42 Office,
43 Pdf,
44 Sevenz,
45 Svg,
46 Torrent,
47 Video,
48
49 #[default]
50 Default,
51}
52
53impl ExtensionKind {
54 #[rustfmt::skip]
56 pub fn matcher(ext: &str) -> Self {
57 match ext {
58 "zip" | "gzip" | "bzip2" | "xz" | "lzip" | "lzma" | "tar" | "mtree" | "raw" | "gz" | "zst" | "deb" | "rpm"
59 => Self::Archive,
60 "7z" | "7za"
61 => Self::Sevenz,
62 "png" | "jpg" | "jpeg" | "tiff" | "heif" | "gif" | "cr2" | "nef" | "orf" | "sr2"
63 => Self::Image,
64 "ogg" | "ogm" | "riff" | "mp2" | "mp3" | "wm" | "qt" | "ac3" | "dts" | "aac" | "mac" | "flac"
65 => Self::Audio,
66 "mkv" | "webm" | "mpeg" | "mp4" | "avi" | "flv" | "mpg" | "wmv" | "m4v" | "mov"
67 => Self::Video,
68 "ttf" | "otf" | "woff"
69 => Self::Font,
70 "svg" | "svgz"
71 => Self::Svg,
72 "pdf"
73 => Self::Pdf,
74 "iso"
75 => Self::Iso,
76 "ipynb"
77 => Self::Notebook,
78 "doc" | "docx" | "odt" | "sxw" | "xlsx" | "xls"
79 => Self::Office,
80 "epub"
81 => Self::Epub,
82 "torrent"
83 => Self::Torrent,
84 _
85 => Self::Default,
86 }
87 }
88
89 #[rustfmt::skip]
90 fn has_programs(&self) -> bool {
91 match self {
92 Self::Archive => is_in_path(BSDTAR),
93 Self::Epub => is_in_path(PANDOC),
94 Self::Iso => is_in_path(ISOINFO),
95 Self::Notebook => is_in_path(JUPYTER),
96 Self::Audio => is_in_path(MEDIAINFO),
97 Self::Office => is_in_path(LIBREOFFICE),
98 Self::Torrent => is_in_path(TRANSMISSION_SHOW),
99 Self::Sevenz => is_in_path(SEVENZ),
100 Self::Svg => is_in_path(RSVG_CONVERT),
101 Self::Video => is_in_path(FFMPEG),
102 Self::Font => is_in_path(FONTIMAGE),
103 Self::Pdf => {
104 is_in_path(PDFINFO)
105 && is_in_path(PDFTOPPM)
106 }
107
108 _ => true,
109 }
110 }
111
112 fn is_image_kind(&self) -> bool {
113 matches!(
114 &self,
115 ExtensionKind::Font
116 | ExtensionKind::Image
117 | ExtensionKind::Office
118 | ExtensionKind::Pdf
119 | ExtensionKind::Svg
120 | ExtensionKind::Video
121 )
122 }
123}
124
125impl std::fmt::Display for ExtensionKind {
126 #[rustfmt::skip]
127 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
128 match self {
129 Self::Archive => write!(f, "archive"),
130 Self::Image => write!(f, "image"),
131 Self::Audio => write!(f, "audio"),
132 Self::Video => write!(f, "video"),
133 Self::Font => write!(f, "font"),
134 Self::Sevenz => write!(f, "7zip"),
135 Self::Svg => write!(f, "svg"),
136 Self::Pdf => write!(f, "pdf"),
137 Self::Iso => write!(f, "iso"),
138 Self::Notebook => write!(f, "notebook"),
139 Self::Office => write!(f, "office"),
140 Self::Epub => write!(f, "epub"),
141 Self::Torrent => write!(f, "torrent"),
142 Self::Default => write!(f, "default"),
143 }
144 }
145}
146
147#[derive(Default)]
151pub enum Preview {
152 Syntaxed(HLContent),
153 Text(Text),
154 Binary(BinaryContent),
155 Image(DisplayedImage),
156 Tree(Tree),
157 #[default]
158 Empty,
159}
160
161impl Preview {
162 pub fn len(&self) -> usize {
165 match self {
166 Self::Empty => 0,
167 Self::Syntaxed(preview) => preview.len(),
168 Self::Text(preview) => preview.len(),
169 Self::Binary(preview) => preview.len(),
170 Self::Image(preview) => preview.len(),
171 Self::Tree(tree) => tree.displayable().lines().len(),
172 }
173 }
174
175 pub fn kind_display(&self) -> &str {
176 match self {
177 Self::Empty => "empty",
178 Self::Syntaxed(_) => "an highlighted text",
179 Self::Text(text) => text.kind.for_first_line(),
180 Self::Binary(_) => "a binary file",
181 Self::Image(image) => image.kind.for_first_line(),
182 Self::Tree(_) => "a tree",
183 }
184 }
185
186 pub fn is_empty(&self) -> bool {
188 matches!(self, Self::Empty)
189 }
190
191 pub fn window_for_second_pane(&self, height: usize) -> ContentWindow {
193 ContentWindow::new(self.len(), height)
194 }
195
196 pub fn filepath(&self) -> String {
197 match self {
198 Self::Empty => "".to_owned(),
199 Self::Syntaxed(preview) => preview.filepath().to_owned(),
200 Self::Text(preview) => preview.title.to_owned(),
201 Self::Binary(preview) => preview.path.to_string_lossy().to_string(),
202 Self::Image(preview) => preview.identifier.to_owned(),
203 Self::Tree(tree) => tree.root_path().to_string_lossy().to_string(),
204 }
205 }
206}
207
208pub struct PreviewBuilder {
211 path: PathBuf,
212}
213
214impl PreviewBuilder {
215 const CONTENT_INSPECTOR_MIN_SIZE: usize = 1024;
216
217 pub fn new(path: &Path) -> Self {
218 Self {
219 path: path.to_owned(),
220 }
221 }
222
223 pub fn empty() -> Preview {
225 clear_tmp_files();
226 Preview::Empty
227 }
228
229 pub fn build(self) -> Result<Preview> {
236 clear_tmp_files();
237 let file_kind = FileKind::new(&symlink_metadata(&self.path)?, &self.path);
238 match file_kind {
239 FileKind::Directory => self.directory(),
240 FileKind::NormalFile => self.normal_file(),
241 FileKind::Socket if is_in_path(SS) => self.socket(),
242 FileKind::BlockDevice if is_in_path(LSBLK) => self.block_device(),
243 FileKind::Fifo | FileKind::CharDevice if is_in_path(UDEVADM) => self.fifo_chardevice(),
244 FileKind::SymbolicLink(true) => self.valid_symlink(),
245 _ => Ok(Preview::default()),
246 }
247 }
248
249 fn directory(&self) -> Result<Preview> {
253 let users = Users::default();
254 Ok(Preview::Tree(
255 TreeBuilder::new(std::sync::Arc::from(self.path.as_path()), &users)
256 .with_max_depth(4)
257 .with_hidden(false)
258 .with_filter_kind(&FilterKind::All)
259 .build(),
260 ))
261 }
262
263 fn valid_symlink(&self) -> Result<Preview> {
264 Self::new(&std::fs::read_link(&self.path).unwrap_or_default()).build()
265 }
266
267 fn normal_file(&self) -> Result<Preview> {
268 let extension = extract_extension(&self.path).to_lowercase();
269 let kind = ExtensionKind::matcher(&extension);
270 match kind {
271 ExtensionKind::Archive if kind.has_programs() => {
272 Ok(Preview::Text(Text::archive(&self.path, &extension)?))
273 }
274 ExtensionKind::Sevenz if kind.has_programs() => {
275 Ok(Preview::Text(Text::sevenz(&self.path)?))
276 }
277 ExtensionKind::Iso if kind.has_programs() => Ok(Preview::Text(Text::iso(&self.path)?)),
278 ExtensionKind::Epub if kind.has_programs() => Ok(Preview::Text(
279 Text::epub(&self.path).context("Preview: Couldn't read epub")?,
280 )),
281 ExtensionKind::Torrent if kind.has_programs() => Ok(Preview::Text(
282 Text::torrent(&self.path).context("Preview: Couldn't read torrent")?,
283 )),
284 ExtensionKind::Notebook if kind.has_programs() => {
285 Ok(Self::notebook(&self.path).context("Preview: Couldn't parse notebook")?)
286 }
287 ExtensionKind::Audio if kind.has_programs() => {
288 Ok(Preview::Text(Text::media_content(&self.path)?))
289 }
290 _ if kind.is_image_kind() && kind.has_programs() => Self::image(&self.path, kind),
291 _ => match self.syntaxed(&extension) {
292 Some(syntaxed_preview) => Ok(syntaxed_preview),
293 None => self.text_or_binary(),
294 },
295 }
296 }
297
298 fn image(path: &Path, kind: ExtensionKind) -> Result<Preview> {
299 let preview = DisplayedImageBuilder::new(path, kind.into()).build()?;
300 if preview.is_empty() {
301 Ok(Preview::Empty)
302 } else {
303 Ok(Preview::Image(preview))
304 }
305 }
306
307 fn socket(&self) -> Result<Preview> {
308 Ok(Preview::Text(Text::socket(&self.path)?))
309 }
310
311 fn block_device(&self) -> Result<Preview> {
312 Ok(Preview::Text(Text::block_device(&self.path)?))
313 }
314
315 fn fifo_chardevice(&self) -> Result<Preview> {
316 Ok(Preview::Text(Text::fifo_chardevice(&self.path)?))
317 }
318
319 fn syntaxed(&self, ext: &str) -> Option<Preview> {
320 if symlink_metadata(&self.path).ok()?.len() > HLContent::SIZE_LIMIT as u64 {
321 return None;
322 };
323 let ss = SyntaxSet::load_defaults_nonewlines();
324 Some(Preview::Syntaxed(
325 HLContent::new(&self.path, ss.clone(), ss.find_syntax_by_extension(ext)?)
326 .unwrap_or_default(),
327 ))
328 }
329
330 fn notebook(path: &Path) -> Option<Preview> {
331 let path_str = path.to_str()?;
332 let output = execute_and_capture_output_without_check(
334 JUPYTER,
335 &["nbconvert", "--to", "markdown", path_str, "--stdout"],
336 )
337 .ok()?;
338 Self::syntaxed_from_str(output, "md")
339 }
340
341 fn syntaxed_from_str(output: String, ext: &str) -> Option<Preview> {
342 let ss = SyntaxSet::load_defaults_nonewlines();
343 Some(Preview::Syntaxed(
344 HLContent::from_str(
345 "command".to_owned(),
346 &output,
347 ss.clone(),
348 ss.find_syntax_by_extension(ext)?,
349 )
350 .unwrap_or_default(),
351 ))
352 }
353
354 fn text_or_binary(&self) -> Result<Preview> {
355 if let Some(elf) = self.read_elf() {
356 Ok(Preview::Text(Text::from_readelf(&self.path, elf)?))
357 } else if self.is_binary()? {
358 Ok(Preview::Binary(BinaryContent::new(&self.path)?))
359 } else {
360 Ok(Preview::Text(Text::from_file(&self.path)?))
361 }
362 }
363
364 fn read_elf(&self) -> Option<String> {
365 let Ok(output) = execute_and_capture_output_without_check(
366 READELF,
367 &["-WCa", self.path.to_string_lossy().as_ref()],
368 ) else {
369 return None;
370 };
371 if output.is_empty() {
372 None
373 } else {
374 Some(output)
375 }
376 }
377
378 fn is_binary(&self) -> Result<bool> {
379 let mut file = std::fs::File::open(&self.path)?;
380 let mut buffer = [0; Self::CONTENT_INSPECTOR_MIN_SIZE];
381 let Ok(metadata) = self.path.metadata() else {
382 return Ok(false);
383 };
384
385 Ok(metadata.len() >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64
386 && file.read_exact(&mut buffer).is_ok()
387 && inspect(&buffer) == ContentType::BINARY)
388 }
389
390 pub fn help(help: &str) -> Preview {
392 Preview::Text(Text::help(help))
393 }
394
395 pub fn log(log: Vec<String>) -> Preview {
396 Preview::Text(Text::log(log))
397 }
398
399 pub fn cli_info(output: &str, command: String) -> Preview {
400 crate::log_info!("cli_info. command {command} - output\n{output}");
401 Preview::Text(Text::command_stdout(output, command))
402 }
403}
404
405fn read_nb_lines(path: &Path, size_limit: usize) -> Result<Vec<String>> {
407 let reader = std::io::BufReader::new(std::fs::File::open(path)?);
408 Ok(reader
409 .lines()
410 .take(size_limit)
411 .map(|line| line.unwrap_or_else(|_| "".to_owned()))
412 .collect())
413}
414
415#[derive(Clone, Default, Debug)]
418pub enum TextKind {
419 #[default]
420 TEXTFILE,
421
422 Archive,
423 Blockdevice,
424 CommandStdout,
425 Elf,
426 Epub,
427 FifoChardevice,
428 Help,
429 Iso,
430 Log,
431 Mediacontent,
432 Sevenz,
433 Socket,
434 Torrent,
435}
436
437impl TextKind {
438 pub fn for_first_line(&self) -> &'static str {
440 match self {
441 Self::TEXTFILE => "a textfile",
442 Self::Archive => "an archive",
443 Self::Blockdevice => "a Blockdevice file",
444 Self::CommandStdout => "a command stdout",
445 Self::Elf => "an elf file",
446 Self::Epub => "an epub",
447 Self::FifoChardevice => "a Fifo or Chardevice file",
448 Self::Help => "Help",
449 Self::Iso => "Iso",
450 Self::Log => "Log",
451 Self::Mediacontent => "a media content",
452 Self::Sevenz => "a 7z archive",
453 Self::Socket => "a Socket file",
454 Self::Torrent => "a torrent",
455 }
456 }
457}
458
459impl Display for TextKind {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 writeln!(f, "{kind_str}", kind_str = self.for_first_line())
462 }
463}
464
465#[derive(Clone, Default, Debug)]
468pub struct Text {
469 pub kind: TextKind,
470 pub title: String,
471 content: Vec<String>,
472 length: usize,
473}
474
475impl Text {
476 const SIZE_LIMIT: usize = 1 << 20;
478
479 fn help(help: &str) -> Self {
480 let content: Vec<String> = help.lines().map(|line| line.to_owned()).collect();
481 Self {
482 title: "Help".to_string(),
483 kind: TextKind::Help,
484 length: content.len(),
485 content,
486 }
487 }
488
489 fn log(content: Vec<String>) -> Self {
490 Self {
491 title: "Logs".to_string(),
492 kind: TextKind::Log,
493 length: content.len(),
494 content,
495 }
496 }
497
498 fn epub(path: &Path) -> Option<Self> {
499 let path_str = path.to_str()?;
500 let output = execute_and_capture_output_without_check(
501 PANDOC,
502 &["-s", "-t", "plain", "--", path_str],
503 )
504 .ok()?;
505 let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect();
506 Some(Self {
507 title: "Epub".to_string(),
508 kind: TextKind::Epub,
509 length: content.len(),
510 content,
511 })
512 }
513
514 fn from_file(path: &Path) -> Result<Self> {
515 let content = read_nb_lines(path, Self::SIZE_LIMIT)?;
516 Ok(Self {
517 title: filename_from_path(path).context("")?.to_owned(),
518 kind: TextKind::TEXTFILE,
519 length: content.len(),
520 content,
521 })
522 }
523
524 fn from_readelf(path: &Path, elf: String) -> Result<Self> {
525 Ok(Self {
526 title: filename_from_path(path).context("")?.to_owned(),
527 kind: TextKind::Elf,
528 length: elf.len(),
529 content: elf.lines().map(|line| line.to_owned()).collect(),
530 })
531 }
532
533 fn from_command_output(kind: TextKind, command: &str, args: &[&str]) -> Result<Self> {
534 let content: Vec<String> = execute_and_capture_output_without_check(command, args)?
535 .lines()
536 .map(|s| s.to_owned())
537 .collect();
538 Ok(Self {
539 title: command.to_owned(),
540 kind,
541 length: content.len(),
542 content,
543 })
544 }
545
546 fn media_content(path: &Path) -> Result<Self> {
547 Self::from_command_output(
548 TextKind::Mediacontent,
549 MEDIAINFO,
550 &[path_to_string(&path).as_str()],
551 )
552 }
553
554 fn archive(path: &Path, ext: &str) -> Result<Self> {
558 let content = match ext {
559 "zip" => list_files_zip(path).unwrap_or(vec!["Invalid Zip content".to_owned()]),
560 "zst" | "gz" | "bz" | "xz" | "gzip" | "bzip2" | "deb" | "rpm" => {
561 list_files_tar(path).unwrap_or(vec!["Invalid Tar content".to_owned()])
562 }
563 _ => vec![format!("Unsupported format: {ext}")],
564 };
565
566 Ok(Self {
567 title: filename_from_path(path).context("")?.to_owned(),
568 kind: TextKind::Archive,
569 length: content.len(),
570 content,
571 })
572 }
573
574 fn sevenz(path: &Path) -> Result<Self> {
575 Self::from_command_output(TextKind::Sevenz, SEVENZ, &["l", &path_to_string(&path)])
576 }
577
578 fn iso(path: &Path) -> Result<Self> {
579 Self::from_command_output(
580 TextKind::Iso,
581 ISOINFO,
582 &["-l", "-i", &path_to_string(&path)],
583 )
584 }
585
586 fn torrent(path: &Path) -> Result<Self> {
587 Self::from_command_output(
588 TextKind::Torrent,
589 TRANSMISSION_SHOW,
590 &[&path_to_string(&path)],
591 )
592 }
593
594 fn socket(path: &Path) -> Result<Self> {
597 let mut preview = Self::from_command_output(TextKind::Socket, SS, &["-lpmepiT"])?;
598 preview.content = preview
599 .content
600 .iter()
601 .filter(|l| l.contains(path.file_name().unwrap().to_string_lossy().as_ref()))
602 .map(|s| s.to_owned())
603 .collect();
604 Ok(preview)
605 }
606
607 fn block_device(path: &Path) -> Result<Self> {
610 Self::from_command_output(
611 TextKind::Blockdevice,
612 LSBLK,
613 &[
614 "-lfo",
615 "FSTYPE,PATH,LABEL,UUID,FSVER,MOUNTPOINT,MODEL,SIZE,FSAVAIL,FSUSE%",
616 &path_to_string(&path),
617 ],
618 )
619 }
620
621 fn fifo_chardevice(path: &Path) -> Result<Self> {
624 Self::from_command_output(
625 TextKind::FifoChardevice,
626 UDEVADM,
627 &[
628 "info",
629 "-a",
630 "-n",
631 path_to_string(&path).as_str(),
632 "--no-pager",
633 ],
634 )
635 }
636 pub fn command_stdout(output: &str, title: String) -> Self {
638 let content: Vec<String> = output.lines().map(|line| line.to_owned()).collect();
639 let length = content.len();
640 Self {
641 title,
642 kind: TextKind::CommandStdout,
643 content,
644 length,
645 }
646 }
647
648 fn len(&self) -> usize {
649 self.length
650 }
651}
652
653#[derive(Clone, Default)]
656pub struct HLContent {
657 path: String,
658 content: Vec<Vec<SyntaxedString>>,
659 length: usize,
660}
661
662impl HLContent {
663 const SIZE_LIMIT: usize = 1 << 15;
665
666 fn new(path: &Path, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> Result<Self> {
671 let raw_content = read_nb_lines(path, Self::SIZE_LIMIT)?;
672 Self::build(
673 path.to_string_lossy().to_string(),
674 raw_content,
675 syntax_set,
676 syntax_ref,
677 )
678 }
679
680 fn from_str(
681 name: String,
682 text: &str,
683 syntax_set: SyntaxSet,
684 syntax_ref: &SyntaxReference,
685 ) -> Result<Self> {
686 let raw_content = text
687 .lines()
688 .take(Self::SIZE_LIMIT)
689 .map(|s| s.to_owned())
690 .collect();
691 Self::build(name, raw_content, syntax_set, syntax_ref)
692 }
693
694 fn build(
695 path: String,
696 raw_content: Vec<String>,
697 syntax_set: SyntaxSet,
698 syntax_ref: &SyntaxReference,
699 ) -> Result<Self> {
700 let highlighted_content = Self::parse_raw_content(raw_content, syntax_set, syntax_ref)?;
701 Ok(Self {
702 path,
703 length: highlighted_content.len(),
704 content: highlighted_content,
705 })
706 }
707
708 fn len(&self) -> usize {
709 self.length
710 }
711
712 fn filepath(&self) -> &str {
713 &self.path
714 }
715
716 fn parse_raw_content(
717 raw_content: Vec<String>,
718 syntax_set: SyntaxSet,
719 syntax_ref: &SyntaxReference,
720 ) -> Result<Vec<Vec<SyntaxedString>>> {
721 let mut highlighted_content = vec![];
722 let syntect_theme = get_syntect_theme().context("Syntect set should be set")?;
723 let mut highlighter = HighlightLines::new(syntax_ref, syntect_theme);
724
725 for line in raw_content.iter() {
726 let mut v_line = vec![];
727 if let Ok(v) = highlighter.highlight_line(line, &syntax_set) {
728 for (style, token) in v.iter() {
729 v_line.push(SyntaxedString::from_syntect(token, *style));
730 }
731 }
732 highlighted_content.push(v_line)
733 }
734
735 Ok(highlighted_content)
736 }
737}
738
739#[derive(Clone)]
743pub struct SyntaxedString {
744 pub content: String,
745 pub style: Style,
746}
747
748impl SyntaxedString {
749 fn from_syntect(content: &str, style: SyntectStyle) -> Self {
753 let content = content.to_owned();
754 let fg = style.foreground;
755 let style = Style {
756 fg: Some(Color::Rgb(fg.r, fg.g, fg.b)),
757 bg: None,
758 add_modifier: Self::font_style_to_effect(&style.font_style),
759 sub_modifier: Modifier::empty(),
760 underline_color: None,
761 };
762 Self { content, style }
763 }
764
765 fn font_style_to_effect(font_style: &FontStyle) -> Modifier {
766 let mut modifier = Modifier::empty();
767
768 if font_style.contains(FontStyle::BOLD) {
770 modifier |= Modifier::BOLD;
771 }
772
773 if font_style.contains(FontStyle::UNDERLINE) {
775 modifier |= Modifier::UNDERLINED;
776 }
777
778 modifier
779 }
780}
781
782#[derive(Clone, Default)]
787pub struct BinaryContent {
788 pub path: PathBuf,
789 length: u64,
790 content: Vec<Line>,
791}
792
793impl BinaryContent {
794 const LINE_WIDTH: usize = 16;
795 const SIZE_LIMIT: usize = 1048576;
796
797 fn new(path: &Path) -> Result<Self> {
798 let Ok(metadata) = path.metadata() else {
799 return Ok(Self::default());
800 };
801 let length = metadata.len() / Self::LINE_WIDTH as u64;
802 let content = Self::read_content(path)?;
803
804 Ok(Self {
805 path: path.to_path_buf(),
806 length,
807 content,
808 })
809 }
810
811 fn read_content(path: &Path) -> Result<Vec<Line>> {
812 let mut reader = BufReader::new(std::fs::File::open(path)?);
813 let mut buffer = [0; Self::LINE_WIDTH];
814 let mut content = vec![];
815 while let Ok(nb_bytes_read) = reader.read(&mut buffer[..]) {
816 if nb_bytes_read != Self::LINE_WIDTH {
817 content.push(Line::new((&buffer[0..nb_bytes_read]).into()));
818 break;
819 }
820 content.push(Line::new(buffer.into()));
821 if content.len() >= Self::SIZE_LIMIT {
822 break;
823 }
824 }
825 Ok(content)
826 }
827
828 pub fn len(&self) -> usize {
833 self.length as usize
834 }
835
836 pub fn is_empty(&self) -> bool {
837 self.length == 0
838 }
839
840 pub fn number_width_hex(&self) -> usize {
841 format!("{:x}", self.len() * 16).len()
842 }
843}
844
845#[derive(Clone)]
848pub struct Line {
849 line: Vec<u8>,
850}
851
852impl Line {
853 fn new(line: Vec<u8>) -> Self {
854 Self { line }
855 }
856
857 pub fn format_hex(&self) -> String {
860 let mut hex_repr = String::new();
861 for (i, byte) in self.line.iter().enumerate() {
862 let _ = write!(hex_repr, "{byte:02x}");
863 if i % 2 == 1 {
864 hex_repr.push(' ');
865 }
866 }
867 hex_repr
868 }
869
870 fn byte_to_char(byte: &u8) -> char {
873 let ch = *byte as char;
874 if ch.is_ascii_graphic() {
875 ch
876 } else {
877 '.'
878 }
879 }
880
881 pub fn format_as_ascii(&self) -> String {
884 self.line.iter().map(Self::byte_to_char).collect()
885 }
886
887 pub fn format_line_nr_hex(line_nr: usize, width: usize) -> String {
888 format!("{line_nr:0width$x} ")
889 }
890}
891
892pub trait TakeSkip<T> {
896 fn take_skip(&self, top: usize, bottom: usize, length: usize) -> Take<Skip<Iter<'_, T>>>;
897}
898
899macro_rules! impl_take_skip {
900 ($t:ident, $u:ident) => {
901 impl TakeSkip<$u> for $t {
902 fn take_skip(
903 &self,
904 top: usize,
905 bottom: usize,
906 length: usize,
907 ) -> Take<Skip<Iter<'_, $u>>> {
908 self.content.iter().skip(top).take(min(length, bottom + 1))
909 }
910 }
911 };
912}
913pub trait TakeSkipEnum<T> {
917 fn take_skip_enum(
918 &self,
919 top: usize,
920 bottom: usize,
921 length: usize,
922 ) -> Take<Skip<Enumerate<Iter<'_, T>>>>;
923}
924
925macro_rules! impl_take_skip_enum {
926 ($t:ident, $u:ident) => {
927 impl TakeSkipEnum<$u> for $t {
928 fn take_skip_enum(
929 &self,
930 top: usize,
931 bottom: usize,
932 length: usize,
933 ) -> Take<Skip<Enumerate<Iter<'_, $u>>>> {
934 self.content
935 .iter()
936 .enumerate()
937 .skip(top)
938 .take(min(length, bottom + 1))
939 }
940 }
941 };
942}
943
944pub type VecSyntaxedString = Vec<SyntaxedString>;
946
947impl_take_skip_enum!(HLContent, VecSyntaxedString);
948impl_take_skip_enum!(Text, String);
949impl_take_skip_enum!(BinaryContent, Line);
950impl_take_skip_enum!(TreeLines, TLine);
951
952impl_take_skip!(HLContent, VecSyntaxedString);
953impl_take_skip!(Text, String);
954impl_take_skip!(BinaryContent, Line);
955impl_take_skip!(TreeLines, TLine);