use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::types::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Hook {
OnNavigate {
from: PathBuf,
to: PathBuf,
},
OnScanStart {
path: PathBuf,
},
OnScanComplete {
path: PathBuf,
total_files: u64,
total_dirs: u64,
total_size: u64,
},
OnScanError {
path: PathBuf,
error: String,
},
OnDeleteStart {
items: Vec<PathBuf>,
use_trash: bool,
},
OnDeleteComplete {
deleted: usize,
failed: usize,
bytes_freed: u64,
},
OnCopyComplete {
succeeded: usize,
failed: usize,
bytes_copied: u64,
},
OnMoveComplete {
succeeded: usize,
failed: usize,
},
OnRenameComplete {
source: PathBuf,
new_path: PathBuf,
},
OnDuplicatesFound {
group_count: usize,
wasted_bytes: u64,
},
OnAgeAnalysisComplete {
stale_dirs: usize,
files: u64,
average_age_secs: u64,
},
OnStartup,
OnShutdown,
}
impl Hook {
pub fn name(&self) -> &'static str {
match self {
Self::OnNavigate { .. } => "on_navigate",
Self::OnScanStart { .. } => "on_scan_start",
Self::OnScanComplete { .. } => "on_scan_complete",
Self::OnScanError { .. } => "on_scan_error",
Self::OnDeleteStart { .. } => "on_delete_start",
Self::OnDeleteComplete { .. } => "on_delete_complete",
Self::OnCopyComplete { .. } => "on_copy_complete",
Self::OnMoveComplete { .. } => "on_move_complete",
Self::OnRenameComplete { .. } => "on_rename_complete",
Self::OnDuplicatesFound { .. } => "on_duplicates_found",
Self::OnAgeAnalysisComplete { .. } => "on_age_analysis_complete",
Self::OnStartup => "on_startup",
Self::OnShutdown => "on_shutdown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NoticeLevel {
#[default]
Info,
Warn,
Error,
}
impl NoticeLevel {
pub fn parse(name: &str) -> Self {
match name {
"warn" | "warning" => Self::Warn,
"error" => Self::Error,
_ => Self::Info,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Notice {
pub level: NoticeLevel,
pub message: String,
}
impl Notice {
pub const MAX_PER_CALL: usize = 8;
pub const MAX_BYTES: usize = 500;
pub fn new(level: NoticeLevel, message: &str) -> Self {
let mut end = message.len().min(Self::MAX_BYTES);
while !message.is_char_boundary(end) {
end -= 1;
}
Self {
level,
message: message[..end].to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct HookContext {
pub data: HashMap<String, Value>,
pub cwd: Option<PathBuf>,
pub view_root: Option<PathBuf>,
pub theme: Option<String>,
}
impl HookContext {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
self.data.insert(key.into(), value.into());
self
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.data.get(key)
}
pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
self.cwd = Some(cwd);
self
}
pub fn with_view_root(mut self, view_root: PathBuf) -> Self {
self.view_root = Some(view_root);
self
}
pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
self.theme = Some(theme.into());
self
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HookResult {
pub handled: bool,
pub prevent_default: bool,
pub stop_propagation: bool,
pub value: Option<Value>,
pub error: Option<String>,
#[serde(default)]
pub notices: Vec<Notice>,
#[serde(default)]
pub plugin: Option<String>,
}
impl HookResult {
pub fn ok() -> Self {
Self {
handled: true,
..Default::default()
}
}
pub fn with_value(value: impl Into<Value>) -> Self {
Self {
handled: true,
value: Some(value.into()),
..Default::default()
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
handled: true,
error: Some(message.into()),
..Default::default()
}
}
pub fn prevent_default(mut self) -> Self {
self.prevent_default = true;
self
}
pub fn stop_propagation(mut self) -> Self {
self.stop_propagation = true;
self
}
pub fn is_ok(&self) -> bool {
self.error.is_none()
}
pub fn is_err(&self) -> bool {
self.error.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn every_hook() -> Vec<Hook> {
let all = vec![
Hook::OnNavigate {
from: PathBuf::new(),
to: PathBuf::new(),
},
Hook::OnScanStart {
path: PathBuf::new(),
},
Hook::OnScanComplete {
path: PathBuf::new(),
total_files: 0,
total_dirs: 0,
total_size: 0,
},
Hook::OnScanError {
path: PathBuf::new(),
error: String::new(),
},
Hook::OnDeleteStart {
items: Vec::new(),
use_trash: true,
},
Hook::OnDeleteComplete {
deleted: 0,
failed: 0,
bytes_freed: 0,
},
Hook::OnCopyComplete {
succeeded: 0,
failed: 0,
bytes_copied: 0,
},
Hook::OnMoveComplete {
succeeded: 0,
failed: 0,
},
Hook::OnRenameComplete {
source: PathBuf::new(),
new_path: PathBuf::new(),
},
Hook::OnDuplicatesFound {
group_count: 0,
wasted_bytes: 0,
},
Hook::OnAgeAnalysisComplete {
stale_dirs: 0,
files: 0,
average_age_secs: 0,
},
Hook::OnStartup,
Hook::OnShutdown,
];
for hook in &all {
match hook {
Hook::OnNavigate { .. }
| Hook::OnScanStart { .. }
| Hook::OnScanComplete { .. }
| Hook::OnScanError { .. }
| Hook::OnDeleteStart { .. }
| Hook::OnDeleteComplete { .. }
| Hook::OnCopyComplete { .. }
| Hook::OnMoveComplete { .. }
| Hook::OnRenameComplete { .. }
| Hook::OnDuplicatesFound { .. }
| Hook::OnAgeAnalysisComplete { .. }
| Hook::OnStartup
| Hook::OnShutdown => {}
}
}
all
}
#[test]
fn plugin_discovery_knows_every_hook_and_nothing_else() {
let names: Vec<&str> = every_hook().iter().map(Hook::name).collect();
assert_eq!(names, crate::runtime::HOOK_NAMES);
}
#[test]
fn long_notices_are_cut_at_a_character_boundary() {
let notice = Notice::new(NoticeLevel::Info, &"é".repeat(Notice::MAX_BYTES));
assert!(notice.message.len() <= Notice::MAX_BYTES);
assert!(notice.message.chars().all(|c| c == 'é'));
assert_eq!(NoticeLevel::parse("warning"), NoticeLevel::Warn);
assert_eq!(NoticeLevel::parse("loud"), NoticeLevel::Info);
}
}