cosmic_theme/output/
gtk4_output.rs

1use crate::{composite::over, steps::steps, Component, Theme};
2use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba};
3use std::{
4    fs::{self, File},
5    io::{self, Write},
6    num::NonZeroUsize,
7    path::Path,
8};
9
10use super::{to_rgba, OutputError};
11
12impl Theme {
13    #[must_use]
14    #[cold]
15    /// turn the theme into css
16    pub fn as_gtk4(&self) -> String {
17        let Self {
18            background,
19            primary,
20            secondary,
21            accent,
22            destructive,
23            warning,
24            success,
25            palette,
26            ..
27        } = self;
28
29        let window_bg = to_rgba(background.base);
30        let window_fg = to_rgba(background.on);
31
32        let view_bg = to_rgba(primary.base);
33        let view_fg = to_rgba(primary.on);
34
35        let headerbar_bg = to_rgba(background.base);
36        let headerbar_fg = to_rgba(background.on);
37        let headerbar_border_color = to_rgba(background.divider);
38
39        let sidebar_bg = to_rgba(primary.base);
40        let sidebar_fg = to_rgba(primary.on);
41        let sidebar_shade = to_rgba(if self.is_dark {
42            Rgba::new(0.0, 0.0, 0.0, 0.08)
43        } else {
44            Rgba::new(0.0, 0.0, 0.0, 0.32)
45        });
46        let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 });
47        let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base));
48
49        let secondary_sidebar_bg = to_rgba(secondary.base);
50        let secondary_sidebar_fg = to_rgba(secondary.on);
51        let secondary_sidebar_shade = to_rgba(if self.is_dark {
52            Rgba::new(0.0, 0.0, 0.0, 0.08)
53        } else {
54            Rgba::new(0.0, 0.0, 0.0, 0.32)
55        });
56        let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base));
57
58        let headerbar_backdrop = to_rgba(background.base);
59
60        let card_bg = to_rgba(background.component.base);
61        let card_fg = to_rgba(background.component.on);
62
63        let thumbnail_bg = to_rgba(background.component.base);
64        let thumbnail_fg = to_rgba(background.component.on);
65
66        let dialog_bg = to_rgba(primary.base);
67        let dialog_fg = to_rgba(primary.on);
68
69        let popover_bg = to_rgba(background.component.base);
70        let popover_fg = to_rgba(background.component.on);
71
72        let shade = to_rgba(if self.is_dark {
73            Rgba::new(0.0, 0.0, 0.0, 0.32)
74        } else {
75            Rgba::new(0.0, 0.0, 0.0, 0.08)
76        });
77
78        let mut inverted_bg_divider = background.base;
79        inverted_bg_divider.alpha = 0.5;
80        let scrollbar_outline = to_rgba(inverted_bg_divider);
81
82        let mut css = format! {r#"/* GENERATED BY COSMIC */
83@define-color window_bg_color {window_bg};
84@define-color window_fg_color {window_fg};
85
86@define-color view_bg_color {view_bg};
87@define-color view_fg_color {view_fg};
88
89@define-color headerbar_bg_color {headerbar_bg};
90@define-color headerbar_fg_color {headerbar_fg};
91@define-color headerbar_border_color_color {headerbar_border_color};
92@define-color headerbar_backdrop_color {headerbar_backdrop};
93
94@define-color sidebar_bg_color {sidebar_bg};
95@define-color sidebar_fg_color {sidebar_fg};
96@define-color sidebar_shade_color {sidebar_shade};
97@define-color sidebar_backdrop_color {sidebar_backdrop};
98
99@define-color secondary_sidebar_bg_color {secondary_sidebar_bg};
100@define-color secondary_sidebar_fg_color {secondary_sidebar_fg};
101@define-color secondary_sidebar_shade_color {secondary_sidebar_shade};
102@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop};
103
104@define-color card_bg_color {card_bg};
105@define-color card_fg_color {card_fg};
106
107@define-color thumbnail_bg_color {thumbnail_bg};
108@define-color thumbnail_fg_color {thumbnail_fg};
109
110@define-color dialog_bg_color {dialog_bg};
111@define-color dialog_fg_color {dialog_fg};
112
113@define-color popover_bg_color {popover_bg};
114@define-color popover_fg_color {popover_fg};
115
116@define-color shade_color {shade};
117@define-color scrollbar_outline_color {scrollbar_outline};
118"#};
119
120        css.push_str(&component_gtk4_css("accent", accent));
121        css.push_str(&component_gtk4_css("destructive", destructive));
122        css.push_str(&component_gtk4_css("warning", warning));
123        css.push_str(&component_gtk4_css("success", success));
124        css.push_str(&component_gtk4_css("accent", accent));
125        css.push_str(&component_gtk4_css("error", destructive));
126
127        css.push_str(&color_css("blue", palette.accent_blue));
128        css.push_str(&color_css("green", palette.accent_green));
129        css.push_str(&color_css("yellow", palette.accent_yellow));
130        css.push_str(&color_css("red", palette.accent_red));
131        css.push_str(&color_css("orange", palette.ext_orange));
132        css.push_str(&color_css("purple", palette.ext_purple));
133        let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
134        for (i, c) in neutral_steps[..5].iter().enumerate() {
135            css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c)));
136        }
137        for (i, c) in neutral_steps[5..].iter().enumerate() {
138            css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c)));
139        }
140        css
141    }
142
143    /// write the CSS to the appropriate directory
144    /// Should be written in the XDG config directory for gtk-4.0
145    ///
146    /// # Errors
147    ///
148    /// Returns an `OutputError` if there is an error writing the CSS file.
149    #[cold]
150    pub fn write_gtk4(&self) -> Result<(), OutputError> {
151        let css_str = self.as_gtk4();
152        let Some(config_dir) = dirs::config_dir() else {
153            return Err(OutputError::MissingConfigDir);
154        };
155
156        let name = if self.is_dark {
157            "dark.css"
158        } else {
159            "light.css"
160        };
161
162        let config_dir = config_dir.join("gtk-4.0").join("cosmic");
163        if !config_dir.exists() {
164            std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
165        }
166
167        let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?;
168        file.write_all(css_str.as_bytes())
169            .map_err(OutputError::Io)?;
170
171        Ok(())
172    }
173
174    /// Apply gtk color variable settings
175    ///
176    /// # Errors
177    ///
178    /// Returns an `OutputError` if there is an error applying the CSS file.
179    #[cold]
180    pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> {
181        let Some(config_dir) = dirs::config_dir() else {
182            return Err(OutputError::MissingConfigDir);
183        };
184
185        let gtk4 = config_dir.join("gtk-4.0");
186        let gtk3 = config_dir.join("gtk-3.0");
187
188        fs::create_dir_all(&gtk4).map_err(OutputError::Io)?;
189        fs::create_dir_all(&gtk3).map_err(OutputError::Io)?;
190
191        let cosmic_css_dir = gtk4.join("cosmic");
192        let cosmic_css =
193            cosmic_css_dir
194                .clone()
195                .join(if is_dark { "dark.css" } else { "light.css" });
196
197        let gtk4_dest = gtk4.join("gtk.css");
198        let gtk3_dest = gtk3.join("gtk.css");
199
200        #[cfg(target_family = "unix")]
201        for gtk_dest in [&gtk4_dest, &gtk3_dest] {
202            use std::os::unix::fs::symlink;
203            Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?;
204
205            if gtk_dest.exists() {
206                fs::remove_file(gtk_dest).map_err(OutputError::Io)?;
207            }
208
209            symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?;
210        }
211        Ok(())
212    }
213
214    /// Reset the applied gtk css
215    ///
216    /// # Errors
217    ///
218    /// Returns an `OutputError` if there is an error resetting the CSS file.
219    #[cold]
220    pub fn reset_gtk() -> Result<(), OutputError> {
221        let Some(config_dir) = dirs::config_dir() else {
222            return Err(OutputError::MissingConfigDir);
223        };
224
225        let gtk4 = config_dir.join("gtk-4.0");
226        let gtk3 = config_dir.join("gtk-3.0");
227        let gtk4_dest = gtk4.join("gtk.css");
228        let cosmic_css = gtk4.join("cosmic");
229        let gtk3_dest = gtk3.join("gtk.css");
230
231        let res = Self::reset_cosmic_css(&gtk3_dest, &cosmic_css).map_err(OutputError::Io);
232        Self::reset_cosmic_css(&gtk4_dest, &cosmic_css).map_err(OutputError::Io)?;
233        res
234    }
235
236    #[cold]
237    fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
238        if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) {
239            let backup_path = path.with_extension("css.bak");
240            fs::rename(path, &backup_path)?;
241        }
242        Ok(())
243    }
244
245    #[cold]
246    fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
247        if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() {
248            fs::remove_file(path)?;
249        }
250        Ok(())
251    }
252
253    fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<Option<bool>> {
254        if !path.exists() {
255            return Ok(None);
256        }
257
258        if let Ok(metadata) = fs::symlink_metadata(path) {
259            if metadata.file_type().is_symlink() {
260                if let Ok(actual_cosmic_css) = fs::read_link(path) {
261                    let canonical_target = fs::canonicalize(&actual_cosmic_css)?;
262                    let canonical_base = fs::canonicalize(cosmic_css)?;
263                    return Ok(Some(
264                        canonical_target == canonical_base
265                            || canonical_target.starts_with(&canonical_base),
266                    ));
267                }
268            }
269        }
270        Ok(Some(false))
271    }
272}
273
274fn component_gtk4_css(prefix: &str, c: &Component) -> String {
275    format!(
276        r#"
277@define-color {prefix}_color {};
278@define-color {prefix}_bg_color {};
279@define-color {prefix}_fg_color {};
280"#,
281        to_rgba(c.base),
282        to_rgba(c.base),
283        to_rgba(c.on),
284    )
285}
286
287fn color_css(prefix: &str, c_3: Srgba) -> String {
288    let oklch: palette::Oklch = c_3.into_color();
289    let c_2: Srgba = oklch.lighten(0.1).into_color();
290    let c_1: Srgba = oklch.lighten(0.2).into_color();
291    let c_4: Srgba = oklch.darken(0.1).into_color();
292    let c_5: Srgba = oklch.darken(0.2).into_color();
293    let c_1 = to_rgba(c_1);
294    let c_2 = to_rgba(c_2);
295    let c_3 = to_rgba(c_3);
296    let c_4 = to_rgba(c_4);
297    let c_5 = to_rgba(c_5);
298
299    format! {r#"
300@define-color {prefix}_1 {c_1};
301@define-color {prefix}_2 {c_2};
302@define-color {prefix}_3 {c_3};
303@define-color {prefix}_4 {c_4};
304@define-color {prefix}_5 {c_5};
305"#}
306}