mod table;
mod user;
#[cfg(test)]
mod tests;
pub use user::UserFolders;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum KindFamily {
Folder,
Text,
Document,
Sheet,
Code,
Data,
Image,
Audio,
Video,
Archive,
Package,
Executable,
Key,
Font,
File,
}
impl KindFamily {
#[must_use]
pub fn tone(self) -> Option<&'static str> {
match self {
Self::Folder => Some("accent"),
Self::Code | Self::Text | Self::Document | Self::Sheet => Some("series-2"),
Self::Image | Self::Audio | Self::Video | Self::Font => Some("series-3"),
Self::Data | Self::Key => Some("series-4"),
Self::Archive | Self::Package | Self::Executable => Some("series-5"),
Self::File => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileKind {
icon: &'static str,
family: KindFamily,
}
impl FileKind {
const FILE: Self = Self { icon: "file", family: KindFamily::File };
const FOLDER: Self = Self { icon: "folder", family: KindFamily::Folder };
const EXECUTABLE: Self = Self { icon: "file-executable", family: KindFamily::Executable };
fn of(key: &'static str) -> Self {
let family =
table::KINDS.binary_search_by(|(kind, _)| kind.cmp(&key)).map_or(KindFamily::File, |at| table::KINDS[at].1);
Self { icon: key, family }
}
#[must_use]
pub fn icon(&self) -> &'static str {
self.icon
}
#[must_use]
pub fn family(&self) -> KindFamily {
self.family
}
}
#[must_use]
pub fn file_kind(name: &str, folder: bool, executable: bool) -> FileKind {
let lower = name.to_ascii_lowercase();
if folder {
let hidden = lower.starts_with('.').then_some("folder-hidden");
return find(table::FOLDERS, &lower).or(hidden).map_or(FileKind::FOLDER, FileKind::of);
}
let known = find(table::NAMES, &lower).or_else(|| by_lead(&lower)).or_else(|| by_ending(&lower));
match known {
Some(key) => FileKind::of(key),
None if executable => FileKind::EXECUTABLE,
None => FileKind::FILE,
}
}
fn find(table: &'static [(&'static str, &'static str)], key: &str) -> Option<&'static str> {
table.binary_search_by(|(name, _)| (*name).cmp(key)).ok().map(|at| table[at].1)
}
const LEADS: &[(&str, &str)] =
&[("copying", "file-license"), ("licence", "file-license"), ("license", "file-license"), ("readme", "file-readme")];
const TEXT_ENDINGS: &[&str] = &["adoc", "markdown", "md", "org", "rst", "txt"];
fn by_lead(lower: &str) -> Option<&'static str> {
LEADS.iter().find_map(|&(lead, key)| {
let rest = lower.strip_prefix(lead)?;
let joined = rest.is_empty() || rest.starts_with(['.', '-', '_']);
let ending = rest.rsplit_once('.').map(|(_, ending)| ending);
(joined && ending.is_none_or(|ending| TEXT_ENDINGS.contains(&ending))).then_some(key)
})
}
fn by_ending(lower: &str) -> Option<&'static str> {
let mut dots = [0usize; 3];
let mut count = 0;
for (at, _) in lower.rmatch_indices('.').filter(|(at, _)| *at > 0).take(dots.len()) {
dots[count] = at;
count += 1;
}
dots[..count].iter().rev().find_map(|&at| {
let ending = &lower[at + 1..];
if ending.contains('.') { find(table::DOUBLES, ending) } else { find(table::EXTENSIONS, ending) }
})
}