use color_eyre::{eyre::Context, Result};
use log::info;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
ops::Deref,
path::{Path, PathBuf},
};
use crate::Outputs;
#[derive(Debug, Deserialize)]
pub struct Cfgs(HashMap<String, Config>);
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DesiredOutput {
pub name: String,
pub scale: Option<f64>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub outputs: Vec<DesiredOutput>,
pub priority: Option<i64>,
}
impl Deref for Cfgs {
type Target = HashMap<String, Config>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&toml_edit::Table> for Cfgs {
type Error = color_eyre::Report;
fn try_from(table: &toml_edit::Table) -> std::result::Result<Self, Self::Error> {
let cfg: Result<HashMap<String, Config>> = table
.into_iter()
.map(|(name, inner)| {
let section_str = inner
.as_table()
.map(|t| t.to_string())
.unwrap_or(inner.as_str().unwrap_or("").to_string());
let cfg_entry: Config =
toml_edit::de::from_str(§ion_str).wrap_err_with(|| {
format!(
"Missing outputs in configuration {}: {}",
&name,
&inner.to_string(),
)
})?;
let name = name.to_string();
Ok((name, cfg_entry))
})
.collect();
Ok(Cfgs(cfg?))
}
}
impl Cfgs {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let cfg_str = std::fs::read_to_string(&path)
.wrap_err_with(|| format!("Failed to read {}", path.as_ref().display()))?;
let cfgs_doc: toml_edit::Document = cfg_str
.parse()
.wrap_err("Failed to parse configurtion file")?;
let cfgs = cfgs_doc.as_table();
Self::try_from(cfgs)
}
pub fn find(&self, key: &str) -> Option<&Config> {
self.0.get(key)
}
pub fn default_path() -> PathBuf {
dirs::config_dir()
.unwrap_or("/etc/xdg/".into())
.join("oswo.toml")
}
pub fn add(&mut self, name: &str, outputs: &Outputs) -> Result<()> {
let active_outputs: Vec<_> = outputs
.iter()
.filter(|o| o.enabled())
.map(|o| DesiredOutput {
name: o.name().to_string(),
scale: Some(o.scale()),
})
.collect();
match self.0.insert(
name.to_string(),
Config {
outputs: active_outputs,
priority: None,
},
) {
Some(_) => info!("Updated config {name}"),
None => info!("Added new config {name}"),
}
Ok(())
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let mut doc = if path.exists() {
let s = std::fs::read_to_string(path)
.wrap_err_with(|| format!("Failed to read {}", path.display()))?;
s.parse::<toml_edit::Document>()
.wrap_err("Failed to parse existing TOML file")?
} else {
toml_edit::Document::new()
};
for (name, cfg) in &self.0 {
let mut section = toml_edit::Table::new();
let mut outputs_array = toml_edit::Array::new();
for output in &cfg.outputs {
let mut output_table = toml_edit::InlineTable::new();
output_table.insert("name", output.name.clone().into());
if let Some(scale) = output.scale {
output_table.insert("scale", scale.into());
}
outputs_array.push(output_table);
}
section["outputs"] = toml_edit::Item::Value(toml_edit::Value::Array(outputs_array));
if let Some(p) = cfg.priority {
section["priority"] = toml_edit::value(p);
} else {
}
doc[name.as_str()] = toml_edit::Item::Table(section);
}
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, doc.to_string())
.wrap_err_with(|| format!("Failed to write temp file {}", tmp.display()))?;
std::fs::rename(&tmp, path).wrap_err_with(|| {
format!("Failed to rename {} -> {}", tmp.display(), path.display())
})?;
Ok(())
}
}
impl std::fmt::Display for Cfgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.iter().try_fold((), |_, (name, cfg)| {
let setup_str = cfg
.outputs
.iter()
.map(|o| format!("{}", o))
.collect::<Vec<_>>()
.join("\n ");
let priority_str = cfg
.priority
.map(|p| format!(" (priority: {})", p))
.unwrap_or_default();
write!(f, "{}{}:\n {}\n", name, priority_str, setup_str)
})
}
}
impl std::fmt::Display for DesiredOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (scale: {})", self.name, self.scale.unwrap_or(1.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_priority() {
let s = r#"
[a]
outputs = [{ name = "Foo", scale = 1.0 }]
priority = 5
"#;
let doc: toml_edit::Document = s.parse().unwrap();
let cfgs = Cfgs::try_from(doc.as_table()).unwrap();
let cfg = cfgs.find("a").expect("config 'a' present");
assert_eq!(cfg.priority.unwrap(), 5);
assert_eq!(cfg.outputs.len(), 1);
assert_eq!(cfg.outputs[0].name, "Foo");
}
}