Skip to main content

gecol_core/template/
template_struct.rs

1use std::{
2    path::{Path, PathBuf},
3    process::Command,
4};
5
6use minijinja::Environment;
7use serde::{Deserialize, Serialize};
8
9use crate::{error::Error, template::jinja_context, theme::Theme};
10
11/// Holds the template configuration:
12/// - `source` path - file path to the template.
13/// - `target` path - template build destination.
14///
15/// # Template syntax
16///
17/// In the template, you have access to a rich object-oriented color API.
18///
19/// ## Colors in template
20///
21/// You have access to all colors defined in the
22/// [`Theme`](crate::theme::Theme). To access a color, you can add this
23/// anywhere into your template file:
24///
25/// ```text
26/// {{ color_name }}
27/// ```
28///
29/// Where `color_name` should be replace by the color you want to use.
30/// Available colors are:
31///
32/// - `primary`
33/// - `secondary`
34/// - `background`
35/// - `surface`
36/// - `border`
37/// - `foreground`
38/// - `muted`
39/// - `success`
40/// - `warning`
41/// - `error`
42///
43/// ## Color methods
44///
45/// The following methods are available to manipulate the color object. They
46/// can be chained together.
47///
48/// - `lighten(amount)`: lightens the color by adding a value to it.
49/// - `brighten(amount)`: brightens the color relatively by a given multiplier.
50/// - `darken(amount)`: darkens the color by adding a value to it.
51/// - `dim(amount)`: dims the color relatively by a given multiplier.
52/// - `saturate(amount)`: increases the saturation (chroma component) of the
53///   color.
54/// - `desaturate(amount)`: descreases the saturation (chroma component) of the
55///   color.
56///
57/// ## Color formats
58///
59/// You can also convert the color into multiple text formats. Note that
60/// after a color is formatted, it becomes a string and you cannot access the
61/// menipulation methods anymore. You should use these last.
62///
63/// - `hex`: `#rrggbb` (default if no formatter is specified)
64/// - `hexa(alpha)`: `#rrggbbaa`, where `alpha` is the provided float
65///   (0.0 to 1.0).
66/// - `rgb`: `r,g,b` format (e.g. `42,128,56`).
67/// - `rgba(alpha)`: `r,g,b,a` format (e.g. `42,128,56,0.8`), where `alpha` is
68///   the provided float (0.0 to 1.0).
69/// - `strip`: hex without the leading `#` character - `rrggbb`.
70/// - `r`, `g`, `b`: extracts the corresponding raw RGB color component.
71///
72/// # Example
73///
74/// ```text
75/// /* Defaults to hex format. */
76/// bg_color = "{{ background }}"
77///
78/// /* Chains methods before formatting. */
79/// bg_hover = "rgb({{ background.lighten(0.1).rgb }})"
80///
81/// /* Creates transparent color. */
82/// border = "rgba({{ primary.rgba(0.8) }})"
83/// ```
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Template {
86    pub source: PathBuf,
87    pub target: PathBuf,
88    // A command to be executed after the template is built.
89    //
90    // Some program require restart after their configuration is edited, such
91    // as waybar, so you can restart them here.
92    #[serde(default)]
93    pub hook: Option<String>,
94}
95
96impl Template {
97    /// Creates new template with `source` as the template file path and
98    /// `target` as the build destination.
99    pub fn new<P1, P2>(source: P1, target: P2) -> Self
100    where
101        P1: AsRef<Path>,
102        P2: AsRef<Path>,
103    {
104        Self {
105            source: source.as_ref().to_owned(),
106            target: target.as_ref().to_owned(),
107            hook: None,
108        }
109    }
110
111    /// Builds the template at `source` and saves it to `target`.
112    ///
113    /// Note that it's more efficient to build multiple templates using
114    /// [`build_templates`](crate::template::build_templates), instead of
115    /// using this function on each of the templates.
116    pub fn build(&self, theme: &Theme) -> Result<(), Error> {
117        let content = std::fs::read_to_string(&self.source)?;
118
119        let mut env = Environment::new();
120        let source = self.source.to_string_lossy();
121        env.add_template(&source, &content)?;
122
123        let template = env.get_template(&source)?;
124        let ctx = jinja_context(theme.clone());
125        let built = template.render(ctx)?;
126
127        if let Some(parent) = self.target.parent() {
128            std::fs::create_dir_all(parent)?;
129        }
130        std::fs::write(&self.target, built)?;
131
132        self.run_hook();
133        Ok(())
134    }
135
136    /// Runs the template hook if set.
137    ///
138    /// It runs the set hook in the home directory (or in `/` if home cannot
139    /// be found).
140    pub fn run_hook(&self) {
141        let Some(hook) = &self.hook else {
142            return;
143        };
144
145        let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
146        run_hook(hook, &home_dir);
147    }
148}
149
150/// Runs the given hook on the given directory.
151pub fn run_hook(hook: &str, dir: &Path) {
152    let status = Command::new("sh")
153        .arg("-c")
154        .arg(hook)
155        .current_dir(dir)
156        .status();
157    if let Err(e) = status {
158        eprintln!("Warning: Failed to execute hook '{hook}': {e}",);
159    }
160}