1use color_eyre::{eyre::Context, Result};
2use log::info;
3use serde::{Deserialize, Serialize};
4use std::{
5 collections::HashMap,
6 ops::Deref,
7 path::{Path, PathBuf},
8};
9
10use crate::Outputs;
11
12#[derive(Debug, Deserialize)]
13pub struct Cfgs(HashMap<String, Config>);
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct DesiredOutput {
17 pub name: String,
18 pub scale: Option<f64>,
19}
20
21#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct Config {
24 pub outputs: Vec<DesiredOutput>,
25 pub priority: Option<i64>,
27}
28
29impl Deref for Cfgs {
30 type Target = HashMap<String, Config>;
31
32 fn deref(&self) -> &Self::Target {
33 &self.0
34 }
35}
36
37impl TryFrom<&toml_edit::Table> for Cfgs {
45 type Error = color_eyre::Report;
46
47 fn try_from(table: &toml_edit::Table) -> std::result::Result<Self, Self::Error> {
48 let cfg: Result<HashMap<String, Config>> = table
49 .into_iter()
50 .map(|(name, inner)| {
51 let section_str = inner
52 .as_table()
53 .map(|t| t.to_string())
54 .unwrap_or(inner.as_str().unwrap_or("").to_string());
55 let cfg_entry: Config =
56 toml_edit::de::from_str(§ion_str).wrap_err_with(|| {
57 format!(
58 "Missing outputs in configuration {}: {}",
59 &name,
60 &inner.to_string(),
61 )
62 })?;
63 let name = name.to_string();
64 Ok((name, cfg_entry))
65 })
66 .collect();
67 Ok(Cfgs(cfg?))
68 }
69}
70
71impl Cfgs {
72 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
73 let cfg_str = std::fs::read_to_string(&path)
74 .wrap_err_with(|| format!("Failed to read {}", path.as_ref().display()))?;
75 let cfgs_doc: toml_edit::Document = cfg_str
76 .parse()
77 .wrap_err("Failed to parse configurtion file")?;
78 let cfgs = cfgs_doc.as_table();
79 Self::try_from(cfgs)
80 }
81
82 pub fn find(&self, key: &str) -> Option<&Config> {
84 self.0.get(key)
85 }
86
87 pub fn default_path() -> PathBuf {
88 dirs::config_dir()
89 .unwrap_or("/etc/xdg/".into())
90 .join("oswo.toml")
91 }
92
93 pub fn add(&mut self, name: &str, outputs: &Outputs) -> Result<()> {
94 let active_outputs: Vec<_> = outputs
95 .iter()
96 .filter(|o| o.enabled())
97 .map(|o| DesiredOutput {
98 name: o.name().to_string(),
99 scale: Some(o.scale()),
100 })
101 .collect();
102
103 match self.0.insert(
104 name.to_string(),
105 Config {
106 outputs: active_outputs,
107 priority: None,
108 },
109 ) {
110 Some(_) => info!("Updated config {name}"),
111 None => info!("Added new config {name}"),
112 }
113 Ok(())
114 }
115
116 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
120 let path = path.as_ref();
121
122 let mut doc = if path.exists() {
124 let s = std::fs::read_to_string(path)
125 .wrap_err_with(|| format!("Failed to read {}", path.display()))?;
126 s.parse::<toml_edit::Document>()
127 .wrap_err("Failed to parse existing TOML file")?
128 } else {
129 toml_edit::Document::new()
130 };
131
132 for (name, cfg) in &self.0 {
135 let mut section = toml_edit::Table::new();
136
137 let mut outputs_array = toml_edit::Array::new();
139 for output in &cfg.outputs {
140 let mut output_table = toml_edit::InlineTable::new();
141 output_table.insert("name", output.name.clone().into());
142 if let Some(scale) = output.scale {
143 output_table.insert("scale", scale.into());
144 }
145 outputs_array.push(output_table);
146 }
147 section["outputs"] = toml_edit::Item::Value(toml_edit::Value::Array(outputs_array));
148
149 if let Some(p) = cfg.priority {
150 section["priority"] = toml_edit::value(p);
151 } else {
152 }
154
155 doc[name.as_str()] = toml_edit::Item::Table(section);
156 }
157
158 let tmp = path.with_extension("tmp");
160 std::fs::write(&tmp, doc.to_string())
161 .wrap_err_with(|| format!("Failed to write temp file {}", tmp.display()))?;
162 std::fs::rename(&tmp, path).wrap_err_with(|| {
163 format!("Failed to rename {} -> {}", tmp.display(), path.display())
164 })?;
165
166 Ok(())
167 }
168}
169
170impl std::fmt::Display for Cfgs {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 self.0.iter().try_fold((), |_, (name, cfg)| {
173 let setup_str = cfg
174 .outputs
175 .iter()
176 .map(|o| format!("{}", o))
177 .collect::<Vec<_>>()
178 .join("\n ");
179 let priority_str = cfg
180 .priority
181 .map(|p| format!(" (priority: {})", p))
182 .unwrap_or_default();
183 write!(f, "{}{}:\n {}\n", name, priority_str, setup_str)
184 })
185 }
186}
187
188impl std::fmt::Display for DesiredOutput {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(f, "{} (scale: {})", self.name, self.scale.unwrap_or(1.0))
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn parse_priority() {
200 let s = r#"
201 [a]
202 outputs = [{ name = "Foo", scale = 1.0 }]
203 priority = 5
204 "#;
205 let doc: toml_edit::Document = s.parse().unwrap();
206 let cfgs = Cfgs::try_from(doc.as_table()).unwrap();
207 let cfg = cfgs.find("a").expect("config 'a' present");
208 assert_eq!(cfg.priority.unwrap(), 5);
209 assert_eq!(cfg.outputs.len(), 1);
210 assert_eq!(cfg.outputs[0].name, "Foo");
211 }
212}