summaryrefslogtreecommitdiffstats
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
parent0c669840cac86bdf59074d643d56a6ff3554c3cd (diff)
parent809448520c282f30d748e02499ddb13915e9c34b (diff)
Merge pull request #88 from qkzk/v0.1.26-searchHEADv0.1.26master
V0.1.26 search
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--build.rs19
-rw-r--r--config_files/fm/config.yaml1
-rw-r--r--development.md124
-rw-r--r--readme.md5
-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
54 files changed, 2525 insertions, 1427 deletions
diff --git a/Cargo.lock b/Cargo.lock
index be79440..80f0c91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -797,7 +797,7 @@ dependencies = [
[[package]]
name = "fm-tui"
-version = "0.1.25"
+version = "0.1.26"
dependencies = [
"anyhow",
"cairo-rs",
diff --git a/Cargo.toml b/Cargo.toml
index 89a8fbb..cf99e2b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fm-tui"
-version = "0.1.25"
+version = "0.1.26"
authors = ["Quentin Konieczko <qu3nt1n@gmail.com>"]
edition = "2021"
license-file = "LICENSE.txt"
diff --git a/build.rs b/build.rs
index 3157f93..93dc3f2 100644
--- a/build.rs
+++ b/build.rs
@@ -20,4 +20,23 @@ fn main() {
Ok(_) => (),
Err(e) => eprintln!("{e:?}"),
}
+
+ update_breaking_config()
+}
+
+/// Remove old binds from user config file.
+///
+/// Remove all binds to `Jump` and `Mocp...` variants since they were removed from fm.
+fn update_breaking_config() {
+ let config = shellexpand::tilde("~/.config/fm/config.yaml");
+ let config: &str = config.borrow();
+ let content = std::fs::read_to_string(config)
+ .expect("config file should be readable")
+ .lines()
+ .map(String::from)
+ .filter(|line| !line.contains("Jump"))
+ .filter(|line| !line.contains("Mocp"))
+ .collect::<Vec<String>>()
+ .join("\n");
+ std::fs::write(config, content).expect("config should be writabe");
}
diff --git a/config_files/fm/config.yaml b/config_files/fm/config.yaml
index 0f41ead..7a638b5 100644
--- a/config_files/fm/config.yaml
+++ b/config_files/fm/config.yaml
@@ -49,7 +49,6 @@ menu_colors:
# You can bind any key to any action.
# List of valid actions is accessible from `help` (default key H) and from the readme.md file.
# Invalid actions are skipped.
-# AltPageUp can't be bound, it is reserved for internal use.
keys:
'esc': ResetMode
'up': MoveUp
diff --git a/development.md b/development.md
index e106377..5e25362 100644
--- a/development.md
+++ b/development.md
@@ -711,8 +711,6 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally.
- [x] describe what was done succintly
- [x] test every mode
-## Current dev
-
### Version 0.1.25
#### Summary
@@ -854,25 +852,114 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally.
- [x] describe what was done succintly
- [ ] test every mode
-## Version 0.1.26
-
-- [ ] display all specific binds for every mode with a key ?
-- [ ] merge sort & regex, display nb of matches, completion + flag on the fly
- - [ ] display number of matches while searching
- - [ ] Sort refactoring
- - [ ] entering
- - [ ] setting
- - [ ] leaving aka reseting
-- [ ] remove MOCP control from fm ???
-- [ ] focusable windows
- - [ ] move display to status ? it would be easier to know where I clicked
- - [ ] allow to change focus, only color the focused window border.
- - [ ] Change focus with ctrl+hjkl or ctrl+arrow
- - [ ] When a menu is opened, it should still be possible to navigate in files and preview or whatever
- - [ ] Clicking an unfocused window should only give the focus, not execute anything
+## Current dev
+
+### Version 0.1.26
+
+#### Summary
+
+- BREAKING: removed jump mode completeley.
+ You can see your flagged files in the display::flagged mode, default bind: <F>.
+- BREAKING: removed all MOCP controls from fm. What was it doing there anyway ?.
+ Those change won't break your config file. While building the application, line with reference to removed binds will be erased.
+- search with regex. You can search (Char('/')) a regex pattern. Search next (Char('f')) will use that regex.
+- left or right aligned and clickable elements in header
+- shift+up, shift+down while typing something cycle trough previous entries.
+ Those are filtered: while typing a path, suggestions are limited to previous pathes, not previous commands.
+- shift+left erases the whole input line
+- wrap tuikit::event into custom event. Use an mpsc to request refresh and bulk execution.
+ While editing filenames in bulk, the application isn't bloked anymore.
+- improve neovim filepicking. While ran from a neovim terminal emulator, use the flag `--neovim`. Every _text_ file will be opened directly in current neovim session.
+ Watchout, if you try to open text & non text files at the same time, it will run a new terminal with your text editor instead. Don't mix file kinds.
+- Dynamic filtering while typing a filter
+- Search as you type: do / then type a pattern and you will jump to the match.
+- replace `tar tvf` by `bsdtar -v --list --file`. Which can preview .deb and .rpm files
+- preview torrent files with `transmission-show`
+- preview mark, shortcut & history content in second pane while navigating
+- zoxide integration. While typing a path in "Goto mode" (default keybind "alt+g"), the first proposition will come from your zoxide answers.
+
+#### Changelog
+
+- [x] focusable windows
+
+ - [x] simple focus enum, mostly following what's being done
+ - [x] allow to change focus, only color the focused window border.
+ - [x] Change focus with ctrl+hjkl
+ - [x] Change focus with ctrl+arrow. Removed MOCP completely
+ - [x] single pane borders
+ - [x] give focus with click
+ - [x] give focus with wheel
+ - [x] remove flagged mode completely
+ - [x] merge Action::Delete & Action::DeleteFile
+ - [x] test open file from menu (context ? header ?)
+ - [x] in Display::Flagged, open a single file with o, all files with ctrl+o
+ - [x] dispatch event according to focus
+ - [x] FIX: changing focus left or right only affects the border. Moving does nothing
+ - [x] test everything
+
+- [x] setting second pane as preview should enable dual pane at the same time
+- [x] FIX: leaving mount mode with enter when device is mounted should move to it
+- [x] FIX: clicking footer row execute directory actions, even in flagged display mode
+- [x] display all specific binds for every mode.
+
+- [x] search, display nb of matches, completion + flag on the fly
+
+ - [x] use regex in search
+ - [x] save the regex ???
+ - [x] simplify navigation to skim output
+ - [x] display number of matches while searching.
+ - [x] search refactoring
+
+- [x] input history.
+
+ - [x] require logging to save on disk.
+ - [x] record every typed into as human as possible file
+ - [x] navigate history with shift+up, shift+down, ctrl+left should erase input
+
+- [x] FIX: skim in tree doesn't select the match
+- [x] remove MOCP control from fm
+- [x] allow header & footer to be right aligned
+- [x] merge both bulkthing modes. If more files, just create them. Like [oil](https://github.com/stevearc/oil.nvim)
+- [x] allow different ports in remote
+- [x] sort trash by reversed deletion date
+- [x] gradient over listing, using an iter instead of a vector
+- [x] FIX win second use 1 more line
+- [x] FIX: entering sort doesn't set focus
+- [x] update config from build file by removing references to removed binds.
+- [x] move to encrypted drive when mounting is successful
+- [x] wrap event into an MPSC to allow internal events
+ - [x] wrap
+ - [x] send/receive custom event
+ - [x] bulk: do not freeze the application while waiting for the thread to complete
+ - [x] refresher
+ - [x] copy move
+- [x] improve filepicking from neovim
+ - [x] flag to force neovim filepicking for text files
+ - [x] open single files
+ - [x] open temp file from bulk
+ - [x] open multiple files
+- [x] FIX: too many open files. pdf opened by Poppler...new_from_file aren't closed properly.
+ Open manually and and use Poppler...new_from_data.
+- [x] FIX: in dual pane mode, right aligned elements aren't displayed.
+- [x] FIX: Right pane search & filter click don't match on correct position.
+- [x] dynamic filtering while typing
+- [x] FIX: leaving (with escape) should reset the filter, not leave
+- [x] setting a filter reset the "found" searched path & index
+- [x] search as you type
+- [x] replace `tar tvf` by `bsdtar -v --list --file`. Which can preview .deb and .rpm files
+- [x] torrent with `transmission-show`
+- [x] preview mark, shortcut & history content in second pane while navigating
+- [x] zoxide support for "alt+g" aka goto mode.
+- [x] FIX: `q` while second window should exit the menu
## TODO
+- [ ] floating windows ?
+- [ ] rclone
+- [ ] FIX: leaving flagged file should reset the window correctly. Can't reproduce...
+- [ ] move as you type in Alt+g
+- [ ] use the new mpsc event parser to read commands from stdin or RPC
+- [ ] [opener file kind](./src/io/opener.rs): move associations to a config file
- [ ] open a shell while hiding fm, restore after leaving
- [ ] refactor & unify all shell commands
- [ ] config loading : https://www.reddit.com/r/rust/comments/17v65j8/implement_configuration_files_without_reading_the/
@@ -888,7 +975,6 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally.
- https://github.com/KillTheMule/nvim-rs/blob/master/examples/basic.rs
- https://neovim.io/doc/user/api.html
-- [ ] zoxide support
- [ ] temporary marks
- [ ] context switch
- [ ] read events from stdin ? can't be done from tuikit. Would require another thread ?
diff --git a/readme.md b/readme.md
index 9779a4b..bbaff36 100644
--- a/readme.md
+++ b/readme.md
@@ -9,7 +9,7 @@
[docrs]: https://docs.rs/fm-tui/0.1.24
```
- A TUI file manager inspired by dired and ranger
+A TUI file manager inspired by dired and ranger
Usage: fm [OPTIONS]
@@ -18,8 +18,9 @@ Options:
-s, --server <SERVER> Nvim server [default: ]
-A, --all Display all files (hidden)
-l, --log Enable logging
+ --neovim Started inside neovim terminal emulator
-h, --help Print help
- -V, --version Print version
+ -V, --version Print version [3,8s]
```
## Platform
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: