use crate::discover::{self, Entry, EntryKind};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootOrigin {
Cwd,
Configured,
Recent,
Desktop,
}
impl RootOrigin {
pub fn note(self) -> &'static str {
match self {
RootOrigin::Cwd => "current directory",
RootOrigin::Configured => "configured",
RootOrigin::Recent => "recent",
RootOrigin::Desktop => "opened elsewhere",
}
}
}
pub fn desktop_recent_dirs() -> Vec<PathBuf> {
let Some(data_dir) = dirs::data_dir() else {
return Vec::new();
};
let path = data_dir.join("recently-used.xbel");
let Ok(contents) = std::fs::read_to_string(&path) else {
return Vec::new();
};
dirs_from_xbel(&contents)
}
pub fn dirs_from_xbel(contents: &str) -> Vec<PathBuf> {
const PREFIX: &str = "href=\"file://";
let mut dirs: Vec<PathBuf> = Vec::new();
for chunk in contents.split(PREFIX).skip(1) {
let Some(end) = chunk.find('"') else { continue };
let decoded = percent_decode(&chunk[..end]);
let file = PathBuf::from(decoded);
if !crate::discover::is_data_file(&file) || !file.is_file() {
continue;
}
let Some(parent) = file.parent() else {
continue;
};
if parent.as_os_str().is_empty() {
continue;
}
let parent = parent.to_path_buf();
if !dirs.contains(&parent) {
dirs.push(parent);
}
}
dirs
}
fn percent_decode(raw: &str) -> String {
let bytes = raw.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
if let Some(byte) = hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
const MAX_RECENT_ROOTS: usize = 8;
pub fn is_remote_path(path: &Path) -> bool {
!matches!(
crate::source::input_source(path),
crate::source::InputSource::Local(_)
) || is_network_path(path)
}
pub fn is_network_path(path: &Path) -> bool {
let Ok(mountinfo) = std::fs::read_to_string("/proc/self/mountinfo") else {
return false;
};
network_fs_for_test(&mountinfo, path)
}
#[doc(hidden)]
pub fn network_fs_for_test(mountinfo: &str, path: &Path) -> bool {
crate::locality::Mounts::parse(mountinfo).is_network(path)
}
#[derive(Debug, Clone)]
pub struct Root {
pub path: PathBuf,
pub origin: RootOrigin,
pub network: bool,
pub available: bool,
}
#[derive(Debug, Clone)]
pub struct Section {
pub title: String,
pub subtitle: Option<String>,
pub rows: Vec<Entry>,
pub unavailable: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Measured {
pub rows: Option<usize>,
pub cols: Option<usize>,
pub size: Option<u64>,
pub columns: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SortMode {
#[default]
Natural,
Size,
Modified,
Rows,
}
impl SortMode {
pub fn label_in(self, section_is_recency_ordered: bool) -> &'static str {
match self {
SortMode::Natural if section_is_recency_ordered => "recent",
SortMode::Natural => "name",
SortMode::Size => "size",
SortMode::Modified => "modified",
SortMode::Rows => "rows",
}
}
pub fn next(self) -> Self {
match self {
SortMode::Natural => SortMode::Size,
SortMode::Size => SortMode::Modified,
SortMode::Modified => SortMode::Rows,
SortMode::Rows => SortMode::Natural,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Row<'a> {
Header {
section: usize,
matches: usize,
collapsed: bool,
},
Entry {
section: usize,
entry: &'a Entry,
},
}
impl Row<'_> {
pub fn section(&self) -> usize {
match self {
Row::Header { section, .. } | Row::Entry { section, .. } => *section,
}
}
}
#[derive(Debug)]
pub struct HomeState {
pub sections: Vec<Section>,
pub filter: String,
pub selected: usize,
pub scroll: usize,
pub path_input_active: bool,
pub path_input: String,
pub browsing: Option<PathBuf>,
pub status: Option<String>,
pub network_check: fn(&Path) -> bool,
pub root_paths: Vec<PathBuf>,
pub probed: std::collections::HashMap<PathBuf, Vec<Entry>>,
pub unreachable: std::collections::HashSet<PathBuf>,
pub sort: SortMode,
pub listing_in_flight: bool,
pub measure_in_flight: bool,
pub pending_enrich: bool,
pub enriched: std::collections::HashMap<PathBuf, Measured>,
pub collapsed: std::collections::HashSet<String>,
pub search: SearchState,
}
#[derive(Debug, Clone, Default)]
pub struct SearchState {
pub root: Option<PathBuf>,
pub results: Vec<Entry>,
pub scanned: usize,
pub running: bool,
pub done: bool,
pub limited: Option<String>,
}
impl SearchState {
pub fn reset(&mut self) {
*self = Self::default();
}
}
impl Default for HomeState {
fn default() -> Self {
Self {
sections: Vec::new(),
filter: String::new(),
selected: 0,
scroll: 0,
path_input_active: false,
path_input: String::new(),
browsing: None,
status: None,
network_check: is_remote_path,
sort: SortMode::default(),
listing_in_flight: false,
measure_in_flight: false,
root_paths: Vec::new(),
probed: std::collections::HashMap::new(),
unreachable: std::collections::HashSet::new(),
pending_enrich: false,
enriched: std::collections::HashMap::new(),
collapsed: std::collections::HashSet::new(),
search: SearchState::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct ListingRequest {
pub config_dirs: Vec<PathBuf>,
pub recents: Vec<PathBuf>,
pub desktop_dirs: Vec<PathBuf>,
pub browsing: Option<PathBuf>,
pub probed: std::collections::HashMap<PathBuf, Vec<Entry>>,
pub unreachable: std::collections::HashSet<PathBuf>,
pub network_check: fn(&Path) -> bool,
pub known: std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
}
#[derive(Debug, Clone, Default)]
pub struct Listing {
pub sections: Vec<Section>,
pub root_paths: Vec<PathBuf>,
}
pub fn measured_from(probe: &Entry, original: &Entry) -> Measured {
Measured {
rows: probe.rows,
cols: probe.cols,
size: probe.size.or(original.size),
columns: probe.columns.clone(),
}
}
fn probed_entry(
probed: &std::collections::HashMap<PathBuf, Vec<Entry>>,
path: &Path,
) -> Option<Entry> {
probed.values().flatten().find(|e| e.path == path).cloned()
}
pub fn build_listing(request: &ListingRequest) -> Listing {
let ListingRequest {
config_dirs,
recents,
desktop_dirs,
browsing,
probed,
unreachable,
network_check,
known,
} = request;
let network_check = *network_check;
let mounts = crate::locality::Mounts::current();
let mut sections: Vec<Section> = Vec::new();
let mut root_paths: Vec<PathBuf> = Vec::new();
if let Some(dir) = browsing.clone() {
let rows = discover::scan_dir(&dir);
sections.push(Section {
title: display_path(&dir),
subtitle: None,
rows,
unavailable: false,
});
annotate(&mut sections, known, network_check, &mounts);
return Listing {
sections,
root_paths,
};
}
let recent_rows: Vec<Entry> = recents
.iter()
.filter(|p| network_check(p) || p.exists())
.map(|p| {
if let Some(known) = probed_entry(probed, p) {
return known;
}
entry_for_path(p, network_check(p))
})
.collect();
if !recent_rows.is_empty() {
sections.push(Section {
title: "Recent".to_string(),
subtitle: None,
rows: recent_rows,
unavailable: false,
});
}
let mut elsewhere: Vec<Entry> = Vec::new();
let roots = HomeState::roots_with(config_dirs, recents, desktop_dirs, network_check);
root_paths = roots.iter().map(|r| r.path.clone()).collect();
for root in roots {
if root.origin == RootOrigin::Desktop {
if root.available {
let mut entry = Entry::directory(&root.path);
entry.name = display_path(&root.path);
elsewhere.push(entry);
}
continue;
}
let mut truncated = false;
let rows = if root.network {
probed.get(&root.path).cloned().unwrap_or_default()
} else if root.available {
let scan = discover::scan_dir_bounded(&root.path);
truncated = scan.truncated;
scan.entries
} else {
Vec::new()
};
let unreachable = root.network && unreachable.contains(&root.path);
let waiting = root.network && !unreachable && !probed.contains_key(&root.path);
if rows.is_empty() && root.origin == RootOrigin::Recent && root.available && !root.network {
continue;
}
let described = mounts.describe(&root.path);
let fstype = if described.network() {
described.fstype
} else {
"network".to_string()
};
let mut subtitle = if waiting {
format!("{fstype} · checking · {}", root.origin.note())
} else if root.network {
format!("{fstype} · {}", root.origin.note())
} else {
root.origin.note().to_string()
};
if truncated {
subtitle = format!("first {} · {}", discover::MAX_ENTRIES_PER_DIR, subtitle);
}
sections.push(Section {
title: display_path(&root.path),
subtitle: Some(subtitle),
rows,
unavailable: !root.available || unreachable,
});
}
if !elsewhere.is_empty() {
sections.push(Section {
title: "Elsewhere".to_string(),
subtitle: Some("opened elsewhere".to_string()),
rows: elsewhere,
unavailable: false,
});
}
annotate(&mut sections, known, network_check, &mounts);
Listing {
sections,
root_paths,
}
}
fn annotate(
sections: &mut [Section],
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
network_check: fn(&Path) -> bool,
mounts: &crate::locality::Mounts,
) {
for section in sections {
for row in &mut section.rows {
apply_known_facts(row, known, network_check(&row.path));
row.cost.source = Some(mounts.describe(&row.path).fstype);
}
}
}
fn apply_known_facts(
row: &mut Entry,
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
remote: bool,
) {
let Some(facts) = known.get(&row.path) else {
return;
};
if !remote {
let same_bytes = row.size.map(|s| s == facts.size).unwrap_or(false)
&& row
.modified
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() == facts.mtime)
.unwrap_or(false);
if !same_bytes {
return;
}
}
row.rows = facts.rows;
row.cols = facts.cols;
if !facts.columns.is_empty() {
row.columns = facts.columns.clone();
}
let source = row.cost.source.take();
row.cost = facts.cost.clone();
row.cost.source = source;
if remote {
row.size = row.size.or(Some(facts.size));
if row.kind == EntryKind::Unknown {
if let Some(kind) = facts.kind {
row.kind = kind;
}
}
}
}
pub fn facts_for(entry: &Entry) -> Option<(PathBuf, crate::cache::DatasetFacts)> {
let size = entry.size?;
let mtime = entry
.modified?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
if entry.rows.is_none() && entry.columns.is_empty() && entry.cost == Default::default() {
return None; }
Some((
entry.path.clone(),
crate::cache::DatasetFacts {
mtime,
size,
rows: entry.rows,
cols: entry.cols,
columns: entry.columns.clone(),
kind: Some(entry.kind),
cost: crate::discover::Cost {
source: None,
..entry.cost.clone()
},
},
))
}
pub fn match_score(filter: &str, entry: &Entry) -> Option<i32> {
if let Some(m) = crate::fuzzy::best_match(filter, &entry.name) {
return Some(m.score);
}
if filter.is_empty() {
return Some(0);
}
matching_column(filter, entry).map(|_| -COLUMN_MATCH_PENALTY)
}
const COLUMN_MATCH_PENALTY: i32 = 1_000_000;
pub fn matching_column<'a>(filter: &str, entry: &'a Entry) -> Option<&'a str> {
if filter.is_empty() {
return None;
}
let needle = filter.to_lowercase();
entry
.columns
.iter()
.find(|c| c.to_lowercase().contains(&needle))
.map(|c| c.as_str())
}
pub fn fuzzy_positions(needle: &str, haystack: &str) -> Vec<usize> {
crate::fuzzy::best_match(needle, haystack)
.map(|m| m.positions)
.unwrap_or_default()
}
pub fn substring_positions(needle: &str, haystack: &str) -> Vec<usize> {
if needle.is_empty() {
return Vec::new();
}
let hay: Vec<char> = haystack.to_lowercase().chars().collect();
let need: Vec<char> = needle.to_lowercase().chars().collect();
if need.len() > hay.len() {
return Vec::new();
}
for start in 0..=(hay.len() - need.len()) {
if hay[start..start + need.len()] == need[..] {
return (start..start + need.len()).collect();
}
}
Vec::new()
}
pub fn fuzzy_score(needle: &str, haystack: &str) -> Option<i32> {
crate::fuzzy::best_match(needle, haystack).map(|m| m.score)
}
impl HomeState {
pub fn roots(
config_dirs: &[PathBuf],
recents: &[PathBuf],
desktop_dirs: &[PathBuf],
) -> Vec<Root> {
Self::roots_with(config_dirs, recents, desktop_dirs, is_remote_path)
}
pub fn roots_with(
config_dirs: &[PathBuf],
recents: &[PathBuf],
desktop_dirs: &[PathBuf],
is_network: fn(&Path) -> bool,
) -> Vec<Root> {
let mut roots: Vec<Root> = Vec::new();
let mut seen: Vec<PathBuf> = Vec::new();
let push =
|path: PathBuf, origin: RootOrigin, roots: &mut Vec<Root>, seen: &mut Vec<PathBuf>| {
let network = is_network(&path);
let key = if network {
path.clone()
} else {
path.canonicalize().unwrap_or_else(|_| path.clone())
};
if seen.contains(&key) {
return;
}
seen.push(key);
let available = if network {
true } else {
std::fs::read_dir(&path).is_ok()
};
roots.push(Root {
path,
origin,
available,
network,
});
};
let cwd = std::env::current_dir().ok();
let cwd_key = cwd.as_ref().map(|c| {
if is_network(c) {
c.clone()
} else {
c.canonicalize().unwrap_or_else(|_| c.clone())
}
});
if let Some(cwd) = cwd.clone() {
push(cwd, RootOrigin::Cwd, &mut roots, &mut seen);
}
for dir in config_dirs {
push(dir.clone(), RootOrigin::Configured, &mut roots, &mut seen);
}
let mut derived = 0usize;
for recent in recents {
if derived >= MAX_RECENT_ROOTS {
break;
}
if let Some(parent) = recent.parent() {
if parent.as_os_str().is_empty() {
continue;
}
let key = if is_network(parent) {
parent.to_path_buf()
} else {
parent
.canonicalize()
.unwrap_or_else(|_| parent.to_path_buf())
};
if Some(&key) == cwd_key.as_ref() {
continue;
}
let before = roots.len();
push(
parent.to_path_buf(),
RootOrigin::Recent,
&mut roots,
&mut seen,
);
if roots.len() > before {
derived += 1;
}
}
}
for dir in desktop_dirs {
push(dir.clone(), RootOrigin::Desktop, &mut roots, &mut seen);
}
roots
}
pub fn rebuild(&mut self, config_dirs: &[PathBuf], recents: &[PathBuf]) {
self.rebuild_with(config_dirs, recents, &[])
}
pub fn rebuild_with(
&mut self,
config_dirs: &[PathBuf],
recents: &[PathBuf],
desktop_dirs: &[PathBuf],
) {
let request = ListingRequest {
config_dirs: config_dirs.to_vec(),
recents: recents.to_vec(),
desktop_dirs: desktop_dirs.to_vec(),
browsing: self.browsing.clone(),
probed: self.probed.clone(),
unreachable: self.unreachable.clone(),
network_check: self.network_check,
known: Default::default(),
};
let listing = build_listing(&request);
self.apply_listing(listing);
}
pub fn apply_listing(&mut self, listing: Listing) {
let previous = self.selected_entry().map(|e| e.path);
self.sections = listing.sections;
self.root_paths = listing.root_paths;
self.sync_search_section();
if let Some(path) = previous {
if let Some(idx) = self
.visible()
.iter()
.position(|r| matches!(r, Row::Entry { entry, .. } if entry.path == path))
{
self.selected = idx;
return;
}
}
self.select_first_entry();
}
pub fn is_collapsed(&self, section: usize) -> bool {
self.sections
.get(section)
.is_some_and(|s| self.collapsed.contains(&s.title))
}
pub fn toggle_collapsed(&mut self, section: usize) {
let Some(title) = self.sections.get(section).map(|s| s.title.clone()) else {
return;
};
if !self.collapsed.remove(&title) {
self.collapsed.insert(title);
}
}
pub fn set_collapsed(&mut self, section: usize, collapsed: bool) {
let Some(title) = self.sections.get(section).map(|s| s.title.clone()) else {
return;
};
if collapsed {
self.collapsed.insert(title);
} else {
self.collapsed.remove(&title);
}
}
pub const SEARCH_SECTION: &'static str = "Found";
pub fn sync_search_section(&mut self) {
self.sections.retain(|s| s.title != Self::SEARCH_SECTION);
if self.filter.is_empty() || self.search.root.is_none() {
return;
}
if self.search.results.is_empty() && !self.search.running {
return;
}
let listed: std::collections::HashSet<&PathBuf> = self
.sections
.iter()
.flat_map(|s| s.rows.iter().map(|r| &r.path))
.collect();
let rows: Vec<Entry> = self
.search
.results
.iter()
.filter(|e| !listed.contains(&e.path))
.cloned()
.collect();
if rows.is_empty() && !self.search.running {
return;
}
let root = self.search.root.clone().unwrap_or_default();
let mut subtitle = display_path(&root);
if self.search.running {
subtitle = format!("{subtitle} · searching {}", self.search.scanned);
} else if let Some(limit) = &self.search.limited {
subtitle = format!("{subtitle} · {limit} · {} searched", self.search.scanned);
} else {
subtitle = format!("{subtitle} · {} searched", self.search.scanned);
}
self.sections.push(Section {
title: Self::SEARCH_SECTION.to_string(),
subtitle: Some(subtitle),
rows,
unavailable: false,
});
}
pub fn search_batch(&mut self, root: &Path, mut found: Vec<Entry>, scanned: usize) {
if self.search.root.as_deref() != Some(root) {
return;
}
self.search.scanned = scanned;
self.search.results.append(&mut found);
self.sync_search_section();
}
pub fn search_finished(&mut self, root: &Path, scanned: usize, limited: Option<String>) {
if self.search.root.as_deref() != Some(root) {
return;
}
self.search.running = false;
self.search.done = true;
self.search.scanned = scanned;
self.search.limited = limited;
self.sync_search_section();
}
pub fn visible(&self) -> Vec<Row<'_>> {
let mut out: Vec<Row<'_>> = Vec::new();
for (si, section) in self.sections.iter().enumerate() {
let mut matched: Vec<(&Entry, i32)> = section
.rows
.iter()
.filter_map(|row| match_score(&self.filter, row).map(|s| (row, s)))
.collect();
let keep_empty = section.unavailable
|| matches!(
section.subtitle.as_deref(),
Some("configured") | Some("current directory")
);
if matched.is_empty() && !(keep_empty && self.filter.is_empty()) {
continue;
}
if !self.filter.is_empty() {
matched.sort_by(|(a, sa), (b, sb)| {
sb.cmp(sa).then_with(|| a.name.len().cmp(&b.name.len()))
});
}
match self.sort {
SortMode::Natural => {}
SortMode::Size => {
matched.sort_by_key(|(e, _)| std::cmp::Reverse(e.size.unwrap_or(0)));
}
SortMode::Rows => {
matched.sort_by_key(|(e, _)| std::cmp::Reverse(e.rows.unwrap_or(0)));
}
SortMode::Modified => {
matched.sort_by_key(|(e, _)| {
std::cmp::Reverse(
e.modified
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0),
)
});
}
}
let collapsed = self.collapsed.contains(§ion.title);
out.push(Row::Header {
section: si,
matches: matched.len(),
collapsed,
});
if !collapsed {
out.extend(
matched
.into_iter()
.map(|(entry, _)| Row::Entry { section: si, entry }),
);
}
}
out
}
pub fn selected_entry(&self) -> Option<Entry> {
match self.visible().get(self.selected) {
Some(Row::Entry { entry, .. }) => Some((*entry).clone()),
_ => None,
}
}
pub fn selected_section(&self) -> Option<usize> {
self.visible().get(self.selected).map(|r| r.section())
}
pub fn selection_is_header(&self) -> bool {
matches!(self.visible().get(self.selected), Some(Row::Header { .. }))
}
pub fn pending_probes(&self) -> Vec<PathBuf> {
let check = self.network_check;
let mut out = Vec::new();
for section in &self.sections {
let Some(sub) = §ion.subtitle else {
continue;
};
if !sub.starts_with("network") {
continue;
}
for root in &self.root_paths {
if display_path(root) == section.title
&& check(root)
&& !self.probed.contains_key(root)
&& !self.unreachable.contains(root)
&& !out.contains(root)
{
out.push(root.clone());
}
}
}
out
}
pub fn probe_ready(&mut self, root: PathBuf, rows: Vec<Entry>) {
self.unreachable.remove(&root);
self.probed.insert(root, rows);
}
pub fn probe_failed(&mut self, root: PathBuf) {
self.probed.remove(&root);
self.unreachable.insert(root);
}
pub fn measure_now(&mut self, limit: usize) -> bool {
let wanted = self.unmeasured_visible(limit);
let more = self.unmeasured_visible(limit + 1).len() > wanted.len();
for entry in wanted {
let mut probe = entry.clone();
discover::enrich(&mut probe);
self.enriched
.insert(entry.path.clone(), measured_from(&probe, &entry));
}
self.apply_measurements();
more
}
pub fn unmeasured_visible(&self, limit: usize) -> Vec<Entry> {
let mut out = Vec::new();
for row in self.visible() {
let Row::Entry { entry, .. } = row else {
continue;
};
if entry.rows.is_some() || self.enriched.contains_key(&entry.path) {
continue;
}
if (self.network_check)(&entry.path) {
continue;
}
if !matches!(entry.kind, EntryKind::Directory | EntryKind::Unknown) {
out.push(entry.clone());
}
if out.len() >= limit {
break;
}
}
out
}
pub fn apply_measurements(&mut self) {
for section in &mut self.sections {
for row in &mut section.rows {
if let Some(m) = self.enriched.get(&row.path) {
row.rows = m.rows;
row.cols = m.cols;
if m.size.is_some() {
row.size = m.size;
}
if !m.columns.is_empty() {
row.columns = m.columns.clone();
}
}
}
}
}
pub fn select_first_entry(&mut self) {
let rows = self.visible();
self.selected = rows
.iter()
.position(|r| matches!(r, Row::Entry { .. }))
.unwrap_or(0);
}
pub fn clamp_selection(&mut self) {
let n = self.visible().len();
if n == 0 {
self.selected = 0;
} else if self.selected >= n {
self.selected = n - 1;
}
}
pub fn move_selection(&mut self, delta: isize) {
let n = self.visible().len();
if n == 0 {
return;
}
let cur = self.selected as isize;
let next = (cur + delta).rem_euclid(n as isize);
self.selected = next as usize;
}
}
fn entry_for_path(path: &Path, remote: bool) -> Entry {
let kind = if remote {
if discover::is_data_file(path) {
EntryKind::File
} else {
EntryKind::Unknown
}
} else if path.is_dir() {
discover::classify_directory(path)
} else {
EntryKind::File
};
let mut entry = Entry {
path: path.to_path_buf(),
kind,
name: path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned()),
size: None,
modified: None,
rows: None,
cols: None,
columns: Vec::new(),
cost: Default::default(),
};
if !remote {
if let Ok(meta) = std::fs::metadata(path) {
if meta.is_file() {
entry.size = Some(meta.len());
}
entry.modified = meta.modified().ok();
}
}
entry
}
pub fn display_path(path: &Path) -> String {
if let Some(home) = dirs::home_dir() {
if let Ok(rest) = path.strip_prefix(&home) {
if rest.as_os_str().is_empty() {
return "~".to_string();
}
return format!("~/{}", rest.display());
}
}
path.display().to_string()
}
pub fn complete_path(typed: &str) -> (String, usize) {
let expanded = expand_user_path(typed);
let typed_ends_in_sep = typed.ends_with('/');
let (dir, prefix) = if typed_ends_in_sep {
(expanded.clone(), String::new())
} else {
match (expanded.parent(), expanded.file_name()) {
(Some(parent), Some(name)) => {
(parent.to_path_buf(), name.to_string_lossy().into_owned())
}
_ => (expanded.clone(), String::new()),
}
};
let Ok(entries) = std::fs::read_dir(&dir) else {
return (typed.to_string(), 0);
};
let mut names: Vec<String> = entries
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with('.') && !prefix.starts_with('.') {
return None;
}
name.starts_with(&prefix).then_some(name)
})
.collect();
if names.is_empty() {
return (typed.to_string(), 0);
}
names.sort();
let shared = names
.iter()
.skip(1)
.fold(names[0].clone(), |acc, name| common_prefix(&acc, name));
let mut completed = typed.to_string();
completed.truncate(typed.len() - prefix.len());
completed.push_str(&shared);
if names.len() == 1 && dir.join(&shared).is_dir() && !completed.ends_with('/') {
completed.push('/');
}
(completed, names.len())
}
fn common_prefix(a: &str, b: &str) -> String {
a.chars()
.zip(b.chars())
.take_while(|(x, y)| x == y)
.map(|(x, _)| x)
.collect()
}
pub fn expand_user_path(raw: &str) -> PathBuf {
crate::config::expand_config_path(raw)
}