From 98e91dc2e58dd7534a4b2cde9f87875584095921 Mon Sep 17 00:00:00 2001 From: qkzk Date: Sun, 20 Nov 2022 11:47:03 +0100 Subject: separate display from event reader --- src/display.rs | 475 --------------------------------------------------- src/lib.rs | 2 +- src/main.rs | 5 +- src/term_manager.rs | 484 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 488 insertions(+), 478 deletions(-) delete mode 100644 src/display.rs create mode 100644 src/term_manager.rs diff --git a/src/display.rs b/src/display.rs deleted file mode 100644 index 687b4ca..0000000 --- a/src/display.rs +++ /dev/null @@ -1,475 +0,0 @@ -use std::cmp::min; -use std::sync::Arc; - -use log::info; -use sysinfo::{DiskExt, System, SystemExt}; -use tuikit::attr::*; -use tuikit::event::Event; -use tuikit::term::Term; - -use crate::config::Colors; -use crate::content_window::ContentWindow; -use crate::fileinfo::{fileinfo_attr, human_size}; -use crate::fm_error::{FmError, FmResult}; -use crate::git::git; -use crate::last_edition::LastEdition; -use crate::mode::{MarkAction, Mode}; -use crate::preview::Preview; -use crate::status::Status; -use crate::tab::Tab; - -/// Is responsible for displaying content in the terminal. -/// It uses an already created terminal. -pub struct Display { - /// The Tuikit terminal attached to the display. - /// It will print every symbol shown on screen. - term: Arc, - colors: Colors, - /// system info - sys: System, -} - -const fn color_to_attr(color: Color) -> Attr { - Attr { - fg: color, - bg: Color::Default, - effect: Effect::empty(), - } -} - -impl Display { - /// Returns a new `Display` instance from a `tuikit::term::Term` object. - pub fn new(term: Arc, colors: Colors) -> Self { - Self { - term, - colors, - sys: System::new_all(), - } - } - - pub fn show_cursor(&self) -> FmResult<()> { - Ok(self.term.show_cursor(true)?) - } - - pub fn poll_event(&self) -> FmResult { - Ok(self.term.poll_event()?) - } - - const EDIT_BOX_OFFSET: usize = 10; - const SORT_CURSOR_OFFSET: usize = 36; - const ATTR_LINE_NR: Attr = color_to_attr(Color::CYAN); - const ATTR_YELLOW: Attr = color_to_attr(Color::YELLOW); - const LINE_COLORS: [Attr; 6] = [ - color_to_attr(Color::Rgb(231, 162, 156)), - color_to_attr(Color::Rgb(124, 152, 166)), - color_to_attr(Color::Rgb(214, 125, 83)), - color_to_attr(Color::Rgb(91, 152, 119)), - color_to_attr(Color::Rgb(152, 87, 137)), - color_to_attr(Color::Rgb(230, 189, 87)), - ]; - /// Display every possible content in the terminal. - /// - /// The top line - /// - /// The files if we're displaying them - /// - /// The cursor if a content is editable - /// - /// The help if `Mode::Help` - /// - /// The jump_list if `Mode::Jump` - /// - /// The completion list if any. - /// - /// The preview in preview mode. - pub fn display_all(&mut self, status: &Status) -> FmResult<()> { - self.term.clear()?; - match status.selected_non_mut().mode { - Mode::Jump => self.jump_list(status), - Mode::History => self.history(status), - Mode::Exec | Mode::Goto | Mode::Search => self.completion(status), - Mode::NeedConfirmation => self.confirmation(status), - Mode::Preview | Mode::Help => self.preview(status), - Mode::Shortcut => self.shortcuts(status), - Mode::Marks(MarkAction::New) | Mode::Marks(MarkAction::Jump) => self.marks(status), - _ => self.files(status), - }?; - self.cursor(status)?; - self.first_line(status)?; - Ok(self.term.present()?) - } - - /// Reads and returns the `tuikit::term::Term` height. - pub fn height(&self) -> FmResult { - let (_, height) = self.term.term_size()?; - Ok(height) - } - - /// Display the top line on terminal. - /// Its content depends on the mode. - /// In normal mode we display the path and number of files. - /// When a confirmation is needed we ask the user to input `'y'` or - /// something else. - fn first_line(&mut self, status: &Status) -> FmResult<()> { - let first_row = self.create_first_row(status)?; - self.draw_colored_strings(first_row)?; - Ok(()) - } - - fn create_first_row(&mut self, status: &Status) -> FmResult> { - let tab = status.selected_non_mut(); - Ok(match tab.mode { - Mode::Normal => { - vec![ - format!("Tab: {}/{} ", status.index + 1, status.len()), - format!("{} ", tab.path_content.path_to_str()?), - format!("{} files ", tab.path_content.files.len()), - format!("{} ", tab.path_content.used_space()), - format!( - "Avail: {} ", - self.disk_space(tab.path_str().unwrap_or_default()) - ), - format!("{} ", git(&tab.path_content.path)?), - ] - } - Mode::NeedConfirmation => { - vec![ - format!("Confirm {}", tab.last_edition), - "(y/n) : ".to_owned(), - ] - } - Mode::Preview => match tab.path_content.selected_file() { - Some(fileinfo) => { - vec![ - format!("{:?}", tab.mode.clone()), - format!("{}", fileinfo.path.to_string_lossy()), - ] - } - None => vec!["".to_owned()], - }, - Mode::Help => vec![ - "fm: a dired like file manager.".to_owned(), - "Default keybindings.".to_owned(), - ], - Mode::Marks(MarkAction::Jump) => vec!["Jump to...".to_owned()], - Mode::Marks(MarkAction::New) => vec!["Save mark...".to_owned()], - _ => { - vec![ - format!("{:?}", tab.mode.clone()), - format!("{}", tab.input.string.clone()), - ] - } - }) - } - - fn draw_colored_strings(&self, first_row: Vec) -> FmResult<()> { - let mut col = 0; - for (text, color) in std::iter::zip(first_row.iter(), Self::LINE_COLORS.iter().cycle()) { - self.term.print_with_attr(0, col, text, *color)?; - col += text.len() - } - Ok(()) - } - - /// Displays the current directory content, one line per item like in - /// `ls -l`. - /// - /// Those files are always shown, which make it a little bit faster in; - /// normal (ie. default) mode. - /// When there's too much files, only those around the selected one are - /// displayed. - fn files(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - let len = tab.path_content.files.len(); - for (i, (file, string)) in std::iter::zip( - tab.path_content.files.iter(), - tab.path_content.strings().iter(), - ) - .enumerate() - .take(min(len, tab.window.bottom + 1)) - .skip(tab.window.top) - { - let row = i + ContentWindow::WINDOW_MARGIN_TOP - tab.window.top; - let mut attr = fileinfo_attr(status, file, &self.colors); - if status.flagged.contains(&file.path) { - attr.effect |= Effect::BOLD | Effect::UNDERLINE; - } - self.term.print_with_attr(row, 0, string, attr)?; - } - Ok(()) - } - - /// Display a cursor in the top row, at a correct column. - fn cursor(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - match tab.mode { - Mode::Normal | Mode::Help | Mode::Marks(_) => { - self.term.show_cursor(false)?; - } - Mode::NeedConfirmation => { - self.term.set_cursor(0, tab.last_edition.offset())?; - } - Mode::Sort => { - self.term.set_cursor(0, Self::SORT_CURSOR_OFFSET)?; - } - _ => { - self.term - .set_cursor(0, tab.input.cursor_index + Self::EDIT_BOX_OFFSET)?; - } - } - Ok(()) - } - - /// Display the possible jump destination from flagged files. - fn jump_list(&mut self, tabs: &Status) -> FmResult<()> { - self.term.print(0, 0, "Jump to...")?; - for (row, path) in tabs.flagged.iter().enumerate() { - let mut attr = Attr::default(); - if row == tabs.jump_index { - attr.effect |= Effect::REVERSE; - } - let _ = self.term.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP, - 4, - path.to_str() - .ok_or_else(|| FmError::new("Unreadable filename"))?, - attr, - ); - } - Ok(()) - } - - /// Display the history of visited directories. - fn history(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - self.term.print(0, 0, "Go to...")?; - for (row, path) in tab.history.visited.iter().rev().enumerate() { - let mut attr = Attr::default(); - if row == tab.history.len() - tab.history.index - 1 { - attr.effect |= Effect::REVERSE; - } - self.term.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP, - 4, - path.to_str() - .ok_or_else(|| FmError::new("Unreadable filename"))?, - attr, - )?; - } - Ok(()) - } - - /// Display the predefined shortcuts. - fn shortcuts(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - self.term.print(0, 0, "Go to...")?; - for (row, path) in tab.shortcut.shortcuts.iter().enumerate() { - let mut attr = Attr::default(); - if row == tab.shortcut.index { - attr.effect |= Effect::REVERSE; - } - let _ = self.term.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP, - 4, - path.to_str() - .ok_or_else(|| FmError::new("Unreadable filename"))?, - attr, - ); - } - Ok(()) - } - - /// Display the possible completion items. The currently selected one is - /// reversed. - fn completion(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - self.term - .set_cursor(0, tab.input.cursor_index + Self::EDIT_BOX_OFFSET)?; - for (row, candidate) in tab.completion.proposals.iter().enumerate() { - let mut attr = Attr::default(); - if row == tab.completion.index { - attr.effect |= Effect::REVERSE; - } - self.term.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP, - 4, - candidate, - attr, - )?; - } - Ok(()) - } - - /// Display a list of edited (deleted, copied, moved) files for confirmation - fn confirmation(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - for (row, path) in status.flagged.iter().enumerate() { - self.term.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP + 2, - 4, - path.to_str() - .ok_or_else(|| FmError::new("Unreadable filename"))?, - Attr::default(), - )?; - } - info!("last_edition: {}", tab.last_edition); - if let LastEdition::CopyPaste = tab.last_edition { - let attr = Attr { - fg: Color::YELLOW, - bg: Color::Default, - effect: Effect::BOLD, - }; - let content = format!( - "Files will be copied to {}", - tab.path_content.path_to_str()? - ); - self.term.print_with_attr(2, 3, &content, attr)?; - } - Ok(()) - } - - fn preview_line_numbers( - &mut self, - tab: &Tab, - line_index: usize, - row_index: usize, - ) -> FmResult { - Ok(self.term.print_with_attr( - row_index, - 0, - &(line_index + 1 + tab.window.top).to_string(), - Self::ATTR_LINE_NR, - )?) - } - - /// Display a scrollable preview of a file. - /// Multiple modes are supported : - /// if the filename extension is recognized, the preview is highlighted, - /// if the file content is recognized as binary, an hex dump is previewed with 16 bytes lines, - /// else the content is supposed to be text and shown as such. - /// It may fail to recognize some usual extensions, notably `.toml`. - /// It may fail to recognize small files (< 1024 bytes). - fn preview(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - - let length = tab.preview.len(); - let line_number_width = length.to_string().len(); - match &tab.preview { - // TODO: should it belong to separate methods ? - Preview::Syntaxed(syntaxed) => { - for (i, vec_line) in syntaxed - .content - .iter() - .enumerate() - .skip(tab.window.top) - .take(min(length, tab.window.bottom + 1)) - { - let row = Self::calc_line_row(i, tab); - self.preview_line_numbers(tab, i, row)?; - for token in vec_line.iter() { - token.print(&self.term, row, line_number_width)?; - } - } - } - Preview::Text(text) => { - for (i, line) in text - .content - .iter() - .enumerate() - .skip(tab.window.top) - .take(min(length, tab.window.bottom + 1)) - { - let row = Self::calc_line_row(i, tab); - self.term.print(row, line_number_width + 3, line)?; - } - } - Preview::Binary(bin) => { - let line_number_width_hex = format!("{:x}", bin.len() * 16).len(); - - for (i, line) in bin - .content - .iter() - .enumerate() - .skip(tab.window.top) - .take(min(length, tab.window.bottom + 1)) - { - let row = Self::calc_line_row(i, tab); - - self.term.print_with_attr( - row, - 0, - &format_line_nr_hex(i + 1 + tab.window.top, line_number_width_hex), - Self::ATTR_LINE_NR, - )?; - line.print(&self.term, row, line_number_width_hex + 1); - } - } - Preview::Pdf(text) => { - for (i, line) in text - .content - .iter() - .enumerate() - .skip(tab.window.top) - .take(min(length, tab.window.bottom + 1)) - { - let row = Self::calc_line_row(i, tab); - self.term.print(row, line_number_width + 3, line)?; - } - } - Preview::Compressed(zip) => { - for (i, line) in zip - .content - .iter() - .enumerate() - .skip(tab.window.top) - .take(min(length, tab.window.bottom + 1)) - { - let row = Self::calc_line_row(i, tab); - self.term.print_with_attr( - row, - 0, - &(i + 1 + tab.window.top).to_string(), - Self::ATTR_LINE_NR, - )?; - self.term.print(row, line_number_width + 3, line)?; - } - } - Preview::Empty => (), - } - Ok(()) - } - - fn marks(&mut self, status: &Status) -> FmResult<()> { - let tab = status.selected_non_mut(); - - self.term - .print_with_attr(2, 1, "mark path", Self::ATTR_YELLOW)?; - - for (i, line) in status.marks.as_strings()?.iter().enumerate() { - let row = Self::calc_line_row(i, tab) + 2; - self.term.print(row, 3, line)?; - } - Ok(()) - } - - fn calc_line_row(i: usize, status: &Tab) -> usize { - i + ContentWindow::WINDOW_MARGIN_TOP - status.window.top - } - - fn disk_space(&mut self, path_str: String) -> String { - self.sys.refresh_disks(); - let mut size = 0_u64; - let mut disks: Vec<&sysinfo::Disk> = self.sys.disks().iter().collect(); - disks.sort_by_key(|disk| disk.mount_point().as_os_str().len()); - for disk in disks { - if path_str.contains(disk.mount_point().as_os_str().to_str().unwrap()) { - size = disk.available_space(); - }; - } - human_size(size) - } -} - -fn format_line_nr_hex(line_nr: usize, width: usize) -> String { - format!("{:0width$x}", line_nr) -} diff --git a/src/lib.rs b/src/lib.rs index 90a07a8..49e60dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ pub mod compress; pub mod config; pub mod content_window; pub mod copy_move; -pub mod display; pub mod event_char; pub mod fileinfo; pub mod filter; @@ -25,4 +24,5 @@ pub mod shortcut; pub mod skim; pub mod status; pub mod tab; +pub mod term_manager; pub mod visited; diff --git a/src/main.rs b/src/main.rs index da9b694..e249ee0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,10 @@ use tuikit::term::Term; use fm::actioner::Actioner; use fm::args::Args; use fm::config::load_config; -use fm::display::Display; use fm::fm_error::FmResult; use fm::log::set_logger; use fm::status::Status; +use fm::term_manager::{Display, EventReader}; static CONFIG_PATH: &str = "~/.config/fm/config.yaml"; @@ -31,10 +31,11 @@ fn main() -> FmResult<()> { let config = load_config(CONFIG_PATH); let term = Arc::new(init_term()?); let actioner = Actioner::new(&config.keybindings, term.clone()); + let term_manager = EventReader::new(term.clone()); let mut display = Display::new(term.clone(), config.colors.clone()); let mut status = Status::new(Args::parse(), config, display.height()?, term)?; - while let Ok(event) = display.poll_event() { + while let Ok(event) = term_manager.poll_event() { actioner.read_event(&mut status, event)?; display.display_all(&status)?; diff --git a/src/term_manager.rs b/src/term_manager.rs new file mode 100644 index 0000000..55d6dfc --- /dev/null +++ b/src/term_manager.rs @@ -0,0 +1,484 @@ +use std::cmp::min; +use std::sync::Arc; + +use log::info; +use sysinfo::{DiskExt, System, SystemExt}; +use tuikit::attr::*; +use tuikit::event::Event; +use tuikit::term::Term; + +use crate::config::Colors; +use crate::content_window::ContentWindow; +use crate::fileinfo::{fileinfo_attr, human_size}; +use crate::fm_error::{FmError, FmResult}; +use crate::git::git; +use crate::last_edition::LastEdition; +use crate::mode::{MarkAction, Mode}; +use crate::preview::Preview; +use crate::status::Status; +use crate::tab::Tab; + +pub struct EventReader { + term: Arc, +} + +impl EventReader { + pub fn new(term: Arc) -> Self { + Self { term } + } + + pub fn poll_event(&self) -> FmResult { + Ok(self.term.poll_event()?) + } +} + +/// Is responsible for displaying content in the terminal. +/// It uses an already created terminal. +pub struct Display { + /// The Tuikit terminal attached to the display. + /// It will print every symbol shown on screen. + term: Arc, + colors: Colors, + /// system info + sys: System, +} +impl Display { + /// Returns a new `Display` instance from a `tuikit::term::Term` object. + pub fn new(term: Arc, colors: Colors) -> Self { + Self { + term, + colors, + sys: System::new_all(), + } + } + + pub fn show_cursor(&self) -> FmResult<()> { + Ok(self.term.show_cursor(true)?) + } + + const EDIT_BOX_OFFSET: usize = 10; + const SORT_CURSOR_OFFSET: usize = 36; + const ATTR_LINE_NR: Attr = color_to_attr(Color::CYAN); + const ATTR_YELLOW: Attr = color_to_attr(Color::YELLOW); + const LINE_COLORS: [Attr; 6] = [ + color_to_attr(Color::Rgb(231, 162, 156)), + color_to_attr(Color::Rgb(124, 152, 166)), + color_to_attr(Color::Rgb(214, 125, 83)), + color_to_attr(Color::Rgb(91, 152, 119)), + color_to_attr(Color::Rgb(152, 87, 137)), + color_to_attr(Color::Rgb(230, 189, 87)), + ]; + /// Display every possible content in the terminal. + /// + /// The top line + /// + /// The files if we're displaying them + /// + /// The cursor if a content is editable + /// + /// The help if `Mode::Help` + /// + /// The jump_list if `Mode::Jump` + /// + /// The completion list if any. + /// + /// The preview in preview mode. + pub fn display_all(&mut self, status: &Status) -> FmResult<()> { + self.term.clear()?; + match status.selected_non_mut().mode { + Mode::Jump => self.jump_list(status), + Mode::History => self.history(status), + Mode::Exec | Mode::Goto | Mode::Search => self.completion(status), + Mode::NeedConfirmation => self.confirmation(status), + Mode::Preview | Mode::Help => self.preview(status), + Mode::Shortcut => self.shortcuts(status), + Mode::Marks(MarkAction::New) | Mode::Marks(MarkAction::Jump) => self.marks(status), + _ => self.files(status), + }?; + self.cursor(status)?; + self.first_line(status)?; + Ok(self.term.present()?) + } + + /// Reads and returns the `tuikit::term::Term` height. + pub fn height(&self) -> FmResult { + let (_, height) = self.term.term_size()?; + Ok(height) + } + + /// Display the top line on terminal. + /// Its content depends on the mode. + /// In normal mode we display the path and number of files. + /// When a confirmation is needed we ask the user to input `'y'` or + /// something else. + fn first_line(&mut self, status: &Status) -> FmResult<()> { + let first_row = self.create_first_row(status)?; + self.draw_colored_strings(first_row)?; + Ok(()) + } + + fn create_first_row(&mut self, status: &Status) -> FmResult> { + let tab = status.selected_non_mut(); + Ok(match tab.mode { + Mode::Normal => { + vec![ + format!("Tab: {}/{} ", status.index + 1, status.len()), + format!("{} ", tab.path_content.path_to_str()?), + format!("{} files ", tab.path_content.files.len()), + format!("{} ", tab.path_content.used_space()), + format!( + "Avail: {} ", + self.disk_space(tab.path_str().unwrap_or_default()) + ), + format!("{} ", git(&tab.path_content.path)?), + ] + } + Mode::NeedConfirmation => { + vec![ + format!("Confirm {}", tab.last_edition), + "(y/n) : ".to_owned(), + ] + } + Mode::Preview => match tab.path_content.selected_file() { + Some(fileinfo) => { + vec![ + format!("{:?}", tab.mode.clone()), + format!("{}", fileinfo.path.to_string_lossy()), + ] + } + None => vec!["".to_owned()], + }, + Mode::Help => vec![ + "fm: a dired like file manager.".to_owned(), + "Default keybindings.".to_owned(), + ], + Mode::Marks(MarkAction::Jump) => vec!["Jump to...".to_owned()], + Mode::Marks(MarkAction::New) => vec!["Save mark...".to_owned()], + _ => { + vec![ + format!("{:?}", tab.mode.clone()), + format!("{}", tab.input.string.clone()), + ] + } + }) + } + + fn draw_colored_strings(&self, first_row: Vec) -> FmResult<()> { + let mut col = 0; + for (text, color) in std::iter::zip(first_row.iter(), Self::LINE_COLORS.iter().cycle()) { + self.term.print_with_attr(0, col, text, *color)?; + col += text.len() + } + Ok(()) + } + + /// Displays the current directory content, one line per item like in + /// `ls -l`. + /// + /// Those files are always shown, which make it a little bit faster in; + /// normal (ie. default) mode. + /// When there's too much files, only those around the selected one are + /// displayed. + fn files(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + let len = tab.path_content.files.len(); + for (i, (file, string)) in std::iter::zip( + tab.path_content.files.iter(), + tab.path_content.strings().iter(), + ) + .enumerate() + .take(min(len, tab.window.bottom + 1)) + .skip(tab.window.top) + { + let row = i + ContentWindow::WINDOW_MARGIN_TOP - tab.window.top; + let mut attr = fileinfo_attr(status, file, &self.colors); + if status.flagged.contains(&file.path) { + attr.effect |= Effect::BOLD | Effect::UNDERLINE; + } + self.term.print_with_attr(row, 0, string, attr)?; + } + Ok(()) + } + + /// Display a cursor in the top row, at a correct column. + fn cursor(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + match tab.mode { + Mode::Normal | Mode::Help | Mode::Marks(_) => { + self.term.show_cursor(false)?; + } + Mode::NeedConfirmation => { + self.term.set_cursor(0, tab.last_edition.offset())?; + } + Mode::Sort => { + self.term.set_cursor(0, Self::SORT_CURSOR_OFFSET)?; + } + _ => { + self.term + .set_cursor(0, tab.input.cursor_index + Self::EDIT_BOX_OFFSET)?; + } + } + Ok(()) + } + + /// Display the possible jump destination from flagged files. + fn jump_list(&mut self, tabs: &Status) -> FmResult<()> { + self.term.print(0, 0, "Jump to...")?; + for (row, path) in tabs.flagged.iter().enumerate() { + let mut attr = Attr::default(); + if row == tabs.jump_index { + attr.effect |= Effect::REVERSE; + } + let _ = self.term.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP, + 4, + path.to_str() + .ok_or_else(|| FmError::new("Unreadable filename"))?, + attr, + ); + } + Ok(()) + } + + /// Display the history of visited directories. + fn history(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + self.term.print(0, 0, "Go to...")?; + for (row, path) in tab.history.visited.iter().rev().enumerate() { + let mut attr = Attr::default(); + if row == tab.history.len() - tab.history.index - 1 { + attr.effect |= Effect::REVERSE; + } + self.term.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP, + 4, + path.to_str() + .ok_or_else(|| FmError::new("Unreadable filename"))?, + attr, + )?; + } + Ok(()) + } + + /// Display the predefined shortcuts. + fn shortcuts(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + self.term.print(0, 0, "Go to...")?; + for (row, path) in tab.shortcut.shortcuts.iter().enumerate() { + let mut attr = Attr::default(); + if row == tab.shortcut.index { + attr.effect |= Effect::REVERSE; + } + let _ = self.term.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP, + 4, + path.to_str() + .ok_or_else(|| FmError::new("Unreadable filename"))?, + attr, + ); + } + Ok(()) + } + + /// Display the possible completion items. The currently selected one is + /// reversed. + fn completion(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + self.term + .set_cursor(0, tab.input.cursor_index + Self::EDIT_BOX_OFFSET)?; + for (row, candidate) in tab.completion.proposals.iter().enumerate() { + let mut attr = Attr::default(); + if row == tab.completion.index { + attr.effect |= Effect::REVERSE; + } + self.term.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP, + 4, + candidate, + attr, + )?; + } + Ok(()) + } + + /// Display a list of edited (deleted, copied, moved) files for confirmation + fn confirmation(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + for (row, path) in status.flagged.iter().enumerate() { + self.term.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP + 2, + 4, + path.to_str() + .ok_or_else(|| FmError::new("Unreadable filename"))?, + Attr::default(), + )?; + } + info!("last_edition: {}", tab.last_edition); + if let LastEdition::CopyPaste = tab.last_edition { + let attr = Attr { + fg: Color::YELLOW, + bg: Color::Default, + effect: Effect::BOLD, + }; + let content = format!( + "Files will be copied to {}", + tab.path_content.path_to_str()? + ); + self.term.print_with_attr(2, 3, &content, attr)?; + } + Ok(()) + } + + fn preview_line_numbers( + &mut self, + tab: &Tab, + line_index: usize, + row_index: usize, + ) -> FmResult { + Ok(self.term.print_with_attr( + row_index, + 0, + &(line_index + 1 + tab.window.top).to_string(), + Self::ATTR_LINE_NR, + )?) + } + + /// Display a scrollable preview of a file. + /// Multiple modes are supported : + /// if the filename extension is recognized, the preview is highlighted, + /// if the file content is recognized as binary, an hex dump is previewed with 16 bytes lines, + /// else the content is supposed to be text and shown as such. + /// It may fail to recognize some usual extensions, notably `.toml`. + /// It may fail to recognize small files (< 1024 bytes). + fn preview(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + + let length = tab.preview.len(); + let line_number_width = length.to_string().len(); + match &tab.preview { + // TODO: should it belong to separate methods ? + Preview::Syntaxed(syntaxed) => { + for (i, vec_line) in syntaxed + .content + .iter() + .enumerate() + .skip(tab.window.top) + .take(min(length, tab.window.bottom + 1)) + { + let row = Self::calc_line_row(i, tab); + self.preview_line_numbers(tab, i, row)?; + for token in vec_line.iter() { + token.print(&self.term, row, line_number_width)?; + } + } + } + Preview::Text(text) => { + for (i, line) in text + .content + .iter() + .enumerate() + .skip(tab.window.top) + .take(min(length, tab.window.bottom + 1)) + { + let row = Self::calc_line_row(i, tab); + self.term.print(row, line_number_width + 3, line)?; + } + } + Preview::Binary(bin) => { + let line_number_width_hex = format!("{:x}", bin.len() * 16).len(); + + for (i, line) in bin + .content + .iter() + .enumerate() + .skip(tab.window.top) + .take(min(length, tab.window.bottom + 1)) + { + let row = Self::calc_line_row(i, tab); + + self.term.print_with_attr( + row, + 0, + &format_line_nr_hex(i + 1 + tab.window.top, line_number_width_hex), + Self::ATTR_LINE_NR, + )?; + line.print(&self.term, row, line_number_width_hex + 1); + } + } + Preview::Pdf(text) => { + for (i, line) in text + .content + .iter() + .enumerate() + .skip(tab.window.top) + .take(min(length, tab.window.bottom + 1)) + { + let row = Self::calc_line_row(i, tab); + self.term.print(row, line_number_width + 3, line)?; + } + } + Preview::Compressed(zip) => { + for (i, line) in zip + .content + .iter() + .enumerate() + .skip(tab.window.top) + .take(min(length, tab.window.bottom + 1)) + { + let row = Self::calc_line_row(i, tab); + self.term.print_with_attr( + row, + 0, + &(i + 1 + tab.window.top).to_string(), + Self::ATTR_LINE_NR, + )?; + self.term.print(row, line_number_width + 3, line)?; + } + } + Preview::Empty => (), + } + Ok(()) + } + + fn marks(&mut self, status: &Status) -> FmResult<()> { + let tab = status.selected_non_mut(); + + self.term + .print_with_attr(2, 1, "mark path", Self::ATTR_YELLOW)?; + + for (i, line) in status.marks.as_strings()?.iter().enumerate() { + let row = Self::calc_line_row(i, tab) + 2; + self.term.print(row, 3, line)?; + } + Ok(()) + } + + fn calc_line_row(i: usize, status: &Tab) -> usize { + i + ContentWindow::WINDOW_MARGIN_TOP - status.window.top + } + + fn disk_space(&mut self, path_str: String) -> String { + self.sys.refresh_disks(); + let mut size = 0_u64; + let mut disks: Vec<&sysinfo::Disk> = self.sys.disks().iter().collect(); + disks.sort_by_key(|disk| disk.mount_point().as_os_str().len()); + for disk in disks { + if path_str.contains(disk.mount_point().as_os_str().to_str().unwrap()) { + size = disk.available_space(); + }; + } + human_size(size) + } +} + +fn format_line_nr_hex(line_nr: usize, width: usize) -> String { + format!("{:0width$x}", line_nr) +} + +const fn color_to_attr(color: Color) -> Attr { + Attr { + fg: color, + bg: Color::Default, + effect: Effect::empty(), + } +} -- cgit v1.2.3