diff --git a/Cargo.lock b/Cargo.lock index c4dbe3d06..0334b693f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,6 +1011,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.11" @@ -1275,6 +1286,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" @@ -1441,6 +1461,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.9.4", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "globset" version = "0.4.16" @@ -1574,12 +1607,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" @@ -1839,6 +1995,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" @@ -1855,6 +2023,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" @@ -1867,6 +2047,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 = "0.4.2" @@ -2604,6 +2790,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" @@ -3444,6 +3639,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" @@ -3495,6 +3696,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" @@ -3642,6 +3854,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.47.1" @@ -3933,12 +4155,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" @@ -3972,6 +4212,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" @@ -4554,6 +4800,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" @@ -4838,7 +5090,9 @@ dependencies = [ "core-foundation-sys", "dirs", "foldhash 0.2.0", + "git2", "hashbrown 0.16.0", + "ignore", "libc", "objc", "parking_lot", @@ -5086,6 +5340,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" @@ -5106,12 +5384,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 792cddd11..dc2a98925 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,58 +1,86 @@ [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" -anyhow = "1.0.100" -base64 = "0.22.1" -bitflags = { version = "2.9.4", features = [ "serde" ] } -clap = { version = "4.5.48", features = [ "derive" ] } +ansi-to-tui = "7.0.0" +anyhow = "1.0.100" +base64 = "0.22.1" +bitflags = { version = "2.9.4", features = ["serde"] } +clap = { version = "4.5.48", features = ["derive"] } core-foundation-sys = "0.8.7" -crossterm = { version = "0.29.0", features = [ "event-stream" ] } -dirs = "6.0.0" -foldhash = "0.2.0" -futures = "0.3.31" -globset = "0.4.16" -hashbrown = { version = "0.16.0", features = [ "serde" ] } -indexmap = { version = "2.11.4", features = [ "serde" ] } -libc = "0.2.177" -lru = "0.16.1" -mlua = { version = "0.11.4", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } -objc = "0.2.7" -ordered-float = { version = "5.1.0", features = [ "serde" ] } -parking_lot = "0.12.5" -paste = "1.0.15" -percent-encoding = "2.3.2" -ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } -regex = "1.12.1" -russh = { version = "0.54.5", default-features = false, features = [ "ring", "rsa" ] } -scopeguard = "1.2.0" -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.145" -syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -tokio = { version = "1.47.1", features = [ "full" ] } -tokio-stream = "0.1.17" -tokio-util = "0.7.16" -toml = { version = "0.9.8" } -tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } -twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } -unicode-width = { version = "0.2.0", default-features = false } -uzers = "0.12.1" +crossterm = { version = "0.29.0", features = ["event-stream"] } +dirs = "6.0.0" +foldhash = "0.2.0" +futures = "0.3.31" +git2 = { version = "0.19.0", default-features = false, features = [ + "vendored-libgit2", +] } +globset = "0.4.16" +hashbrown = { version = "0.16.0", features = ["serde"] } +ignore = "0.4.23" +indexmap = { version = "2.11.4", features = ["serde"] } +libc = "0.2.177" +lru = "0.16.1" +mlua = { version = "0.11.4", features = [ + "anyhow", + "async", + "error-send", + "lua54", + "macros", + "serde", +] } +objc = "0.2.7" +ordered-float = { version = "5.1.0", features = ["serde"] } +parking_lot = "0.12.5" +paste = "1.0.15" +percent-encoding = "2.3.2" +ratatui = { version = "0.29.0", features = [ + "unstable-rendered-line-info", + "unstable-widget-ref", +] } +regex = "1.12.1" +russh = { version = "0.54.5", default-features = false, features = [ + "ring", + "rsa", +] } +scopeguard = "1.2.0" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +syntect = { version = "5.3.0", default-features = false, features = [ + "parsing", + "plist-load", + "regex-onig", +] } +tokio = { version = "1.47.1", features = ["full"] } +tokio-stream = "0.1.17" +tokio-util = "0.7.16" +toml = { version = "0.9.8" } +tracing = { version = "0.1.41", features = [ + "max_level_debug", + "release_max_level_debug", +] } +twox-hash = { version = "2.1.2", default-features = false, features = [ + "std", + "random", + "xxhash3_128", +] } +unicode-width = { version = "0.2.0", default-features = false } +uzers = "0.12.1" diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 6b85e9551..9bb0884ff 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 000000000..388de6934 --- /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 29842efb4..49f646d0b 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 f56327164..ac99b07cc 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -23,6 +23,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/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 6c42dde30..841ed7649 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -16,6 +16,14 @@ scrolloff = 5 mouse_events = [ "click", "scroll" ] title_format = "Yazi: {cwd}" +# Git ignore integration +# gitignore_enable = false +# ignore_override = [ +# "*.log", # Hide all .log files +# "tmp/", # Hide tmp directory +# "!target/", # Show target/ even if gitignored (negation) +# ] + [preview] wrap = "no" tab_size = 2 diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 2796db07e..968b38074 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 992f58c9c..fe959eb65 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 60a8b2f2f..676ee11ed 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 49c7fa1c7..bc48e24de 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 } -objc = { workspace = true } +objc = { workspace = true } [target.'cfg(not(target_os = "android"))'.dependencies] trash = "5.2.3" diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index f014382db..1c532f79e 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, url::{Urn, UrnBuf}}; -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 000000000..2ed9896f5 --- /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 eb70639c0..ed8b9614f 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 sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd file files filter fns hash ignore op sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default());