smolbar 0.7.0

smol status command for sway
// copyright (C) 2022-2023 Nissa <and-nissa@protonmail.com>
// licensed under GPL-3.0-or-later

//! Configuration structures for the bar and its blocks.

use anyhow::Context;
use serde_derive::{Deserialize, Serialize};
use tracing::{info, trace};

use std::fs;
use std::path::{Path, PathBuf};

use crate::protocol::{Body, Header};

/// Bar configuration, directly deserialized.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TomlBar {
    command_dir: Option<String>,
    /// Configured [`Header`]
    #[serde(default = "Header::default")]
    pub header: Header,
    /// [`Body`] configured at `global` scope
    #[serde(flatten)]
    pub body: Body,
    /// The bar's configured [blocks](TomlBlock)
    #[serde(default = "Vec::new", rename = "block")]
    pub blocks: Vec<TomlBlock>,
}

/// Block configuration, directly deserialized.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TomlBlock {
    /// Command to execute to configure body at `immediate` scope
    pub command: Option<String>,
    /// String prefixing `full_text`
    pub prefix: Option<String>,
    /// String appended to `full_text`
    pub postfix: Option<String>,
    /// Interval, in seconds, at which to refresh the block
    ///
    /// If the interval is negative, overflows
    /// [`Duration`](core::time::Duration), or is not finite, it is ignored.
    pub interval: Option<f32>,
    /// Operating system signal to refresh the block when received
    pub signal: Option<i32>,

    /// Body configured at `local` scope
    #[serde(flatten)]
    pub body: Body,
}

/// Convenience struct for easy access to all configuration options.
#[derive(Debug)]
pub struct Config {
    /// Path of the TOML configuration file
    pub path: PathBuf,
    /// Path to execute block commands in
    pub command_dir: PathBuf,
    /// Bar's direct TOML configuration
    pub toml: TomlBar,
}

impl Config {
    /// Read a TOML configuration from the given `path`, and return it
    /// as a [`Config`].
    ///
    /// # Errors
    ///
    /// - Canonicalizing `path` may fail
    /// - Reading from `path` may fail
    /// - `path` contents may be invalid TOML
    #[tracing::instrument]
    pub fn read_from_path(path: &Path) -> anyhow::Result<Self> {
        // canonicalize path before doing anything else. this is important for
        // getting `command_dir` bc its `path`'s parent
        let path = path
            .canonicalize()
            .context("failed to canonicalize config path")?;

        let toml: TomlBar = toml::from_str(
            &fs::read_to_string(&path).context("failed to read config to utf-8 string")?,
        )?;

        // command_dir is either the config's parent path or whatever is
        // specified in toml
        let mut command_dir = path.parent().unwrap_or(&path).to_path_buf();
        if let Some(ref dir) = toml.command_dir {
            // if the toml command_dir is relative, its appended to the config
            // path parent. otherwise, it replaces it.
            command_dir.push(dir);
        }

        // before pushing toml specified dir, it is canonical. however, since we
        // push an uncanonicalized path, we should canonicalize here.
        trace!(
            path = command_dir.display().to_string(),
            "canonicalizing command_dir",
        );
        command_dir = command_dir
            .canonicalize()
            .context("failed to canonicalize command_dir")?;

        info!(path = command_dir.display().to_string(), "set command_dir");

        trace!(
            num = toml.blocks.len(),
            path = path.display().to_string(),
            "read block(s)",
        );

        Ok(Self {
            path,
            command_dir,
            toml,
        })
    }
}