diff --git a/Cargo.lock b/Cargo.lock index ecd39549..2b53889a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,17 @@ 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" @@ -1286,6 +1297,15 @@ 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" @@ -1452,6 +1472,19 @@ 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" @@ -1622,12 +1655,135 @@ 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" @@ -1932,6 +2088,18 @@ 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" @@ -1948,6 +2116,18 @@ 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" @@ -1960,6 +2140,12 @@ 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" @@ -2693,6 +2879,15 @@ 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" @@ -3534,6 +3729,12 @@ 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" @@ -3585,6 +3786,17 @@ 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" @@ -3732,6 +3944,16 @@ 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" @@ -4020,12 +4242,30 @@ 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" @@ -4070,6 +4310,12 @@ 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" @@ -4628,6 +4874,12 @@ 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" @@ -4912,7 +5164,9 @@ dependencies = [ "core-foundation-sys", "dirs", "foldhash 0.2.0", + "git2", "hashbrown 0.16.0", + "ignore", "libc", "objc2", "parking_lot", @@ -5160,6 +5414,30 @@ 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" @@ -5180,12 +5458,66 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index 55fb0ce3..2a7d9ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 3ed6b1ec..e6b15ca6 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -60,6 +60,7 @@ impl Actor for Cd { err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); act!(mgr:hidden, cx)?; act!(mgr:sort, cx)?; + act!(mgr:ignore, cx)?; act!(mgr:hover, cx)?; act!(mgr:refresh, cx)?; succ!(render!()); diff --git a/yazi-actor/src/mgr/ignore.rs b/yazi-actor/src/mgr/ignore.rs new file mode 100644 index 00000000..388de693 --- /dev/null +++ b/yazi-actor/src/mgr/ignore.rs @@ -0,0 +1,155 @@ +use anyhow::Result; +use yazi_config::YAZI; +use yazi_core::tab::Folder; +use yazi_fs::{FolderStage, IgnoreFilter}; +use yazi_macro::{act, render, render_and, succ}; +use yazi_parser::VoidOpt; +use yazi_shared::{data::Data, url::UrlLike}; + +use crate::{Actor, Ctx}; + +pub struct Ignore; + +impl Actor for Ignore { + type Options = VoidOpt; + + const NAME: &str = "ignore"; + + fn act(cx: &mut Ctx, _: Self::Options) -> Result { + let gitignore_enabled = YAZI.mgr.gitignore_enable; + let override_patterns = &YAZI.mgr.ignore_override; + + // If gitignore is disabled but we have override patterns, apply them + if !gitignore_enabled && !override_patterns.is_empty() { + // Load ignore filter from override patterns only + let ignore_filter = if let Some(path) = cx.cwd().as_path() { + IgnoreFilter::from_patterns(path, override_patterns) + } else { + None + }; + + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let apply = |f: &mut Folder, filter: Option| { + // 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_filter = if let Some(path) = h.url.as_path() { + IgnoreFilter::from_patterns(path, override_patterns) + } 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 gitignore is disabled and no override patterns, remove any ignore filter + if !gitignore_enabled { + 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, override_patterns) + } else { + None + }; + + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let apply = |f: &mut Folder, filter: Option| { + // Always set the filter, even when loading, so files are filtered as they + // arrive + 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 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_filter = if let Some(path) = h.url.as_path() { + IgnoreFilter::from_dir(path, override_patterns) + } 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!(); + } +} diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index 29842efb..49f646d0 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -19,6 +19,7 @@ yazi_macro::mod_flat!( hardlink hidden hover + ignore leave linemode link diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index ed9b231c..51933028 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -24,6 +24,9 @@ impl Actor for Refresh { execute!(TTY.writer(), SetTitle(s)).ok(); } + // Apply ignore filter before triggering file loads + act!(mgr:ignore, cx)?; + if let Some(p) = cx.parent() { Self::trigger_dirs(&[cx.current(), p]); } else { diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 6be3ebfb..22d9df2f 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -24,6 +24,12 @@ pub struct Mgr { pub scrolloff: SyncCell, pub mouse_events: SyncCell, pub title_format: String, + + // Filtering + #[serde(default)] + pub gitignore_enable: bool, + #[serde(default)] + pub ignore_override: Vec, } impl Mgr { diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 992f58c9..fe959eb6 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -32,6 +32,7 @@ pub enum Spark<'a> { Forward(yazi_parser::VoidOpt), Hardlink(yazi_parser::mgr::HardlinkOpt), Hidden(yazi_parser::mgr::HiddenOpt), + Ignore(yazi_parser::VoidOpt), Hover(yazi_parser::mgr::HoverOpt), Leave(yazi_parser::VoidOpt), Linemode(yazi_parser::mgr::LinemodeOpt), @@ -155,6 +156,7 @@ impl<'a> IntoLua for Spark<'a> { Self::Forward(b) => b.into_lua(lua), Self::Hardlink(b) => b.into_lua(lua), Self::Hidden(b) => b.into_lua(lua), + Self::Ignore(b) => b.into_lua(lua), Self::Hover(b) => b.into_lua(lua), Self::Leave(b) => b.into_lua(lua), Self::Linemode(b) => b.into_lua(lua), @@ -253,6 +255,7 @@ try_from_spark!( mgr:escape_visual, mgr:follow, mgr:forward, + mgr:ignore, mgr:leave, mgr:refresh, mgr:search_stop, diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 60a8b2f2..676ee11e 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -111,6 +111,7 @@ impl<'a> Executor<'a> { on!(copy); on!(shell); on!(hidden); + on!(ignore); on!(linemode); on!(search); on!(search_do); diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index edd1e4d7..e4b827b9 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -1,44 +1,46 @@ [package] -name = "yazi-fs" -version = "25.9.15" -edition = "2024" -license = "MIT" -authors = [ "sxyazi " ] +name = "yazi-fs" +version = "25.9.15" +edition = "2024" +license = "MIT" +authors = ["sxyazi "] 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 } -hashbrown = { workspace = true } -parking_lot = { workspace = true } +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 } 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 } -objc2 = { workspace = true } +objc = { workspace = true } [target.'cfg(not(target_os = "android"))'.dependencies] trash = "5.2.5" diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 557c4891..53d8c85c 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -3,7 +3,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; use yazi_shared::{Id, path::{PathBufDyn, PathBufLike, PathDyn, PathLike}}; -use super::{FilesSorter, Filter}; +use super::{FilesSorter, Filter, IgnoreFilter}; use crate::{FILES_TICKET, File, SortBy}; #[derive(Default)] @@ -16,9 +16,10 @@ pub struct Files { pub sizes: HashMap, - sorter: FilesSorter, - filter: Option, - show_hidden: bool, + sorter: FilesSorter, + filter: Option, + show_hidden: bool, + ignore_filter: Option, } impl Deref for Files { @@ -257,9 +258,15 @@ impl Files { fn split_files(&self, files: impl IntoIterator) -> (Vec, Vec) { if let Some(filter) = &self.filter { + files.into_iter().partition(|f| { + (f.is_hidden() && !self.show_hidden) + || !filter.matches(f.urn()) + || self.ignore_filter.as_ref().is_some_and(|ig| ig.matches_url(&f.url)) + }) + } else if let Some(ignore_filter) = &self.ignore_filter { files .into_iter() - .partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) + .partition(|f| (f.is_hidden() && !self.show_hidden) || ignore_filter.matches_url(&f.url)) } else if self.show_hidden { (vec![], files.into_iter().collect()) } else { @@ -316,6 +323,24 @@ impl Files { true } + // --- Ignore filter + #[inline] + pub fn ignore_filter(&self) -> Option<&IgnoreFilter> { self.ignore_filter.as_ref() } + + pub fn set_ignore_filter(&mut self, ignore_filter: Option) -> bool { + if self.ignore_filter.is_none() && ignore_filter.is_none() { + return false; + } + + self.ignore_filter = ignore_filter; + + // Re-split files with the new ignore filter + let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden)); + (self.hidden, self.items) = self.split_files(it); + self.sorter.sort(&mut self.items, &self.sizes); + true + } + // --- Show hidden pub fn set_show_hidden(&mut self, state: bool) { if self.show_hidden == state { diff --git a/yazi-fs/src/ignore.rs b/yazi-fs/src/ignore.rs new file mode 100644 index 00000000..2ed9896f --- /dev/null +++ b/yazi-fs/src/ignore.rs @@ -0,0 +1,158 @@ +use std::{collections::HashSet, path::{Path, PathBuf}}; + +use yazi_shared::url::AsUrl; + +#[derive(Clone, Debug)] +pub struct IgnoreFilter { + ignored_paths: HashSet, + gitignore: Option, +} + +impl IgnoreFilter { + /// Create a new IgnoreFilter by checking git ignore status for files in the + /// given directory + pub fn from_dir(dir: impl AsRef, override_patterns: &[String]) -> Option { + let dir = dir.as_ref(); + + // 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()?; + + // 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 { + if let Some(path) = status.path() { + ignored_paths.insert(workdir.join(path)); + } + } + } + + // Build custom gitignore from override patterns if provided + let gitignore = if !override_patterns.is_empty() { + let mut builder = ignore::gitignore::GitignoreBuilder::new(workdir); + + // Add each override pattern + for pattern in override_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 override patterns, still create + // the filter + if gitignore.is_some() { + return Some(Self { ignored_paths, gitignore }); + } + 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 }) + } + + /// Create a new IgnoreFilter from only override patterns (no git integration) + pub fn from_patterns(dir: impl AsRef, patterns: &[String]) -> Option { + if patterns.is_empty() { + return None; + } + + 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); + } + + let gitignore = builder.build().ok()?; + + Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) }) + } + + /// Check if a file should be ignored based on its URL + pub fn matches_url(&self, url: impl AsUrl) -> bool { + let url = url.as_url(); + let path = url.loc.as_path(); + + // First 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 { + use super::*; + + #[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(¤t_dir).is_ok()); + } +} diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index bec80dd3..74d944b6 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -2,7 +2,7 @@ yazi_macro::mod_pub!(cha error mounts path provider); -yazi_macro::mod_flat!(cwd file files filter fns hash op scheme sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd file files filter fns hash ignore op scheme sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default());