use std::path::{Path, PathBuf};
pub struct PathResolver<'a> {
vault_root: &'a Path,
}
impl<'a> PathResolver<'a> {
pub fn new(vault_root: &'a Path) -> Self {
Self { vault_root }
}
pub fn inbox_task(&self, id: &str) -> PathBuf {
self.vault_root.join(format!("Inbox/{id}.md"))
}
pub fn project_task(&self, project: &str, id: &str) -> PathBuf {
self.vault_root.join(format!("Projects/{project}/Tasks/{id}.md"))
}
pub fn project_dir(&self, project: &str) -> PathBuf {
self.vault_root.join(format!("Projects/{project}"))
}
pub fn project_note(&self, project: &str) -> PathBuf {
self.vault_root.join(format!("Projects/{project}/{project}.md"))
}
pub fn archive_project_note(&self, project: &str) -> PathBuf {
self.vault_root.join(format!("Projects/_archive/{project}/{project}.md"))
}
pub fn daily_note(&self, date: &str) -> PathBuf {
let year = &date[..4];
self.vault_root.join(format!("Journal/{year}/Daily/{date}.md"))
}
pub fn weekly_note(&self, week: &str) -> PathBuf {
let year = &week[..4];
self.vault_root.join(format!("Journal/{year}/Weekly/{week}.md"))
}
pub fn meeting_note(&self, date: &str, id: &str) -> PathBuf {
let year = &date[..4];
self.vault_root.join(format!("Meetings/{year}/{id}.md"))
}
pub fn zettel(&self, slug: &str) -> PathBuf {
self.vault_root.join(format!("zettels/{slug}.md"))
}
pub fn custom_type(&self, type_name: &str, slug: &str) -> PathBuf {
self.vault_root.join(format!("{type_name}s/{slug}.md"))
}
pub fn meetings_dir(&self, year: &str) -> PathBuf {
self.vault_root.join(format!("Meetings/{year}"))
}
pub fn index_db(&self) -> PathBuf {
self.vault_root.join(".mdvault/index.db")
}
pub fn state_dir(&self) -> PathBuf {
self.vault_root.join(".mdvault/state")
}
pub fn state_file(&self) -> PathBuf {
self.vault_root.join(".mdvault/state/context.toml")
}
pub fn activity_log(&self) -> PathBuf {
self.vault_root.join(".mdvault/activity.jsonl")
}
pub fn activity_archive_dir(&self) -> PathBuf {
self.vault_root.join(".mdvault/activity_archive")
}
pub fn is_project_task(task_path: &str, project_folder: &str) -> bool {
task_path.contains(&format!("Projects/{project_folder}/"))
|| task_path.contains(&format!("Projects/_archive/{project_folder}/"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn resolver() -> PathResolver<'static> {
PathResolver::new(Path::new("/vault"))
}
#[test]
fn inbox_task_path() {
assert_eq!(
resolver().inbox_task("INB-001"),
Path::new("/vault/Inbox/INB-001.md")
);
}
#[test]
fn project_task_path() {
assert_eq!(
resolver().project_task("my-proj", "MP-001"),
Path::new("/vault/Projects/my-proj/Tasks/MP-001.md")
);
}
#[test]
fn project_dir_path() {
assert_eq!(
resolver().project_dir("my-proj"),
Path::new("/vault/Projects/my-proj")
);
}
#[test]
fn project_note_path() {
assert_eq!(
resolver().project_note("my-proj"),
Path::new("/vault/Projects/my-proj/my-proj.md")
);
}
#[test]
fn archive_project_note_path() {
assert_eq!(
resolver().archive_project_note("old-proj"),
Path::new("/vault/Projects/_archive/old-proj/old-proj.md")
);
}
#[test]
fn daily_note_path() {
assert_eq!(
resolver().daily_note("2026-03-15"),
Path::new("/vault/Journal/2026/Daily/2026-03-15.md")
);
}
#[test]
fn weekly_note_path() {
assert_eq!(
resolver().weekly_note("2026-W13"),
Path::new("/vault/Journal/2026/Weekly/2026-W13.md")
);
}
#[test]
fn meeting_note_path() {
assert_eq!(
resolver().meeting_note("2026-01-15", "MTG-2026-01-15-001"),
Path::new("/vault/Meetings/2026/MTG-2026-01-15-001.md")
);
}
#[test]
fn zettel_path() {
assert_eq!(
resolver().zettel("my-knowledge-note"),
Path::new("/vault/zettels/my-knowledge-note.md")
);
}
#[test]
fn custom_type_path() {
assert_eq!(
resolver().custom_type("contact", "john-doe"),
Path::new("/vault/contacts/john-doe.md")
);
}
#[test]
fn index_db_path() {
assert_eq!(resolver().index_db(), Path::new("/vault/.mdvault/index.db"));
}
#[test]
fn state_paths() {
assert_eq!(resolver().state_dir(), Path::new("/vault/.mdvault/state"));
assert_eq!(
resolver().state_file(),
Path::new("/vault/.mdvault/state/context.toml")
);
}
#[test]
fn activity_paths() {
assert_eq!(
resolver().activity_log(),
Path::new("/vault/.mdvault/activity.jsonl")
);
assert_eq!(
resolver().activity_archive_dir(),
Path::new("/vault/.mdvault/activity_archive")
);
}
#[test]
fn is_project_task_active() {
assert!(PathResolver::is_project_task(
"Projects/my-proj/Tasks/MP-001.md",
"my-proj"
));
}
#[test]
fn is_project_task_archived() {
assert!(PathResolver::is_project_task(
"Projects/_archive/my-proj/Tasks/MP-001.md",
"my-proj"
));
}
#[test]
fn is_project_task_wrong_project() {
assert!(!PathResolver::is_project_task(
"Projects/other/Tasks/MP-001.md",
"my-proj"
));
}
#[test]
fn is_project_task_inbox() {
assert!(!PathResolver::is_project_task("Inbox/INB-001.md", "my-proj"));
}
#[test]
fn meetings_dir_path() {
assert_eq!(resolver().meetings_dir("2026"), Path::new("/vault/Meetings/2026"));
}
#[test]
fn is_project_task_not_confused_by_substring() {
assert!(!PathResolver::is_project_task(
"Projects/my-proj/Tasks/MP-001.md",
"proj"
));
}
}