From 1bb11ff321a1385647cecce8058f43da4dd94dfc Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 26 Oct 2026 03:03:39 -0700 Subject: [PATCH] Local: on Windows the folder trees leave out hidden folders such as AppData The Local trees left out folders whose names start with a dot, but on Windows a folder is hidden by its attribute. Home's tree listed AppData, and browsing a folder inside the temp folder opened Home's tree down through AppData\Local\Temp, drawing one row for every folder there on each frame. On a machine with 148,400 folders in Temp each frame took about 4 s, and tests_panels::local_photos_cannot_be_added_to_albums_until_they_join_my_photos timed out on its ui.drag. list_subfolders now also skips folders with FILE_ATTRIBUTE_HIDDEN set. Test: panels::left::tests::subfolders_leave_out_hidden_folders. --- crates/ui-egui/src/panels/left.rs | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 5a81edfab..3304b4b8f 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -1040,7 +1040,7 @@ fn list_subfolders(path: &str) -> Vec<(String, String)> { let mut v: Vec<(String, String)> = std::fs::read_dir(path) .map(|rd| { rd.flatten() - .filter(|e| e.file_type().is_ok_and(|t| t.is_dir())) + .filter(|e| e.file_type().is_ok_and(|t| t.is_dir()) && !hidden_attribute(e)) .filter_map(|e| { let name = e.file_name().to_string_lossy().to_string(); (!name.starts_with('.')).then(|| (name, e.path().to_string_lossy().to_string())) @@ -1052,6 +1052,20 @@ fn list_subfolders(path: &str) -> Vec<(String, String)> { v } +/// On Windows a folder is hidden by an attribute (AppData in the home folder is), not by a +/// leading dot. +#[cfg(windows)] +fn hidden_attribute(e: &std::fs::DirEntry) -> bool { + use std::os::windows::fs::MetadataExt; + const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; + e.metadata().is_ok_and(|m| m.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0) +} + +#[cfg(not(windows))] +fn hidden_attribute(_: &std::fs::DirEntry) -> bool { + false +} + /// How many [`fs_cached`] answers for `ctx` are being worked out right now. Rows appear (and /// the sidebar below them moves) when they land, so the headless driver counts them as pending /// work and waits for them before acting on widget positions. @@ -2370,6 +2384,27 @@ mod tests { assert_eq!((names(&l.places), l.owner), (vec!["Pictures"], Some(0))); } + /// A folder tree leaves out hidden subfolders: a leading dot everywhere, and on Windows the + /// hidden attribute, so Home's tree does not list AppData and open down into its temp folder. + #[test] + fn subfolders_leave_out_hidden_folders() { + let dir = std::env::temp_dir().join(format!("lc-ui-hidden-subfolders-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + for name in ["Shown", ".dot", "Hidden"] { + std::fs::create_dir_all(dir.join(name)).unwrap(); + } + let expected = if cfg!(windows) { + let status = std::process::Command::new("attrib").arg("+h").arg(dir.join("Hidden")).status().unwrap(); + assert!(status.success(), "attrib +h: {status}"); + vec!["Shown"] + } else { + vec!["Hidden", "Shown"] + }; + let listed = super::list_subfolders(&dir.to_string_lossy()); + let _ = std::fs::remove_dir_all(&dir); + assert_eq!(listed.iter().map(|(n, _)| n.as_str()).collect::>(), expected); + } + #[test] fn a_local_folder_lines_up_with_the_rows_above_and_its_triangle_stays_left_of_the_icon() { use crate::Services;