From c1cfa024d6424d964841345b951cb6035ce54b65 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 12 May 2024 22:16:53 -0400 Subject: [PATCH] feat: basic vscode theme export support --- cosmic-theme/Cargo.toml | 19 +- cosmic-theme/src/image.rs | 1 - cosmic-theme/src/lib.rs | 2 + cosmic-theme/src/output/gtk4_output.rs | 14 +- cosmic-theme/src/output/mod.rs | 45 +++- cosmic-theme/src/output/vs_code.rs | 320 +++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 23 deletions(-) delete mode 100644 cosmic-theme/src/image.rs create mode 100644 cosmic-theme/src/output/vs_code.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 5acc85a..0e6c5c2 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -10,20 +10,23 @@ features = ["test_all_features"] rustdoc-args = ["--cfg", "docsrs"] [features] -default = [] -gtk4-output = [] +default = ["export"] +export = ["serde_json"] no-default = [] -theme-from-image = ["kmeans_colors", "image"] [dependencies] -palette = {version = "0.7.3", features = ["serializing"] } +palette = { version = "0.7.3", features = ["serializing"] } almost = "0.2" -kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } -image = {version = "0.24.1", optional = true } serde = { version = "1.0.129", features = ["derive"] } +serde_json = { version = "1.0.64", optional = true, features = [ + "preserve_order", +] } ron = "0.8" lazy_static = "1.4.0" -csscolorparser = {version = "0.6.2", features = ["serde"]} -cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription", "macro"] } +csscolorparser = { version = "0.6.2", features = ["serde"] } +cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ + "subscription", + "macro", +] } dirs.workspace = true thiserror = "1.0.5" diff --git a/cosmic-theme/src/image.rs b/cosmic-theme/src/image.rs deleted file mode 100644 index 52a319e..0000000 --- a/cosmic-theme/src/image.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO theme from image diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs index 6c46cb3..c30234b 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -9,6 +9,8 @@ pub use model::*; mod model; + +#[cfg(feature = "export")] mod output; /// composite colors in srgb diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index ccfbf2e..db21b8d 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -5,15 +5,8 @@ use std::{ io::Write, num::NonZeroUsize, }; -use thiserror::Error; -#[derive(Error, Debug)] -pub enum OutputError { - #[error("IO Error: {0}")] - Io(std::io::Error), - #[error("Missing config directory")] - MissingConfigDir, -} +use super::{to_hex, OutputError}; impl Theme { #[must_use] @@ -249,11 +242,6 @@ fn component_gtk4_css(prefix: &str, c: &Component) -> String { ) } -fn to_hex(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!("{:02x}{:02x}{:02x}", c_u8.red, c_u8.green, c_u8.blue) -} - 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(); diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index d2ea785..31b0f77 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,3 +1,46 @@ -#[cfg(feature = "gtk4-output")] +use palette::{rgb::Rgba, Srgba}; +use thiserror::Error; + +use crate::Theme; + /// Module for outputting the Cosmic gtk4 theme type as CSS pub mod gtk4_output; + +pub mod vs_code; + +#[derive(Error, Debug)] +pub enum OutputError { + #[error("IO Error: {0}")] + Io(std::io::Error), + #[error("Missing config directory")] + MissingConfigDir, +} + +impl Theme { + pub fn apply_exports(&self) -> Result<(), OutputError> { + let gtk_res = Theme::apply_gtk(self.is_dark); + let vs_res = self.clone().apply_vs_code(); + gtk_res?; + vs_res?; + Ok(()) + } + + pub fn write_exports(&self) -> Result<(), OutputError> { + let gtk_res = self.write_gtk4(); + gtk_res?; + Ok(()) + } + + pub fn reset_exports() -> Result<(), OutputError> { + let gtk_res = Theme::reset_gtk(); + let vs_res = Theme::reset_vs_code(); + gtk_res?; + vs_res?; + Ok(()) + } +} + +pub fn to_hex(c: Srgba) -> String { + let c_u8: Rgba = c.into_format(); + format!("{:02x}{:02x}{:02x}", c_u8.red, c_u8.green, c_u8.blue) +} diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs new file mode 100644 index 0000000..7557b0c --- /dev/null +++ b/cosmic-theme/src/output/vs_code.rs @@ -0,0 +1,320 @@ +use serde::{Deserialize, Serialize}; + +use crate::Theme; + +use super::{to_hex, OutputError}; + +/// Represents the workbench.colorCustomizations section of a VS Code settings.json file +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VsTheme { + #[serde(rename = "editor.background")] + editor_background: String, + #[serde(rename = "sideBar.background")] + sidebar_background: String, + #[serde(rename = "activityBar.background")] + activity_bar_background: String, + #[serde(rename = "notificationCenterHeader.background")] + notification_center_header_background: String, + #[serde(rename = "notifications.background")] + notifications_background: String, + #[serde(rename = "activityBarTop.activeBackground")] + activity_bar_top_active_background: String, + #[serde(rename = "editorGroupHeader.tabsBackground")] + editor_group_header_tabs_background: String, + #[serde(rename = "editorGroupHeader.noTabsBackground")] + editor_group_header_no_tabs_background: String, + #[serde(rename = "titleBar.activeBackground")] + title_bar_active_background: String, + #[serde(rename = "titleBar.inactiveBackground")] + title_bar_inactive_background: String, + #[serde(rename = "statusBar.background")] + status_bar_background: String, + #[serde(rename = "statusBar.noFolderBackground")] + status_bar_no_folder_background: String, + #[serde(rename = "statusBar.debuggingBackground")] + status_bar_debugging_background: String, + #[serde(rename = "tab.activeBackground")] + tab_active_background: String, + #[serde(rename = "tab.activeBorder")] + tab_active_border: String, + #[serde(rename = "tab.activeBorderTop")] + tab_active_border_top: String, + #[serde(rename = "tab.hoverBackground")] + tab_hover_background: String, + #[serde(rename = "quickInput.background")] + quick_input_background: String, + #[serde(rename = "tab.inactiveBackground")] + tab_inactive_background: String, + #[serde(rename = "sideBarSectionHeader.background")] + side_bar_section_header_background: String, + #[serde(rename = "list.focusOutline")] + list_focus_outline: String, + #[serde(rename = "banner.background")] + banner_background: String, + #[serde(rename = "breadcrumb.background")] + breadcrumb_background: String, + #[serde(rename = "commandCenter.background")] + command_center_background: String, + #[serde(rename = "terminal.background")] + terminal_background: String, + #[serde(rename = "menu.background")] + menu_background: String, + #[serde(rename = "panel.background")] + panel_background: String, + #[serde(rename = "peekViewEditorGutter.background")] + peek_view_editor_gutter_background: String, + #[serde(rename = "peekViewResult.background")] + peek_view_result_background: String, + #[serde(rename = "peekViewTitle.background")] + peek_view_title_background: String, + #[serde(rename = "peekViewEditor.background")] + peek_view_editor_background: String, + #[serde(rename = "peekViewResult.selectionBackground")] + peek_view_result_selection_background: String, + #[serde(rename = "editorWidget.background")] + editor_widget_background: String, + #[serde(rename = "editorSuggestWidget.background")] + editor_suggest_widget_background: String, + #[serde(rename = "editorHoverWidget.background")] + editor_hover_widget_background: String, + #[serde(rename = "input.background")] + input_background: String, + #[serde(rename = "dropdown.background")] + dropdown_background: String, + #[serde(rename = "settings.checkboxBackground")] + settings_checkbox_background: String, + #[serde(rename = "settings.textInputBackground")] + settings_text_input_background: String, + #[serde(rename = "settings.numberInputBackground")] + settings_number_input_background: String, + #[serde(rename = "settings.dropdownBackground")] + settings_dropdown_background: String, + #[serde(rename = "sideBar.dropBackground")] + side_bar_drop_background: String, + #[serde(rename = "list.activeSelectionBackground")] + list_active_selection_background: String, + #[serde(rename = "list.inactiveSelectionBackground")] + list_inactive_selection_background: String, + #[serde(rename = "list.focusBackground")] + list_focus_background: String, + #[serde(rename = "list.hoverBackground")] + list_hover_background: String, + + // text colors + #[serde(rename = "editor.foreground")] + editor_foreground: String, + #[serde(rename = "editorLineNumber.foreground")] + editor_line_number_foreground: String, + #[serde(rename = "editorCursor.foreground")] + editor_cursor_foreground: String, + #[serde(rename = "sideBar.foreground")] + side_bar_foreground: String, + #[serde(rename = "activityBar.foreground")] + activity_bar_foreground: String, + #[serde(rename = "statusBar.foreground")] + status_bar_foreground: String, + #[serde(rename = "tab.activeForeground")] + tab_active_foreground: String, + #[serde(rename = "tab.inactiveForeground")] + tab_inactive_foreground: String, + #[serde(rename = "editorGroupHeader.tabsForeground")] + editor_group_header_tabs_foreground: String, + #[serde(rename = "sideBarSectionHeader.foreground")] + side_bar_section_header_foreground: String, + #[serde(rename = "statusBar.debuggingForeground")] + status_bar_debugging_foreground: String, + #[serde(rename = "statusBar.noFolderForeground")] + status_bar_no_folder_foreground: String, + #[serde(rename = "editorWidget.foreground")] + editor_widget_foreground: String, + #[serde(rename = "editorSuggestWidget.foreground")] + editor_suggest_widget_foreground: String, + #[serde(rename = "editorHoverWidget.foreground")] + editor_hover_widget_foreground: String, + #[serde(rename = "input.foreground")] + input_foreground: String, + #[serde(rename = "dropdown.foreground")] + dropdown_foreground: String, + #[serde(rename = "terminal.foreground")] + terminal_foreground: String, + #[serde(rename = "menu.foreground")] + menu_foreground: String, + #[serde(rename = "panel.foreground")] + panel_foreground: String, + #[serde(rename = "peekViewEditorGutter.foreground")] + peek_view_editor_gutter_foreground: String, + #[serde(rename = "peekViewResult.selectionForeground")] + peek_view_result_selection_foreground: String, + #[serde(rename = "inputOption.activeBorder")] + input_option_active_border: String, + + // accent colors + #[serde(rename = "activityBarBadge.background")] + activity_bar_badge_background: String, + #[serde(rename = "statusBar.debuggingBorder")] + status_bar_debugging_border: String, + #[serde(rename = "button.background")] + button_background: String, + #[serde(rename = "button.hoverBackground")] + button_hover_background: String, + #[serde(rename = "statusBarItem.remoteBackground")] + status_bar_item_remote_background: String, + + + // accent fg colors + #[serde(rename = "activityBarBadge.foreground")] + activity_bar_badge_foreground: String, + #[serde(rename = "button.foreground")] + button_foreground: String, + #[serde(rename = "textLink.foreground")] + text_link_foreground: String, + #[serde(rename = "textLink.activeForeground")] + text_link_active_foreground: String, + #[serde(rename = "peekView.border")] + peek_view_border: String, + #[serde(rename = "settings.checkboxForeground")] + settings_checkbox_foreground: String, +} + +impl From for VsTheme { + fn from(theme: Theme) -> Self { + Self { + editor_background: format!("#{}", to_hex(theme.background.base)), + sidebar_background: format!("#{}", to_hex(theme.primary.base)), + activity_bar_background: format!("#{}", to_hex(theme.primary.base)), + notification_center_header_background: format!("#{}", to_hex(theme.background.base)), + notifications_background: format!("#{}", to_hex(theme.background.base)), + activity_bar_top_active_background: format!("#{}", to_hex(theme.primary.base)), + editor_group_header_tabs_background: format!("#{}", to_hex(theme.background.base)), + editor_group_header_no_tabs_background: format!("#{}", to_hex(theme.background.base)), + title_bar_active_background: format!("#{}", to_hex(theme.background.component.base)), + title_bar_inactive_background: format!( + "#{}", + to_hex( + theme + .background + .component.disabled + ) + ), + status_bar_background: format!("#{}", to_hex(theme.background.base)), + status_bar_no_folder_background: format!("#{}", to_hex(theme.background.base)), + status_bar_debugging_background: format!("#{}", to_hex(theme.background.base)), + tab_active_background: format!("#{}", to_hex(theme.primary.component.pressed)), + tab_active_border: format!("#{}", to_hex(theme.accent.base)), + tab_active_border_top: format!("#{}", to_hex(theme.accent.base)), + tab_hover_background: format!("#{}", to_hex(theme.primary.component.hover)), + tab_inactive_background: format!( + "#{}", + to_hex( + theme + .primary + .component + .base + ) + ), + quick_input_background: format!("#{}", to_hex(theme.primary.base)), + side_bar_section_header_background: format!("#{}", to_hex(theme.primary.base)), + banner_background: format!("#{}", to_hex(theme.primary.base)), + breadcrumb_background: format!("#{}", to_hex(theme.primary.base)), + command_center_background: format!("#{}", to_hex(theme.primary.base)), + terminal_background: format!("#{}", to_hex(theme.primary.base)), + menu_background: format!("#{}", to_hex(theme.primary.base)), + panel_background: format!("#{}", to_hex(theme.primary.base)), + peek_view_editor_gutter_background: format!("#{}", to_hex(theme.background.base)), + peek_view_result_background: format!("#{}", to_hex(theme.background.base)), + peek_view_title_background: format!("#{}", to_hex(theme.background.base)), + peek_view_editor_background: format!("#{}", to_hex(theme.background.base)), + peek_view_result_selection_background: format!("#{}", to_hex(theme.background.base)), + editor_widget_background: format!("#{}", to_hex(theme.background.base)), + editor_suggest_widget_background: format!("#{}", to_hex(theme.background.base)), + editor_hover_widget_background: format!("#{}", to_hex(theme.background.base)), + input_background: format!("#{}", to_hex(theme.background.base)), + dropdown_background: format!("#{}", to_hex(theme.background.base)), + settings_checkbox_background: format!("#{}", to_hex(theme.background.base)), + settings_text_input_background: format!("#{}", to_hex(theme.background.base)), + settings_number_input_background: format!("#{}", to_hex(theme.background.base)), + settings_dropdown_background: format!("#{}", to_hex(theme.background.base)), + side_bar_drop_background: format!("#{}", to_hex(theme.background.base)), + list_active_selection_background: format!("#{}", to_hex(theme.primary.base)), + list_inactive_selection_background: format!("#{}", to_hex(theme.primary.base)), + list_focus_background: format!("#{}", to_hex(theme.primary.base)), + list_hover_background: format!("#{}", to_hex(theme.primary.base)), + editor_foreground: format!("#{}", to_hex(theme.background.on)), + editor_line_number_foreground: format!("#{}", to_hex(theme.background.on)), + editor_cursor_foreground: format!("#{}", to_hex(theme.background.on)), + side_bar_foreground: format!("#{}", to_hex(theme.primary.on)), + activity_bar_foreground: format!("#{}", to_hex(theme.primary.on)), + status_bar_foreground: format!("#{}", to_hex(theme.primary.on)), + tab_active_foreground: format!("#{}", to_hex(theme.primary.on)), + tab_inactive_foreground: format!("#{}", to_hex(theme.primary.on)), + editor_group_header_tabs_foreground: format!("#{}", to_hex(theme.primary.on)), + side_bar_section_header_foreground: format!("#{}", to_hex(theme.primary.on)), + status_bar_debugging_foreground: format!("#{}", to_hex(theme.primary.on)), + status_bar_no_folder_foreground: format!("#{}", to_hex(theme.primary.on)), + editor_widget_foreground: format!("#{}", to_hex(theme.primary.on)), + editor_suggest_widget_foreground: format!("#{}", to_hex(theme.primary.on)), + editor_hover_widget_foreground: format!("#{}", to_hex(theme.primary.on)), + input_foreground: format!("#{}", to_hex(theme.primary.on)), + dropdown_foreground: format!("#{}", to_hex(theme.primary.on)), + terminal_foreground: format!("#{}", to_hex(theme.primary.on)), + menu_foreground: format!("#{}", to_hex(theme.primary.on)), + panel_foreground: format!("#{}", to_hex(theme.primary.on)), + peek_view_editor_gutter_foreground: format!("#{}", to_hex(theme.primary.on)), + peek_view_result_selection_foreground: format!("#{}", to_hex(theme.primary.on)), + input_option_active_border: format!("#{}", to_hex(theme.accent.base)), + activity_bar_badge_background: format!("#{}", to_hex(theme.accent.base)), + activity_bar_badge_foreground: format!("#{}", to_hex(theme.accent.on)), + status_bar_debugging_border: format!("#{}", to_hex(theme.accent.base)), + list_focus_outline: format!("#{}", to_hex(theme.accent.base)), + button_background: format!("#{}", to_hex(theme.accent_button.base)), + button_hover_background: format!("#{}", to_hex(theme.accent_button.hover)), + status_bar_item_remote_background: format!("#{}", to_hex(theme.accent.base)), + button_foreground: format!("#{}", to_hex(theme.accent_button.on)), + text_link_foreground: format!("#{}", to_hex(theme.accent.base)), + text_link_active_foreground: format!("#{}", to_hex(theme.accent.base)), + peek_view_border: format!("#{}", to_hex(theme.accent.base)), + settings_checkbox_foreground: format!("#{}", to_hex(theme.accent.base)), + } + } +} + +impl Theme { + pub fn apply_vs_code(self) -> Result<(), OutputError> { + let vs_theme = VsTheme::from(self); + let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; + let vs_code_dir = config_dir.join("Code").join("User"); + if !vs_code_dir.exists() { + std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?; + } + + // just add the json entry for workbench.colorCustomizations + let settings_file = vs_code_dir.join("settings.json"); + let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); + let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default(); + settings["workbench.colorCustomizations"] = serde_json::to_value(vs_theme).unwrap(); + std::fs::write( + &settings_file, + serde_json::to_string_pretty(&settings).unwrap(), + ) + .map_err(OutputError::Io)?; + + Ok(()) + } + + pub fn reset_vs_code() -> Result<(), OutputError> { + let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; + let vs_code_dir = config_dir.join("Code").join("User"); + let settings_file = vs_code_dir.join("settings.json"); + // just remove the json entry for workbench.colorCustomizations + let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); + let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default(); + settings["workbench.colorCustomizations"] = serde_json::Value::Null; + std::fs::write( + &settings_file, + serde_json::to_string_pretty(&settings).unwrap(), + ) + .map_err(OutputError::Io)?; + + Ok(()) + } +}