cosmic_theme/output/
gtk4_output.rs1use crate::{composite::over, steps::steps, Component, Theme};
2use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba, WithAlpha};
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 inverted_bg_divider = background.base.with_alpha(0.5);
79 let scrollbar_outline = to_rgba(inverted_bg_divider);
80
81 let mut css = format! {r#"/* GENERATED BY COSMIC */
82@define-color window_bg_color {window_bg};
83@define-color window_fg_color {window_fg};
84
85@define-color view_bg_color {view_bg};
86@define-color view_fg_color {view_fg};
87
88@define-color headerbar_bg_color {headerbar_bg};
89@define-color headerbar_fg_color {headerbar_fg};
90@define-color headerbar_border_color_color {headerbar_border_color};
91@define-color headerbar_backdrop_color {headerbar_backdrop};
92
93@define-color sidebar_bg_color {sidebar_bg};
94@define-color sidebar_fg_color {sidebar_fg};
95@define-color sidebar_shade_color {sidebar_shade};
96@define-color sidebar_backdrop_color {sidebar_backdrop};
97
98@define-color secondary_sidebar_bg_color {secondary_sidebar_bg};
99@define-color secondary_sidebar_fg_color {secondary_sidebar_fg};
100@define-color secondary_sidebar_shade_color {secondary_sidebar_shade};
101@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop};
102
103@define-color card_bg_color {card_bg};
104@define-color card_fg_color {card_fg};
105
106@define-color thumbnail_bg_color {thumbnail_bg};
107@define-color thumbnail_fg_color {thumbnail_fg};
108
109@define-color dialog_bg_color {dialog_bg};
110@define-color dialog_fg_color {dialog_fg};
111
112@define-color popover_bg_color {popover_bg};
113@define-color popover_fg_color {popover_fg};
114
115@define-color shade_color {shade};
116@define-color scrollbar_outline_color {scrollbar_outline};
117"#};
118
119 css.push_str(&component_gtk4_css("accent", accent));
120 css.push_str(&component_gtk4_css("destructive", destructive));
121 css.push_str(&component_gtk4_css("warning", warning));
122 css.push_str(&component_gtk4_css("success", success));
123 css.push_str(&component_gtk4_css("accent", accent));
124 css.push_str(&component_gtk4_css("error", destructive));
125
126 css.push_str(&color_css("blue", palette.accent_blue));
127 css.push_str(&color_css("green", palette.accent_green));
128 css.push_str(&color_css("yellow", palette.accent_yellow));
129 css.push_str(&color_css("red", palette.accent_red));
130 css.push_str(&color_css("orange", palette.ext_orange));
131 css.push_str(&color_css("purple", palette.ext_purple));
132 let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
133 for (i, c) in neutral_steps[..5].iter().enumerate() {
134 css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c)));
135 }
136 for (i, c) in neutral_steps[5..].iter().enumerate() {
137 css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c)));
138 }
139 css
140 }
141
142 #[cold]
149 pub fn write_gtk4(&self) -> Result<(), OutputError> {
150 let css_str = self.as_gtk4();
151 let Some(config_dir) = dirs::config_dir() else {
152 return Err(OutputError::MissingConfigDir);
153 };
154
155 let name = if self.is_dark {
156 "dark.css"
157 } else {
158 "light.css"
159 };
160
161 let config_dir = config_dir.join("gtk-4.0").join("cosmic");
162 if !config_dir.exists() {
163 std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
164 }
165
166 let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?;
167 file.write_all(css_str.as_bytes())
168 .map_err(OutputError::Io)?;
169
170 Ok(())
171 }
172
173 #[cold]
179 pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> {
180 let Some(config_dir) = dirs::config_dir() else {
181 return Err(OutputError::MissingConfigDir);
182 };
183
184 let gtk4 = config_dir.join("gtk-4.0");
185 let gtk3 = config_dir.join("gtk-3.0");
186
187 fs::create_dir_all(>k4).map_err(OutputError::Io)?;
188 fs::create_dir_all(>k3).map_err(OutputError::Io)?;
189
190 let cosmic_css_dir = gtk4.join("cosmic");
191 let cosmic_css =
192 cosmic_css_dir
193 .clone()
194 .join(if is_dark { "dark.css" } else { "light.css" });
195
196 let gtk4_dest = gtk4.join("gtk.css");
197 let gtk3_dest = gtk3.join("gtk.css");
198
199 #[cfg(target_family = "unix")]
200 for gtk_dest in [>k4_dest, >k3_dest] {
201 use std::os::unix::fs::symlink;
202 Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?;
203
204 if gtk_dest.exists() {
205 fs::remove_file(gtk_dest).map_err(OutputError::Io)?;
206 }
207
208 symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?;
209 }
210 Ok(())
211 }
212
213 #[cold]
219 pub fn reset_gtk() -> Result<(), OutputError> {
220 let Some(config_dir) = dirs::config_dir() else {
221 return Err(OutputError::MissingConfigDir);
222 };
223
224 let gtk4 = config_dir.join("gtk-4.0");
225 let gtk3 = config_dir.join("gtk-3.0");
226 let gtk4_dest = gtk4.join("gtk.css");
227 let cosmic_css = gtk4.join("cosmic");
228 let gtk3_dest = gtk3.join("gtk.css");
229
230 let res = Self::reset_cosmic_css(>k3_dest, &cosmic_css).map_err(OutputError::Io);
231 Self::reset_cosmic_css(>k4_dest, &cosmic_css).map_err(OutputError::Io)?;
232 res
233 }
234
235 #[cold]
236 fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
237 if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) {
238 let backup_path = path.with_extension("css.bak");
239 fs::rename(path, &backup_path)?;
240 }
241 Ok(())
242 }
243
244 #[cold]
245 fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
246 if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() {
247 fs::remove_file(path)?;
248 }
249 Ok(())
250 }
251
252 fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<Option<bool>> {
253 if !path.exists() {
254 return Ok(None);
255 }
256
257 if let Ok(metadata) = fs::symlink_metadata(path) {
258 if metadata.file_type().is_symlink() {
259 if let Ok(actual_cosmic_css) = fs::read_link(path) {
260 let canonical_target = fs::canonicalize(&actual_cosmic_css)?;
261 let canonical_base = fs::canonicalize(cosmic_css)?;
262 return Ok(Some(
263 canonical_target == canonical_base
264 || canonical_target.starts_with(&canonical_base),
265 ));
266 }
267 }
268 }
269 Ok(Some(false))
270 }
271}
272
273fn component_gtk4_css(prefix: &str, c: &Component) -> String {
274 format!(
275 r#"
276@define-color {prefix}_color {};
277@define-color {prefix}_bg_color {};
278@define-color {prefix}_fg_color {};
279"#,
280 to_rgba(c.base),
281 to_rgba(c.base),
282 to_rgba(c.on),
283 )
284}
285
286fn color_css(prefix: &str, c_3: Srgba) -> String {
287 let oklch: palette::Oklch = c_3.into_color();
288 let c_2: Srgba = oklch.lighten(0.1).into_color();
289 let c_1: Srgba = oklch.lighten(0.2).into_color();
290 let c_4: Srgba = oklch.darken(0.1).into_color();
291 let c_5: Srgba = oklch.darken(0.2).into_color();
292 let c_1 = to_rgba(c_1);
293 let c_2 = to_rgba(c_2);
294 let c_3 = to_rgba(c_3);
295 let c_4 = to_rgba(c_4);
296 let c_5 = to_rgba(c_5);
297
298 format! {r#"
299@define-color {prefix}_1 {c_1};
300@define-color {prefix}_2 {c_2};
301@define-color {prefix}_3 {c_3};
302@define-color {prefix}_4 {c_4};
303@define-color {prefix}_5 {c_5};
304"#}
305}