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";
|
const NAME: &str = "ignore";
|
||||||
|
|
||||||
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
|
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
|
||||||
let gitignore_enabled = YAZI.mgr.gitignore_enable;
|
let gitignores = YAZI.files.gitignores;
|
||||||
let override_patterns = &YAZI.mgr.ignore_override;
|
|
||||||
|
|
||||||
// If gitignore is disabled but we have override patterns, apply them
|
// Get exclude patterns for the current context
|
||||||
if !gitignore_enabled && !override_patterns.is_empty() {
|
// Use path string for context matching
|
||||||
// Load ignore filter from override patterns only
|
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() {
|
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
|
||||||
IgnoreFilter::from_patterns(path, override_patterns)
|
IgnoreFilter::from_patterns(path, &exclude_patterns)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -48,8 +52,10 @@ impl Actor for Ignore {
|
||||||
|
|
||||||
// Apply to hovered
|
// Apply to hovered
|
||||||
if let Some(h) = cx.hovered_folder_mut() {
|
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() {
|
let hovered_filter = if let Some(path) = h.url.as_path() {
|
||||||
IgnoreFilter::from_patterns(path, override_patterns)
|
IgnoreFilter::from_patterns(path, &hovered_excludes)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -66,8 +72,8 @@ impl Actor for Ignore {
|
||||||
succ!();
|
succ!();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If gitignore is disabled and no override patterns, remove any ignore filter
|
// If gitignores is disabled and no exclude patterns, remove any ignore filter
|
||||||
if !gitignore_enabled {
|
if !gitignores {
|
||||||
let hovered = cx.hovered().map(|f| f.urn().to_owned());
|
let hovered = cx.hovered().map(|f| f.urn().to_owned());
|
||||||
let apply = |f: &mut Folder| {
|
let apply = |f: &mut Folder| {
|
||||||
// Always clear the filter, even when loading
|
// Always clear the filter, even when loading
|
||||||
|
|
@ -104,7 +110,7 @@ impl Actor for Ignore {
|
||||||
|
|
||||||
// Load ignore filter from the current directory
|
// Load ignore filter from the current directory
|
||||||
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -135,8 +141,10 @@ impl Actor for Ignore {
|
||||||
// Apply to hovered
|
// Apply to hovered
|
||||||
if let Some(h) = cx.hovered_folder_mut() {
|
if let Some(h) = cx.hovered_folder_mut() {
|
||||||
// Load ignore filter for hovered directory if it's a directory
|
// 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() {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,24 @@ scrolloff = 5
|
||||||
mouse_events = [ "click", "scroll" ]
|
mouse_events = [ "click", "scroll" ]
|
||||||
title_format = "Yazi: {cwd}"
|
title_format = "Yazi: {cwd}"
|
||||||
|
|
||||||
# Git ignore integration
|
[files]
|
||||||
# gitignore_enable = false
|
# Enable git ignore integration
|
||||||
# ignore_override = [
|
# gitignores = false
|
||||||
# "*.log", # Hide all .log files
|
|
||||||
# "tmp/", # Hide tmp directory
|
# Context-specific exclude patterns
|
||||||
# "!target/", # Show target/ even if gitignored (negation)
|
# 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]
|
[preview]
|
||||||
|
|
@ -34,7 +46,7 @@ image_delay = 30
|
||||||
image_filter = "triangle"
|
image_filter = "triangle"
|
||||||
image_quality = 75
|
image_quality = 75
|
||||||
ueberzug_scale = 1
|
ueberzug_scale = 1
|
||||||
ueberzug_offset = [ 0, 0, 0, 0 ]
|
ueberzug_offset = [0, 0, 0, 0]
|
||||||
|
|
||||||
[opener]
|
[opener]
|
||||||
edit = [
|
edit = [
|
||||||
|
|
@ -63,9 +75,7 @@ reveal = [
|
||||||
{ 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 },
|
{ run = "clear; exiftool %s1; echo 'Press enter to exit'; read _", desc = "Show EXIF", for = "unix", block = true },
|
||||||
]
|
]
|
||||||
extract = [
|
extract = [{ run = "ya pub extract --list %s", desc = "Extract here" }]
|
||||||
{ run = "ya pub extract --list %s", desc = "Extract here" },
|
|
||||||
]
|
|
||||||
download = [
|
download = [
|
||||||
{ run = "ya emit download --open %S", desc = "Download and open" },
|
{ run = "ya emit download --open %S", desc = "Download and open" },
|
||||||
{ run = "ya emit download %S", desc = "Download" },
|
{ run = "ya emit download %S", desc = "Download" },
|
||||||
|
|
@ -74,24 +84,52 @@ download = [
|
||||||
[open]
|
[open]
|
||||||
rules = [
|
rules = [
|
||||||
# Folder
|
# Folder
|
||||||
{ url = "*/", use = [ "edit", "open", "reveal" ] },
|
{ url = "*/", use = [
|
||||||
|
"edit",
|
||||||
|
"open",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Text
|
# Text
|
||||||
{ mime = "text/*", use = [ "edit", "reveal" ] },
|
{ mime = "text/*", use = [
|
||||||
|
"edit",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Image
|
# Image
|
||||||
{ mime = "image/*", use = [ "open", "reveal" ] },
|
{ mime = "image/*", use = [
|
||||||
|
"open",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Media
|
# Media
|
||||||
{ mime = "{audio,video}/*", use = [ "play", "reveal" ] },
|
{ mime = "{audio,video}/*", use = [
|
||||||
|
"play",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Archive
|
# 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
|
# JSON
|
||||||
{ mime = "application/{json,ndjson}", use = [ "edit", "reveal" ] },
|
{ mime = "application/{json,ndjson}", use = [
|
||||||
{ mime = "*/javascript", use = [ "edit", "reveal" ] },
|
"edit",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
|
{ mime = "*/javascript", use = [
|
||||||
|
"edit",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Empty file
|
# Empty file
|
||||||
{ mime = "inode/empty", use = [ "edit", "reveal" ] },
|
{ mime = "inode/empty", use = [
|
||||||
|
"edit",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
# Virtual file system
|
# Virtual file system
|
||||||
{ mime = "vfs/{absent,stale}", use = "download" },
|
{ mime = "vfs/{absent,stale}", use = "download" },
|
||||||
# Fallback
|
# Fallback
|
||||||
{ url = "*", use = [ "open", "reveal" ] },
|
{ url = "*", use = [
|
||||||
|
"open",
|
||||||
|
"reveal",
|
||||||
|
] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks]
|
[tasks]
|
||||||
|
|
@ -99,7 +137,7 @@ micro_workers = 10
|
||||||
macro_workers = 10
|
macro_workers = 10
|
||||||
bizarre_retry = 3
|
bizarre_retry = 3
|
||||||
image_alloc = 536870912 # 512MB
|
image_alloc = 536870912 # 512MB
|
||||||
image_bound = [ 10000, 10000 ]
|
image_bound = [10000, 10000]
|
||||||
suppress_preload = false
|
suppress_preload = false
|
||||||
|
|
||||||
[plugin]
|
[plugin]
|
||||||
|
|
@ -178,65 +216,65 @@ cursor_blink = false
|
||||||
# cd
|
# cd
|
||||||
cd_title = "Change directory:"
|
cd_title = "Change directory:"
|
||||||
cd_origin = "top-center"
|
cd_origin = "top-center"
|
||||||
cd_offset = [ 0, 2, 50, 3 ]
|
cd_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
# create
|
# create
|
||||||
create_title = [ "Create:", "Create (dir):" ]
|
create_title = ["Create:", "Create (dir):"]
|
||||||
create_origin = "top-center"
|
create_origin = "top-center"
|
||||||
create_offset = [ 0, 2, 50, 3 ]
|
create_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
# rename
|
# rename
|
||||||
rename_title = "Rename:"
|
rename_title = "Rename:"
|
||||||
rename_origin = "hovered"
|
rename_origin = "hovered"
|
||||||
rename_offset = [ 0, 1, 50, 3 ]
|
rename_offset = [0, 1, 50, 3]
|
||||||
|
|
||||||
# filter
|
# filter
|
||||||
filter_title = "Filter:"
|
filter_title = "Filter:"
|
||||||
filter_origin = "top-center"
|
filter_origin = "top-center"
|
||||||
filter_offset = [ 0, 2, 50, 3 ]
|
filter_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
# find
|
# find
|
||||||
find_title = [ "Find next:", "Find previous:" ]
|
find_title = ["Find next:", "Find previous:"]
|
||||||
find_origin = "top-center"
|
find_origin = "top-center"
|
||||||
find_offset = [ 0, 2, 50, 3 ]
|
find_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
# search
|
# search
|
||||||
search_title = "Search via {n}:"
|
search_title = "Search via {n}:"
|
||||||
search_origin = "top-center"
|
search_origin = "top-center"
|
||||||
search_offset = [ 0, 2, 50, 3 ]
|
search_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
# shell
|
# shell
|
||||||
shell_title = [ "Shell:", "Shell (block):" ]
|
shell_title = ["Shell:", "Shell (block):"]
|
||||||
shell_origin = "top-center"
|
shell_origin = "top-center"
|
||||||
shell_offset = [ 0, 2, 50, 3 ]
|
shell_offset = [0, 2, 50, 3]
|
||||||
|
|
||||||
[confirm]
|
[confirm]
|
||||||
# trash
|
# trash
|
||||||
trash_title = "Trash {n} selected file{s}?"
|
trash_title = "Trash {n} selected file{s}?"
|
||||||
trash_origin = "center"
|
trash_origin = "center"
|
||||||
trash_offset = [ 0, 0, 70, 20 ]
|
trash_offset = [0, 0, 70, 20]
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
delete_title = "Permanently delete {n} selected file{s}?"
|
delete_title = "Permanently delete {n} selected file{s}?"
|
||||||
delete_origin = "center"
|
delete_origin = "center"
|
||||||
delete_offset = [ 0, 0, 70, 20 ]
|
delete_offset = [0, 0, 70, 20]
|
||||||
|
|
||||||
# overwrite
|
# overwrite
|
||||||
overwrite_title = "Overwrite file?"
|
overwrite_title = "Overwrite file?"
|
||||||
overwrite_body = "Will overwrite the following file:"
|
overwrite_body = "Will overwrite the following file:"
|
||||||
overwrite_origin = "center"
|
overwrite_origin = "center"
|
||||||
overwrite_offset = [ 0, 0, 50, 15 ]
|
overwrite_offset = [0, 0, 50, 15]
|
||||||
|
|
||||||
# quit
|
# quit
|
||||||
quit_title = "Quit?"
|
quit_title = "Quit?"
|
||||||
quit_body = "The following tasks are still running, are you sure you want to quit?"
|
quit_body = "The following tasks are still running, are you sure you want to quit?"
|
||||||
quit_origin = "center"
|
quit_origin = "center"
|
||||||
quit_offset = [ 0, 0, 50, 15 ]
|
quit_offset = [0, 0, 50, 15]
|
||||||
|
|
||||||
[pick]
|
[pick]
|
||||||
open_title = "Open with:"
|
open_title = "Open with:"
|
||||||
open_origin = "hovered"
|
open_origin = "hovered"
|
||||||
open_offset = [ 0, 1, 50, 7 ]
|
open_offset = [0, 1, 50, 7]
|
||||||
|
|
||||||
[which]
|
[which]
|
||||||
sort_by = "none"
|
sort_by = "none"
|
||||||
|
|
|
||||||
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);
|
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 scrolloff: SyncCell<u8>,
|
||||||
pub mouse_events: SyncCell<MouseEvents>,
|
pub mouse_events: SyncCell<MouseEvents>,
|
||||||
pub title_format: String,
|
pub title_format: String,
|
||||||
|
|
||||||
// Filtering
|
|
||||||
#[serde(default)]
|
|
||||||
pub gitignore_enable: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub ignore_override: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mgr {
|
impl Mgr {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ use serde::Deserialize;
|
||||||
use yazi_codegen::DeserializeOver1;
|
use yazi_codegen::DeserializeOver1;
|
||||||
use yazi_fs::{Xdg, ok_or_not_found};
|
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)]
|
#[derive(Deserialize, DeserializeOver1)]
|
||||||
pub struct Yazi {
|
pub struct Yazi {
|
||||||
pub mgr: mgr::Mgr,
|
pub mgr: mgr::Mgr,
|
||||||
|
#[serde(default)]
|
||||||
|
pub files: files::Files,
|
||||||
pub preview: preview::Preview,
|
pub preview: preview::Preview,
|
||||||
pub opener: opener::Opener,
|
pub opener: opener::Opener,
|
||||||
pub open: open::Open,
|
pub open: open::Open,
|
||||||
|
|
@ -26,9 +28,13 @@ impl Yazi {
|
||||||
.with_context(|| format!("Failed to read config {p:?}"))
|
.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 {
|
Ok(Self {
|
||||||
mgr: self.mgr.reshape()?,
|
mgr: self.mgr.reshape()?,
|
||||||
|
files: self.files,
|
||||||
preview: self.preview.reshape()?,
|
preview: self.preview.reshape()?,
|
||||||
opener: self.opener.reshape()?,
|
opener: self.opener.reshape()?,
|
||||||
open: self.open.reshape()?,
|
open: self.open.reshape()?,
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,59 @@ use std::{collections::HashSet, path::{Path, PathBuf}};
|
||||||
|
|
||||||
use yazi_shared::url::AsUrl;
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct IgnoreFilter {
|
pub struct IgnoreFilter {
|
||||||
|
/// Set of paths ignored by git (from git status)
|
||||||
ignored_paths: HashSet<PathBuf>,
|
ignored_paths: HashSet<PathBuf>,
|
||||||
|
/// Custom gitignore matcher for exclude patterns
|
||||||
gitignore: Option<ignore::gitignore::Gitignore>,
|
gitignore: Option<ignore::gitignore::Gitignore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IgnoreFilter {
|
impl IgnoreFilter {
|
||||||
/// Create a new IgnoreFilter by checking git ignore status for files in the
|
/// Creates a new `IgnoreFilter` by checking git ignore status for the given
|
||||||
/// given directory
|
/// directory.
|
||||||
pub fn from_dir(dir: impl AsRef<Path>, override_patterns: &[String]) -> Option<Self> {
|
///
|
||||||
|
/// # 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();
|
let dir = dir.as_ref();
|
||||||
|
|
||||||
|
let (workdir, ignored_paths) = if use_git {
|
||||||
// Try to open the git repository for this directory
|
// Try to open the git repository for this directory
|
||||||
let repo = git2::Repository::discover(dir).ok()?;
|
let repo = git2::Repository::discover(dir).ok()?;
|
||||||
|
|
||||||
// Get the workdir (root of the git repository)
|
// Get the workdir (root of the git repository)
|
||||||
let workdir = repo.workdir()?;
|
let workdir = repo.workdir()?.to_path_buf();
|
||||||
|
|
||||||
// Get git statuses for the repository
|
// Get git statuses for the repository
|
||||||
let statuses = match repo.statuses(None) {
|
let statuses = match repo.statuses(None) {
|
||||||
|
|
@ -34,19 +70,25 @@ impl IgnoreFilter {
|
||||||
|
|
||||||
// Add all ignored files from git status
|
// Add all ignored files from git status
|
||||||
for status in statuses.iter() {
|
for status in statuses.iter() {
|
||||||
if status.status() == git2::Status::IGNORED {
|
if status.status() == git2::Status::IGNORED
|
||||||
if let Some(path) = status.path() {
|
&& let Some(path) = status.path()
|
||||||
|
{
|
||||||
ignored_paths.insert(workdir.join(path));
|
ignored_paths.insert(workdir.join(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build custom gitignore from override patterns if provided
|
(workdir, ignored_paths)
|
||||||
let gitignore = if !override_patterns.is_empty() {
|
} else {
|
||||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(workdir);
|
// No git integration, use current directory as base
|
||||||
|
(dir.to_path_buf(), HashSet::new())
|
||||||
|
};
|
||||||
|
|
||||||
// Add each override pattern
|
// Build custom gitignore from exclude patterns if provided
|
||||||
for pattern in override_patterns {
|
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);
|
let _ = builder.add_line(None, pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +100,7 @@ impl IgnoreFilter {
|
||||||
if ignored_paths.is_empty()
|
if ignored_paths.is_empty()
|
||||||
|| (ignored_paths.len() == 1 && ignored_paths.contains(&workdir.join(".git")))
|
|| (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
|
// the filter
|
||||||
if gitignore.is_some() {
|
if gitignore.is_some() {
|
||||||
return Some(Self { ignored_paths, gitignore });
|
return Some(Self { ignored_paths, gitignore });
|
||||||
|
|
@ -71,7 +113,21 @@ impl IgnoreFilter {
|
||||||
Some(Self { ignored_paths, gitignore })
|
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> {
|
pub fn from_patterns(dir: impl AsRef<Path>, patterns: &[String]) -> Option<Self> {
|
||||||
if patterns.is_empty() {
|
if patterns.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -90,7 +146,28 @@ impl IgnoreFilter {
|
||||||
Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) })
|
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 {
|
pub fn matches_url(&self, url: impl AsUrl) -> bool {
|
||||||
let url = url.as_url();
|
let url = url.as_url();
|
||||||
let path = url.loc.as_path();
|
let path = url.loc.as_path();
|
||||||
|
|
@ -147,8 +224,6 @@ impl IgnoreFilter {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_git_repo_discovery() {
|
fn test_git_repo_discovery() {
|
||||||
// This test assumes we're running in the yazi git repo
|
// This test assumes we're running in the yazi git repo
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue