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 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 #[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 #[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(>k4).map_err(OutputError::Io)?;
189 fs::create_dir_all(>k3).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 [>k4_dest, >k3_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 #[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(>k3_dest, &cosmic_css).map_err(OutputError::Io);
232 Self::reset_cosmic_css(>k4_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}