From 3b1452d3542671814da42691d52ea12c40afeddf Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 11 Feb 2026 00:18:56 +0000 Subject: [PATCH] feat: configure qt5ct and qt6ct --- cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/output/mod.rs | 8 ++ cosmic-theme/src/output/qt56ct_output.rs | 113 +++++++++++++++++++++++ cosmic-theme/src/output/qt_output.rs | 50 +++++----- 4 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 cosmic-theme/src/output/qt56ct_output.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 44b0df5a..2315d77e 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -29,3 +29,4 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features ] } dirs.workspace = true thiserror = "2.0.16" +configparser = "3.1.0" diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 4cc76eaf..61f0e49d 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -6,6 +6,8 @@ use crate::Theme; /// Module for outputting the Cosmic gtk4 theme type as CSS pub mod gtk4_output; +/// Module for configuring qt5ct and qt6ct to use our qt theme +pub mod qt56ct_output; /// Module for outputting the Cosmic qt theme type as kdeglobals pub mod qt_output; @@ -21,6 +23,8 @@ pub enum OutputError { MissingDataDir, #[error("Serde Error: {0}")] Serde(#[from] serde_json::Error), + #[error("Ini Error: {0}")] + Ini(String), } impl Theme { @@ -28,9 +32,11 @@ impl Theme { pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); let qt_res = Theme::apply_qt(self.is_dark); + let qt56ct_res = Theme::apply_qt56ct(self.is_dark); let vs_res = self.clone().apply_vs_code(); gtk_res?; qt_res?; + qt56ct_res?; vs_res?; Ok(()) } @@ -41,8 +47,10 @@ impl Theme { pub fn apply_exports_static(is_dark: bool) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(is_dark); let qt_res = Theme::apply_qt(is_dark); + let qt56ct_res = Theme::apply_qt56ct(is_dark); gtk_res?; qt_res?; + qt56ct_res?; Ok(()) } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs new file mode 100644 index 00000000..d4736597 --- /dev/null +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -0,0 +1,113 @@ +use crate::Theme; +use configparser::ini::Ini; +use std::{ + fs::{self, File}, + path::PathBuf, +}; + +use super::OutputError; + +impl Theme { + /// The "version" of this theme. + /// + /// To avoid repeatedly overwriting the user's config, we use a version system. + /// + /// Increment this value when changes to qt{5,6}ct.conf are needed. + /// If the config's version is outdated, we update several sections. + /// Otherwise, only the light/dark mode is updated. + const COSMIC_QT_VERSION: u64 = 1; + + /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. + #[cold] + pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> { + let qt5ct_res = Self::apply_ct("qt5ct", is_dark); + let qt6ct_res = Self::apply_ct("qt6ct", is_dark); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> { + let path = Self::get_conf_path(ct)?; + let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; + let mut ini = Ini::new_cs(); + ini.read(file_content).map_err(OutputError::Ini)?; + + let old_version = ini + .getuint("Appearance", "cosmic_qt_version") + .map_err(OutputError::Ini)? + .unwrap_or_default(); + + let color_scheme_path = Self::get_qt_colors_path(is_dark)?; + let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; + + ini.set( + "Appearance", + "cosmic_qt_version", + Some(Theme::COSMIC_QT_VERSION.to_string()), + ); + + if old_version < Theme::COSMIC_QT_VERSION { + // Config is outdated, update it unconditionally! + + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + // Enable the above color scheme, instead of using the default color scheme of e.g. Breeze + ini.setstr("Appearance", "custom_palette", Some("true")); + // COSMIC icons are stuck in light mode, so use breeze icons instead + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + // Use COSMIC dialogs instead of KDE's + ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal")); + + // TODO: Add fonts section to match COSMIC + } else { + // Config is not outdated, check before updating light/dark mode only! + + let old_color_scheme_path = ini + .get("Appearance", "color_scheme_path") + .unwrap_or_else(|| "CosmicPlease".to_owned()); + if old_color_scheme_path.contains("Cosmic") { + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + } + + let old_icon_theme = ini + .get("Appearance", "icon_theme") + .unwrap_or_else(|| "breeze".to_owned()); + if old_icon_theme.contains("breeze") { + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + } + } + + ini.write(path).map_err(OutputError::Io)?; + Ok(()) + } + + /// Returns the file paths of the form `~/.config/ct/ct.conf`: + /// e.g. `~/.config/qt6ct/qt6ct.conf`. + /// + /// The file and its parent directory are created if they don't exist. + fn get_conf_path(ct: &str) -> Result { + let Some(mut config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + config_dir.push(&ct); + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + } + + let file_path = config_dir.join(ct.to_owned() + ".conf"); + if !file_path.exists() { + File::create_new(&file_path).map_err(OutputError::Io)?; + } + + Ok(file_path) + } +} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index e2858260..bfbe44f4 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -125,7 +125,7 @@ impl Theme { } else { "COSMIC Light" }; - // COSMIC icons are stuck in light mode, so use breeze instead + // COSMIC icons are stuck in light mode, so use breeze icons instead let icons_theme = if self.is_dark { "breeze-dark" } else { @@ -207,22 +207,7 @@ widgetStyle=qt6ct-style #[cold] pub fn write_qt(&self) -> Result<(), OutputError> { let colors = self.as_qt(); - let Some(mut data_dir) = dirs::data_dir() else { - return Err(OutputError::MissingDataDir); - }; - - let file_name = if self.is_dark { - "CosmicDark.colors" - } else { - "CosmicLight.colors" - }; - - data_dir.push("color-schemes"); - if !data_dir.exists() { - std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?; - } - - let file_path = data_dir.join(file_name); + let file_path = Self::get_qt_colors_path(self.is_dark)?; let tmp_file_path = file_path.with_extension("colors.new"); // Write to tmp_file_path first, then move it to file_path @@ -251,16 +236,7 @@ widgetStyle=qt6ct-style let Some(config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; - let Some(mut data_dir) = dirs::data_dir() else { - return Err(OutputError::MissingDataDir); - }; - data_dir.push("color-schemes"); - - let src_file = data_dir.join(if is_dark { - "CosmicDark.colors" - } else { - "CosmicLight.colors" - }); + let src_file = Self::get_qt_colors_path(is_dark)?; let kdeglobals_file = config_dir.join("kdeglobals"); @@ -304,6 +280,26 @@ widgetStyle=qt6ct-style Ok(()) } + /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` + pub fn get_qt_colors_path(is_dark: bool) -> Result { + let Some(mut data_dir) = dirs::data_dir() else { + return Err(OutputError::MissingDataDir); + }; + + let file_name = if is_dark { + "CosmicDark.colors" + } else { + "CosmicLight.colors" + }; + + data_dir.push("color-schemes"); + if !data_dir.exists() { + std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?; + } + + Ok(data_dir.join(file_name)) + } + #[cold] fn backup_non_cosmic_kdeglobals(path: &Path) -> io::Result<()> { if !Self::is_cosmic_kdeglobals(path)?.unwrap_or(true) {