You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
xplr/src/path.rs

496 lines
14 KiB
Rust

Release 0.21.0 (#602) * Add xplr.util.lscolor and xplr.util.paint (#569) * Add xplr.util.lscolor and xplr.util.style * Fix formatting * Fix clippy suggestions * Remove redundant closures * Optimize, support NO_COLOR, and rename style to paint * Use xplr.util.paint and xplr.util.color in init.lua Co-authored-by: Noah Mayr <dev@noahmayr.com> * Add utility function xplr.util.textwrap (#567) * Add utility function xplr.util.wrap * Cleanup and fix formatting * Update src/lua/util.rs Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com> * Update wrap to return lines instead * Fix doc * Rename wrap -> text wrap Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com> Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Add xplr.util.relative_to and xplr.util.path_shorthand (#568) * Add xplr.util.relative_to and xplr.util.path_shorthand * Remove duplicate slash at end * Use pwd from env and remove pathdiff package * Some fixes and improvements * Generate docs * Some more improvements * Improve selection rendering * Improve functions with test cases * Update docs * Minor doc fix * Rename path_shorthand -> shortened * Handle homedir edgecase Also fix init.lua * Minor fix * Use config argument for relative and shortened paths * Prefix relative paths with "." and fix edge cases where we're not showing the file name * Use and_then instead of map and flatten * WIP: Move selection rendering to lua * Make selection renderer function configurable on lua side * Some improvements * Some impovements * Minor doc fix * Remove symlink style --------- Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Add xplr.util.layout_replaced (#574) Closes: https://github.com/sayanarijit/xplr/issues/573 * Improve selection operations (#575) - `:sl` to list selection. - `:ss` to softlink. - `:sh` to hardlink. - Avoid conflict by adding suffix. - Unselect individual path only on operation success. Closes: - https://github.com/sayanarijit/xplr/issues/572 - https://github.com/sayanarijit/xplr/issues/571 - https://github.com/sayanarijit/xplr/issues/570 * Minor updates * Add more features (#581) * Add more features - Key binding ":se" to edit selection list in $EDITOR - New utility functions: - xplr.util.clone - xplr.util.exists - xplr.util.is_dir - xplr.util.is_file - xplr.util.is_symlink - xplr.util.is_absolute - xplr.util.path_split - xplr.util.node Closes: https://github.com/sayanarijit/xplr/issues/580 Closes: https://github.com/sayanarijit/xplr/issues/579 Closes: https://github.com/sayanarijit/xplr/issues/577 * Fix edit selection list * Fix clippy lints * Fix layout link in doc * xplr.util.shortened -> xplr.util.shorten * Fix more clippy lints * Fix xplr.util.shorten name change * More UI utilities and improvements (#582) * More UI utilities and improvements - Apply style only to the file column in the table. - Properly quote paths. - Expose the applicable style from config in the table renderer argument. - Add utility functions: - xplr.util.node_type - xplr.util.style_mix - xplr.util.shell_escape * Make escaping play nice with shorten * Fix tests * Fix doc * Some fixes * Fix selection editor * Fix clear selection for selection editor * Add selection navigation (#583) * Add selection navigation - FocusNextSelection (ctrl-n) - FocusPreviousSelection (ctrl-p) Also improve batch operations * Minor doc fixes * Minor doc fix * Remove tab -> ctrl-i binding * Improve batch operation interaction - More robust focus operation. - Focus on failed to delete paths. * Fix Rust compatibility * Fix panic on permission denial Also, improve the error messages. * More logging improvements * Fix layout_replace only working with table parameters (#586) * Improve builtin search mode (#585) * Improve builtin search mode * Remove commented out code * Make search ranking and algorithm more extensible * Flatten messages BREAKING: xplr.config.general.sort_and_filter_ui.search_identifier -> xplr.config.general.sort_and_filter_ui.search_identifiers Messages: - Search - SearchFromInput - SearchFuzzy - SearchFuzzyUnranked - SearchFuzzyUnrankedFromInput - SearchRegexUnrankedFromInput - SearchRegex - SearchRegexUnranked - SearchRegexUnrankedFromInput - SearchRegexUnrankedFromInput - CycleSearchAlgorithm - EnableRankedSearch - DisableRankedSearch - ToggleRankedSearch Static config: xplr.config.general.search.algorithm = "Fuzzy" * Handle search ranking in search algorithm * Make CycleSearchAlgorithm only cycle between algorithms, without changing ranking * Separate algorithm and ordering * Minor doc updates * Some cleanup * Final touch * Cycle -> Toggle --------- Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Fix layout replace for unit layouts (#588) * Allow custom title and ui config in custom layout. (#589) * Allow custom title and ui config in custom layout. Adds the following layouts: - Static - Dynamic Deprecates `CustomContent` (but won't be removed to maintain compatibility). Closes: https://github.com/sayanarijit/xplr/issues/563 * Delete init.lua * Update docs/en/src/layout.md * Update docs/en/src/layout.md * Rename - Paragraph => CustomParagraph - List => CustomList - Table => CustomTable Also update init.lua * Fix clippy errs * Fix doc links * Fix search order * Improve working with file permissions (#591) * Improve working with file permissions Implements: - xplr.util.permissions_rwx - xplr.util.permissions_octal * Edit permissions * Add permissions in Resolved Node (#592) * Add permissions in Relolved Node And handle application/x-executable mime type. * Fix bench * Improve permissions editor * More permissions editor improvements * Doc updates * Remove ResolvedNode.permissions (#593) Reason: Too much serialization making lua calls slow. * Add workaround for macos with legacy coreutils (#595) Refs: - https://github.com/sayanarijit/xplr/issues/594 - https://github.com/sayanarijit/xplr/issues/559 * Use H:M:S format to display logs (#596) * Keep the selection list and clear manually (#597) * Keep the selection list and clear manually Ref: https://github.com/sayanarijit/map.xplr/issues/4 * Fix linting err * Fix broken history (#599) * Fix broken hostory Fixes: https://github.com/sayanarijit/xplr/issues/598 * Minor cleanup * Slightly optimize selection retention (#600) * Update deps * chrono -> time * update: 0.20.2 -> 0.21.1 * Update post-install.md * Upgrade guide * Minor fix * Fix tests * Add missing doc * Fix clippy lints --------- Co-authored-by: Noah Mayr <dev@noahmayr.com>
1 year ago
use anyhow::{bail, Result};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
pub use snailquote::escape;
use std::path::{Component, Path, PathBuf};
lazy_static! {
pub static ref HOME: Option<PathBuf> = dirs::home_dir();
}
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = base.as_ref();
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Ok(PathBuf::from(path))
} else {
let path = path.to_string_lossy();
bail!("{path}: is not absolute")
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => {
let path = path.to_string_lossy();
let base = base.to_string_lossy();
bail!("{base} is not a parent of {path}")
}
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Ok(comps.iter().map(|c| c.as_os_str()).collect())
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RelativityConfig<B: AsRef<Path>> {
base: Option<B>,
with_prefix_dots: Option<bool>,
without_suffix_dots: Option<bool>,
}
impl<B: AsRef<Path>> RelativityConfig<B> {
pub fn with_base(mut self, base: B) -> Self {
self.base = Some(base);
self
}
pub fn with_prefix_dots(mut self) -> Self {
self.with_prefix_dots = Some(true);
self
}
pub fn without_suffix_dots(mut self) -> Self {
self.without_suffix_dots = Some(true);
self
}
}
pub fn relative_to<P, B>(
path: P,
config: Option<&RelativityConfig<B>>,
) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = match config.and_then(|c| c.base.as_ref()) {
Some(base) => PathBuf::from(base.as_ref()),
None => std::env::current_dir()?,
};
let diff = diff(path, base)?;
let relative = if diff.to_str() == Some("") {
".".into()
} else {
diff
};
let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false)
&& !relative.starts_with(".")
&& !relative.starts_with("..")
{
PathBuf::from(".").join(relative)
} else {
relative
};
let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) {
relative
} else if relative.ends_with(".") {
match (path.parent(), path.file_name()) {
(Some(_), Some(name)) => PathBuf::from("..").join(name),
(_, _) => relative,
}
} else if relative.ends_with("..") {
match (path.parent(), path.file_name()) {
(Some(parent), Some(name)) => {
if parent.parent().is_some() {
relative.join("..").join(name)
} else {
// always prefer absolute path if it's a child of the root directory
// to guarantee that the basename is included
path.into()
}
}
(_, _) => relative,
}
} else {
relative
};
Ok(relative)
}
pub fn shorten<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let pathstring = path.to_string_lossy().to_string();
let relative = relative_to(path, config)?;
let relative = relative.to_string_lossy().to_string();
let fromhome = HOME
.as_ref()
.and_then(|h| {
path.strip_prefix(h).ok().map(|p| {
if p.to_str() == Some("") {
"~".into()
} else {
PathBuf::from("~").join(p).to_string_lossy().to_string()
}
})
})
.unwrap_or(pathstring);
if relative.len() < fromhome.len() {
Ok(relative)
} else {
Ok(fromhome)
}
}
#[cfg(test)]
mod tests {
use super::*;
type Config<'a> = Option<&'a RelativityConfig<String>>;
const NONE: Config = Config::None;
fn default<'a>() -> RelativityConfig<&'a str> {
Default::default()
}
#[test]
fn test_relative_to_pwd() {
let path = std::env::current_dir().unwrap();
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative =
relative_to(&path, Some(&default().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
}
#[test]
fn test_relative_to_parent() {
let path = std::env::current_dir().unwrap();
let parent = path.parent().unwrap();
let relative = relative_to(parent, NONE).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative = relative_to(parent, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative =
relative_to(parent, Some(&default().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
let relative = relative_to(
parent,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
}
#[test]
fn test_relative_to_file() {
let path = std::env::current_dir().unwrap().join("foo").join("bar");
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("foo/bar"));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
}
#[test]
fn test_relative_to_root() {
let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("foo"));
let relative = relative_to(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo"));
let relative = relative_to("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from(".."));
}
#[test]
fn test_relative_to_base() {
let path = "/some/directory";
let base = "/another/foo/bar";
let relative = relative_to(path, Some(&default().with_base(base))).unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
let relative = relative_to(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
}
#[test]
fn test_shorten_home() {
let path = HOME.as_ref().unwrap();
let res = shorten(path, NONE).unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(path.join("foo"), NONE).unwrap();
assert_eq!(res, "~/foo");
let res = shorten(
path.join("foo"),
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~/foo");
let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap();
assert_ne!(res, "~/foo");
assert_eq!(res, format!("{}foo", path.to_string_lossy()));
}
#[test]
fn test_shorten_base() {
let path = "/present/working/directory";
let base = "/present/foo/bar";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "../../working/directory");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working/directory");
}
#[test]
fn test_shorten_pwd() {
let path = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(path))).unwrap();
assert_eq!(res, ".");
let res = shorten(
path,
Some(
&default()
.with_base(path)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../directory");
}
#[test]
fn test_shorten_parent() {
let path = "/present/working";
let base = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "..");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working");
}
#[test]
fn test_shorten_root() {
let res = shorten("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "/");
let res = shorten(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
let res = shorten("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "foo");
let res = shorten(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/foo");
let res = shorten(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
}
#[test]
fn test_path_escape() {
let text = "foo".to_string();
assert_eq!(escape(&text), "foo");
let text = "foo bar".to_string();
assert_eq!(escape(&text), "'foo bar'");
let text = "foo\nbar".to_string();
assert_eq!(escape(&text), "\"foo\\nbar\"");
let text = "foo$bar".to_string();
assert_eq!(escape(&text), "'foo$bar'");
let text = "foo'$\n'bar".to_string();
assert_eq!(escape(&text), "\"foo'\\$\\n'bar\"");
let text = "a'b\"c".to_string();
assert_eq!(escape(&text), "\"a'b\\\"c\"");
}
}