mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Improve file exclude integration with context-specific exclude patterns
This commit is contained in:
parent
81c0b230f5
commit
545aad9eea
9 changed files with 419 additions and 128 deletions
|
|
@ -16,14 +16,18 @@ impl Actor for Ignore {
|
|||
const NAME: &str = "ignore";
|
||||
|
||||
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
|
||||
let gitignore_enabled = YAZI.mgr.gitignore_enable;
|
||||
let override_patterns = &YAZI.mgr.ignore_override;
|
||||
let gitignores = YAZI.files.gitignores;
|
||||
|
||||
// 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
|
||||
// Get exclude patterns for the current context
|
||||
// Use path string for context matching
|
||||
let cwd_str = cx.cwd().as_path().map(|p| p.display().to_string()).unwrap_or_default();
|
||||
let exclude_patterns = YAZI.files.excludes_for_context(&cwd_str);
|
||||
|
||||
// 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, override_patterns)
|
||||
IgnoreFilter::from_patterns(path, &exclude_patterns)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -48,8 +52,10 @@ impl Actor for Ignore {
|
|||
|
||||
// Apply to hovered
|
||||
if let Some(h) = cx.hovered_folder_mut() {
|
||||
let hovered_str = 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_filter = if let Some(path) = h.url.as_path() {
|
||||
IgnoreFilter::from_patterns(path, override_patterns)
|
||||
IgnoreFilter::from_patterns(path, &hovered_excludes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -66,8 +72,8 @@ impl Actor for Ignore {
|
|||
succ!();
|
||||
}
|
||||
|
||||
// If gitignore is disabled and no override patterns, remove any ignore filter
|
||||
if !gitignore_enabled {
|
||||
// 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
|
||||
|
|
@ -104,7 +110,7 @@ impl Actor for Ignore {
|
|||
|
||||
// Load ignore filter from the current directory
|
||||
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
|
||||
IgnoreFilter::from_dir(path, override_patterns)
|
||||
IgnoreFilter::from_dir(path, &exclude_patterns, gitignores)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -135,8 +141,10 @@ impl Actor for Ignore {
|
|||
// 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 = 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_filter = if let Some(path) = h.url.as_path() {
|
||||
IgnoreFilter::from_dir(path, override_patterns)
|
||||
IgnoreFilter::from_dir(path, &hovered_excludes, gitignores)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,90 +16,128 @@ 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)
|
||||
[files]
|
||||
# Enable git ignore integration
|
||||
# gitignores = false
|
||||
|
||||
# Context-specific exclude patterns
|
||||
# Patterns are compiled into glob matchers for efficient matching
|
||||
# Patterns starting with '!' negate (whitelist) previous matches
|
||||
# excludes = [
|
||||
# # SFTP temporary files
|
||||
# { urn = "*.tmp", in = "sftp://**" },
|
||||
# # 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 = "*" },
|
||||
# ]
|
||||
|
||||
[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 ]
|
||||
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]
|
||||
|
|
@ -176,70 +214,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
|
||||
|
|
|
|||
109
yazi-config/src/files/exclude.rs
Normal file
109
yazi-config/src/files/exclude.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use std::path::Path;
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Represents a single exclude rule with patterns and context
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Exclude {
|
||||
/// Pattern(s) to match files/directories against
|
||||
/// Can be a single glob pattern string or an array of glob patterns
|
||||
/// Patterns starting with '!' negate (whitelist) previously matched patterns
|
||||
#[serde(deserialize_with = "deserialize_urn")]
|
||||
pub urn: Vec<String>,
|
||||
|
||||
/// Context where this exclude rule applies
|
||||
/// Supports glob patterns like "/code/**", "sftp://**", "search://**", or "*" for all
|
||||
#[serde(rename = "in")]
|
||||
pub context: String,
|
||||
|
||||
#[serde(skip)]
|
||||
compiled: Option<CompiledPatterns>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CompiledPatterns {
|
||||
/// Regular patterns (to ignore)
|
||||
ignores: GlobSet,
|
||||
/// Negated patterns (to whitelist/un-ignore)
|
||||
whitelists: GlobSet,
|
||||
}
|
||||
|
||||
fn deserialize_urn<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum UrnOrUrns {
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
match UrnOrUrns::deserialize(deserializer)? {
|
||||
UrnOrUrns::Single(s) => Ok(vec![s]),
|
||||
UrnOrUrns::Multiple(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
impl Exclude {
|
||||
/// Compile the glob patterns into GlobSets for efficient matching
|
||||
pub fn compile(&mut self) -> Result<(), globset::Error> {
|
||||
let mut ignore_builder = GlobSetBuilder::new();
|
||||
let mut whitelist_builder = GlobSetBuilder::new();
|
||||
|
||||
for pattern in &self.urn {
|
||||
if let Some(negated) = pattern.strip_prefix('!') {
|
||||
// Negation pattern - add to whitelist
|
||||
let glob = Glob::new(negated)?;
|
||||
whitelist_builder.add(glob);
|
||||
} else {
|
||||
// Regular pattern - add to ignore list
|
||||
let glob = Glob::new(pattern)?;
|
||||
ignore_builder.add(glob);
|
||||
}
|
||||
}
|
||||
|
||||
self.compiled = Some(CompiledPatterns {
|
||||
ignores: ignore_builder.build()?,
|
||||
whitelists: whitelist_builder.build()?,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a path matches this exclude rule
|
||||
/// Returns Some(true) if path should be ignored, Some(false) if whitelisted,
|
||||
/// None if no match
|
||||
pub fn matches_path(&self, path: &Path) -> Option<bool> {
|
||||
let compiled = self.compiled.as_ref()?;
|
||||
|
||||
// Check whitelist first (negation takes precedence)
|
||||
if compiled.whitelists.is_match(path) {
|
||||
return Some(false); // Explicitly NOT ignored
|
||||
}
|
||||
|
||||
// Check ignore patterns
|
||||
if compiled.ignores.is_match(path) {
|
||||
return Some(true); // Should be ignored
|
||||
}
|
||||
|
||||
None // No match
|
||||
}
|
||||
|
||||
/// 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 == "*" {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle glob patterns
|
||||
if self.context.ends_with("/**") {
|
||||
let prefix = &self.context[..self.context.len() - 3];
|
||||
path.starts_with(prefix)
|
||||
} else {
|
||||
path == self.context || path.starts_with(&format!("{}/", self.context))
|
||||
}
|
||||
}
|
||||
}
|
||||
56
yazi-config/src/files/files.rs
Normal file
56
yazi-config/src/files/files.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_codegen::DeserializeOver2;
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Compile all glob patterns in exclude rules
|
||||
pub fn compile(&mut self) -> Result<(), String> {
|
||||
for exclude in &mut self.excludes {
|
||||
exclude.compile().map_err(|e| format!("Failed to compile glob pattern: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all exclude patterns that apply to a given context
|
||||
pub fn excludes_for_context(&self, context: &str) -> Vec<String> {
|
||||
self
|
||||
.excludes
|
||||
.iter()
|
||||
.filter(|e| e.matches_context(context))
|
||||
.flat_map(|e| e.urn.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if a path should be excluded based on compiled patterns for a given
|
||||
/// context Returns Some(true) if should be ignored, Some(false) if
|
||||
/// whitelisted, None if no match
|
||||
pub fn matches_path(&self, path: &Path, context: &str) -> Option<bool> {
|
||||
// Process rules in order, last match wins
|
||||
let mut result = None;
|
||||
|
||||
for exclude in &self.excludes {
|
||||
if exclude.matches_context(context) {
|
||||
if let Some(should_ignore) = exclude.matches_path(path) {
|
||||
result = Some(should_ignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
5
yazi-config/src/files/mod.rs
Normal file
5
yazi-config/src/files/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod exclude;
|
||||
mod files;
|
||||
|
||||
pub use exclude::*;
|
||||
pub use files::*;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs);
|
||||
yazi_macro::mod_pub!(files keymap mgr open opener plugin popup preview tasks theme which vfs);
|
||||
|
||||
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,6 @@ pub struct Mgr {
|
|||
pub scrolloff: SyncCell<u8>,
|
||||
pub mouse_events: SyncCell<MouseEvents>,
|
||||
pub title_format: String,
|
||||
|
||||
// Filtering
|
||||
#[serde(default)]
|
||||
pub gitignore_enable: bool,
|
||||
#[serde(default)]
|
||||
pub ignore_override: Vec<String>,
|
||||
}
|
||||
|
||||
impl Mgr {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ use serde::Deserialize;
|
|||
use yazi_codegen::DeserializeOver1;
|
||||
use yazi_fs::{Xdg, ok_or_not_found};
|
||||
|
||||
use crate::{mgr, open, opener, plugin, popup, preview, tasks, which};
|
||||
use crate::{files, mgr, open, opener, plugin, popup, preview, tasks, which};
|
||||
|
||||
#[derive(Deserialize, DeserializeOver1)]
|
||||
pub struct Yazi {
|
||||
pub mgr: mgr::Mgr,
|
||||
#[serde(default)]
|
||||
pub files: files::Files,
|
||||
pub preview: preview::Preview,
|
||||
pub opener: opener::Opener,
|
||||
pub open: open::Open,
|
||||
|
|
@ -26,9 +28,13 @@ impl Yazi {
|
|||
.with_context(|| format!("Failed to read config {p:?}"))
|
||||
}
|
||||
|
||||
pub(super) fn reshape(self) -> Result<Self> {
|
||||
pub(super) fn reshape(mut self) -> Result<Self> {
|
||||
// Compile glob patterns in exclude rules
|
||||
self.files.compile().map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
Ok(Self {
|
||||
mgr: self.mgr.reshape()?,
|
||||
files: self.files,
|
||||
preview: self.preview.reshape()?,
|
||||
opener: self.opener.reshape()?,
|
||||
open: self.open.reshape()?,
|
||||
|
|
|
|||
|
|
@ -2,51 +2,93 @@ use std::{collections::HashSet, path::{Path, PathBuf}};
|
|||
|
||||
use yazi_shared::url::AsUrl;
|
||||
|
||||
/// Filter for ignoring files based on git ignore rules and 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.
|
||||
#[derive(Clone, Debug)]
|
||||
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>,
|
||||
}
|
||||
|
||||
impl IgnoreFilter {
|
||||
/// Create a new IgnoreFilter by checking git ignore status for files in the
|
||||
/// given directory
|
||||
pub fn from_dir(dir: impl AsRef<Path>, override_patterns: &[String]) -> Option<Self> {
|
||||
/// Creates a new `IgnoreFilter` by checking git ignore status for the given
|
||||
/// directory.
|
||||
///
|
||||
/// # 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
|
||||
///
|
||||
/// # 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,
|
||||
) -> Option<Self> {
|
||||
let dir = dir.as_ref();
|
||||
|
||||
// Try to open the git repository for this directory
|
||||
let repo = git2::Repository::discover(dir).ok()?;
|
||||
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()?;
|
||||
// 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,
|
||||
};
|
||||
// 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();
|
||||
// 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"));
|
||||
// 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() {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build custom gitignore from override patterns if provided
|
||||
let gitignore = if !override_patterns.is_empty() {
|
||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(workdir);
|
||||
(workdir, ignored_paths)
|
||||
} else {
|
||||
// No git integration, use current directory as base
|
||||
(dir.to_path_buf(), HashSet::new())
|
||||
};
|
||||
|
||||
// Add each override pattern
|
||||
for pattern in override_patterns {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +100,7 @@ impl IgnoreFilter {
|
|||
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
|
||||
// If we have no git-ignored paths but we have exclude patterns, still create
|
||||
// the filter
|
||||
if gitignore.is_some() {
|
||||
return Some(Self { ignored_paths, gitignore });
|
||||
|
|
@ -71,7 +113,21 @@ impl IgnoreFilter {
|
|||
Some(Self { ignored_paths, gitignore })
|
||||
}
|
||||
|
||||
/// Create a new IgnoreFilter from only override patterns (no git integration)
|
||||
/// 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.
|
||||
pub fn from_patterns(dir: impl AsRef<Path>, patterns: &[String]) -> Option<Self> {
|
||||
if patterns.is_empty() {
|
||||
return None;
|
||||
|
|
@ -90,7 +146,28 @@ impl IgnoreFilter {
|
|||
Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) })
|
||||
}
|
||||
|
||||
/// Check if a file should be ignored based on its URL
|
||||
/// Checks if a file should be ignored based on its URL.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL of the file to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the file should be ignored, `false` otherwise.
|
||||
///
|
||||
/// # 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.
|
||||
pub fn matches_url(&self, url: impl AsUrl) -> bool {
|
||||
let url = url.as_url();
|
||||
let path = url.loc.as_path();
|
||||
|
|
@ -147,8 +224,6 @@ impl IgnoreFilter {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_git_repo_discovery() {
|
||||
// This test assumes we're running in the yazi git repo
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue