1use anyhow::{Context, Result};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, Copy)]
7pub enum InitTarget {
8 Xdg,
12 Local,
14}
15
16pub struct InitReport {
17 pub target: InitTarget,
18 pub created_dirs: Vec<PathBuf>,
19 pub skipped_dirs: Vec<PathBuf>,
20 pub wrote_config: Option<PathBuf>,
21 pub config_existed: Option<PathBuf>,
22}
23
24pub fn init_workspace(target: InitTarget, cwd: &Path) -> Result<InitReport> {
34 match target {
35 InitTarget::Xdg => init_xdg(),
36 InitTarget::Local => init_local(cwd),
37 }
38}
39
40fn init_xdg() -> Result<InitReport> {
41 let xdg = crate::paths::xdg_dirs().ok_or_else(|| {
42 anyhow::anyhow!("XDG paths unavailable ($HOME unset); retry with `--local`")
43 })?;
44 let dirs = [xdg.data_dir, xdg.topics_dir, xdg.config_dir];
45 let (created, skipped) = ensure_dirs(&dirs)?;
46 Ok(InitReport {
47 target: InitTarget::Xdg,
48 created_dirs: created,
49 skipped_dirs: skipped,
50 wrote_config: None,
51 config_existed: None,
52 })
53}
54
55fn init_local(cwd: &Path) -> Result<InitReport> {
56 let base = cwd.join(".miku");
57 let dirs = [base.join("data"), base.join("topics"), base.join("config")];
58 let (created, skipped) = ensure_dirs(&dirs)?;
59
60 let config_path = cwd.join("miku.toml");
61 let (wrote, existed) = if config_path.exists() {
62 (None, Some(config_path))
63 } else {
64 fs::write(&config_path, DEFAULT_LOCAL_CONFIG)
65 .with_context(|| format!("write {}", config_path.display()))?;
66 (Some(config_path), None)
67 };
68
69 Ok(InitReport {
70 target: InitTarget::Local,
71 created_dirs: created,
72 skipped_dirs: skipped,
73 wrote_config: wrote,
74 config_existed: existed,
75 })
76}
77
78fn ensure_dirs(dirs: &[PathBuf]) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
79 let mut created = Vec::new();
80 let mut skipped = Vec::new();
81 for dir in dirs {
82 if dir.exists() {
83 skipped.push(dir.clone());
84 } else {
85 fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
86 created.push(dir.clone());
87 }
88 }
89 Ok((created, skipped))
90}
91
92const DEFAULT_LOCAL_CONFIG: &str = "\
93# Miku project-local configuration.
94# Paths are resolved relative to this file.
95
96data_dir = \".miku/data\"
97config_dir = \".miku/config\"
98topics_dir = \".miku/topics\"
99";
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use tempfile::tempdir;
105
106 #[test]
107 fn local_creates_dirs_and_config_first_time() {
108 let dir = tempdir().unwrap();
109 let report = init_workspace(InitTarget::Local, dir.path()).unwrap();
110
111 assert!(report.wrote_config.is_some());
112 assert_eq!(report.created_dirs.len(), 3);
113 assert!(report.skipped_dirs.is_empty());
114 assert!(dir.path().join("miku.toml").exists());
115 assert!(dir.path().join(".miku/data").is_dir());
116 assert!(dir.path().join(".miku/topics").is_dir());
117 assert!(dir.path().join(".miku/config").is_dir());
118 }
119
120 #[test]
121 fn local_idempotent_second_run() {
122 let dir = tempdir().unwrap();
123 init_workspace(InitTarget::Local, dir.path()).unwrap();
124 let report = init_workspace(InitTarget::Local, dir.path()).unwrap();
125
126 assert!(report.wrote_config.is_none());
127 assert!(report.config_existed.is_some());
128 assert!(report.created_dirs.is_empty());
129 assert_eq!(report.skipped_dirs.len(), 3);
130 }
131
132 #[test]
133 fn xdg_creates_unified_root_under_xdg_data_home() {
134 let dir = tempdir().unwrap();
135 let data = dir.path().join("xdg-data");
136
137 let saved = std::env::var("XDG_DATA_HOME").ok();
138 unsafe {
139 std::env::set_var("XDG_DATA_HOME", &data);
140 }
141
142 let report = init_workspace(InitTarget::Xdg, dir.path()).unwrap();
143
144 match saved {
145 Some(val) => unsafe { std::env::set_var("XDG_DATA_HOME", val) },
146 None => unsafe { std::env::remove_var("XDG_DATA_HOME") },
147 }
148
149 assert!(matches!(report.target, InitTarget::Xdg));
150 let root = data.join("miku");
151 assert!(root.join("data").is_dir());
152 assert!(root.join("topics").is_dir());
153 assert!(root.join("config").is_dir());
154 }
155
156 #[test]
157 fn local_preserves_existing_config() {
158 let dir = tempdir().unwrap();
159 let cfg = dir.path().join("miku.toml");
160 fs::write(&cfg, "# user-customised\n").unwrap();
161
162 init_workspace(InitTarget::Local, dir.path()).unwrap();
163 assert_eq!(fs::read_to_string(&cfg).unwrap(), "# user-customised\n");
164 }
165}