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}