diff options
author | qkzk <qkzk@users.noreply.github.com> | 2024-03-06 17:37:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-06 17:37:43 +0100 |
commit | 09a4135c804f28ded45e38bcb6f6dd21f30416f9 (patch) | |
tree | cb5607f749320964a422ce512996d923b44be5a6 /src | |
parent | 0c669840cac86bdf59074d643d56a6ff3554c3cd (diff) | |
parent | 809448520c282f30d748e02499ddb13915e9c34b (diff) |
V0.1.26 search
Diffstat (limited to 'src')
48 files changed, 2396 insertions, 1403 deletions
diff --git a/src/app/application.rs b/src/app/application.rs index 7b00f93..fba900e 100644 --- a/src/app/application.rs +++ b/src/app/application.rs @@ -3,8 +3,6 @@ use std::sync::Mutex; use anyhow::anyhow; use anyhow::Result; -use tuikit::error::TuikitError; -use tuikit::prelude::Event; use crate::app::Displayer; use crate::app::Refresher; @@ -15,6 +13,7 @@ use crate::config::load_config; use crate::config::START_FOLDER; use crate::event::EventDispatcher; use crate::event::EventReader; +use crate::event::FmEvents; use crate::io::set_loggers; use crate::io::Opener; use crate::log_info; @@ -53,6 +52,7 @@ impl FM { /// /// May fail if the [`tuikit::prelude::term`] can't be started or crashes pub fn start() -> Result<Self> { + let (fm_sender, fm_receiver) = std::sync::mpsc::channel::<FmEvents>(); set_loggers()?; let Ok(config) = load_config(CONFIG_PATH) else { exit_wrong_config() @@ -62,7 +62,8 @@ impl FM { startfolder = &START_FOLDER.display() ); let term = Arc::new(init_term()?); - let event_reader = EventReader::new(term.clone()); + let fm_sender = Arc::new(fm_sender); + let event_reader = EventReader::new(term.clone(), fm_receiver); let event_dispatcher = EventDispatcher::new(config.binds.clone()); let opener = Opener::new(&config.terminal, &config.terminal_flag); let status = Arc::new(Mutex::new(Status::new( @@ -70,10 +71,12 @@ impl FM { term.clone(), opener, &config.binds, + fm_sender.clone(), )?)); drop(config); - let refresher = Refresher::new(term.clone()); + // let refresher = Refresher::new(term.clone()); + let refresher = Refresher::new(fm_sender); let displayer = Displayer::new(term, status.clone()); Ok(Self { event_reader, @@ -89,12 +92,12 @@ impl FM { /// # Errors /// /// May fail if the terminal crashes - fn poll_event(&self) -> Result<Event, TuikitError> { + fn poll_event(&self) -> Result<FmEvents> { self.event_reader.poll_event() } /// Update itself, changing its status. - fn update(&mut self, event: Event) -> Result<()> { + fn update(&mut self, event: FmEvents) -> Result<()> { match self.status.lock() { Ok(mut status) => { self.event_dispatcher.dispatch(&mut status, event)?; diff --git a/src/app/header_footer.rs b/src/app/header_footer.rs index a17a346..3969712 100644 --- a/src/app/header_footer.rs +++ b/src/app/header_footer.rs @@ -1,233 +1,273 @@ mod inner { use anyhow::{Context, Result}; - use unicode_segmentation::UnicodeSegmentation; use crate::app::{Status, Tab}; + use crate::common::{ + UtfWidth, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE, LOG_FIRST_SENTENCE, + LOG_SECOND_SENTENCE, + }; use crate::event::ActionMap; - use crate::modes::Selectable; - use crate::modes::{shorten_path, Display}; - use crate::modes::{Content, FilterKind}; - - /// Action for every element of the first line. - /// It should match the order of the `FirstLine::make_string` static method. - const HEADER_ACTIONS: [ActionMap; 4] = [ - ActionMap::Cd, - ActionMap::Rename, - ActionMap::Search, - ActionMap::Filter, - ]; - - const FOOTER_ACTIONS: [ActionMap; 7] = [ - ActionMap::Nothing, // position - ActionMap::Ncdu, - ActionMap::Sort, - ActionMap::LazyGit, - ActionMap::Jump, - ActionMap::Sort, - ActionMap::Nothing, // for out of bounds - ]; - - pub trait ClickableLine: ClickableLineInner { - fn strings(&self) -> &Vec<String>; + use crate::modes::{ + shorten_path, ColoredText, Content, Display, FileInfo, FilterKind, Preview, Search, + Selectable, TextKind, + }; + + #[derive(Clone, Copy)] + pub enum HorizontalAlign { + Left, + Right, + } + + /// A footer or header element that can be clicked + /// + /// Holds a text and an action. + /// It knows where it's situated on the line + #[derive(Clone)] + pub struct ClickableString { + text: String, + action: ActionMap, + width: usize, + left: usize, + right: usize, + } + + impl ClickableString { + /// Creates a new `ClickableString`. + /// It calculates its position with `col` and `align`. + /// If left aligned, the text size will be added to `col` and the text will span from col to col + width. + /// otherwise, the text will spawn from col - width to col. + fn new(text: String, align: HorizontalAlign, action: ActionMap, col: usize) -> Self { + let width = text.utf_width(); + let (left, right) = match align { + HorizontalAlign::Left => (col, col + width), + HorizontalAlign::Right => (col - width - 3, col - 3), + }; + Self { + text, + action, + width, + left, + right, + } + } + + /// Text content of the element. + pub fn text(&self) -> &str { + self.text.as_str() + } + + pub fn col(&self) -> usize { + self.left + } + + pub fn width(&self) -> usize { + self.width + } + } + + /// A line of element that can be clicked on. + pub trait ClickableLine { + /// Reference to the elements + fn elems(&self) -> &Vec<ClickableString>; /// Action for each associated file. fn action(&self, col: usize, is_right: bool) -> &ActionMap { - let mut sum = 0; let offset = self.offset(is_right); - for (index, size) in self.sizes().iter().enumerate() { - sum += size; - if col <= sum + offset { - return self.action_index(index); + let col = col - offset; + for clickable in self.elems().iter() { + if clickable.left <= col && col < clickable.right { + return &clickable.action; } } + + crate::log_info!("no action found"); &ActionMap::Nothing } - } - - pub trait ClickableLineInner { - fn width(&self) -> usize; - fn sizes(&self) -> &Vec<usize>; - fn action_index(&self, index: usize) -> &ActionMap; - + /// Full width of the terminal + fn full_width(&self) -> usize; + /// canvas width of the window + fn canvas_width(&self) -> usize; + /// used offset. + /// 1 if the text is on left tab, + /// width / 2 + 2 otherwise. fn offset(&self, is_right: bool) -> usize { if is_right { - self.width() / 2 + 2 + self.full_width() / 2 + 2 } else { 1 } } - - /// Returns the lengths of every displayed string. - /// It uses `unicode_segmentation::UnicodeSegmentation::graphemes` - /// to measure used space. - /// It's not the number of bytes used since those strings may contain - /// any UTF-8 grapheme. - fn make_sizes(strings: &[String]) -> Vec<usize> { - strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .collect() - } } - /// A bunch of strings displaying the status of the current directory. - /// It provides an `action` method to make the first line clickable. + /// Header for tree & directory display mode. pub struct Header { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, - actions: Vec<ActionMap>, - } - - impl ClickableLine for Header { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } - impl ClickableLineInner for Header { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() - } - - fn action_index(&self, index: usize) -> &ActionMap { - self.actions(index) - } - - fn width(&self) -> usize { - self.width - } - } - - // should it be held somewhere ? impl Header { - /// Create the strings associated with the selected tab directory + /// Creates a new header pub fn new(status: &Status, tab: &Tab) -> Result<Self> { - let (width, _) = status.internal_settings.term.term_size()?; - let (strings, actions) = Self::make_strings_actions(tab, width)?; - let sizes = Self::make_sizes(&strings); + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(tab, canvas_width)?; Ok(Self { - strings, - sizes, - width, - actions, + elems, + canvas_width, + full_width, }) } - fn actions(&self, index: usize) -> &ActionMap { - &self.actions[index] - } - - // TODO! refactor using a `struct thing { string, start, end, action }` - /// Returns a bunch of displayable strings. - /// Watchout: - /// 1. the length of the vector MUST BE the length of `ACTIONS` minus one. - /// 2. the order must be respected. - fn make_strings_actions(tab: &Tab, width: usize) -> Result<(Vec<String>, Vec<ActionMap>)> { - let mut strings = vec![ - Self::string_shorten_path(tab)?, - Self::string_first_row_selected_file(tab, width)?, - ]; - let mut actions: Vec<ActionMap> = HEADER_ACTIONS[0..2].into(); - if let Some(searched) = &tab.searched { - strings.push(Self::string_searched(searched)); - actions.push(HEADER_ACTIONS[2].clone()); - } - if !matches!(tab.settings.filter, FilterKind::All) { - strings.push(Self::string_filter(tab)); - actions.push(HEADER_ACTIONS[3].clone()); + fn make_elems(tab: &Tab, width: usize) -> Result<Vec<ClickableString>> { + let mut left = 0; + let mut right = width; + let shorten_path = Self::elem_shorten_path(tab, left)?; + left += shorten_path.width(); + + let filename = Self::elem_filename(tab, width, left)?; + + let mut elems = vec![shorten_path, filename]; + + if !tab.search.is_empty() { + let search = Self::elem_search(&tab.search, right); + right -= search.width(); + elems.push(search); } - Ok((strings, actions)) - } - fn string_filter(tab: &Tab) -> String { - format!(" {filter} ", filter = tab.settings.filter) - } + let filter_kind = &tab.settings.filter; + if !matches!(filter_kind, FilterKind::All) { + let filter = Self::elem_filter(filter_kind, right); + elems.push(filter); + } - fn string_searched(searched: &str) -> String { - format!(" Searched: {searched} ") + Ok(elems) } - fn string_shorten_path(tab: &Tab) -> Result<String> { - Ok(format!(" {}", shorten_path(&tab.directory.path, None)?)) + fn elem_shorten_path(tab: &Tab, left: usize) -> Result<ClickableString> { + Ok(ClickableString::new( + format!(" {}", shorten_path(&tab.directory.path, None)?), + HorizontalAlign::Left, + ActionMap::Cd, + left, + )) } - fn string_first_row_selected_file(tab: &Tab, width: usize) -> Result<String> { - match tab.display_mode { - Display::Tree => Ok(format!( + fn elem_filename(tab: &Tab, width: usize, left: usize) -> Result<ClickableString> { + let text = match tab.display_mode { + Display::Tree => format!( "/{rel}", rel = shorten_path(tab.tree.selected_path_relative_to_root()?, Some(width / 2))? - )), + ), _ => { if let Some(fileinfo) = tab.directory.selected() { - Ok(fileinfo.filename_without_dot_dotdot()) + fileinfo.filename_without_dot_dotdot() } else { - Ok("".to_owned()) + "".to_owned() } } - } + }; + Ok(ClickableString::new( + text, + HorizontalAlign::Left, + ActionMap::Rename, + left, + )) + } + + fn elem_search(search: &Search, right: usize) -> ClickableString { + ClickableString::new( + search.to_string(), + HorizontalAlign::Right, + ActionMap::Search, + right, + ) + } + + fn elem_filter(filter: &FilterKind, right: usize) -> ClickableString { + ClickableString::new( + format!(" {filter}"), + HorizontalAlign::Right, + ActionMap::Filter, + right, + ) } } - /// A clickable footer. - /// Every displayed element knows were it starts and ends. - /// It allows the user to click on them. - /// Those element are linked by their index to an action. - pub struct Footer { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + impl ClickableLine for Header { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems + } + fn canvas_width(&self) -> usize { + self.canvas_width + } + fn full_width(&self) -> usize { + self.full_width + } } - impl ClickableLine for Footer { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } + /// Default footer for display directory & tree. + pub struct Footer { + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } - impl ClickableLineInner for Footer { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() + impl ClickableLine for Footer { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems } - - fn action_index(&self, index: usize) -> &ActionMap { - &FOOTER_ACTIONS[index] + fn canvas_width(&self) -> usize { + self.canvas_width } - fn width(&self) -> usize { - self.width + fn full_width(&self) -> usize { + self.full_width } } impl Footer { - /// Create the strings associated with the selected tab directory + const FOOTER_ACTIONS: [ActionMap; 6] = [ + ActionMap::Nothing, // position + ActionMap::Ncdu, + ActionMap::Sort, + ActionMap::LazyGit, + ActionMap::DisplayFlagged, + ActionMap::Sort, + ]; + + /// Creates a new footer pub fn new(status: &Status, tab: &Tab) -> Result<Self> { - let (width, _) = status.internal_settings.term.term_size()?; - let used_width = if status.display_settings.use_dual_tab(width) { - width / 2 - } else { - width - }; - let disk_space = status.disk_spaces_of_selected(); - let raw_strings = Self::make_raw_strings(status, tab, disk_space)?; - let strings = Self::make_padded_strings(&raw_strings, used_width); - let sizes = Self::make_sizes(&strings); - + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(status, tab, canvas_width)?; Ok(Self { - strings, - sizes, - width, + elems, + canvas_width, + full_width, }) } - // TODO! refactor using a `struct thing { string, start, end, action }` - /// Returns a bunch of displayable strings. - /// Watchout: - /// 1. the length of the vector MUST BE the length of `ACTIONS` minus one. - /// 2. the order must be respected. + fn make_elems(status: &Status, tab: &Tab, width: usize) -> Result<Vec<ClickableString>> { + let disk_space = status.disk_spaces_of_selected(); + let raw_strings = Self::make_raw_strings(status, tab, disk_space)?; + let padded_strings = Self::make_padded_strings(&raw_strings, width); + let mut left = 0; + let mut elems = vec![]; + for (index, string) in padded_strings.iter().enumerate() { + let elem = ClickableString::new( + string.to_owned(), + HorizontalAlign::Left, + Self::FOOTER_ACTIONS[index].to_owned(), + left, + ); + left += elem.width(); + elems.push(elem) + } + Ok(elems) + } + fn make_raw_strings(status: &Status, tab: &Tab, disk_space: String) -> Result<Vec<String>> { Ok(vec![ Self::string_first_row_position(tab)?, @@ -241,10 +281,7 @@ mod inner { /// Pad every string of `raw_strings` with enough space to fill a line. fn make_padded_strings(raw_strings: &[String], total_width: usize) -> Vec<String> { - let used_width: usize = raw_strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .sum(); + let used_width: usize = raw_strings.iter().map(|s| s.utf_width()).sum(); let available_width = total_width.checked_sub(used_width).unwrap_or_default(); let margin_width = available_width / (2 * raw_strings.len()); let margin = " ".repeat(margin_width); @@ -290,30 +327,52 @@ mod inner { } } + /// Header for the display of flagged files pub struct FlaggedHeader { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, + } + + impl ClickableLine for FlaggedHeader { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems + } + fn canvas_width(&self) -> usize { + self.canvas_width + } + fn full_width(&self) -> usize { + self.full_width + } } impl FlaggedHeader { - const ACTIONS: [ActionMap; 2] = [ActionMap::ResetMode, ActionMap::OpenFile]; + const ACTIONS: [ActionMap; 3] = + [ActionMap::ResetMode, ActionMap::OpenFile, ActionMap::Search]; + /// Creates a new header. pub fn new(status: &Status) -> Result<Self> { - let strings = Self::make_strings(status); - let sizes = Self::make_sizes(&strings); - let (width, _) = status.internal_settings.term.term_size()?; + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(status, full_width); Ok(Self { - strings, - sizes, - width, + elems, + canvas_width, + full_width, }) } - fn make_strings(status: &Status) -> Vec<String> { - vec![ + fn make_elems(status: &Status, width: usize) -> Vec<ClickableString> { + let title = ClickableString::new( "Fuzzy files".to_owned(), + HorizontalAlign::Left, + Self::ACTIONS[0].to_owned(), + 0, + ); + let left = title.width(); + + let flagged = ClickableString::new( status .menu .flagged @@ -321,89 +380,68 @@ mod inner { .unwrap_or(&std::path::PathBuf::new()) .to_string_lossy() .to_string(), - ] - } - - fn make_sizes(strings: &[String]) -> Vec<usize> { - strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .collect() - } - - fn actions(&self, index: usize) -> &ActionMap { - &Self::ACTIONS[index] + HorizontalAlign::Left, + Self::ACTIONS[1].to_owned(), + left, + ); + let searched = Header::elem_search(&status.current_tab().search, width); + vec![title, flagged, searched] } } - impl ClickableLine for FlaggedHeader { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } - } - - impl ClickableLineInner for FlaggedHeader { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() - } - - fn action_index(&self, index: usize) -> &ActionMap { - self.actions(index) - } - - fn width(&self) -> usize { - self.width - } - } + /// Footer for the flagged files display pub struct FlaggedFooter { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } impl ClickableLine for FlaggedFooter { |