use super::devices::mounted_device_items;
use crate::{
config::{BuiltinPlace, PlaceEntrySpec, PlacesConfig},
core::{SidebarItem, SidebarItemKind, SidebarRow},
};
use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};
const CUSTOM_PLACE_ICON: &str = "";
const SYMLINKED_PLACE_ICON: &str = "";
const BROKEN_SYMLINK_PLACE_ICON: &str = "";
#[derive(Clone, Debug)]
pub(super) struct PlaceResolutionContext {
pub(super) home: PathBuf,
pub(super) desktop: Option<PathBuf>,
pub(super) documents: Option<PathBuf>,
pub(super) downloads: Option<PathBuf>,
pub(super) pictures: Option<PathBuf>,
pub(super) music: Option<PathBuf>,
pub(super) videos: Option<PathBuf>,
pub(super) root: Option<PathBuf>,
pub(super) trash: Option<PathBuf>,
}
pub(crate) fn home_dir() -> Option<PathBuf> {
dirs::home_dir()
}
pub(crate) fn build_sidebar_rows() -> Vec<SidebarRow> {
let home = home_dir().unwrap_or_else(|| {
#[cfg(windows)]
return PathBuf::from("C:\\");
#[cfg(not(windows))]
return PathBuf::from("/");
});
let context = system_place_resolution_context(home);
build_sidebar_rows_with_context(crate::config::places(), &context)
}
pub(super) fn build_sidebar_rows_with_context(
places: &PlacesConfig,
context: &PlaceResolutionContext,
) -> Vec<SidebarRow> {
let pinned_items = build_pinned_sidebar_items(places, context);
let pinned_paths = pinned_items
.iter()
.map(|item| item.identity_path.clone())
.collect::<HashSet<_>>();
let mut rows = pinned_items
.into_iter()
.map(SidebarRow::Item)
.collect::<Vec<_>>();
let device_items = if places.show_devices {
mounted_device_items(&context.home, &pinned_paths)
} else {
Vec::new()
};
if !device_items.is_empty() {
rows.push(SidebarRow::Section { title: "Devices" });
rows.extend(device_items.into_iter().map(SidebarRow::Item));
}
rows
}
fn system_place_resolution_context(home: PathBuf) -> PlaceResolutionContext {
PlaceResolutionContext {
desktop: dirs::desktop_dir().filter(|path| path.exists()),
documents: dirs::document_dir().filter(|path| path.exists()),
downloads: dirs::download_dir().filter(|path| path.exists()),
pictures: dirs::picture_dir().filter(|path| path.exists()),
music: dirs::audio_dir().filter(|path| path.exists()),
videos: dirs::video_dir().filter(|path| path.exists()),
root: if cfg!(unix) {
Some(PathBuf::from("/"))
} else {
None
},
trash: trash_dir(&home),
home,
}
}
fn build_pinned_sidebar_items(
places: &PlacesConfig,
context: &PlaceResolutionContext,
) -> Vec<SidebarItem> {
let mut items = Vec::new();
let mut seen_paths = HashSet::new();
for entry in &places.entries {
let Some(item) = resolve_place_entry(entry, context) else {
continue;
};
if seen_paths.insert(item.identity_path.clone()) {
items.push(item);
}
}
items
}
fn resolve_place_entry(
entry: &PlaceEntrySpec,
context: &PlaceResolutionContext,
) -> Option<SidebarItem> {
match entry {
PlaceEntrySpec::Builtin { place, icon } => {
resolve_builtin_place(*place, icon.as_deref(), context)
}
PlaceEntrySpec::Custom { title, path, icon } => Some(sidebar_item(
SidebarItemKind::Custom,
title.clone(),
place_icon(path, icon.as_deref(), CUSTOM_PLACE_ICON),
path.clone(),
)),
}
}
fn resolve_builtin_place(
place: BuiltinPlace,
icon_override: Option<&str>,
context: &PlaceResolutionContext,
) -> Option<SidebarItem> {
match place {
BuiltinPlace::Home => Some(sidebar_item(
SidebarItemKind::Home,
"Home",
place_icon(&context.home, icon_override, ""),
context.home.clone(),
)),
BuiltinPlace::Desktop => context.desktop.clone().map(|path| {
sidebar_item(
SidebarItemKind::Desktop,
localized_place_title(&path, "Desktop"),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Documents => context.documents.clone().map(|path| {
sidebar_item(
SidebarItemKind::Documents,
localized_place_title(&path, "Documents"),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Downloads => context.downloads.clone().map(|path| {
sidebar_item(
SidebarItemKind::Downloads,
localized_place_title(&path, "Downloads"),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Pictures => context.pictures.clone().map(|path| {
sidebar_item(
SidebarItemKind::Pictures,
localized_place_title(&path, "Pictures"),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Music => context.music.clone().map(|path| {
sidebar_item(
SidebarItemKind::Music,
localized_place_title(&path, "Music"),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Videos => context.videos.clone().map(|path| {
sidebar_item(
SidebarItemKind::Videos,
localized_place_title(&path, videos_label()),
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Root => context.root.clone().map(|path| {
sidebar_item(
SidebarItemKind::Root,
"Root",
place_icon(&path, icon_override, ""),
path,
)
}),
BuiltinPlace::Trash => context.trash.clone().map(|path| {
sidebar_item(
SidebarItemKind::Trash,
"Trash",
place_icon(&path, icon_override, ""),
path,
)
}),
}
}
fn place_icon<'a>(path: &Path, icon_override: Option<&'a str>, default_icon: &'a str) -> &'a str {
icon_override.unwrap_or_else(|| match place_symlink_state(path) {
Some(PlaceSymlinkState::Directory) => SYMLINKED_PLACE_ICON,
Some(PlaceSymlinkState::Broken) => BROKEN_SYMLINK_PLACE_ICON,
None => default_icon,
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PlaceSymlinkState {
Directory,
Broken,
}
fn place_symlink_state(path: &Path) -> Option<PlaceSymlinkState> {
if !fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
return None;
}
Some(
if fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) {
PlaceSymlinkState::Directory
} else {
PlaceSymlinkState::Broken
},
)
}
pub(super) fn sidebar_item(
kind: SidebarItemKind,
title: impl Into<String>,
icon: impl Into<String>,
path: PathBuf,
) -> SidebarItem {
let identity_path = path_identity_key(&path);
SidebarItem::new(kind, title, icon, path, identity_path)
}
fn localized_place_title(path: &Path, fallback: &'static str) -> String {
path.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| fallback.to_string())
}
fn videos_label() -> &'static str {
if cfg!(target_os = "macos") {
"Movies"
} else {
"Videos"
}
}
pub(super) fn path_identity_key(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| normalize_absolute_path(path))
}
fn normalize_absolute_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
let _ = normalized.pop();
}
_ => normalized.push(component.as_os_str()),
}
}
normalized
}
pub(crate) fn trash_dir(home: &Path) -> Option<PathBuf> {
if let Some(data_dir) = dirs::data_dir() {
let xdg_trash = data_dir.join("Trash/files");
if xdg_trash.exists() {
return Some(xdg_trash);
}
}
let mac_trash = home.join(".Trash");
if mac_trash.exists() {
return Some(mac_trash);
}
None
}