diff options
author | Kyohei Uto <im@kyoheiu.dev> | 2024-04-07 15:00:51 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-07 15:00:51 +0900 |
commit | e18cc37da7efc3cb45ba6adaa1012de4f17f436f (patch) | |
tree | b66ef832eccad0e8d70b21d533044521a3d7cb66 | |
parent | 2acf98b6ff2410b5dcdd00c61610036e1fe7f013 (diff) | |
parent | 4628a59ab031dd8b65d1778fd01696e438d5545d (diff) |
Merge pull request #291 from kyoheiu/develop
v2.13.0
-rw-r--r-- | CHANGELOG.md | 13 | ||||
-rw-r--r-- | Cargo.lock | 10 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | config.yaml | 3 | ||||
-rw-r--r-- | src/config.rs | 74 | ||||
-rw-r--r-- | src/run.rs | 54 | ||||
-rw-r--r-- | src/state.rs | 128 |
8 files changed, 226 insertions, 81 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e2156d..08434c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ ## Unreleased +## v2.13.0 (2024-04-07) + +### Added +- `ignore_case` option to the do case-insensitie search by `/`. +- Symbolic link destinations are now displayed when the cursor is hovered over them. + +### Changed +- Symlink items linked to directory now appears in the directory section, not the file section. +- MSRV is now v1.74.1 + +### fixed +- `z` command can now receive multiple arguments: `z dot files<CR>` works as in your terminal. + ## v2.12.1 (2024-02-04) ### Fixed @@ -346,6 +346,7 @@ dependencies = [ "lzma-rs", "natord", "nix", + "normpath", "rayon", "serde", "serde_yaml", @@ -642,6 +643,15 @@ dependencies = [ ] [[package]] +name = "normpath" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5831952a9476f2fed74b77d74182fa5ddc4d21c72ec45a333b250e3ed0272804" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,6 +1,6 @@ [package] name = "felix" -version = "2.12.1" +version = "2.13.0" authors = ["Kyohei Uto <im@kyoheiu.dev>"] edition = "2021" description = "tui file manager with vim-like key mapping" @@ -35,6 +35,7 @@ lzma-rs = "0.3.0" zstd = "0.12.4" unicode-width = "0.1.10" git2 = {version = "0.18.0", default-features = false } +normpath = "1.2.0" [dev-dependencies] bwrap = { version = "1.3.0", features = ["use_std"] } @@ -25,6 +25,22 @@ For more detailed document, visit https://kyoheiu.dev/felix. ## New release +## v2.13.0 (2024-04-07) + +### Added + +- `ignore_case` option to the do case-insensitie search by `/`. +- Symbolic link destinations are now displayed when the cursor is hovered over them. + +### Changed + +- Symlink items linked to directory now appears in the directory section, not the file section. +- MSRV is now v1.74.1 + +### fixed + +- `z` command can now receive multiple arguments: `z dot files<CR>` works as in your terminal. + ## v2.12.1 (2024-02-04) ### Fixed @@ -70,16 +86,16 @@ report any problems._ | package | installation command | notes | | ---------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| crates.io | `cargo install felix` | Minimum Supported rustc Version: **1.67.1** | +| crates.io | `cargo install felix` | Minimum Supported rustc Version: **1.74.1** | | Arch Linux | `pacman -S felix-rs` | The binary name is `felix` if you install via pacman. Alias `fx='felix'` if you want, as this document (and other installations) uses `fx`. | | NetBSD | `pkgin install felix` | | ### From this repository - Make sure that `gcc` is installed. -- MSRV(Minimum Supported rustc Version): **1.67.1** +- MSRV(Minimum Supported rustc Version): **1.74.1** -Update Rust if rustc < 1.67.1: +Update Rust if rustc < 1.74.1: ``` rustup update diff --git a/config.yaml b/config.yaml index c56aec8..c235d2e 100644 --- a/config.yaml +++ b/config.yaml @@ -16,6 +16,9 @@ # 'feh -.': # [jpg, jpeg, png, gif, svg, hdr] +# Whether to do the case-insensitive search by `/`. +# ignore_case: true + # The foreground color of directory, file and symlink. # Pick one of the following: # Black // 0 diff --git a/src/config.rs b/src/config.rs index ca20ffd..75a1d14 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,10 +20,11 @@ pub struct Config { pub default: Option<String>, pub match_vim_exit_behavior: Option<bool>, pub exec: Option<BTreeMap<String, Vec<String>>>, + pub ignore_case: Option<bool>, pub color: Option<ConfigColor>, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct ConfigColor { pub dir_fg: Colorname, pub file_fg: Colorname, @@ -42,7 +43,7 @@ impl Default for ConfigColor { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Colorname { Black, // 0 Red, // 1 @@ -70,6 +71,7 @@ impl Default for Config { default: Default::default(), match_vim_exit_behavior: Default::default(), exec: Default::default(), + ignore_case: Some(false), color: Some(Default::default()), } } @@ -141,3 +143,71 @@ pub fn read_config_or_default() -> Result<ConfigWithPath, FxError> { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_default_config() { + let default_config: Config = serde_yaml::from_str("").unwrap(); + assert_eq!(default_config.default, None); + assert_eq!(default_config.match_vim_exit_behavior, None); + assert_eq!(default_config.exec, None); + assert_eq!(default_config.ignore_case, None); + assert_eq!(default_config.color, None); + } + + #[test] + fn test_read_full_config() { + let full_config: Config = serde_yaml::from_str( + r#" +default: nvim +match_vim_exit_behavior: true +exec: + zathura: + [pdf] + 'feh -.': + [jpg, jpeg, png, gif, svg, hdr] +ignore_case: true +color: + dir_fg: LightCyan + file_fg: LightWhite + symlink_fg: LightYellow + dirty_fg: Red +"#, + ) + .unwrap(); + assert_eq!(full_config.default, Some("nvim".to_string())); + assert_eq!(full_config.match_vim_exit_behavior, Some(true)); + assert_eq!( + full_config.exec.clone().unwrap().get("zathura"), + Some(&vec!["pdf".to_string()]) + ); + assert_eq!( + full_config.exec.unwrap().get("feh -."), + Some(&vec![ + "jpg".to_string(), + "jpeg".to_string(), + "png".to_string(), + "gif".to_string(), + "svg".to_string(), + "hdr".to_string() + ]) + ); + assert_eq!(full_config.ignore_case, Some(true)); + assert_eq!( + full_config.color.clone().unwrap().dir_fg, + Colorname::LightCyan + ); + assert_eq!( + full_config.color.clone().unwrap().file_fg, + Colorname::LightWhite + ); + assert_eq!( + full_config.color.clone().unwrap().symlink_fg, + Colorname::LightYellow + ); + assert_eq!(full_config.color.unwrap().dirty_fg, Colorname::Red); + } +} @@ -13,6 +13,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifier use crossterm::execute; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; use log::{error, info}; +use normpath::PathExt; use std::env; use std::io::{stdout, Write}; use std::panic; @@ -118,12 +119,14 @@ pub fn run(arg: PathBuf, log: bool) -> Result<(), FxError> { let mut state = State::new(&session_path)?; state.trash_dir = trash_dir_path; state.lwd_file = lwd_file_path; - state.current_dir = if cfg!(not(windows)) { - // If executed this on windows, "//?" will be inserted at the beginning of the path. - arg.canonicalize()? - } else { - arg - }; + let normalized_arg = arg.normalize(); + if normalized_arg.is_err() { + return Err(FxError::Arg(format!( + "Invalid path: {}\n`fx -h` shows help.", + &arg.display() + ))); + } + state.current_dir = normalized_arg.unwrap().into_path_buf(); state.jumplist.add(&state.current_dir); state.is_ro = match has_write_permission(&state.current_dir) { Ok(b) => !b, @@ -746,15 +749,7 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { let commands = command .split_whitespace() .collect::<Vec<&str>>(); - if commands.len() > 2 { - //Invalid argument. - print_warning( - "Invalid argument for zoxide.", - state.layout.y, - ); - state.move_cursor(state.layout.y); - break 'zoxide; - } else if commands.len() == 1 { + if commands.len() == 1 { //go to the home directory let home_dir = dirs::home_dir().ok_or_else(|| { @@ -770,7 +765,8 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { break 'zoxide; } else if let Ok(output) = std::process::Command::new("zoxide") - .args(["query", commands[1]]) + .arg("query") + .args(&commands[1..]) .output() { let output = output.stdout; @@ -1520,11 +1516,18 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { let key = &keyword.iter().collect::<String>(); - let target = state - .list - .iter() - .position(|x| x.file_name.contains(key)); - + let target = match state.ignore_case { + Some(true) => { + state.list.iter().position(|x| { + x.file_name + .to_lowercase() + .contains(&key.to_lowercase()) + }) + } + _ => state.list.iter().position(|x| { + x.file_name.contains(key) + }), + }; match target { Some(i) => { state.layout.nums.skip = i as u16; @@ -2241,12 +2244,13 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { } else if commands.len() == 2 && command == "cd" { if let Ok(target) = std::path::Path::new(commands[1]) - .canonicalize() + .normalize() { if target.exists() { - if let Err(e) = - state.chdir(&target, Move::Jump) - { + if let Err(e) = state.chdir( + &target.into_path_buf(), + Move::Jump, + ) { print_warning(e, state.layout.y); } break 'command; diff --git a/src/state.rs b/src/state.rs index 6b0ec76..632a818 100644 --- a/src/state.rs +++ b/src/state.rs @@ -16,6 +16,7 @@ use crossterm::event::KeyEventKind; use crossterm::event::{Event, KeyCode, KeyEvent}; use crossterm::style::Stylize; use log::info; +use normpath::PathExt; use std::collections::VecDeque; use std::collections::{BTreeMap, BTreeSet}; use std::env; @@ -52,6 +53,7 @@ pub struct State { pub has_zoxide: bool, pub default: String, pub commands: Option<BTreeMap<String, String>>, + pub ignore_case: Option<bool>, pub registers: Registers, pub operations: Operation, pub jumplist: JumpList, @@ -263,6 +265,7 @@ impl State { .unwrap_or_else(|| env::var("EDITOR").unwrap_or_default()); self.match_vim_exit_behavior = config.match_vim_exit_behavior.unwrap_or_default(); self.commands = to_extension_map(&config.exec); + self.ignore_case = config.ignore_case; let colors = config.color.unwrap_or_default(); self.layout.colors = colors; } @@ -1205,7 +1208,14 @@ impl State { } match entry.file_type { FileType::Directory => dir_v.push(entry), - FileType::File | FileType::Symlink => file_v.push(entry), + FileType::File => file_v.push(entry), + FileType::Symlink => { + if entry.symlink_dir_path.is_some() { + dir_v.push(entry); + } else { + file_v.push(entry); + } + } } } @@ -1285,7 +1295,13 @@ impl State { /// Highlight matched items. pub fn highlight_matches(&mut self, keyword: &str) { for item in self.list.iter_mut() { - item.matches = item.file_name.contains(keyword); + item.matches = match self.ignore_case { + Some(true) => item + .file_name + .to_lowercase() + .contains(&keyword.to_lowercase()), + _ => item.file_name.contains(keyword), + } } } @@ -1599,7 +1615,10 @@ impl State { let count = self .list .iter() - .filter(|x| x.file_name.contains(keyword)) + .filter(|x| match self.ignore_case { + Some(true) => x.file_name.to_lowercase().contains(&keyword.to_lowercase()), + _ => x.file_name.contains(keyword), + }) .count(); let count = if count <= 1 { format!("{} match", count) @@ -1629,56 +1648,70 @@ impl State { /// Return footer string. fn make_footer(&self, item: &ItemInfo) -> String { - match &item.file_ext { - Some(ext) => { - let footer = match item.permissions { - Some(permissions) => { - format!( - " {}/{} {} {} {}", + let mut footer = String::new(); + if item.file_type == FileType::Symlink { + footer = " linked to: ".to_owned(); + match &item.symlink_dir_path { + Some(true_path) => { + footer.push_str(true_path.to_str().unwrap_or("(invalid unicode path)")) + } + None => match fs::read_link(&item.file_path) { + Ok(true_path) => match true_path.normalize() { + Ok(p) => footer + .push_str(p.as_path().to_str().unwrap_or("(invalid univode path)")), + Err(_) => footer.push_str("(invalid path)"), + }, + Err(_) => footer.push_str("(broken link)"), + }, + } + } else { + match &item.file_ext { + Some(ext) => { + footer = match item.permissions { + Some(permissions) => { + format!( + " {}/{} {} {} {}", + self.layout.nums.index + 1, + self.list.len(), + ext.clone(), + to_proper_size(item.file_size), + convert_to_permissions(permissions) + ) + } + None => format!( + " {}/{} {} {}", self.layout.nums.index + 1, self.list.len(), ext.clone(), to_proper_size(item.file_size), - convert_to_permissions(permissions) - ) - } - None => format!( - " {}/{} {} {}", - self.layout.nums.index + 1, - self.list.len(), - ext.clone(), - to_proper_size(item.file_size), - ), - }; - footer - .chars() - .take(self.layout.terminal_column.into()) - .collect() - } - None => { - let footer = match item.permissions { - Some(permissions) => { - format!( - " {}/{} {} {}", + ), + }; + } + None => { + footer = match item.permissions { + Some(permissions) => { + format!( + " {}/{} {} {}", + self.layout.nums.index + 1, + self.list.len(), + to_proper_size(item.file_size), + convert_to_permissions(permissions) + ) + } + None => format!( + " {}/{} {}", self.layout.nums.index + 1, self.list.len(), to_proper_size(item.file_size), - convert_to_permissions(permissions) - ) - } - None => format!( - " {}/{} {}", - self.layout.nums.index + 1, - self.list.len(), - to_proper_size(item.file_size), - ), - }; - footer - .chars() - .take(self.layout.terminal_column.into()) - .collect() + ), + }; + } } } + footer + .chars() + .take(self.layout.terminal_column.into()) + .collect() } /// Scroll down previewed text. @@ -1788,12 +1821,7 @@ fn read_item(entry: fs::DirEntry) -> ItemInfo { if filetype == FileType::Symlink { if let Ok(sym_meta) = fs::metadata(&path) { if sym_meta.is_dir() { - if cfg!(not(windows)) { - // Avoid error on Windows - path.canonicalize().ok() - } else { - Some(path.clone()) - } + path.normalize().map(|p| p.into_path_buf()).ok() } else { None } |