This commit is contained in:
Carlos de Paula 2025-10-20 17:39:10 -03:00
parent 9fc3d9fa10
commit d7d9ef12b3
No known key found for this signature in database
8 changed files with 214 additions and 819 deletions

332
Cargo.lock generated
View file

@ -1022,17 +1022,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "document-features"
version = "0.2.12"
@ -1297,15 +1286,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@ -1472,19 +1452,6 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "git2"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.10.0",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "globset"
version = "0.4.18"
@ -1655,135 +1622,12 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "image"
version = "0.25.8"
@ -2088,18 +1932,6 @@ dependencies = [
"cc",
]
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.15"
@ -2116,18 +1948,6 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@ -2140,12 +1960,6 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "1.0.0"
@ -2879,15 +2693,6 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -3729,12 +3534,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -3786,17 +3585,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syntect"
version = "5.3.0"
@ -3944,16 +3732,6 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.48.0"
@ -4242,30 +4020,12 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -4310,12 +4070,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "9.0.6"
@ -4874,12 +4628,6 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wyz"
version = "0.5.1"
@ -5164,9 +4912,7 @@ dependencies = [
"core-foundation-sys",
"dirs",
"foldhash 0.2.0",
"git2",
"hashbrown 0.16.0",
"ignore",
"libc",
"objc2",
"parking_lot",
@ -5414,30 +5160,6 @@ dependencies = [
"yazi-term",
]
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.27"
@ -5458,66 +5180,12 @@ dependencies = [
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View file

@ -1,23 +1,23 @@
[workspace]
resolver = "2"
members = ["yazi-*"]
default-members = ["yazi-fm", "yazi-cli"]
resolver = "2"
members = [ "yazi-*" ]
default-members = [ "yazi-fm", "yazi-cli" ]
[profile.release]
codegen-units = 1
lto = true
panic = "abort"
strip = true
lto = true
panic = "abort"
strip = true
[profile.release-windows]
inherits = "release"
panic = "unwind"
panic = "unwind"
[profile.dev-opt]
inherits = "release"
inherits = "release"
codegen-units = 256
incremental = true
lto = false
incremental = true
lto = false
[workspace.dependencies]
ansi-to-tui = "7.0.0"

View file

@ -18,8 +18,6 @@ impl Actor for Ignore {
const NAME: &str = "ignore";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let gitignores = YAZI.files.gitignores;
// Get the appropriate context string for matching exclude rules
// For search directories, use "search://**" as the context
let cwd = cx.cwd();
@ -28,119 +26,24 @@ impl Actor for Ignore {
} else {
cwd.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let exclude_patterns = YAZI.files.excludes_for_context(&cwd_str);
// Create glob matcher function for compiled patterns
let glob_matcher = {
let context = cwd_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
// If gitignores is disabled but we have exclude patterns, apply them
if !gitignores && !exclude_patterns.is_empty() {
// Load ignore filter from exclude patterns only
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_patterns(path, &exclude_patterns, Some(glob_matcher.clone()))
let glob_matcher: Option<Arc<dyn Fn(&std::path::Path) -> Option<bool> + Send + Sync>> =
if !exclude_patterns.is_empty() {
let context = cwd_str.clone();
Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context)))
} else {
None
};
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder, filter: Option<IgnoreFilter>| {
// Always set the filter, even when loading
let changed = f.files.set_ignore_filter(filter);
if f.stage == FolderStage::Loading {
render!();
false
} else {
render_and!(changed && f.files.catchup_revision())
}
};
// Apply to CWD
if apply(cx.current_mut(), ignore_filter.clone()) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
let hovered_str = if h.url.is_search() {
"search://**".to_string()
} else {
h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_matcher = {
let context = hovered_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_patterns(path, &hovered_excludes, Some(hovered_matcher))
} else {
None
};
if apply(h, hovered_filter) {
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
}
succ!();
}
// If gitignores is disabled and no exclude patterns, remove any ignore filter
if !gitignores {
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder| {
// Always clear the filter, even when loading
let changed = f.files.set_ignore_filter(None);
if f.stage == FolderStage::Loading {
render!();
false
} else {
render_and!(changed && f.files.catchup_revision())
}
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut()
&& apply(h)
{
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
succ!();
}
// Load ignore filter from the current directory
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_dir(path, &exclude_patterns, gitignores, Some(glob_matcher.clone()))
} else {
None
};
// Load ignore filter from exclude patterns
let ignore_filter = IgnoreFilter::from_patterns(glob_matcher.clone());
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder, filter: Option<IgnoreFilter>| {
// Always set the filter, even when loading, so files are filtered as they
// arrive
// Always set the filter, even when loading
let changed = f.files.set_ignore_filter(filter);
if f.stage == FolderStage::Loading {
render!();
@ -156,28 +59,22 @@ impl Actor for Ignore {
act!(mgr:update_paged, cx)?;
}
// Apply to parent (they should use their own ignore file, so we don't apply the
// same filter) Parent folders will be updated when they become the current
// directory
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
// Load ignore filter for hovered directory if it's a directory
let hovered_str = if h.url.is_search() {
"search://**".to_string()
} else {
h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_matcher = {
let context = hovered_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_dir(path, &hovered_excludes, gitignores, Some(hovered_matcher))
} else {
None
};
let hovered_matcher: Option<Arc<dyn Fn(&std::path::Path) -> Option<bool> + Send + Sync>> =
if !hovered_excludes.is_empty() {
let context = hovered_str.clone();
Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context)))
} else {
None
};
let hovered_filter = IgnoreFilter::from_patterns(hovered_matcher);
if apply(h, hovered_filter) {
render!(h.repos(None));

View file

@ -16,128 +16,103 @@ scrolloff = 5
mouse_events = [ "click", "scroll" ]
title_format = "Yazi: {cwd}"
[files]
# Enable git ignore integration
# gitignores = false
[preview]
wrap = "no"
tab_size = 2
max_width = 600
max_height = 900
cache_dir = ""
image_delay = 30
image_filter = "triangle"
image_quality = 75
ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ]
[files]
# Context-specific exclude patterns
# Patterns are compiled into glob matchers for efficient matching
# Patterns starting with '!' negate (whitelist) previous matches
# Note: To match directories, use patterns like ".git/" or "**/.git/**"
# excludes = [
# # SFTP temporary files
# { urn = "*.tmp", in = "sftp://**" },
# # Python cache in search results
# # Python cache in search results
# { urn = "/root/**/*.pyc", in = "search://**" },
# # Multiple patterns for /code directory (supports arrays)
# { urn = [".git", ".DS_Store", "__pycache__"], in = "/code/**" },
# # Negation: show target/ even if previously ignored
# { urn = "!target/", in = "/code/**" },
# # Fallback rule for all contexts
# { urn = ".DS_Store", in = "*" },
# { urn = [".git", ".DS_Store", "__pycache__"], in = "*" },
# # Exclude release and debug directories inside target/
# { urn = "release", in = "**/target" },
# # Exclude all json files from all contexts
# { urn = "*.json", in = "*" },
# # Negation: Show cspell.* even if it's excluded by a previous rule
# { urn = "!cspell*", in = "search://**" },
# ]
[preview]
wrap = "no"
tab_size = 2
max_width = 600
max_height = 900
cache_dir = ""
image_delay = 30
image_filter = "triangle"
image_quality = 75
ueberzug_scale = 1
ueberzug_offset = [0, 0, 0, 0]
[opener]
edit = [
{ run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true },
{ run = "code %s", desc = "code", for = "windows", orphan = true },
{ run = "code -w %s", desc = "code (block)", for = "windows", block = true },
{ run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true },
{ run = "code %s", desc = "code", for = "windows", orphan = true },
{ run = "code -w %s", desc = "code (block)", for = "windows", block = true },
]
play = [
{ run = "xdg-open %s1", desc = "Play", for = "linux" },
{ run = "open %s", desc = "Play", for = "macos" },
{ run = "xdg-open %s1", desc = "Play", for = "linux" },
{ run = "open %s", desc = "Play", for = "macos" },
{ run = 'start "" %s1', orphan = true, desc = "Play", for = "windows" },
{ run = "termux-open %s1", desc = "Play", for = "android" },
{ run = "termux-open %s1", desc = "Play", for = "android" },
{ run = "mediainfo %s1; echo 'Press enter to exit'; read _", block = true, desc = "Show media info", for = "unix" },
{ run = "mediainfo %s1 & pause", block = true, desc = "Show media info", for = "windows" },
]
open = [
{ run = "xdg-open %s1", desc = "Open", for = "linux" },
{ run = "open %s", desc = "Open", for = "macos" },
{ run = 'start "" %s1', desc = "Open", for = "windows", orphan = true },
{ run = "xdg-open %s1", desc = "Open", for = "linux" },
{ run = "open %s", desc = "Open", for = "macos" },
{ run = 'start "" %s1', desc = "Open", for = "windows", orphan = true },
{ run = "termux-open %s1", desc = "Open", for = "android" },
]
reveal = [
{ run = "xdg-open %d1", desc = "Reveal", for = "linux" },
{ run = "open -R %s1", desc = "Reveal", for = "macos" },
{ run = "xdg-open %d1", desc = "Reveal", for = "linux" },
{ run = "open -R %s1", desc = "Reveal", for = "macos" },
{ run = "explorer /select,%s1", desc = "Reveal", for = "windows", orphan = true },
{ run = "termux-open %d1", desc = "Reveal", for = "android" },
{ run = "termux-open %d1", desc = "Reveal", for = "android" },
{ run = "clear; exiftool %s1; echo 'Press enter to exit'; read _", desc = "Show EXIF", for = "unix", block = true },
]
extract = [{ run = "ya pub extract --list %s", desc = "Extract here" }]
extract = [
{ run = "ya pub extract --list %s", desc = "Extract here" },
]
download = [
{ run = "ya emit download --open %S", desc = "Download and open" },
{ run = "ya emit download %S", desc = "Download" },
{ run = "ya emit download %S", desc = "Download" },
]
[open]
rules = [
# Folder
{ url = "*/", use = [
"edit",
"open",
"reveal",
] },
{ url = "*/", use = [ "edit", "open", "reveal" ] },
# Text
{ mime = "text/*", use = [
"edit",
"reveal",
] },
{ mime = "text/*", use = [ "edit", "reveal" ] },
# Image
{ mime = "image/*", use = [
"open",
"reveal",
] },
{ mime = "image/*", use = [ "open", "reveal" ] },
# Media
{ mime = "{audio,video}/*", use = [
"play",
"reveal",
] },
{ mime = "{audio,video}/*", use = [ "play", "reveal" ] },
# Archive
{ mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", use = [
"extract",
"reveal",
] },
{ mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", use = [ "extract", "reveal" ] },
# JSON
{ mime = "application/{json,ndjson}", use = [
"edit",
"reveal",
] },
{ mime = "*/javascript", use = [
"edit",
"reveal",
] },
{ mime = "application/{json,ndjson}", use = [ "edit", "reveal" ] },
{ mime = "*/javascript", use = [ "edit", "reveal" ] },
# Empty file
{ mime = "inode/empty", use = [
"edit",
"reveal",
] },
{ mime = "inode/empty", use = [ "edit", "reveal" ] },
# Virtual file system
{ mime = "vfs/{absent,stale}", use = "download" },
# Fallback
{ url = "*", use = [
"open",
"reveal",
] },
{ url = "*", use = [ "open", "reveal" ] },
]
[tasks]
micro_workers = 10
macro_workers = 10
bizarre_retry = 3
image_alloc = 536870912 # 512MB
image_bound = [10000, 10000]
micro_workers = 10
macro_workers = 10
bizarre_retry = 3
image_alloc = 536870912 # 512MB
image_bound = [ 10000, 10000 ]
suppress_preload = false
[plugin]
@ -214,70 +189,70 @@ previewers = [
cursor_blink = false
# cd
cd_title = "Change directory:"
cd_title = "Change directory:"
cd_origin = "top-center"
cd_offset = [0, 2, 50, 3]
cd_offset = [ 0, 2, 50, 3 ]
# create
create_title = ["Create:", "Create (dir):"]
create_title = [ "Create:", "Create (dir):" ]
create_origin = "top-center"
create_offset = [0, 2, 50, 3]
create_offset = [ 0, 2, 50, 3 ]
# rename
rename_title = "Rename:"
rename_title = "Rename:"
rename_origin = "hovered"
rename_offset = [0, 1, 50, 3]
rename_offset = [ 0, 1, 50, 3 ]
# filter
filter_title = "Filter:"
filter_title = "Filter:"
filter_origin = "top-center"
filter_offset = [0, 2, 50, 3]
filter_offset = [ 0, 2, 50, 3 ]
# find
find_title = ["Find next:", "Find previous:"]
find_title = [ "Find next:", "Find previous:" ]
find_origin = "top-center"
find_offset = [0, 2, 50, 3]
find_offset = [ 0, 2, 50, 3 ]
# search
search_title = "Search via {n}:"
search_title = "Search via {n}:"
search_origin = "top-center"
search_offset = [0, 2, 50, 3]
search_offset = [ 0, 2, 50, 3 ]
# shell
shell_title = ["Shell:", "Shell (block):"]
shell_title = [ "Shell:", "Shell (block):" ]
shell_origin = "top-center"
shell_offset = [0, 2, 50, 3]
shell_offset = [ 0, 2, 50, 3 ]
[confirm]
# trash
trash_title = "Trash {n} selected file{s}?"
trash_origin = "center"
trash_offset = [0, 0, 70, 20]
trash_title = "Trash {n} selected file{s}?"
trash_origin = "center"
trash_offset = [ 0, 0, 70, 20 ]
# delete
delete_title = "Permanently delete {n} selected file{s}?"
delete_origin = "center"
delete_offset = [0, 0, 70, 20]
delete_title = "Permanently delete {n} selected file{s}?"
delete_origin = "center"
delete_offset = [ 0, 0, 70, 20 ]
# overwrite
overwrite_title = "Overwrite file?"
overwrite_body = "Will overwrite the following file:"
overwrite_title = "Overwrite file?"
overwrite_body = "Will overwrite the following file:"
overwrite_origin = "center"
overwrite_offset = [0, 0, 50, 15]
overwrite_offset = [ 0, 0, 50, 15 ]
# quit
quit_title = "Quit?"
quit_body = "The following tasks are still running, are you sure you want to quit?"
quit_title = "Quit?"
quit_body = "The following tasks are still running, are you sure you want to quit?"
quit_origin = "center"
quit_offset = [0, 0, 50, 15]
quit_offset = [ 0, 0, 50, 15 ]
[pick]
open_title = "Open with:"
open_title = "Open with:"
open_origin = "hovered"
open_offset = [0, 1, 50, 7]
open_offset = [ 0, 1, 50, 7 ]
[which]
sort_by = "none"
sort_by = "none"
sort_sensitive = false
sort_reverse = false
sort_translit = false
sort_reverse = false
sort_translit = false

View file

@ -14,8 +14,7 @@ pub struct Exclude {
/// Context where this exclude rule applies
/// Supports glob patterns like "/code/**", "sftp://**", "search://**", or "*" for all
#[serde(rename = "in")]
pub context: String,
pub r#in: String,
#[serde(skip)]
compiled: Option<CompiledPatterns>,
@ -78,14 +77,39 @@ impl Exclude {
pub fn matches_path(&self, path: &Path) -> Option<bool> {
let compiled = self.compiled.as_ref()?;
// For absolute paths, try both the full path and relative components
// This helps patterns like **/.git/** match /home/user/project/.git
let paths_to_check: Vec<&Path> = if path.is_absolute() {
// Also check each component as if it's relative
// This allows **/.git/** to match /home/user/.git/config
let mut paths = vec![path];
// Check if any path component matches by checking relative sub-paths
// For /home/user/.git/config, we want to match against .git/config too
if let Some(components) = path.to_str() {
for (i, _) in components.match_indices('/').skip(1) {
if let Some(subpath) = components.get(i + 1..) {
paths.push(Path::new(subpath));
}
}
}
paths
} else {
vec![path]
};
// Check whitelist first (negation takes precedence)
if compiled.whitelists.is_match(path) {
return Some(false); // Explicitly NOT ignored
for p in &paths_to_check {
if compiled.whitelists.is_match(p) {
return Some(false); // Explicitly NOT ignored
}
}
// Check ignore patterns
if compiled.ignores.is_match(path) {
return Some(true); // Should be ignored
for p in &paths_to_check {
if compiled.ignores.is_match(p) {
return Some(true); // Should be ignored
}
}
None // No match
@ -94,23 +118,49 @@ impl Exclude {
/// Check if this exclude rule applies to the given path context
pub fn matches_context(&self, path: &str) -> bool {
// Wildcard matches everything
if self.context == "*" {
if self.r#in == "*" {
return true;
}
// Handle patterns starting with **/ (e.g., **/target or **/target/**)
if self.r#in.starts_with("**/") {
let pattern = &self.r#in[3..]; // Remove leading "**/", e.g., "target" or "target/**"
// Strip trailing /** if present
let pattern = if let Some(p) = pattern.strip_suffix("/**") {
p
} else {
pattern
};
// Check if path ends with the pattern (e.g., /home/user/project/target matches **/target)
if path.ends_with(&format!("/{}", pattern)) || path.ends_with(pattern) {
return true;
}
// Check if path contains the pattern as a directory segment
if path.contains(&format!("/{}/", pattern)) {
return true;
}
return false;
}
// Handle glob patterns with wildcard
if self.context.ends_with("/**") {
let prefix = &self.context[..self.context.len() - 3];
if self.r#in.ends_with("/**") {
let prefix = &self.r#in[..self.r#in.len() - 3];
// Check if path starts with prefix (absolute path match)
if path.starts_with(prefix) {
return true;
}
// Check if path contains the pattern anywhere (for relative patterns like "/target/**")
// This allows "/target/**" to match "/home/user/project/target/debug"
// Check if path contains the pattern anywhere (for relative patterns like
// "/target/**") This allows "/target/**" to match
// "/home/user/project/target/debug"
if prefix.starts_with('/') && !prefix.starts_with("//") {
// Single leading slash means relative pattern - check if path contains this segment
// Single leading slash means relative pattern - check if path contains this
// segment
let pattern = &prefix[1..]; // Remove leading slash
if path.contains(&format!("/{}/", pattern)) || path.ends_with(&format!("/{}", pattern)) {
return true;
@ -121,6 +171,6 @@ impl Exclude {
}
// Exact match or prefix match for non-wildcard patterns
path == self.context || path.starts_with(&format!("{}/", self.context))
path == self.r#in || path.starts_with(&format!("{}/", self.r#in))
}
}

View file

@ -8,10 +8,6 @@ use super::Exclude;
/// Configuration for file filtering
#[derive(Debug, Deserialize, DeserializeOver2, Default)]
pub struct Files {
/// Enable git ignore integration
#[serde(default)]
pub gitignores: bool,
/// List of exclude rules with context-specific patterns
#[serde(default)]
pub excludes: Vec<Exclude>,

View file

@ -1,46 +1,44 @@
[package]
name = "yazi-fs"
version = "25.9.15"
edition = "2024"
license = "MIT"
authors = ["sxyazi <sxyazi@gmail.com>"]
name = "yazi-fs"
version = "25.9.15"
edition = "2024"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi file system"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-shim = { path = "../yazi-shim", version = "25.9.15" }
yazi-shim = { path = "../yazi-shim", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }
arc-swap = "1.7.1"
bitflags = { workspace = true }
dirs = { workspace = true }
foldhash = { workspace = true }
git2 = { workspace = true }
hashbrown = { workspace = true }
ignore = { workspace = true }
parking_lot = { workspace = true }
anyhow = { workspace = true }
arc-swap = "1.7.1"
bitflags = { workspace = true }
dirs = { workspace = true }
foldhash = { workspace = true }
hashbrown = { workspace = true }
parking_lot = { workspace = true }
percent-encoding = { workspace = true }
regex = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
regex = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }
libc = { workspace = true }
uzers = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_Storage_FileSystem"] }
windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation-sys = { workspace = true }
objc = { workspace = true }
objc2 = { workspace = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "5.2.5"

View file

@ -1,171 +1,46 @@
use std::{collections::HashSet, path::{Path, PathBuf}, sync::Arc};
use std::{path::Path, sync::Arc};
use yazi_shared::url::AsUrl;
/// Filter for ignoring files based on git ignore rules and custom exclude
/// patterns.
/// Filter for ignoring files based on custom exclude patterns.
///
/// This filter combines git's native ignore functionality with custom exclude
/// patterns that follow gitignore syntax. Exclude patterns can be
/// context-specific and take precedence over git's ignore rules using the `!`
/// prefix for negation.
/// Exclude patterns can be context-specific and support negation using the `!`
/// prefix for whitelisting.
#[derive(Clone)]
pub struct IgnoreFilter {
/// Set of paths ignored by git (from git status)
ignored_paths: HashSet<PathBuf>,
/// Custom gitignore matcher for exclude patterns
gitignore: Option<ignore::gitignore::Gitignore>,
/// Custom glob-based matcher function for advanced pattern matching
/// Returns Some(true) if should be ignored, Some(false) if whitelisted, None if no match
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
/// Custom glob-based matcher function for pattern matching
/// Returns Some(true) if should be ignored, Some(false) if whitelisted, None
/// if no match
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
}
impl std::fmt::Debug for IgnoreFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IgnoreFilter")
.field("ignored_paths", &self.ignored_paths)
.field("gitignore", &self.gitignore)
.field("glob_matcher", &self.glob_matcher.as_ref().map(|_| "Some(...)"))
.finish()
}
}
impl IgnoreFilter {
/// Creates a new `IgnoreFilter` by checking git ignore status for the given
/// directory.
/// Creates a new `IgnoreFilter` from exclude patterns.
///
/// # Arguments
///
/// * `dir` - Directory to check for git ignore status
/// * `exclude_patterns` - Custom gitignore patterns that override git's rules
/// * `use_git` - Whether to integrate with git ignore rules
/// * `glob_matcher` - Custom glob-based matcher function for advanced pattern
/// matching
///
/// # Returns
///
/// `Some(IgnoreFilter)` if git repository is found (when use_git=true) or
/// exclude patterns exist, `None` if no git repository and no exclude
/// patterns.
///
/// # Behavior
///
/// - Discovers the git repository containing `dir` (if use_git=true)
/// - Collects all ignored paths from git status
/// - Builds a custom gitignore matcher from exclude patterns (relative to
/// repo root)
/// - Exclude patterns are checked first and take precedence over git ignore
/// rules
/// - Negation patterns (starting with `!`) can whitelist git-ignored files
pub fn from_dir(
dir: impl AsRef<Path>,
exclude_patterns: &[String],
use_git: bool,
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
) -> Option<Self> {
let dir = dir.as_ref();
let (workdir, ignored_paths) = if use_git {
// Try to open the git repository for this directory
let repo = git2::Repository::discover(dir).ok()?;
// Get the workdir (root of the git repository)
let workdir = repo.workdir()?.to_path_buf();
// Get git statuses for the repository
let statuses = match repo.statuses(None) {
Ok(s) => s,
Err(_) => return None,
};
// Build a set of ALL ignored paths from git status
let mut ignored_paths = HashSet::new();
// Manually add .git directory as ignored (like eza does)
ignored_paths.insert(workdir.join(".git"));
// Add all ignored files from git status
for status in statuses.iter() {
if status.status() == git2::Status::IGNORED
&& let Some(path) = status.path()
{
ignored_paths.insert(workdir.join(path));
}
}
(workdir, ignored_paths)
} else {
// No git integration, use current directory as base
(dir.to_path_buf(), HashSet::new())
};
// Build custom gitignore from exclude patterns if provided
let gitignore = if !exclude_patterns.is_empty() {
let mut builder = ignore::gitignore::GitignoreBuilder::new(&workdir);
// Add each exclude pattern
for pattern in exclude_patterns {
let _ = builder.add_line(None, pattern);
}
builder.build().ok()
} else {
None
};
if ignored_paths.is_empty()
|| (ignored_paths.len() == 1 && ignored_paths.contains(&workdir.join(".git")))
{
// If we have no git-ignored paths but we have exclude patterns or glob matcher, still create
// the filter
if gitignore.is_some() || glob_matcher.is_some() {
return Some(Self { ignored_paths, gitignore, glob_matcher });
}
return None;
}
// Store ALL ignored paths, not just the ones in the current directory
// This way, when files are loaded later, we can check them against the full set
Some(Self { ignored_paths, gitignore, glob_matcher })
}
/// Creates a new `IgnoreFilter` from only exclude patterns without git
/// integration.
///
/// # Arguments
///
/// * `dir` - Base directory for pattern matching
/// * `patterns` - Gitignore-style patterns to apply
///
/// # Returns
///
/// `Some(IgnoreFilter)` if patterns are provided, `None` if patterns is
/// empty.
///
/// This is useful when `gitignores = false` but custom exclude patterns
/// are still desired.
/// `Some(IgnoreFilter)` if glob_matcher is provided, `None` otherwise.
pub fn from_patterns(
dir: impl AsRef<Path>,
patterns: &[String],
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
) -> Option<Self> {
if patterns.is_empty() && glob_matcher.is_none() {
if glob_matcher.is_none() {
return None;
}
let gitignore = if !patterns.is_empty() {
let dir = dir.as_ref();
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir);
// Add each pattern
for pattern in patterns {
let _ = builder.add_line(None, pattern);
}
builder.build().ok()
} else {
None
};
Some(Self { ignored_paths: HashSet::new(), gitignore, glob_matcher })
Some(Self { glob_matcher })
}
/// Checks if a file should be ignored based on its URL.
@ -180,83 +55,19 @@ impl IgnoreFilter {
///
/// # Matching Logic
///
/// 1. Check override patterns first:
/// - If matched as whitelist (negation `!`), return `false` (not ignored)
/// - If matched as ignore, return `true` (ignored)
/// 2. Check if path is directly in git's ignored set
/// 3. Check if any parent directory is ignored by git
/// - If parent is ignored, also check if override patterns whitelist it
/// - If parent is whitelisted, children are not ignored
///
/// This ensures override patterns take precedence and negation patterns work
/// correctly even when navigating inside ignored directories.
/// Uses the glob matcher to check if the path should be ignored.
/// Returns true if matched as ignore, false if whitelisted or no match.
pub fn matches_url(&self, url: impl AsUrl) -> bool {
let url = url.as_url();
let path = url.loc.as_path();
// First check glob matcher (highest priority)
// Check glob matcher
if let Some(ref matcher) = self.glob_matcher {
if let Some(should_ignore) = matcher(path) {
return should_ignore;
}
}
// Then check if override patterns apply (they can negate ignores)
// Override patterns take absolute priority
if let Some(ref gitignore) = self.gitignore {
let matched = gitignore.matched(path, path.is_dir());
match matched {
ignore::Match::None => {
// No override pattern matched, fall through to git ignore check
}
ignore::Match::Ignore(_) => {
// Override pattern says to ignore
return true;
}
ignore::Match::Whitelist(_) => {
// Override pattern says NOT to ignore (negation pattern like !target/)
// This takes precedence over git ignore
return false;
}
}
}
// Check exact match first
if self.ignored_paths.contains(path) {
return true;
}
// Check if any parent directory is ignored
// BUT also check if parent is whitelisted by override patterns
let mut current = path;
while let Some(parent) = current.parent() {
if self.ignored_paths.contains(parent) {
// Parent is ignored by git, but check if override patterns whitelist it
if let Some(ref gitignore) = self.gitignore {
let matched = gitignore.matched(parent, true); // parent is always a directory
if matches!(matched, ignore::Match::Whitelist(_)) {
// Parent is whitelisted, so children should not be ignored
return false;
}
}
return true;
}
current = parent;
}
false
}
/// Check if a path should be ignored
pub fn matches_path(&self, path: &Path) -> bool { self.ignored_paths.contains(path) }
}
#[cfg(test)]
mod tests {
#[test]
fn test_git_repo_discovery() {
// This test assumes we're running in the yazi git repo
let current_dir = std::env::current_dir().unwrap();
assert!(git2::Repository::discover(&current_dir).is_ok());
}
}