diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 0e14108d..2f7cfa7d 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -17,6 +17,8 @@ pub enum OutputError { Io(std::io::Error), #[error("Missing config directory")] MissingConfigDir, + #[error("Missing data directory")] + MissingDataDir, #[error("Serde Error: {0}")] Serde(#[from] serde_json::Error), } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 1c9d1c08..fbde8a9d 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -14,7 +14,7 @@ impl Theme { #[cold] /// Some high-level documentation for this file can be found at: /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ - pub fn as_qt_color_scheme(&self) -> String { + pub fn as_qt(&self) -> String { let mut output = "# GENERATED BY COSMIC\n".to_string(); // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter @@ -179,10 +179,136 @@ inactiveForeground=161,169,177 output } + + /// Write the color scheme to the appropriate directory. + /// Should be written in `~/.local/share/color-schemes/`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error writing the colors file. + #[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 mut file = File::create(data_dir.join(file_name)).map_err(OutputError::Io)?; + file.write_all(colors.as_bytes()).map_err(OutputError::Io)?; + + Ok(()) + } + + /// Apply the color scheme by copying it to `~/.config/kdeglobals`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error applying the color scheme. + #[cold] + pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> { + 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 dest_file = config_dir.join("kdeglobals"); + + #[cfg(target_family = "unix")] + { + use std::os::unix::fs::symlink; + Self::backup_non_cosmic_kdeglobals(&dest_file).map_err(OutputError::Io)?; + + if dest_file.exists() { + // Currently we overwrite the file with our symlink. + // Maybe in the future we could merge kdeglobals with our color scheme ini? + fs::remove_file(&dest_file).map_err(OutputError::Io)?; + } + + symlink(src_file, dest_file).map_err(OutputError::Io)? + } + + Ok(()) + } + + /// Reset the applied qt colors by deleting the `~/.config/kdeglobals` file. + /// + /// This does not restore the backed up kdeglobals file. + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error resetting the CSS file. + #[cold] + pub fn reset_qt() -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + let dest_file = config_dir.join("kdeglobals"); + + if Self::is_cosmic_kdeglobals(&dest_file).map_err(OutputError::Io)?.unwrap_or_default() { + fs::remove_file(dest_file).map_err(OutputError::Io)? + } + Ok(()) + } + + #[cold] + fn backup_non_cosmic_kdeglobals(path: &Path) -> io::Result<()> { + if !Self::is_cosmic_kdeglobals(path)?.unwrap_or(true) { + let backup_path = path.with_extension("bak"); + fs::rename(path, &backup_path)?; + } + Ok(()) + } + + #[cold] + fn is_cosmic_kdeglobals(path: &Path) -> io::Result> { + if !path.exists() { + return Ok(None); + } + + if let Ok(metadata) = fs::symlink_metadata(path) { + if !metadata.file_type().is_symlink() { + return Ok(Some(false)); + } + let Ok(target) = fs::read_link(path) else { + return Ok(Some(false)); + }; + let Some(target_filename) = target.file_name() else { + return Ok(Some(false)); + }; + return Ok(Some( + target_filename == "CosmicDark.colors" || target_filename == "CosmicLight.colors", + )); + } + Ok(Some(false)) + } } /// Formats a color in the form `r,g,b` e.g. `255,255,255` -pub fn to_rgb(c: Srgba) -> String { +fn to_rgb(c: Srgba) -> String { let c_u8: Rgba = c.into_format(); format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue) }