plot/lib/core/
job_init.rs1use std::{
2 collections::{BTreeSet, HashMap},
3 fs,
4 path::{Path, PathBuf},
5};
6
7use crate::lib::{
8 core::{TOML_DEFAULT_MINIMAL, default_config_path, execution_dir},
9 logic::{file_backup, file_ensure, file_save_safe_if_changed},
10 schema::{RawTomlFileJobs, RawTomlJob},
11};
12
13#[derive(Debug, Clone)]
15pub struct AnchoredRuntime {
16 execution_dir: PathBuf, config_path: PathBuf,
18 jobs_store: Vec<RawTomlJob>, }
20
21impl AnchoredRuntime {
22 pub(crate) fn new(execution_dir: PathBuf, config_path: PathBuf, jobs_store: Vec<RawTomlJob>) -> Self {
24 Self { execution_dir, config_path, jobs_store }
25 }
26
27 pub fn path_xdo(&self) -> &Path { &self.config_path }
29
30 pub fn path_cwd(&self) -> &Path { &self.execution_dir }
32
33 pub fn count_jobs(&self) -> usize { self.jobs_store.len() }
35
36 pub fn get_jobs_index(&self) -> HashMap<String, String> {
38 self.jobs_store.iter().map(|job| (job.id.clone(), job.description.clone().unwrap_or_default())).collect()
39 }
40
41 pub fn get_jobs<F>(&self, mut f: F)
44 where F: FnMut(&RawTomlJob) {
45 for job in &self.jobs_store {
46 f(job);
47 }
48 }
49
50 pub fn get_jobs_some<F>(&self, ids: &BTreeSet<String>, mut f: F)
51 where F: FnMut(&RawTomlJob) {
52 self.jobs_store.iter().filter(|job| ids.contains(&job.id)).for_each(|job| f(job));
53 }
54
55 pub fn get_job(&self, id: &str) -> Option<&RawTomlJob> { self.jobs_store.iter().find(|job| job.id == id) }
58}
59
60#[derive(Debug, serde::Deserialize, serde::Serialize)]
63struct InitTomlJobs {
64 job: Vec<InitTomlJob>,
65}
66
67#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
68struct InitTomlJob {
69 id: String,
70 #[serde(flatten)]
72 extra: HashMap<String, toml::Value>,
73}
74
75pub fn start_blank(path_toml: Option<PathBuf>) -> AnchoredRuntime {
79 let config_path = path_toml.unwrap_or_else(default_config_path);
80
81 file_save_safe_if_changed(&config_path, TOML_DEFAULT_MINIMAL);
84
85 build_normalized_result(&config_path)
86}
87
88pub fn start(path_toml: Option<PathBuf>) -> AnchoredRuntime {
90 let config_path = path_toml.unwrap_or_else(default_config_path);
91
92 if !config_path.exists() {
93 file_ensure(&config_path, TOML_DEFAULT_MINIMAL);
94 return build_normalized_result(&config_path);
95 }
96
97 let content = match fs::read_to_string(&config_path) {
98 Ok(c) => c,
99 Err(_) => {
100 handle_corrupted(&config_path);
101 return build_normalized_result(&config_path);
102 }
103 };
104
105 let mut parsed: InitTomlJobs = match toml::from_str(&content) {
106 Ok(p) => p,
107 Err(_) => {
108 handle_corrupted(&config_path);
109 return build_normalized_result(&config_path);
110 }
111 };
112
113 let mut id_counts = HashMap::new();
114 for job in &parsed.job {
115 *id_counts.entry(job.id.clone()).or_insert(0) += 1;
116 }
117
118 let has_any_unique_id = id_counts.values().any(|&count| count == 1);
119 let has_duplicates = id_counts.values().any(|&count| count > 1);
120
121 if !has_any_unique_id || parsed.job.is_empty() {
122 handle_corrupted(&config_path);
123 return build_normalized_result(&config_path);
124 }
125
126 if has_duplicates {
127 file_backup(&config_path);
128
129 parsed.job.retain(|job| id_counts[&job.id] == 1);
130
131 let fixed_toml = toml::to_string(&parsed).expect("Błąd serializacji naprawionego TOML");
132 fs::write(&config_path, fixed_toml).expect("Błąd zapisu naprawionego TOML");
133 }
134
135 build_normalized_result(&config_path)
136}
137
138fn handle_corrupted(path: &Path) {
139 file_backup(path);
140 file_ensure(path, TOML_DEFAULT_MINIMAL);
141}
142
143fn build_normalized_result(path: &Path) -> AnchoredRuntime {
144 let content = fs::read_to_string(path).expect("Krytyczny błąd odczytu znormalizowanego pliku");
145
146 let parsed: RawTomlFileJobs = toml::from_str(&content).expect("Krytyczny błąd parsowania znormalizowanego pliku");
148
149 AnchoredRuntime::new(execution_dir(), path.to_path_buf(), parsed.job.0)
150}