From cf6eab4a8ad7f1cd8881a9f4203bb8aaddb20e28 Mon Sep 17 00:00:00 2001 From: Canop Date: Mon, 2 Nov 2020 17:08:44 +0100 Subject: display date and size on symlinks --- src/browser/browser_state.rs | 3 +- src/command/completion.rs | 3 +- src/file_sum/sum_computation.rs | 2 +- src/git/ignore.rs | 3 +- src/lib.rs | 1 - src/path.rs | 161 ---------------------------------------- src/path/anchor.rs | 9 +++ src/path/closest.rs | 20 +++++ src/path/escape.rs | 16 ++++ src/path/from.rs | 69 +++++++++++++++++ src/path/mod.rs | 13 ++++ src/path/normalize.rs | 64 ++++++++++++++++ src/path_anchor.rs | 9 --- src/tree/tree.rs | 7 +- src/verb/internal_focus.rs | 3 +- src/verb/invocation_parser.rs | 2 +- src/verb/verb.rs | 3 +- 17 files changed, 204 insertions(+), 184 deletions(-) delete mode 100644 src/path.rs create mode 100644 src/path/anchor.rs create mode 100644 src/path/closest.rs create mode 100644 src/path/escape.rs create mode 100644 src/path/from.rs create mode 100644 src/path/mod.rs create mode 100644 src/path/normalize.rs delete mode 100644 src/path_anchor.rs diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index 61a1591..080452e 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -7,8 +7,7 @@ use { flag::Flag, git, pattern::*, - path, - path_anchor::PathAnchor, + path::{self, PathAnchor}, print, skin::PanelSkin, task_sync::Dam, diff --git a/src/command/completion.rs b/src/command/completion.rs index 709765d..bf647a2 100644 --- a/src/command/completion.rs +++ b/src/command/completion.rs @@ -6,8 +6,7 @@ use { AppContext, Selection, }, - path, - path_anchor::PathAnchor, + path::{self, PathAnchor}, verb::PrefixSearchResult, }, std::io, diff --git a/src/file_sum/sum_computation.rs b/src/file_sum/sum_computation.rs index 343781d..f3e3cbe 100644 --- a/src/file_sum/sum_computation.rs +++ b/src/file_sum/sum_computation.rs @@ -155,7 +155,7 @@ pub fn compute_dir_sum(path: &Path, dam: &Dam) -> Option { /// compute the sum for a regular file (not a folder) pub fn compute_file_sum(path: &Path) -> FileSum { - match fs::metadata(path) { + match fs::symlink_metadata(path) { Ok(md) => { let seconds = extract_seconds(&md); diff --git a/src/git/ignore.rs b/src/git/ignore.rs index e10e77b..7c596bf 100644 --- a/src/git/ignore.rs +++ b/src/git/ignore.rs @@ -26,6 +26,7 @@ struct GitIgnoreRule { } impl GitIgnoreRule { + /// parse a line of a .gitignore file fn from(line: &str, dir: &Path) -> Option { if line.starts_with('#') { return None; // comment line @@ -48,7 +49,7 @@ impl GitIgnoreRule { } if let Ok(pattern) = glob::Pattern::new(&p) { let pattern_options = glob::MatchOptions { - case_sensitive: true, // not really sure about this one + case_sensitive: true, require_literal_leading_dot: false, require_literal_separator: has_separator, }; diff --git a/src/lib.rs b/src/lib.rs index 0def338..4363c72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,6 @@ pub mod keys; pub mod image; pub mod launchable; pub mod path; -pub mod path_anchor; pub mod pattern; pub mod permissions; pub mod preview; diff --git a/src/path.rs b/src/path.rs deleted file mode 100644 index f1e2d63..0000000 --- a/src/path.rs +++ /dev/null @@ -1,161 +0,0 @@ -use { - crate::{ - path_anchor::PathAnchor, - }, - directories::UserDirs, - regex::{self, Captures}, - std::{ - collections::HashMap, - path::{Component, Path, PathBuf}, - }, -}; - -/// build a usable path from a user input which may be absolute -/// (if it starts with / or ~) or relative to the supplied base_dir. -/// (we might want to try detect windows drives in the future, too) -/// -pub fn path_from>(base_dir: P, anchor: PathAnchor, input: &str) -> PathBuf { - let tilde = regex!(r"^~(/|$)"); - if input.starts_with('/') { - // if the input starts with a `/`, we use it as is - input.into() - } else if tilde.is_match(input) { - // if the input starts with `~` as first token, we replace - // this `~` with the user home directory - PathBuf::from( - &*tilde - .replace(input, |c: &Captures| { - if let Some(user_dirs) = UserDirs::new() { - format!("{}{}", user_dirs.home_dir().to_string_lossy(), &c[1],) - } else { - warn!("no user dirs found, no expansion of ~"); - c[0].to_string() - } - }) - ) - } else { - // we put the input behind the source (the selected directory - // or its parent) and we normalize so that the user can type - // paths with `../` - let base_dir = match anchor { - PathAnchor::Parent => base_dir.as_ref() - .parent().unwrap_or_else(||base_dir.as_ref()) - .to_path_buf(), - _ => closest_dir(base_dir.as_ref()), - }; - normalize_path(base_dir.join(input)) - } -} - -pub fn path_str_from>(base_dir: P, input: &str) -> String { - path_from(base_dir, PathAnchor::Unspecified, input).to_string_lossy().to_string() -} - -/// return the closest enclosing directory -pub fn closest_dir(mut path: &Path) -> PathBuf { - loop { - if path.exists() && path.is_dir() { - return path.to_path_buf(); - } - match path.parent() { - Some(parent) => path = parent, - None => { - debug!("no existing parent"); // unexpected - return path.to_path_buf(); - } - } - } -} - -/// replace a group in the execution string, using -/// data from the user input and from the selected line -pub fn do_exec_replacement(ec: &Captures<'_>, replacement_map: &HashMap) -> String { - let name = ec.get(1).unwrap().as_str(); - if let Some(repl) = replacement_map.get(name) { - if let Some(fmt) = ec.get(2) { - match fmt.as_str() { - "path-from-directory" => path_str_from(replacement_map.get("directory").unwrap(), repl), - "path-from-parent" => path_str_from(replacement_map.get("parent").unwrap(), repl), - _ => format!("invalid format: {:?}", fmt.as_str()), - } - } else { - repl.to_string() - } - } else { - format!("{{{}}}", name) - } -} - -/// from a path, build a string usable in a shell command, wrapping -/// it in quotes if necessary (and then escaping internal quotes). -/// Don't do unnecessary transformation, so that the produced string -/// is prettier on screen. -pub fn escape_for_shell(path: &Path) -> String { - let path = path.to_string_lossy(); - if regex!(r"^[\w/.-]*$").is_match(&path) { - path.to_string() - } else { - format!("'{}'", &path.replace('\'', r"'\''")) - } -} - -/// Improve the path to try remove and solve .. token. -/// -/// This assumes that `a/b/../c` is `a/c` which might be different from -/// what the OS would have chosen when b is a link. This is OK -/// for broot verb arguments but can't be generally used elsewhere -/// (a more general solution would probably query the FS and just -/// resolve b in case of links). -/// -/// This function ensures a given path ending with '/' still -/// ends with '/' after normalization. -pub fn normalize_path>(path: P) -> PathBuf { - let ends_with_slash = path.as_ref().to_str().map_or(false, |s| s.ends_with('/')); - let mut normalized = PathBuf::new(); - for component in path.as_ref().components() { - match &component { - Component::ParentDir => { - if !normalized.pop() { - normalized.push(component); - } - } - _ => { - normalized.push(component); - } - } - } - if ends_with_slash { - normalized.push(""); - } - normalized -} - -#[cfg(test)] -mod path_normalize_tests { - - use super::normalize_path; - - fn check(before: &str, after: &str) { - println!("-----------------\nnormalizing {:?}", before); - assert_eq!(normalize_path(before.to_string()).to_string_lossy(), after); - } - - #[test] - fn test_path_normalization() { - check("/abc/test/../thing.png", "/abc/thing.png"); - check("/abc/def/../../thing.png", "/thing.png"); - check("/home/dys/test", "/home/dys/test"); - check("/home/dys", "/home/dys"); - check("/home/dys/", "/home/dys/"); - check("/home/dys/..", "/home"); - check("/home/dys/../", "/home/"); - check("/..", "/.."); - check("../test", "../test"); - check("/home/dys/../../../test", "/../test"); - check("π/2", "π/2"); - check( - "/home/dys/dev/broot/../../../canop/test", - "/home/canop/test", - ); - } -} diff --git a/src/path/anchor.rs b/src/path/anchor.rs new file mode 100644 index 0000000..66e33a3 --- /dev/null +++ b/src/path/anchor.rs @@ -0,0 +1,9 @@ + + +#[derive(Debug, Clone, Copy)] +pub enum PathAnchor { + Unspecified, + Parent, + Directory, +} + diff --git a/src/path/closest.rs b/src/path/closest.rs new file mode 100644 index 0000000..5d46c1d --- /dev/null +++ b/src/path/closest.rs @@ -0,0 +1,20 @@ +use { + std::path::{Path, PathBuf}, +}; + + +/// return the closest enclosing directory +pub fn closest_dir(mut path: &Path) -> PathBuf { + loop { + if path.exists() && path.is_dir() { + return path.to_path_buf(); + } + match path.parent() { + Some(parent) => path = parent, + None => { + debug!("no existing parent"); // unexpected + return path.to_path_buf(); + } + } + } +} diff --git a/src/path/escape.rs b/src/path/escape.rs new file mode 100644 index 0000000..e5f6b69 --- /dev/null +++ b/src/path/escape.rs @@ -0,0 +1,16 @@ +use { + std::path::Path, +}; + +/// from a path, build a string usable in a shell command, wrapping +/// it in quotes if necessary (and then escaping internal quotes). +/// Don't do unnecessary transformation, so that the produced string +/// is prettier on screen. +pub fn escape_for_shell(path: &Path) -> String { + let path = path.to_string_lossy(); + if regex!(r"^[\w/.-]*$").is_match(&path) { + path.to_string() + } else { + format!("'{}'", &path.replace('\'', r"'\''")) + } +} diff --git a/src/path/from.rs b/src/path/from.rs new file mode 100644 index 0000000..214c480 --- /dev/null +++ b/src/path/from.rs @@ -0,0 +1,69 @@ +use { + super::*, + directories::UserDirs, + regex::{self, Captures}, + std::{ + collections::HashMap, + path::{Path, PathBuf}, + }, +}; + +/// build a usable path from a user input which may be absolute +/// (if it starts with / or ~) or relative to the supplied base_dir. +/// (we might want to try detect windows drives in the future, too) +/// +pub fn path_from>(base_dir: P, anchor: PathAnchor, input: &str) -> PathBuf { + let tilde = regex!(r"^~(/|$)"); + if input.starts_with('/') { + // if the input starts with a `/`, we use it as is + input.into() + } else if tilde.is_match(input) { + // if the input starts with `~` as first token, we replace + // this `~` with the user home directory + PathBuf::from( + &*tilde + .replace(input, |c: &Captures| { + if let Some(user_dirs) = UserDirs::new() { + format!("{}{}", user_dirs.home_dir().to_string_lossy(), &c[1],) + } else { + warn!("no user dirs found, no expansion of ~"); + c[0].to_string() + } + }) + ) + } else { + // we put the input behind the source (the selected directory + // or its parent) and we normalize so that the user can type + // paths with `../` + let base_dir = match anchor { + PathAnchor::Parent => base_dir.as_ref() + .parent().unwrap_or_else(||base_dir.as_ref()) + .to_path_buf(), + _ => closest_dir(base_dir.as_ref()), + }; + normalize_path(base_dir.join(input)) + } +} + +pub fn path_str_from>(base_dir: P, input: &str) -> String { + path_from(base_dir, PathAnchor::Unspecified, input).to_string_lossy().to_string() +} + +/// replace a group in the execution string, using +/// data from the user input and from the selected line +pub fn do_exec_replacement(ec: &Captures<'_>, replacement_map: &HashMap) -> String { + let name = ec.get(1).unwrap().as_str(); + if let Some(repl) = replacement_map.get(name) { + if let Some(fmt) = ec.get(2) { + match fmt.as_str() { + "path-from-directory" => path_str_from(replacement_map.get("directory").unwrap(), repl), + "path-from-parent" => path_str_from(replacement_map.get("parent").unwrap(), repl), + _ => format!("invalid format: {:?}", fmt.as_str()), + } + } else { + repl.to_string() + } + } else { + format!("{{{}}}", name) + } +} diff --git a/src/path/mod.rs b/src/path/mod.rs new file mode 100644 index 0000000..02f7aae --- /dev/null +++ b/src/path/mod.rs @@ -0,0 +1,13 @@ +mod anchor; +mod closest; +mod escape; +mod from; +mod normalize; + +pub use { + anchor::*, + closest::*, + escape::*, + from::*, + normalize::*, +}; diff --git a/src/path/normalize.rs b/src/path/normalize.rs new file mode 100644 index 0000000..a2af045 --- /dev/null +++ b/src/path/normalize.rs @@ -0,0 +1,64 @@ +use { + std::path::{Component, Path, PathBuf}, +}; + +/// Improve the path to try remove and solve .. token. +/// +/// This assumes that `a/b/../c` is `a/c` which might be different from +/// what the OS would have chosen when b is a link. This is OK +/// for broot verb arguments but can't be generally used elsewhere +/// (a more general solution would probably query the FS and just +/// resolve b in case of links). +/// +/// This function ensures a given path ending with '/' still +/// ends with '/' after normalization. +pub fn normalize_path>(path: P) -> PathBuf { + let ends_with_slash = path.as_ref().to_str().map_or(false, |s| s.ends_with('/')); + let mut normalized = PathBuf::new(); + for component in path.as_ref().components() { + match &component { + Component::ParentDir => { + if !normalized.pop() { + normalized.push(component); + } + } + _ => { + normalized.push(component); + } + } + } + if ends_with_slash { + normalized.push(""); + } + normalized +} + +#[cfg(test)] +mod path_normalize_tests { + + use super::normalize_path; + + fn check(before: &str, after: &str) { + println!("-----------------\nnormalizing {:?}", before); + assert_eq!(normalize_path(before.to_string()).to_string_lossy(), after); + } + + #[test] + fn test_path_normalization() { + check("/abc/test/../thing.png", "/abc/thing.png"); + check("/abc/def/../../thing.png", "/thing.png"); + check("/home/dys/test", "/home/dys/test"); + check("/home/dys", "/home/dys"); + check("/home/dys/", "/home/dys/"); + check("/home/dys/..", "/home"); + check("/home/dys/../", "/home/"); + check("/..", "/.."); + check("../test", "../test"); + check("/home/dys/../../../test", "/../test"); + check("π/2", "π/2"); + check( + "/home/dys/dev/broot/../../../canop/test", + "/home/canop/test", + ); + } +} diff --git a/src/path_anchor.rs b/src/path_anchor.rs deleted file mode 100644 index 66e33a3..0000000 --- a/src/path_anchor.rs +++ /dev/null @@ -1,9 +0,0 @@ - - -#[derive(Debug, Clone, Copy)] -pub enum PathAnchor { - Unspecified, - Parent, - Directory, -} - diff --git a/src/tree/tree.rs b/src/tree/tree.rs index 00f371b..465e9cf 100644 --- a/src/tree/tree.rs +++ b/src/tree/tree.rs @@ -328,8 +328,11 @@ impl Tree { /// long computation which is needed for directories) pub fn fetch_regular_file_sums(&mut self) { for i in 1..self.lines.len() { - if self.lines[i].is_file() { - self.lines[i].sum = Some(FileSum::from_file(&self.lines[i].path)); + match self.lines[i].line_type { + TreeLineType::Dir | TreeLineType::Pruning => {} + _ => { + self.lines[i].sum = Some(FileSum::from_file(&self.lines[i].path)); + }, } } self.sort_siblings(); diff --git a/src/verb/internal_focus.rs b/src/verb/internal_focus.rs index 7a6a4d8..c7a055a 100644 --- a/src/verb/internal_focus.rs +++ b/src/verb/internal_focus.rs @@ -7,8 +7,7 @@ use { browser::BrowserState, command::TriggerType, display::Screen, - path, - path_anchor::PathAnchor, + path::{self, PathAnchor}, preview::PreviewState, task_sync::Dam, tree::TreeOptions, diff --git a/src/verb/invocation_parser.rs b/src/verb/invocation_parser.rs index 7f60e26..9f7b6c9 100644 --- a/src/verb/invocation_parser.rs +++ b/src/verb/invocation_parser.rs @@ -3,7 +3,7 @@ use { crate::{ app::*, errors::ConfError, - path_anchor::PathAnchor, + path::PathAnchor, }, regex::Regex, std::{ diff --git a/src/verb/verb.rs b/src/verb/verb.rs index 33ab3c5..7a19638 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -4,8 +4,7 @@ use { app::{Selection, SelectionType, Status}, errors::ConfError, keys, - path, - path_anchor::PathAnchor, + path::{self, PathAnchor}, }, crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, std::path::PathBuf, -- cgit v1.2.3