summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKyohei Uto <im@kyoheiu.dev>2024-04-07 15:00:51 +0900
committerGitHub <noreply@github.com>2024-04-07 15:00:51 +0900
commite18cc37da7efc3cb45ba6adaa1012de4f17f436f (patch)
treeb66ef832eccad0e8d70b21d533044521a3d7cb66
parent2acf98b6ff2410b5dcdd00c61610036e1fe7f013 (diff)
parent4628a59ab031dd8b65d1778fd01696e438d5545d (diff)
Merge pull request #291 from kyoheiu/develop
v2.13.0
-rw-r--r--CHANGELOG.md13
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml3
-rw-r--r--README.md22
-rw-r--r--config.yaml3
-rw-r--r--src/config.rs74
-rw-r--r--src/run.rs54
-rw-r--r--src/state.rs128
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
diff --git a/Cargo.lock b/Cargo.lock
index b08d2aa..552a402 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 3ccaf0f..3772f0a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/README.md b/README.md
index ec08da4..29932a0 100644
--- a/README.md
+++ b/README.md
@@ -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);
+ }
+}
diff --git a/src/run.rs b/src/run.rs
index 9a1ce8d..81a782c 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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
}