icon files and separator added back - look and feel updates

This commit is contained in:
2026-06-15 15:38:33 -05:00
parent 2b104efe24
commit c492d7c0f9
18 changed files with 603 additions and 194 deletions
Generated
+41
View File
@@ -1934,6 +1934,7 @@ dependencies = [
"librarian-core",
"librarian-win",
"notify-debouncer-mini",
"winresource",
]
[[package]]
@@ -3374,6 +3375,15 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "shlex"
version = "2.0.1"
@@ -3767,6 +3777,21 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -3797,6 +3822,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "tracing"
version = "0.1.44"
@@ -4791,6 +4822,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winresource"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
dependencies = [
"toml",
"version_check",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
+5
View File
@@ -15,3 +15,8 @@ librarian-win.workspace = true
iced.workspace = true
chrono = "0.4"
notify-debouncer-mini = "0.7.0"
# Embeds the application icon as a Win32 resource (so the .exe shows it in
# Explorer / on shortcuts). Windows-only; see build.rs.
[target.'cfg(windows)'.build-dependencies]
winresource = "0.1"
+35
View File
@@ -0,0 +1,35 @@
//! Build script: embed the application icon as a Win32 resource on Windows, so
//! the icon shows on the `.exe` itself in Explorer and on shortcuts.
//!
//! This is independent of the in-app window/taskbar icon, which `main.rs` sets
//! at runtime via winit. A `.exe` resource icon, by contrast, is baked into the
//! PE binary at link time — which is what this does.
fn main() {
// The resource format is PE-specific, so only do this when *targeting*
// Windows (`CARGO_CFG_TARGET_OS` reflects the target, not the host, so this
// stays correct under cross-compilation).
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
// The icon lives at the repository root, two levels up from this crate.
// Build an absolute path (joining component-by-component keeps native
// separators) so it resolves regardless of the resource compiler's cwd.
let manifest = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let icon = manifest
.join("..")
.join("..")
.join("icon")
.join("librarian.ico");
println!("cargo:rerun-if-changed={}", icon.display());
let mut res = winresource::WindowsResource::new();
res.set_icon(icon.to_str().expect("icon path is valid UTF-8"));
if let Err(e) = res.compile() {
// Don't fail the build if the resource compiler (rc.exe / windres) is
// unavailable — the app still runs and keeps its runtime window icon.
// The .exe just won't carry an embedded icon in this build.
println!("cargo:warning=could not embed exe icon resource: {e}");
}
}
+2 -2
View File
@@ -15,8 +15,8 @@
use iced::advanced::text::paragraph::Paragraph;
use iced::advanced::widget::text as text_widget;
use iced::advanced::widget::{tree, Tree};
use iced::advanced::{layout, mouse, renderer, text, Layout, Widget};
use iced::advanced::widget::{Tree, tree};
use iced::advanced::{Layout, Widget, layout, mouse, renderer, text};
use iced::{Element, Length, Pixels, Rectangle, Size};
/// Create an ellipsizing single-line text cell with the given content.
+6 -1
View File
@@ -9,13 +9,17 @@ use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use iced::widget::image::Handle;
use librarian_win::{folder_icon, icon_for_extension, icon_for_path, IconImage, ShellWorker};
use librarian_win::{
IconImage, ShellWorker, computer_icon, folder_icon, icon_for_extension, icon_for_path,
};
/// What an icon represents. `Path` is used for things with a per-item icon
/// (drives, and later custom-icon files); `Folder`/`Ext` are generic and shared.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IconKey {
Folder,
/// The shell's "This PC" / Computer icon (the drives container node).
Computer,
Ext(String),
Path(PathBuf),
}
@@ -57,6 +61,7 @@ pub fn extract_icons(worker: &ShellWorker, keys: Vec<IconKey>) -> Vec<(IconKey,
.filter_map(|key| {
let image = match &key {
IconKey::Folder => worker.run(|apt| folder_icon(apt, false)),
IconKey::Computer => worker.run(|apt| computer_icon(apt, false)),
IconKey::Ext(ext) => {
let ext = ext.clone();
worker.run(move |apt| icon_for_extension(apt, &ext, false))
+220 -123
View File
@@ -14,23 +14,23 @@ use std::time::{Duration, Instant};
use iced::keyboard::key::Named;
use iced::keyboard::{self, Key};
use iced::widget::{
button, checkbox, column, container, image, mouse_area, pane_grid, row, scrollable, stack,
text, text_input, Space,
Space, button, checkbox, column, container, image, mouse_area, pane_grid, row, scrollable,
stack, text, text_input,
};
use iced::{Border, Center, Element, Length::Fill, Point, Size, Subscription, Task, Theme};
use librarian_core::{
is_visible, read_dir_all, read_subdirs, sort_entries, Entry, History, Location, Sort, SortKey,
SortOrder,
Entry, History, Location, Sort, SortKey, SortOrder, is_visible, read_dir_all, read_subdirs,
sort_entries,
};
use librarian_win::{
copy_items, create_folder, delete_to_recycle, known_folders, list_drives, move_items, rename,
Apartment, DriveInfo, IconImage, KnownFolder, ShellWorker,
Apartment, DriveInfo, IconImage, KnownFolder, ShellWorker, copy_items, create_folder,
delete_to_recycle, known_folders, list_drives, move_items, rename, user_home,
};
use ellipsis::ellipsized;
use icons::{extract_icons, IconCache, IconKey};
use rows::{format_time, human_size, Row};
use icons::{IconCache, IconKey, extract_icons};
use rows::{Row, format_time, human_size};
use selection::Selection;
use tree::{Reveal, Tree, TreeChild, TreeRow};
@@ -78,11 +78,26 @@ fn main() -> iced::Result {
.title("Librarian")
.theme(Librarian::theme)
.subscription(Librarian::subscription)
// Set the window icon before `window_size`/`centered`, which preserve the
// other window fields they don't touch.
.window(iced::window::Settings {
icon: window_icon(),
..Default::default()
})
.window_size(Size::new(1100.0, 720.0))
.centered()
.run()
}
/// The window/taskbar icon, embedded at compile time so the executable stays
/// self-contained and relocatable (no external icon file at runtime). The format
/// is guessed from the bytes. A decode failure is non-fatal — the app falls back
/// to the platform default icon rather than failing to launch.
fn window_icon() -> Option<iced::window::Icon> {
let data = include_bytes!("../../../icon/librarian.ico");
iced::window::icon::from_file_data(data, None).ok()
}
/// The cut/copy buffer for in-app clipboard operations.
struct Clip {
paths: Vec<PathBuf>,
@@ -125,8 +140,11 @@ enum PaneKind {
/// What the file list is currently showing.
enum Content {
ThisPc {
// Only system drives (and, later, mapped network locations) belong in
// the "This PC" landing list — not the user's known folders. Those
// remain reachable through the navigation tree, which loads them
// separately.
drives: Vec<DriveInfo>,
folders: Vec<KnownFolder>,
},
Folder {
entries: Vec<Entry>,
@@ -205,7 +223,7 @@ enum Message {
MoveSelection(Nav, bool, bool),
SelectAll,
Activate,
ThisPcLoaded(Vec<DriveInfo>, Vec<KnownFolder>),
ThisPcLoaded(Vec<DriveInfo>),
Loaded(u64, Result<Vec<Entry>, String>),
/// The current directory changed on disk; re-enumerate it in place.
DirChanged,
@@ -250,10 +268,7 @@ impl Librarian {
tree: Tree::new(),
panes,
pending_reveal: None,
content: Content::ThisPc {
drives: Vec::new(),
folders: Vec::new(),
},
content: Content::ThisPc { drives: Vec::new() },
rows: Vec::new(),
sort: settings.sort,
show_hidden: settings.show_hidden,
@@ -273,10 +288,10 @@ impl Librarian {
scroll_y: 0.0,
viewport_h: 720.0 - CHROME_HEIGHT,
};
// Load the starting location, populate the tree root (drives + known
// folders), and theme the window — all in parallel.
// Load the starting location, populate the tree's top level (the user's
// folders + a "This PC" node), and theme the window — all in parallel.
let load = app.load_current();
let tree_load = app.load_tree_children(tree::ROOT_ID, Location::ThisPc);
let tree_load = app.load_tree_roots();
(app, Task::batch([apply_chrome(), load, tree_load]))
}
@@ -365,8 +380,8 @@ impl Librarian {
config::save(&self.settings());
}
Message::RowClicked(index) => return self.on_click(index),
Message::ThisPcLoaded(drives, folders) => {
self.content = Content::ThisPc { drives, folders };
Message::ThisPcLoaded(drives) => {
self.content = Content::ThisPc { drives };
self.recompute_rows();
self.status = format!("{} items", self.rows.len());
return self.request_icons();
@@ -450,11 +465,22 @@ impl Librarian {
// resolves rather than hanging.
let children = result.unwrap_or_default();
self.tree.set_children(id, children);
// When the top level lands, auto-expand "This PC" so the drives
// show without a manual click (it starts collapsed otherwise).
let expand_pc = if id == tree::ROOT_ID {
self.tree
.this_pc_id()
.and_then(|pc| self.tree.toggle(pc))
.map(|(load_id, location)| self.load_tree_children(load_id, location))
.unwrap_or_else(Task::none)
} else {
Task::none()
};
// A newly-loaded node may let an in-progress reveal continue,
// and its rows need icons.
let reveal = self.drive_reveal();
let icons = self.request_icons();
return Task::batch([icons, reveal]);
return Task::batch([icons, expand_pc, reveal]);
}
Message::PaneResized(event) => {
self.panes.resize(event.split, event.ratio);
@@ -635,16 +661,9 @@ impl Librarian {
let load = match location {
Location::ThisPc => {
self.content = Content::ThisPc {
drives: Vec::new(),
folders: Vec::new(),
};
self.content = Content::ThisPc { drives: Vec::new() };
self.rows.clear();
let worker = self.worker.clone();
Task::perform(
offload(move || (list_drives(), worker.run(|_| known_folders()))),
|(drives, folders)| Message::ThisPcLoaded(drives, folders),
)
Task::perform(offload(list_drives), Message::ThisPcLoaded)
}
Location::Path(path) => {
self.load_token += 1;
@@ -756,12 +775,15 @@ impl Librarian {
if paths.is_empty() {
return;
}
let wanted: std::collections::HashSet<&Path> =
paths.iter().map(PathBuf::as_path).collect();
let indices = self.rows.iter().enumerate().filter_map(|(i, row)| match &row.target {
Location::Path(path) if wanted.contains(path.as_path()) => Some(i),
_ => None,
});
let wanted: std::collections::HashSet<&Path> = paths.iter().map(PathBuf::as_path).collect();
let indices = self
.rows
.iter()
.enumerate()
.filter_map(|(i, row)| match &row.target {
Location::Path(path) if wanted.contains(path.as_path()) => Some(i),
_ => None,
});
self.selection.set_many(indices);
}
@@ -824,7 +846,10 @@ impl Librarian {
self.scroll_y = y;
iced::widget::operation::scroll_to(
LIST_ID,
scrollable::AbsoluteOffset { x: None, y: Some(y) },
scrollable::AbsoluteOffset {
x: None,
y: Some(y),
},
)
}
@@ -842,13 +867,22 @@ impl Librarian {
// --- navigation tree ------------------------------------------------------
/// Load the children of a tree node on a worker thread: drives + known
/// folders for the "This PC" root, or the subdirectories of a real folder.
fn load_tree_children(&self, id: tree::NodeId, location: Location) -> Task<Message> {
/// Load the tree's top-level nodes (the user's folders + a "This PC" node)
/// onto the hidden root, on a worker thread.
fn load_tree_roots(&self) -> Task<Message> {
let worker = self.worker.clone();
Task::perform(
offload(move || Ok(fetch_tree_roots(&worker))),
move |result| Message::TreeChildrenLoaded(tree::ROOT_ID, result),
)
}
/// Load the children of a tree node on a worker thread: the drives for the
/// "This PC" node, or the subdirectories of a real folder.
fn load_tree_children(&self, id: tree::NodeId, location: Location) -> Task<Message> {
let show_hidden = self.show_hidden;
Task::perform(
offload(move || fetch_tree_children(&worker, &location, show_hidden)),
offload(move || fetch_tree_children(&location, show_hidden)),
move |result| Message::TreeChildrenLoaded(id, result),
)
}
@@ -912,12 +946,8 @@ impl Librarian {
/// Rebuild `rows` from `content`, applying the current filter and sort.
fn recompute_rows(&mut self) {
self.rows = match &self.content {
Content::ThisPc { drives, folders } => {
let mut rows: Vec<Row> = drives
.iter()
.map(rows::row_from_drive)
.chain(folders.iter().map(rows::row_from_known))
.collect();
Content::ThisPc { drives } => {
let mut rows: Vec<Row> = drives.iter().map(rows::row_from_drive).collect();
if !self.filter.is_empty() {
let needle = self.filter.to_lowercase();
rows.retain(|r| r.label.to_lowercase().contains(&needle));
@@ -991,7 +1021,13 @@ impl Librarian {
fn view_tree(&self) -> Element<'_, Message> {
let rows = self.tree.visible_rows();
let mut list = column![].width(Fill);
for row in &rows {
for (i, row) in rows.iter().enumerate() {
// Separate the user's folders (above) from the "This PC" drives
// section (below) with a divider. Skip it when "This PC" is the very
// first row, so we never lead with a stray rule.
if i > 0 && matches!(row.location, Location::ThisPc) {
list = list.push(tree_section_divider());
}
list = list.push(self.view_tree_row(row));
}
let scroll = scrollable(list).height(Fill);
@@ -1025,9 +1061,12 @@ impl Librarian {
let selected = self.history.current() == data.location;
let label = button(
row![icon, ellipsized(data.label.to_string()).size(13).width(Fill)]
.spacing(6)
.align_y(Center),
row![
icon,
ellipsized(data.label.to_string()).size(13).width(Fill)
]
.spacing(6)
.align_y(Center),
)
.on_press(Message::TreeNavigate(data.location.clone()))
.width(Fill)
@@ -1093,7 +1132,10 @@ impl Librarian {
};
row![
nav("←", self.history.can_go_back().then_some(Message::GoBack)),
nav("→", self.history.can_go_forward().then_some(Message::GoForward)),
nav(
"→",
self.history.can_go_forward().then_some(Message::GoForward)
),
nav("↑", self.history.current().parent().map(|_| Message::GoUp)),
nav("⟳", Some(Message::Refresh)),
text_input("Path", &self.address)
@@ -1289,7 +1331,12 @@ fn view_header(sort: Sort) -> Element<'static, Message> {
// still line up with the rows below.
row![
heading("Name", SortKey::Name, Fill, Horizontal::Left),
heading("Date modified", SortKey::Modified, 150.0.into(), Horizontal::Left),
heading(
"Date modified",
SortKey::Modified,
150.0.into(),
Horizontal::Left
),
heading("Type", SortKey::Type, 120.0.into(), Horizontal::Left),
// Right-aligned to sit over the right-aligned numeric size values.
heading("Size", SortKey::Size, 90.0.into(), Horizontal::Right),
@@ -1360,6 +1407,26 @@ fn tree_row_button_style(theme: &Theme, status: button::Status, selected: bool)
style
}
/// A thin horizontal divider between the folder tree's sections (the user's
/// folders above, the "This PC" drives section below). Inset from the pane edges
/// so it reads as a subtle separator rather than a hard border.
fn tree_section_divider() -> Element<'static, Message> {
let line =
container(Space::new().width(Fill).height(1.0)).style(|theme: &Theme| container::Style {
background: Some(theme.extended_palette().background.strong.color.into()),
..container::Style::default()
});
container(line)
.padding(iced::Padding {
top: 4.0,
right: 10.0,
bottom: 4.0,
left: 10.0,
})
.width(Fill)
.into()
}
/// One clickable row in the context menu.
fn menu_item(label: &str, message: Message) -> Element<'static, Message> {
button(text(label.to_string()).size(13))
@@ -1405,60 +1472,78 @@ fn address_text(location: &Location) -> String {
}
}
/// Load the children for a folder-tree node: drives + known folders under the
/// "This PC" root, or the (visible) subdirectories of a real folder, sorted by
/// name. Runs on a worker thread via [`offload`].
fn fetch_tree_children(
worker: &ShellWorker,
location: &Location,
show_hidden: bool,
) -> Result<Vec<TreeChild>, String> {
/// The top-level nodes of the folder tree: the user's home folder (with the
/// known user folders nested inside, shown expanded), then a "This PC" node that
/// holds the drives. Mirrors Windows 11's nav pane.
fn fetch_tree_roots(worker: &ShellWorker) -> Vec<TreeChild> {
let folders = worker.run(|_| known_folders());
let home = worker.run(|_| user_home());
let known: Vec<TreeChild> = folders.iter().map(tree_child_from_known).collect();
let mut roots = Vec::new();
match home {
Some(home) => roots.push(home_tree_child(home, known)),
// If the home folder can't be resolved, list the known folders at the
// top level rather than dropping them entirely.
None => roots.extend(known),
}
roots.push(this_pc_tree_child());
roots
}
/// The user's home-folder node, labelled with the home folder's name (the
/// account folder, e.g. `Alice`) and carrying the known user folders nested
/// inside, shown expanded. Uses the home folder's real shell icon.
fn home_tree_child(home: PathBuf, folders: Vec<TreeChild>) -> TreeChild {
let label = home
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "Home".to_string());
TreeChild::branch(
label,
IconKey::Path(home.clone()),
Location::Path(home),
folders,
)
}
/// Load the children for a folder-tree node: the drives under the "This PC"
/// node, or the (visible) subdirectories of a real folder, sorted by name. Runs
/// on a worker thread via [`offload`]. (Neither path needs the COM worker — the
/// top-level known folders that do are loaded by [`fetch_tree_roots`].)
fn fetch_tree_children(location: &Location, show_hidden: bool) -> Result<Vec<TreeChild>, String> {
match location {
Location::ThisPc => {
let drives = list_drives();
let folders = worker.run(|_| known_folders());
let children = drives
.iter()
.map(tree_child_from_drive)
.chain(folders.iter().map(tree_child_from_known))
.collect();
Ok(children)
}
// "This PC" now contains only the system drives; the user's folders are
// their own top-level nodes (see [`fetch_tree_roots`]).
Location::ThisPc => Ok(list_drives().iter().map(tree_child_from_drive).collect()),
Location::Path(dir) => {
let mut dirs = read_subdirs(dir).map_err(|e| e.to_string())?;
dirs.retain(|e| is_visible(e, show_hidden, ""));
dirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
let children = dirs
.into_iter()
.map(|e| TreeChild {
label: e.name,
icon: IconKey::Folder,
location: Location::Path(e.path),
})
.map(|e| TreeChild::lazy(e.name, IconKey::Folder, Location::Path(e.path)))
.collect();
Ok(children)
}
}
}
/// The standalone "This PC" tree node — an expandable container for the drives.
fn this_pc_tree_child() -> TreeChild {
TreeChild::lazy("This PC".to_string(), IconKey::Computer, Location::ThisPc)
}
/// A tree child built from a drive, reusing the file-list row mapping so the
/// label and icon match what the "This PC" listing shows.
fn tree_child_from_drive(drive: &DriveInfo) -> TreeChild {
let row = rows::row_from_drive(drive);
TreeChild {
label: row.label,
icon: row.icon,
location: row.target,
}
TreeChild::lazy(row.label, row.icon, row.target)
}
fn tree_child_from_known(folder: &KnownFolder) -> TreeChild {
let row = rows::row_from_known(folder);
TreeChild {
label: row.label,
icon: row.icon,
location: row.target,
}
TreeChild::lazy(row.label, row.icon, row.target)
}
/// A non-colliding "New folder" name in `dir`, matching Explorer's scheme.
@@ -1509,11 +1594,7 @@ fn key_to_message(key: Key, modifiers: keyboard::Modifiers) -> Option<Message> {
/// `""` for a count of 1, `"s"` otherwise — for pluralizing status text.
fn plural(count: usize) -> &'static str {
if count == 1 {
""
} else {
"s"
}
if count == 1 { "" } else { "s" }
}
/// A stream that emits [`Message::DirChanged`] whenever `path`'s direct
@@ -1529,42 +1610,45 @@ fn plural(count: usize) -> &'static str {
#[allow(clippy::ptr_arg)]
fn watch_stream(path: &PathBuf) -> impl iced::futures::Stream<Item = Message> + use<> {
use iced::futures::{SinkExt, StreamExt};
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebounceEventResult};
use notify_debouncer_mini::{DebounceEventResult, new_debouncer, notify::RecursiveMode};
let path = path.clone();
iced::stream::channel(4, move |mut output: iced::futures::channel::mpsc::Sender<Message>| async move {
// The debouncer callback (sync, on notify's thread) pokes this channel;
// the async loop below drains it and forwards to the UI.
let (mut tx, mut rx) = iced::futures::channel::mpsc::channel::<()>(4);
iced::stream::channel(
4,
move |mut output: iced::futures::channel::mpsc::Sender<Message>| async move {
// The debouncer callback (sync, on notify's thread) pokes this channel;
// the async loop below drains it and forwards to the UI.
let (mut tx, mut rx) = iced::futures::channel::mpsc::channel::<()>(4);
let mut debouncer = match new_debouncer(
Duration::from_millis(200),
move |res: DebounceEventResult| {
// Any non-empty batch of events means the directory changed.
if res.map(|events| !events.is_empty()).unwrap_or(false) {
let _ = tx.try_send(());
}
},
) {
Ok(debouncer) => debouncer,
Err(_) => return,
};
let mut debouncer = match new_debouncer(
Duration::from_millis(200),
move |res: DebounceEventResult| {
// Any non-empty batch of events means the directory changed.
if res.map(|events| !events.is_empty()).unwrap_or(false) {
let _ = tx.try_send(());
}
},
) {
Ok(debouncer) => debouncer,
Err(_) => return,
};
if debouncer
.watcher()
.watch(&path, RecursiveMode::NonRecursive)
.is_err()
{
return;
}
while rx.next().await.is_some() {
if output.send(Message::DirChanged).await.is_err() {
break; // the app dropped this subscription
if debouncer
.watcher()
.watch(&path, RecursiveMode::NonRecursive)
.is_err()
{
return;
}
}
// `debouncer` drops here, stopping its background watcher thread.
})
while rx.next().await.is_some() {
if output.send(Message::DirChanged).await.is_err() {
break; // the app dropped this subscription
}
}
// `debouncer` drops here, stopping its background watcher thread.
},
)
}
/// Apply the dark title bar / Mica backdrop to the main window once it exists.
@@ -1614,7 +1698,10 @@ fn visible_window(scroll_y: f32, viewport_h: f32, total: usize) -> (usize, usize
let first = (scroll_y.max(0.0) / ROW_HEIGHT).floor() as usize;
let onscreen = (viewport_h.max(0.0) / ROW_HEIGHT).ceil() as usize + 1;
let start = first.saturating_sub(OVERSCAN);
let end = first.saturating_add(onscreen).saturating_add(OVERSCAN).min(total);
let end = first
.saturating_add(onscreen)
.saturating_add(OVERSCAN)
.min(total);
(start, end)
}
@@ -1655,6 +1742,13 @@ where
mod tests {
use super::*;
#[test]
fn embedded_window_icon_decodes() {
// The bundled .ico must actually decode (right path + ICO codec enabled),
// otherwise the window would silently fall back to the default icon.
assert!(window_icon().is_some(), "bundled window icon should decode");
}
fn ctrl() -> keyboard::Modifiers {
keyboard::Modifiers::CTRL
}
@@ -1677,7 +1771,10 @@ mod tests {
key_to_message(ch("c"), ctrl()),
Some(Message::Copy)
));
assert!(matches!(key_to_message(ch("x"), ctrl()), Some(Message::Cut)));
assert!(matches!(
key_to_message(ch("x"), ctrl()),
Some(Message::Cut)
));
assert!(matches!(
key_to_message(ch("v"), ctrl()),
Some(Message::Paste)
+7 -2
View File
@@ -61,7 +61,10 @@ pub fn row_from_drive(drive: &DriveInfo) -> Row {
pub fn row_from_known(folder: &KnownFolder) -> Row {
Row {
label: folder.name.to_string(),
icon: IconKey::Folder,
// Resolve the folder's real shell icon (Desktop/Documents/Downloads/…
// each have a distinct one in Explorer), like drives do, instead of the
// generic folder glyph.
icon: IconKey::Path(folder.path.clone()),
is_container: true,
target: Location::Path(folder.path.clone()),
size: None,
@@ -111,7 +114,9 @@ pub fn human_size(bytes: u64) -> String {
/// Format a timestamp in local time (e.g. `2026-06-13 09:41`).
pub fn format_time(time: SystemTime) -> String {
let utc: DateTime<Utc> = time.into();
utc.with_timezone(&Local).format("%Y-%m-%d %H:%M").to_string()
utc.with_timezone(&Local)
.format("%Y-%m-%d %H:%M")
.to_string()
}
#[cfg(test)]
+198 -38
View File
@@ -55,6 +55,45 @@ pub struct TreeChild {
pub label: String,
pub icon: IconKey,
pub location: Location,
/// Children to attach immediately. `None` means the node is loaded lazily on
/// first expand; `Some` (even empty) means its children are already known
/// and grafted along with it — used to nest a curated subtree (e.g. the
/// user's folders under their home node) rather than a raw directory listing.
pub children: Option<Vec<TreeChild>>,
/// Whether the node starts expanded. Only meaningful alongside pre-attached
/// [`children`](Self::children); a lazy node can't show children it hasn't
/// loaded yet.
pub expanded: bool,
}
impl TreeChild {
/// A node whose children are fetched lazily, the first time it's expanded.
pub fn lazy(label: String, icon: IconKey, location: Location) -> Self {
Self {
label,
icon,
location,
children: None,
expanded: false,
}
}
/// A node grafted with its `children` already attached and shown expanded —
/// for a curated, pre-loaded subtree.
pub fn branch(
label: String,
icon: IconKey,
location: Location,
children: Vec<TreeChild>,
) -> Self {
Self {
label,
icon,
location,
children: Some(children),
expanded: true,
}
}
}
/// A flattened, render-ready view of one visible node. Borrows from the tree.
@@ -71,6 +110,7 @@ pub struct TreeRow<'a> {
}
/// The outcome of a [`Tree::reveal`] step.
#[derive(Debug)]
pub enum Reveal {
/// To continue revealing, the children of this node must be loaded first.
Load(NodeId, Location),
@@ -82,18 +122,26 @@ pub enum Reveal {
/// The folder tree and its node-id allocator.
pub struct Tree {
/// A hidden synthetic container whose children are the *top-level* nodes
/// (the user's folders, then a "This PC" node holding the drives). It is
/// never rendered itself — [`visible_rows`](Self::visible_rows) starts at
/// its children — which lets the sidebar show a forest of roots rather than
/// a single "This PC" parent over everything.
root: TreeNode,
next_id: NodeId,
}
impl Tree {
/// A fresh tree whose root is "This PC", expanded and awaiting its child
/// load (drives + known folders). The app kicks that load off at startup.
/// A fresh tree whose (hidden) root is awaiting its top-level children —
/// the user's folders plus a "This PC" node. The app kicks that load off at
/// startup; until it lands, the sidebar is empty.
pub fn new() -> Self {
let root = TreeNode {
id: ROOT_ID,
label: "This PC".to_string(),
label: String::new(),
icon: IconKey::Folder,
// Never navigated or matched against a path; only ever descended
// through. `ThisPc` has no path, so it can't collide with a target.
location: Location::ThisPc,
expanded: true,
children: Children::Loading,
@@ -104,17 +152,40 @@ impl Tree {
}
}
/// Flatten the expanded nodes into render rows, depth-first.
/// Flatten the expanded nodes into render rows, depth-first. The hidden root
/// is skipped, so its children are the top level (depth 0).
pub fn visible_rows(&self) -> Vec<TreeRow<'_>> {
let mut rows = Vec::new();
push_rows(&self.root, 0, &mut rows);
if let Children::Loaded(children) = &self.root.children {
for child in children {
push_rows(child, 0, &mut rows);
}
}
rows
}
/// Append the icon keys of every visible node, so the app can request them
/// from the shared icon cache alongside the main list's.
/// from the shared icon cache alongside the main list's. The hidden root has
/// no icon of its own, so collection starts at its children.
pub fn collect_icon_keys(&self, out: &mut Vec<IconKey>) {
collect_keys(&self.root, out);
if let Children::Loaded(children) = &self.root.children {
for child in children {
collect_keys(child, out);
}
}
}
/// The id of the top-level "This PC" node, once the root's children have
/// loaded. The app uses it to auto-expand "This PC" at startup so the drives
/// are visible without a manual click.
pub fn this_pc_id(&self) -> Option<NodeId> {
match &self.root.children {
Children::Loaded(children) => children
.iter()
.find(|c| matches!(c.location, Location::ThisPc))
.map(|c| c.id),
_ => None,
}
}
/// Expand or collapse the node `id`. Returns the node and location to load
@@ -159,13 +230,19 @@ impl Tree {
.map(|c| {
let id = self.next_id;
self.next_id += 1;
// Pre-attached children are grafted (and recursively built) now;
// otherwise the node is left unloaded for a lazy fetch later.
let children = match c.children {
Some(kids) => Children::Loaded(self.build_nodes(kids)),
None => Children::Unloaded,
};
TreeNode {
id,
label: c.label,
icon: c.icon,
location: c.location,
expanded: false,
children: Children::Unloaded,
expanded: c.expanded,
children,
}
})
.collect()
@@ -240,10 +317,22 @@ fn reveal_node(node: &mut TreeNode, target: &Path) -> Reveal {
Reveal::Load(node.id, node.location.clone())
}
Children::Loading => Reveal::Wait,
Children::Loaded(children) => match best_child_index(children, target) {
Some(i) => reveal_node(&mut children[i], target),
None => Reveal::Stop, // target not under any child (hidden, gone, …)
},
Children::Loaded(children) => {
// Prefer the deepest path-prefix child (e.g. a user-folder shortcut).
if let Some(i) = best_child_index(children, target) {
return reveal_node(&mut children[i], target);
}
// No path child matched. At the top level the drives live under the
// "This PC" node (whose own location is pathless), so fall back to
// descending into it — one of its drives will prefix the target.
if let Some(i) = children
.iter()
.position(|c| matches!(c.location, Location::ThisPc))
{
return reveal_node(&mut children[i], target);
}
Reveal::Stop // target not under any child (hidden, gone, …)
}
}
}
@@ -266,11 +355,16 @@ mod tests {
use std::path::PathBuf;
fn child(label: &str, path: &str) -> TreeChild {
TreeChild {
label: label.to_string(),
icon: IconKey::Folder,
location: Location::Path(PathBuf::from(path)),
}
TreeChild::lazy(
label.to_string(),
IconKey::Folder,
Location::Path(PathBuf::from(path)),
)
}
/// A pathless "This PC" child — the drives' container at the top level.
fn this_pc() -> TreeChild {
TreeChild::lazy("This PC".to_string(), IconKey::Folder, Location::ThisPc)
}
/// Find a visible row by label, for assertions.
@@ -283,22 +377,22 @@ mod tests {
}
#[test]
fn root_is_visible_and_loading() {
fn root_is_hidden_until_children_load() {
// The synthetic root isn't rendered, so the sidebar is empty until its
// top-level children arrive.
let tree = Tree::new();
let rows = tree.visible_rows();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].label, "This PC");
assert!(rows[0].expanded);
// Loading counts as expandable so the chevron shows immediately.
assert!(rows[0].expandable);
assert!(tree.visible_rows().is_empty());
}
#[test]
fn set_children_makes_them_visible_under_expanded_root() {
fn top_level_children_render_at_depth_zero() {
let mut tree = Tree::new();
tree.set_children(ROOT_ID, vec![child("C:", "C:\\"), child("D:", "D:\\")]);
let labels: Vec<&str> = tree.visible_rows().iter().map(|r| r.label).collect();
assert_eq!(labels, ["This PC", "C:", "D:"]);
let rows = tree.visible_rows();
// The hidden root contributes no row of its own.
let labels: Vec<&str> = rows.iter().map(|r| r.label).collect();
assert_eq!(labels, ["C:", "D:"]);
assert!(rows.iter().all(|r| r.depth == 0));
}
#[test]
@@ -320,15 +414,15 @@ mod tests {
let c = row_id(&tree, "C:");
tree.toggle(c); // expand C: (its first expansion requests a load)
tree.set_children(c, vec![child("Users", "C:\\Users")]);
assert_eq!(tree.visible_rows().len(), 3); // This PC, C:, Users
assert_eq!(tree.visible_rows().len(), 2); // C:, Users
// Collapsing C: hides Users; it requests no reload.
assert!(tree.toggle(c).is_none());
assert_eq!(tree.visible_rows().len(), 2);
assert_eq!(tree.visible_rows().len(), 1);
// Re-expanding shows it again with no reload (children cached).
assert!(tree.toggle(c).is_none());
assert_eq!(tree.visible_rows().len(), 3);
assert_eq!(tree.visible_rows().len(), 2);
}
#[test]
@@ -353,11 +447,7 @@ mod tests {
let c = row_id(&tree, "C:");
tree.toggle(c); // expand
tree.set_children(c, Vec::new()); // …discovers it has no subfolders
let row = tree
.visible_rows()
.into_iter()
.find(|r| r.id == c)
.unwrap();
let row = tree.visible_rows().into_iter().find(|r| r.id == c).unwrap();
assert!(!row.expandable, "a confirmed-empty node shows no chevron");
}
@@ -386,9 +476,9 @@ mod tests {
tree.set_children(users, vec![child("me", "C:\\Users\\me")]);
assert!(matches!(tree.reveal(&target), Reveal::Stop));
// The whole chain is now visible.
// The whole chain is now visible (the hidden root adds no row).
let labels: Vec<&str> = tree.visible_rows().iter().map(|r| r.label).collect();
assert_eq!(labels, ["This PC", "C:", "Users", "me", "D:"]);
assert_eq!(labels, ["C:", "Users", "me", "D:"]);
}
#[test]
@@ -424,4 +514,74 @@ mod tests {
Reveal::Stop
));
}
#[test]
fn this_pc_id_finds_the_drives_container() {
let mut tree = Tree::new();
assert_eq!(tree.this_pc_id(), None, "no node before the roots load");
// Top level: a user folder, then the "This PC" node.
tree.set_children(
ROOT_ID,
vec![child("Desktop", "C:\\Users\\me\\Desktop"), this_pc()],
);
let pc = tree.this_pc_id().expect("This PC node present");
assert_eq!(pc, row_id(&tree, "This PC"));
}
#[test]
fn reveal_descends_through_this_pc_to_a_drive_path() {
// Drives are no longer top-level: they sit under the pathless "This PC"
// node, so revealing a drive path must fall back into it.
let mut tree = Tree::new();
tree.set_children(
ROOT_ID,
vec![child("Desktop", "C:\\Users\\me\\Desktop"), this_pc()],
);
let target = PathBuf::from("C:\\Windows");
// No top-level path child prefixes C:\Windows, so reveal descends into
// "This PC" and asks to load it (its drives aren't loaded yet).
let pc = tree.this_pc_id().unwrap();
match tree.reveal(&target) {
Reveal::Load(id, Location::ThisPc) => assert_eq!(id, pc),
other => panic!("expected to load the This PC node, got {other:?}"),
}
// Its drives arrive; the next step descends into C:\ toward the target.
tree.set_children(pc, vec![child("C:", "C:\\")]);
match tree.reveal(&target) {
Reveal::Load(_, Location::Path(p)) => assert_eq!(p, PathBuf::from("C:\\")),
other => panic!("expected to load C:\\, got {other:?}"),
}
}
#[test]
fn branch_grafts_children_and_starts_expanded() {
// A `branch` node arrives with its children already attached and shown,
// so they're visible (nested, depth 1) without any lazy load or toggle.
let mut tree = Tree::new();
let home = TreeChild::branch(
"me".to_string(),
IconKey::Folder,
Location::Path(PathBuf::from("C:\\Users\\me")),
vec![
child("Desktop", "C:\\Users\\me\\Desktop"),
child("Documents", "C:\\Users\\me\\Documents"),
],
);
tree.set_children(ROOT_ID, vec![home]);
let rows = tree.visible_rows();
let labels: Vec<&str> = rows.iter().map(|r| r.label).collect();
assert_eq!(labels, ["me", "Desktop", "Documents"]);
// The home node is depth 0 and expanded; its folders are nested at 1.
assert_eq!(rows[0].depth, 0);
assert!(rows[0].expanded);
assert!(rows[1].depth == 1 && rows[2].depth == 1);
// Collapsing the pre-loaded node requests no reload (children cached).
let me = row_id(&tree, "me");
assert!(tree.toggle(me).is_none());
assert_eq!(tree.visible_rows().len(), 1);
}
}
+3 -1
View File
@@ -97,7 +97,9 @@ pub fn read_subdirs(dir: &Path) -> io::Result<Vec<Entry>> {
let mut dirs = Vec::new();
for dirent in fs::read_dir(dir)? {
let Ok(dirent) = dirent else { continue };
let Ok(meta) = dirent.metadata() else { continue };
let Ok(meta) = dirent.metadata() else {
continue;
};
if !meta.is_dir() {
continue;
}
+2 -2
View File
@@ -11,7 +11,7 @@ pub mod history;
pub mod model;
pub mod sort;
pub use enumerate::{read_dir_all, read_dir_batched, read_subdirs, DEFAULT_BATCH};
pub use enumerate::{DEFAULT_BATCH, read_dir_all, read_dir_batched, read_subdirs};
pub use history::History;
pub use model::{Attributes, Entry, EntryKind, Location};
pub use sort::{is_visible, sort_entries, Sort, SortKey, SortOrder};
pub use sort::{Sort, SortKey, SortOrder, is_visible, sort_entries};
+2 -2
View File
@@ -10,8 +10,8 @@ use core::ffi::c_void;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Dwm::{
DwmSetWindowAttribute, DWMSBT_MAINWINDOW, DWMWA_SYSTEMBACKDROP_TYPE,
DWMWA_USE_IMMERSIVE_DARK_MODE,
DWMSBT_MAINWINDOW, DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_USE_IMMERSIVE_DARK_MODE,
DwmSetWindowAttribute,
};
/// Give the window a dark title bar (to match the dark theme) and request the
+3 -4
View File
@@ -12,9 +12,7 @@ use std::marker::PhantomData;
use std::sync::mpsc::{self, Sender};
use std::thread;
use windows::Win32::System::Com::{
CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED,
};
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx, CoUninitialize};
type Job = Box<dyn FnOnce() + Send + 'static>;
@@ -101,7 +99,8 @@ impl ShellWorker {
let _ = rtx.send(f(&apartment));
});
self.tx.send(job).expect("COM worker thread is gone");
rrx.recv().expect("COM worker dropped the job without replying")
rrx.recv()
.expect("COM worker dropped the job without replying")
}
}
+1 -1
View File
@@ -8,10 +8,10 @@
use std::path::PathBuf;
use windows::core::PCWSTR;
use windows::Win32::Storage::FileSystem::{
GetDiskFreeSpaceExW, GetDriveTypeW, GetLogicalDrives, GetVolumeInformationW,
};
use windows::core::PCWSTR;
use crate::util::{to_wide, wide_to_string};
+4 -4
View File
@@ -13,13 +13,13 @@
use std::path::{Path, PathBuf};
use windows::core::PCWSTR;
use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY;
use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_ALL};
use windows::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance};
use windows::Win32::UI::Shell::{
FileOperation, IFileOperation, IShellItem, SHCreateItemFromParsingName, FILEOPERATION_FLAGS,
FOF_ALLOWUNDO, FOF_NOCONFIRMMKDIR, FOFX_RECYCLEONDELETE,
FILEOPERATION_FLAGS, FOF_ALLOWUNDO, FOF_NOCONFIRMMKDIR, FOFX_RECYCLEONDELETE, FileOperation,
IFileOperation, IShellItem, SHCreateItemFromParsingName,
};
use windows::core::PCWSTR;
use crate::com::Apartment;
use crate::util::to_wide;
+61 -7
View File
@@ -20,19 +20,20 @@ use core::ffi::c_void;
use std::mem::size_of;
use std::path::Path;
use windows::core::PCWSTR;
use windows::Win32::Graphics::Gdi::{
DeleteObject, GetDC, GetDIBits, GetObjectW, ReleaseDC, BITMAP, BITMAPINFO,
BITMAPINFOHEADER, DIB_RGB_COLORS, HBITMAP, HGDIOBJ,
BITMAP, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, DeleteObject, GetDC, GetDIBits,
GetObjectW, HBITMAP, HGDIOBJ, ReleaseDC,
};
use windows::Win32::Storage::FileSystem::{
FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_FLAGS_AND_ATTRIBUTES,
};
use windows::Win32::System::Com::CoTaskMemFree;
use windows::Win32::UI::Shell::{
SHGetFileInfoW, SHFILEINFOW, SHGFI_FLAGS, SHGFI_ICON, SHGFI_LARGEICON, SHGFI_SMALLICON,
SHGFI_USEFILEATTRIBUTES,
FOLDERID_ComputerFolder, SHFILEINFOW, SHGFI_FLAGS, SHGFI_ICON, SHGFI_LARGEICON, SHGFI_PIDL,
SHGFI_SMALLICON, SHGFI_USEFILEATTRIBUTES, SHGetFileInfoW, SHGetKnownFolderIDList,
};
use windows::Win32::UI::WindowsAndMessaging::{DestroyIcon, GetIconInfo, HICON, ICONINFO};
use windows::core::PCWSTR;
use crate::com::Apartment;
use crate::util::to_wide;
@@ -53,7 +54,11 @@ pub fn icon_for_extension(_apt: &Apartment, ext: &str, large: bool) -> Option<Ic
} else {
format!("file.{ext}")
};
extract(&to_wide(&name), FILE_ATTRIBUTE_NORMAL, icon_flags(large, true))
extract(
&to_wide(&name),
FILE_ATTRIBUTE_NORMAL,
icon_flags(large, true),
)
}
/// Generic folder icon.
@@ -75,8 +80,46 @@ pub fn icon_for_path(_apt: &Apartment, path: &Path, large: bool) -> Option<IconI
)
}
/// The shell's "This PC" (Computer) icon, matching what Explorer shows for the
/// machine root. "This PC" is a virtual shell item with no file path, so its
/// icon is resolved from the Computer folder's id list (PIDL) rather than a path
/// string. Must run on the COM STA thread.
pub fn computer_icon(_apt: &Apartment, large: bool) -> Option<IconImage> {
let size = if large {
SHGFI_LARGEICON
} else {
SHGFI_SMALLICON
};
unsafe {
// The Computer folder's id list is allocated by the COM task allocator,
// so it must be freed with `CoTaskMemFree` once we're done with it.
let pidl = SHGetKnownFolderIDList(&FOLDERID_ComputerFolder, 0, None).ok()?;
let mut shfi = SHFILEINFOW::default();
let ok = SHGetFileInfoW(
// With `SHGFI_PIDL` the first argument is a PIDL, not a path string.
PCWSTR(pidl as *const u16),
FILE_FLAGS_AND_ATTRIBUTES(0),
Some(&mut shfi),
size_of::<SHFILEINFOW>() as u32,
SHGFI_ICON | SHGFI_PIDL | size,
);
CoTaskMemFree(Some(pidl as *const c_void));
if ok == 0 || shfi.hIcon.is_invalid() {
return None;
}
let hicon = shfi.hIcon;
let image = hicon_to_rgba(hicon);
_ = DestroyIcon(hicon);
image
}
}
fn icon_flags(large: bool, use_attributes: bool) -> SHGFI_FLAGS {
let size = if large { SHGFI_LARGEICON } else { SHGFI_SMALLICON };
let size = if large {
SHGFI_LARGEICON
} else {
SHGFI_SMALLICON
};
let mut flags = SHGFI_ICON | size;
if use_attributes {
flags |= SHGFI_USEFILEATTRIBUTES;
@@ -249,4 +292,15 @@ mod tests {
.expect("folder icon should resolve");
assert_eq!(icon.rgba.len(), (icon.width * icon.height * 4) as usize);
}
#[test]
fn extracts_the_computer_icon() {
let icon = worker()
.run(|apt| computer_icon(apt, false))
.expect("This PC icon should resolve");
assert!(icon.width > 0 && icon.height > 0);
assert_eq!(icon.rgba.len(), (icon.width * icon.height * 4) as usize);
// A real icon has at least one non-transparent pixel.
assert!(icon.rgba.chunks_exact(4).any(|px| px[3] != 0));
}
}
+9 -3
View File
@@ -8,12 +8,12 @@
use core::ffi::c_void;
use std::path::PathBuf;
use windows::core::GUID;
use windows::Win32::System::Com::CoTaskMemFree;
use windows::Win32::UI::Shell::{
SHGetKnownFolderPath, FOLDERID_Desktop, FOLDERID_Documents, FOLDERID_Downloads,
FOLDERID_Music, FOLDERID_Pictures, FOLDERID_Videos, KNOWN_FOLDER_FLAG,
FOLDERID_Desktop, FOLDERID_Documents, FOLDERID_Downloads, FOLDERID_Music, FOLDERID_Pictures,
FOLDERID_Profile, FOLDERID_Videos, KNOWN_FOLDER_FLAG, SHGetKnownFolderPath,
};
use windows::core::GUID;
#[derive(Debug, Clone)]
pub struct KnownFolder {
@@ -42,6 +42,12 @@ pub fn known_folders() -> Vec<KnownFolder> {
folders
}
/// The current user's home (profile) folder, e.g. `C:\Users\Alice`. Backs the
/// folder tree's home node, under which the known user folders are nested.
pub fn user_home() -> Option<PathBuf> {
resolve(&FOLDERID_Profile)
}
fn resolve(id: &GUID) -> Option<PathBuf> {
unsafe {
let pwstr = SHGetKnownFolderPath(id, KNOWN_FOLDER_FLAG(0), None).ok()?;
+3 -3
View File
@@ -19,8 +19,8 @@ pub mod open;
pub use chrome::apply_window_chrome;
pub use com::{Apartment, ShellWorker};
pub use drives::{list_drives, DriveInfo, DriveKind};
pub use drives::{DriveInfo, DriveKind, list_drives};
pub use fileop::{copy_items, create_folder, delete_to_recycle, move_items, rename};
pub use icon::{folder_icon, icon_for_extension, icon_for_path, IconImage};
pub use known::{known_folders, KnownFolder};
pub use icon::{IconImage, computer_icon, folder_icon, icon_for_extension, icon_for_path};
pub use known::{KnownFolder, known_folders, user_home};
pub use open::open_path;
+1 -1
View File
@@ -2,9 +2,9 @@
use std::path::Path;
use windows::core::{w, PCWSTR};
use windows::Win32::UI::Shell::ShellExecuteW;
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
use windows::core::{PCWSTR, w};
use crate::com::Apartment;
use crate::util::to_wide;