diff --git a/crates/ruf4/src/draw.rs b/crates/ruf4/src/draw.rs index 97e6621..ac95ba5 100644 --- a/crates/ruf4/src/draw.rs +++ b/crates/ruf4/src/draw.rs @@ -389,6 +389,8 @@ fn draw_single_panel( } } else if entry.selected { ctx.attr_foreground_rgba(ctx.indexed(theme.file_selected)); + } else if entry.is_hidden { + ctx.attr_foreground_rgba(ctx.indexed(theme.file_hidden)); } else if entry.is_dir { ctx.attr_foreground_rgba(ctx.indexed(theme.file_dir)); } else if entry.is_executable { diff --git a/crates/ruf4/src/panel.rs b/crates/ruf4/src/panel.rs index f407552..6de2e85 100644 --- a/crates/ruf4/src/panel.rs +++ b/crates/ruf4/src/panel.rs @@ -31,6 +31,7 @@ pub struct FileEntry { pub is_hardlink: bool, pub is_executable: bool, pub is_readonly: bool, + pub is_hidden: bool, pub size: u64, pub modified: Option, pub selected: bool, @@ -134,6 +135,7 @@ impl Panel { is_hardlink: false, is_executable: false, is_readonly: false, + is_hidden: false, size: 0, modified: None, selected: false, @@ -144,18 +146,18 @@ impl Panel { for entry in iter.flatten() { let name = entry.file_name().to_string_lossy().into_owned(); - if !self.show_hidden && name.starts_with('.') { - continue; - } - let is_symlink = entry.file_type().map(|t| t.is_symlink()).unwrap_or(false); - // Follow symlinks: fs::metadata resolves the target, - // while entry.metadata() returns the symlink itself. let metadata = if is_symlink { fs::metadata(entry.path()).ok() } else { entry.metadata().ok() }; + + let is_hidden = platform::is_hidden(&name, metadata.as_ref()); + if !self.show_hidden && is_hidden { + continue; + } + let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false); let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0); let modified = metadata.as_ref().and_then(|m| m.modified().ok()); @@ -178,6 +180,7 @@ impl Panel { is_hardlink, is_executable, is_readonly, + is_hidden, size, modified, selected: false, @@ -449,6 +452,7 @@ pub fn make_entry(name: &str, is_dir: bool, size: u64) -> FileEntry { is_hardlink: false, is_executable: false, is_readonly: false, + is_hidden: false, size, modified: None, selected: false, diff --git a/crates/ruf4/src/platform.rs b/crates/ruf4/src/platform.rs index caa8476..26eac8d 100644 --- a/crates/ruf4/src/platform.rs +++ b/crates/ruf4/src/platform.rs @@ -214,6 +214,35 @@ pub fn disk_free(_path: &Path) -> Option { // ── File properties ──────────────────────────────────────────────────────── +/// Detect whether a file should be considered hidden. +/// On Unix: dot-prefix. On Windows: FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM. +#[cfg(unix)] +pub fn is_hidden(name: &str, _metadata: Option<&std::fs::Metadata>) -> bool { + name.starts_with('.') +} + +#[cfg(windows)] +pub fn is_hidden(name: &str, metadata: Option<&std::fs::Metadata>) -> bool { + use std::os::windows::fs::MetadataExt; + const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; + const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4; + if name.starts_with('.') { + return true; + } + if let Some(m) = metadata { + let attrs = m.file_attributes(); + if attrs & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM) != 0 { + return true; + } + } + false +} + +#[cfg(not(any(unix, windows)))] +pub fn is_hidden(name: &str, _metadata: Option<&std::fs::Metadata>) -> bool { + name.starts_with('.') +} + /// Detect whether a file is a hard link and/or executable. /// Returns `(is_hardlink, is_executable)`. #[cfg(unix)] diff --git a/crates/ruf4/src/theme.rs b/crates/ruf4/src/theme.rs index 1e2cd0c..8cd7b0a 100644 --- a/crates/ruf4/src/theme.rs +++ b/crates/ruf4/src/theme.rs @@ -32,6 +32,7 @@ pub struct Theme { pub file_dir: IndexedColor, pub file_executable: IndexedColor, pub file_readonly: IndexedColor, + pub file_hidden: IndexedColor, pub file_normal: IndexedColor, // ── Preview panel ────────────────────────────────────────────────── @@ -161,6 +162,7 @@ pub const THEME_FIELDS: &[&str] = &[ "file_dir", "file_executable", "file_readonly", + "file_hidden", "file_normal", "preview_border", "preview_text", @@ -230,6 +232,7 @@ impl Theme { "file_dir" => self.file_dir, "file_executable" => self.file_executable, "file_readonly" => self.file_readonly, + "file_hidden" => self.file_hidden, "file_normal" => self.file_normal, "preview_border" => self.preview_border, "preview_text" => self.preview_text, @@ -300,6 +303,7 @@ impl Theme { "file_dir" => self.file_dir = color, "file_executable" => self.file_executable = color, "file_readonly" => self.file_readonly = color, + "file_hidden" => self.file_hidden = color, "file_normal" => self.file_normal = color, "preview_border" => self.preview_border = color, "preview_text" => self.preview_text = color, @@ -375,7 +379,8 @@ impl Theme { file_selected: IndexedColor::BrightYellow, file_dir: IndexedColor::BrightWhite, file_executable: IndexedColor::BrightGreen, - file_readonly: IndexedColor::BrightBlack, + file_readonly: IndexedColor::Red, + file_hidden: IndexedColor::BrightBlack, file_normal: IndexedColor::White, // Preview