use std::path::PathBuf;
command_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExplorerCommand {
MoveUp => "move_up",
MoveDown => "move_down",
PageUp => "page_up",
PageDown => "page_down",
Top => "top",
Bottom => "bottom",
Ascend => "ascend",
Navigate => "navigate",
Confirm => "confirm",
ToggleHidden => "toggle_hidden",
ToggleSizes => "toggle_sizes",
ToggleDirSizes => "toggle_dir_sizes",
Search => "search",
CycleSort => "cycle_sort",
ToggleMark => "toggle_mark",
NewDirectory => "new_directory",
NewFile => "new_file",
Rename => "rename",
Dismiss => "dismiss",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PromptKind {
Rename,
Touch,
Mkdir,
}
impl PromptKind {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Rename => " \u{270F}\u{FE0F} Rename: ",
Self::Touch => " \u{1F4C4} New file: ",
Self::Mkdir => " \u{1F4C2} New folder: ",
}
}
#[must_use]
pub fn verb(self) -> &'static str {
match self {
Self::Rename => "rename",
Self::Touch => "touch",
Self::Mkdir => "mkdir",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prompt {
pub kind: PromptKind,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SelectionMode {
#[default]
Files,
Directories,
Any,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortMode {
#[default]
Name,
SizeDesc,
Extension,
}
impl SortMode {
#[must_use]
pub fn next(self) -> Self {
match self {
Self::Name => Self::SizeDesc,
Self::SizeDesc => Self::Extension,
Self::Extension => Self::Name,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Name => "name",
Self::SizeDesc => "size ↓",
Self::Extension => "ext",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsEntry {
pub name: String,
pub path: PathBuf,
pub is_dir: bool,
pub size: Option<u64>,
pub item_count: Option<usize>,
pub extension: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DiskUsage {
pub total_bytes: u64,
pub free_bytes: u64,
}
impl DiskUsage {
#[must_use]
pub fn used_bytes(&self) -> u64 {
self.total_bytes.saturating_sub(self.free_bytes)
}
#[must_use]
pub fn used_fraction(&self) -> f64 {
if self.total_bytes == 0 {
0.0
} else {
self.used_bytes() as f64 / self.total_bytes as f64
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExplorerOutcome {
Selected(PathBuf),
Dismissed,
Pending,
Unhandled,
MkdirCreated(PathBuf),
TouchCreated(PathBuf),
RenameCompleted {
from: PathBuf,
to: PathBuf,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_mode_default_is_name() {
assert_eq!(SortMode::default(), SortMode::Name);
}
#[test]
fn sort_mode_next_name_gives_size_desc() {
assert_eq!(SortMode::Name.next(), SortMode::SizeDesc);
}
#[test]
fn sort_mode_next_size_desc_gives_extension() {
assert_eq!(SortMode::SizeDesc.next(), SortMode::Extension);
}
#[test]
fn sort_mode_next_extension_wraps_to_name() {
assert_eq!(SortMode::Extension.next(), SortMode::Name);
}
#[test]
fn sort_mode_full_cycle_returns_to_start() {
let start = SortMode::Name;
let cycled = start.next().next().next();
assert_eq!(cycled, start);
}
#[test]
fn sort_mode_label_name() {
assert_eq!(SortMode::Name.label(), "name");
}
#[test]
fn sort_mode_label_size_desc() {
assert_eq!(SortMode::SizeDesc.label(), "size ↓");
}
#[test]
fn sort_mode_label_extension() {
assert_eq!(SortMode::Extension.label(), "ext");
}
#[test]
fn sort_mode_is_copy() {
let a = SortMode::Name;
let b = a;
assert_eq!(a, b);
}
#[test]
fn fs_entry_construction_file() {
let entry = FsEntry {
name: "main.rs".into(),
path: PathBuf::from("/src/main.rs"),
is_dir: false,
size: Some(1024),
item_count: None,
extension: "rs".into(),
};
assert_eq!(entry.name, "main.rs");
assert_eq!(entry.path, PathBuf::from("/src/main.rs"));
assert!(!entry.is_dir);
assert_eq!(entry.size, Some(1024));
assert_eq!(entry.extension, "rs");
}
#[test]
fn fs_entry_construction_directory() {
let entry = FsEntry {
name: "src".into(),
path: PathBuf::from("/src"),
is_dir: true,
size: None,
item_count: Some(3),
extension: String::new(),
};
assert!(entry.is_dir);
assert!(entry.size.is_none());
assert_eq!(entry.item_count, Some(3));
assert!(entry.extension.is_empty());
}
#[test]
fn fs_entry_equality() {
let a = FsEntry {
name: "a.txt".into(),
path: PathBuf::from("/a.txt"),
is_dir: false,
size: Some(42),
item_count: None,
extension: "txt".into(),
};
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn fs_entry_no_extension_is_empty_string() {
let entry = FsEntry {
name: "Makefile".into(),
path: PathBuf::from("/Makefile"),
is_dir: false,
size: Some(256),
item_count: None,
extension: String::new(),
};
assert!(entry.extension.is_empty());
}
#[test]
fn disk_usage_used_bytes_computes_difference() {
let du = DiskUsage {
total_bytes: 1000,
free_bytes: 400,
};
assert_eq!(du.used_bytes(), 600);
}
#[test]
fn disk_usage_used_bytes_saturates_when_free_exceeds_total() {
let du = DiskUsage {
total_bytes: 100,
free_bytes: 500,
};
assert_eq!(du.used_bytes(), 0);
}
#[test]
fn disk_usage_used_fraction_half_full() {
let du = DiskUsage {
total_bytes: 1000,
free_bytes: 500,
};
assert!((du.used_fraction() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn disk_usage_used_fraction_zero_total_does_not_panic() {
let du = DiskUsage {
total_bytes: 0,
free_bytes: 0,
};
assert_eq!(du.used_fraction(), 0.0);
}
#[test]
fn disk_usage_default_is_zeroed() {
let du = DiskUsage::default();
assert_eq!(du.total_bytes, 0);
assert_eq!(du.free_bytes, 0);
}
#[test]
fn explorer_outcome_dismissed_equals_dismissed() {
assert_eq!(ExplorerOutcome::Dismissed, ExplorerOutcome::Dismissed);
}
#[test]
fn explorer_outcome_pending_equals_pending() {
assert_eq!(ExplorerOutcome::Pending, ExplorerOutcome::Pending);
}
#[test]
fn explorer_outcome_unhandled_equals_unhandled() {
assert_eq!(ExplorerOutcome::Unhandled, ExplorerOutcome::Unhandled);
}
#[test]
fn explorer_outcome_selected_carries_path() {
let path = PathBuf::from("/tmp/file.txt");
let outcome = ExplorerOutcome::Selected(path.clone());
assert_eq!(outcome, ExplorerOutcome::Selected(path));
}
#[test]
fn explorer_outcome_selected_neq_dismissed() {
let outcome = ExplorerOutcome::Selected(PathBuf::from("/tmp/x"));
assert_ne!(outcome, ExplorerOutcome::Dismissed);
}
#[test]
fn explorer_outcome_is_clone() {
let a = ExplorerOutcome::Pending;
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn explorer_outcome_rename_completed_carries_path() {
let path = PathBuf::from("/tmp/renamed.txt");
let from = PathBuf::from("/tmp/old.txt");
let outcome = ExplorerOutcome::RenameCompleted {
from: from.clone(),
to: path.clone(),
};
assert_eq!(outcome, ExplorerOutcome::RenameCompleted { from, to: path });
}
#[test]
fn explorer_outcome_rename_completed_neq_touch_created() {
let path = PathBuf::from("/tmp/file.txt");
assert_ne!(
ExplorerOutcome::RenameCompleted {
from: path.clone(),
to: path.clone(),
},
ExplorerOutcome::TouchCreated(path),
);
}
#[test]
fn explorer_outcome_rename_completed_is_clone() {
let a = ExplorerOutcome::RenameCompleted {
from: PathBuf::from("/tmp/w.txt"),
to: PathBuf::from("/tmp/x.txt"),
};
let b = a.clone();
assert_eq!(a, b);
}
}