use std::{
collections::{BTreeSet, HashMap},
fs,
path::{Path, PathBuf},
};
use crate::lib::{
core::{TOML_DEFAULT_MINIMAL, default_config_path, execution_dir},
logic::{file_backup, file_ensure, file_save_safe_if_changed},
schema::{RawTomlFileJobs, RawTomlJob},
};
#[derive(Debug, Clone)]
pub struct AnchoredRuntime {
execution_dir: PathBuf, config_path: PathBuf,
jobs_store: Vec<RawTomlJob>, }
impl AnchoredRuntime {
pub(crate) fn new(execution_dir: PathBuf, config_path: PathBuf, jobs_store: Vec<RawTomlJob>) -> Self {
Self { execution_dir, config_path, jobs_store }
}
pub fn path_xdo(&self) -> &Path { &self.config_path }
pub fn path_cwd(&self) -> &Path { &self.execution_dir }
pub fn count_jobs(&self) -> usize { self.jobs_store.len() }
pub fn get_jobs_index(&self) -> HashMap<String, String> {
self.jobs_store.iter().map(|job| (job.id.clone(), job.description.clone().unwrap_or_default())).collect()
}
pub fn get_jobs<F>(&self, mut f: F)
where F: FnMut(&RawTomlJob) {
for job in &self.jobs_store {
f(job);
}
}
pub fn get_jobs_some<F>(&self, ids: &BTreeSet<String>, mut f: F)
where F: FnMut(&RawTomlJob) {
self.jobs_store.iter().filter(|job| ids.contains(&job.id)).for_each(|job| f(job));
}
pub fn get_job(&self, id: &str) -> Option<&RawTomlJob> { self.jobs_store.iter().find(|job| job.id == id) }
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct InitTomlJobs {
job: Vec<InitTomlJob>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
struct InitTomlJob {
id: String,
#[serde(flatten)]
extra: HashMap<String, toml::Value>,
}
pub fn start_blank(path_toml: Option<PathBuf>) -> AnchoredRuntime {
let config_path = path_toml.unwrap_or_else(default_config_path);
file_save_safe_if_changed(&config_path, TOML_DEFAULT_MINIMAL);
build_normalized_result(&config_path)
}
pub fn start(path_toml: Option<PathBuf>) -> AnchoredRuntime {
let config_path = path_toml.unwrap_or_else(default_config_path);
if !config_path.exists() {
file_ensure(&config_path, TOML_DEFAULT_MINIMAL);
return build_normalized_result(&config_path);
}
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(_) => {
handle_corrupted(&config_path);
return build_normalized_result(&config_path);
}
};
let mut parsed: InitTomlJobs = match toml::from_str(&content) {
Ok(p) => p,
Err(_) => {
handle_corrupted(&config_path);
return build_normalized_result(&config_path);
}
};
let mut id_counts = HashMap::new();
for job in &parsed.job {
*id_counts.entry(job.id.clone()).or_insert(0) += 1;
}
let has_any_unique_id = id_counts.values().any(|&count| count == 1);
let has_duplicates = id_counts.values().any(|&count| count > 1);
if !has_any_unique_id || parsed.job.is_empty() {
handle_corrupted(&config_path);
return build_normalized_result(&config_path);
}
if has_duplicates {
file_backup(&config_path);
parsed.job.retain(|job| id_counts[&job.id] == 1);
let fixed_toml = toml::to_string(&parsed).expect("Błąd serializacji naprawionego TOML");
fs::write(&config_path, fixed_toml).expect("Błąd zapisu naprawionego TOML");
}
build_normalized_result(&config_path)
}
fn handle_corrupted(path: &Path) {
file_backup(path);
file_ensure(path, TOML_DEFAULT_MINIMAL);
}
fn build_normalized_result(path: &Path) -> AnchoredRuntime {
let content = fs::read_to_string(path).expect("Krytyczny błąd odczytu znormalizowanego pliku");
let parsed: RawTomlFileJobs = toml::from_str(&content).expect("Krytyczny błąd parsowania znormalizowanego pliku");
AnchoredRuntime::new(execution_dir(), path.to_path_buf(), parsed.job.0)
}