summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorqkzk <qkzk@users.noreply.github.com>2024-03-06 17:37:43 +0100
committerGitHub <noreply@github.com>2024-03-06 17:37:43 +0100
commit09a4135c804f28ded45e38bcb6f6dd21f30416f9 (patch)
treecb5607f749320964a422ce512996d923b44be5a6 /src
parent0c669840cac86bdf59074d643d56a6ff3554c3cd (diff)
parent809448520c282f30d748e02499ddb13915e9c34b (diff)
Merge pull request #88 from qkzk/v0.1.26-searchHEADv0.1.26master
V0.1.26 search
Diffstat (limited to 'src')
-rw-r--r--src/app/application.rs15
-rw-r--r--src/app/header_footer.rs650
-rw-r--r--src/app/internal_settings.rs4
-rw-r--r--src/app/mod.rs5
-rw-r--r--src/app/refresher.rs14
-rw-r--r--src/app/status.rs330
-rw-r--r--src/app/tab.rs65
-rw-r--r--src/common/constant_strings_paths.rs13
-rw-r--r--src/common/utils.rs32
-rw-r--r--src/config/colors.rs70
-rw-r--r--src/config/configuration.rs5
-rw-r--r--src/config/keybindings.rs47
-rw-r--r--src/config/mod.rs4
-rw-r--r--src/event/action_map.rs51
-rw-r--r--src/event/event_dispatch.rs83
-rw-r--r--src/event/event_exec.rs696
-rw-r--r--src/event/event_poller.rs22
-rw-r--r--src/event/fm_events.rs14
-rw-r--r--src/event/mod.rs2
-rw-r--r--src/io/args.rs4
-rw-r--r--src/io/display.rs247
-rw-r--r--src/io/input_history.rs229
-rw-r--r--src/io/mod.rs2
-rw-r--r--src/io/opener.rs12
-rw-r--r--src/modes/display/content_window.rs2
-rw-r--r--src/modes/display/directory.rs15
-rw-r--r--src/modes/display/fileinfo.rs8
-rw-r--r--src/modes/display/preview.rs59
-rw-r--r--src/modes/display/tree.rs86
-rw-r--r--src/modes/edit/bulkrename.rs308
-rw-r--r--src/modes/edit/completion.rs18
-rw-r--r--src/modes/edit/context.rs3
-rw-r--r--src/modes/edit/copy_move.rs9
-rw-r--r--src/modes/edit/cryptsetup.rs5
-rw-r--r--src/modes/edit/decompress.rs7
-rw-r--r--src/modes/edit/flagged.rs43
-rw-r--r--src/modes/edit/help.rs13
-rw-r--r--src/modes/edit/history.rs1
-rw-r--r--src/modes/edit/input.rs5
-rw-r--r--src/modes/edit/leave_mode.rs88
-rw-r--r--src/modes/edit/menu.rs66
-rw-r--r--src/modes/edit/mocp.rs134
-rw-r--r--src/modes/edit/mod.rs7
-rw-r--r--src/modes/edit/search.rs238
-rw-r--r--src/modes/edit/second_line.rs2
-rw-r--r--src/modes/edit/selectable_content.rs4
-rw-r--r--src/modes/edit/trash.rs19
-rw-r--r--src/modes/mode.rs43
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 {