use crate::{composite::over, steps::steps, Component, Theme}; use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba}; use std::{ fs::{self, File}, io::{self, Write}, num::NonZeroUsize, path::Path, }; use super::{to_rgba, OutputError}; impl Theme { #[must_use] #[cold] /// turn the theme into css pub fn as_gtk4(&self) -> String { let Self { background, primary, secondary, accent, destructive, warning, success, palette, .. } = self; let window_bg = to_rgba(background.base); let window_fg = to_rgba(background.on); let view_bg = to_rgba(primary.base); let view_fg = to_rgba(primary.on); let headerbar_bg = to_rgba(background.base); let headerbar_fg = to_rgba(background.on); let headerbar_border_color = to_rgba(background.divider); let sidebar_bg = to_rgba(primary.base); let sidebar_fg = to_rgba(primary.on); let sidebar_shade = to_rgba(if self.is_dark { Rgba::new(0.0, 0.0, 0.0, 0.08) } else { Rgba::new(0.0, 0.0, 0.0, 0.32) }); let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 }); let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base)); let secondary_sidebar_bg = to_rgba(secondary.base); let secondary_sidebar_fg = to_rgba(secondary.on); let secondary_sidebar_shade = to_rgba(if self.is_dark { Rgba::new(0.0, 0.0, 0.0, 0.08) } else { Rgba::new(0.0, 0.0, 0.0, 0.32) }); let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base)); let headerbar_backdrop = to_rgba(background.base); let card_bg = to_rgba(background.component.base); let card_fg = to_rgba(background.component.on); let thumbnail_bg = to_rgba(background.component.base); let thumbnail_fg = to_rgba(background.component.on); let dialog_bg = to_rgba(primary.base); let dialog_fg = to_rgba(primary.on); let popover_bg = to_rgba(background.component.base); let popover_fg = to_rgba(background.component.on); let shade = to_rgba(if self.is_dark { Rgba::new(0.0, 0.0, 0.0, 0.32) } else { Rgba::new(0.0, 0.0, 0.0, 0.08) }); let mut inverted_bg_divider = background.base; inverted_bg_divider.alpha = 0.5; let scrollbar_outline = to_rgba(inverted_bg_divider); let mut css = format! {r#"/* GENERATED BY COSMIC */ @define-color window_bg_color {window_bg}; @define-color window_fg_color {window_fg}; @define-color view_bg_color {view_bg}; @define-color view_fg_color {view_fg}; @define-color headerbar_bg_color {headerbar_bg}; @define-color headerbar_fg_color {headerbar_fg}; @define-color headerbar_border_color_color {headerbar_border_color}; @define-color headerbar_backdrop_color {headerbar_backdrop}; @define-color sidebar_bg_color {sidebar_bg}; @define-color sidebar_fg_color {sidebar_fg}; @define-color sidebar_shade_color {sidebar_shade}; @define-color sidebar_backdrop_color {sidebar_backdrop}; @define-color secondary_sidebar_bg_color {secondary_sidebar_bg}; @define-color secondary_sidebar_fg_color {secondary_sidebar_fg}; @define-color secondary_sidebar_shade_color {secondary_sidebar_shade}; @define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop}; @define-color card_bg_color {card_bg}; @define-color card_fg_color {card_fg}; @define-color thumbnail_bg_color {thumbnail_bg}; @define-color thumbnail_fg_color {thumbnail_fg}; @define-color dialog_bg_color {dialog_bg}; @define-color dialog_fg_color {dialog_fg}; @define-color popover_bg_color {popover_bg}; @define-color popover_fg_color {popover_fg}; @define-color shade_color {shade}; @define-color scrollbar_outline_color {scrollbar_outline}; "#}; css.push_str(&component_gtk4_css("accent", accent)); css.push_str(&component_gtk4_css("destructive", destructive)); css.push_str(&component_gtk4_css("warning", warning)); css.push_str(&component_gtk4_css("success", success)); css.push_str(&component_gtk4_css("accent", accent)); css.push_str(&component_gtk4_css("error", destructive)); css.push_str(&color_css("blue", palette.accent_blue)); css.push_str(&color_css("green", palette.accent_green)); css.push_str(&color_css("yellow", palette.accent_yellow)); css.push_str(&color_css("red", palette.accent_red)); css.push_str(&color_css("orange", palette.ext_orange)); css.push_str(&color_css("purple", palette.ext_purple)); let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap()); for (i, c) in neutral_steps[..5].iter().enumerate() { css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c))); } for (i, c) in neutral_steps[5..].iter().enumerate() { css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c))); } css } /// write the CSS to the appropriate directory /// Should be written in the XDG config directory for gtk-4.0 /// /// # Errors /// /// Returns an `OutputError` if there is an error writing the CSS file. #[cold] pub fn write_gtk4(&self) -> Result<(), OutputError> { let css_str = self.as_gtk4(); let Some(config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; let name = if self.is_dark { "dark.css" } else { "light.css" }; let config_dir = config_dir.join("gtk-4.0").join("cosmic"); if !config_dir.exists() { std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; } let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?; file.write_all(css_str.as_bytes()) .map_err(OutputError::Io)?; Ok(()) } /// Apply gtk color variable settings /// /// # Errors /// /// Returns an `OutputError` if there is an error applying the CSS file. #[cold] pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> { let Some(config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; let gtk4 = config_dir.join("gtk-4.0"); let gtk3 = config_dir.join("gtk-3.0"); fs::create_dir_all(>k4).map_err(OutputError::Io)?; fs::create_dir_all(>k3).map_err(OutputError::Io)?; let cosmic_css_dir = gtk4.join("cosmic"); let cosmic_css = cosmic_css_dir .clone() .join(if is_dark { "dark.css" } else { "light.css" }); let gtk4_dest = gtk4.join("gtk.css"); let gtk3_dest = gtk3.join("gtk.css"); #[cfg(target_family = "unix")] for gtk_dest in [>k4_dest, >k3_dest] { use std::os::unix::fs::symlink; Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?; if gtk_dest.exists() { fs::remove_file(gtk_dest).map_err(OutputError::Io)?; } symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?; } Ok(()) } /// Reset the applied gtk css /// /// # Errors /// /// Returns an `OutputError` if there is an error resetting the CSS file. #[cold] pub fn reset_gtk() -> Result<(), OutputError> { let Some(config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; let gtk4 = config_dir.join("gtk-4.0"); let gtk3 = config_dir.join("gtk-3.0"); let gtk4_dest = gtk4.join("gtk.css"); let cosmic_css = gtk4.join("cosmic"); let gtk3_dest = gtk3.join("gtk.css"); let res = Self::reset_cosmic_css(>k3_dest, &cosmic_css).map_err(OutputError::Io); Self::reset_cosmic_css(>k4_dest, &cosmic_css).map_err(OutputError::Io)?; res } #[cold] fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> { if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) { let backup_path = path.with_extension("css.bak"); fs::rename(path, &backup_path)?; } Ok(()) } #[cold] fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> { if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() { fs::remove_file(path)?; } Ok(()) } fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result> { if !path.exists() { return Ok(None); } if let Ok(metadata) = fs::symlink_metadata(path) { if metadata.file_type().is_symlink() { if let Ok(actual_cosmic_css) = fs::read_link(path) { let canonical_target = fs::canonicalize(&actual_cosmic_css)?; let canonical_base = fs::canonicalize(cosmic_css)?; return Ok(Some( canonical_target == canonical_base || canonical_target.starts_with(&canonical_base), )); } } } Ok(Some(false)) } } fn component_gtk4_css(prefix: &str, c: &Component) -> String { format!( r#" @define-color {prefix}_color {}; @define-color {prefix}_bg_color {}; @define-color {prefix}_fg_color {}; "#, to_rgba(c.base), to_rgba(c.base), to_rgba(c.on), ) } fn color_css(prefix: &str, c_3: Srgba) -> String { let oklch: palette::Oklch = c_3.into_color(); let c_2: Srgba = oklch.lighten(0.1).into_color(); let c_1: Srgba = oklch.lighten(0.2).into_color(); let c_4: Srgba = oklch.darken(0.1).into_color(); let c_5: Srgba = oklch.darken(0.2).into_color(); let c_1 = to_rgba(c_1); let c_2 = to_rgba(c_2); let c_3 = to_rgba(c_3); let c_4 = to_rgba(c_4); let c_5 = to_rgba(c_5); format! {r#" @define-color {prefix}_1 {c_1}; @define-color {prefix}_2 {c_2}; @define-color {prefix}_3 {c_3}; @define-color {prefix}_4 {c_4}; @define-color {prefix}_5 {c_5}; "#} }