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 | |
parent | 0c669840cac86bdf59074d643d56a6ff3554c3cd (diff) | |
parent | 809448520c282f30d748e02499ddb13915e9c34b (diff) |
Merge pull request #88 from qkzk/v0.1.26-searchv0.1.26
V0.1.26 search
54 files changed, 2525 insertions, 1427 deletions
@@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "fm-tui" -version = "0.1.25" +version = "0.1.26" dependencies = [ "anyhow", "cairo-rs", @@ -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" @@ -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 0f41ead6..7a638b5d 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 e1063776..5e253623 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 ? @@ -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 7b00f93b..fba900e7 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 a17a346b..39697129 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. |