From 0e513185e026a1f12217aae47af4623cf5505476 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 20 Aug 2025 21:20:33 -0400 Subject: [PATCH] support for custom themes --- Common/SettingsData.qml | 19 ++- Common/Theme.qml | 148 +++++++++++++--------- Modals/FileBrowserModal.qml | 4 +- Modules/Settings/ThemeColorsTab.qml | 185 ++++++++++++++++++++++------ Modules/Toast.qml | 8 +- docs/CUSTOM_THEMES.md | 127 +++++++++++++++++++ docs/theme_cyberpunk_electric.json | 42 +++++++ docs/theme_hotline_miami.json | 42 +++++++ docs/theme_miami_vice.json | 42 +++++++ docs/theme_synthwave_electric.json | 42 +++++++ 10 files changed, 554 insertions(+), 105 deletions(-) create mode 100644 docs/CUSTOM_THEMES.md create mode 100644 docs/theme_cyberpunk_electric.json create mode 100644 docs/theme_hotline_miami.json create mode 100644 docs/theme_miami_vice.json create mode 100644 docs/theme_synthwave_electric.json diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index 0c8955ab..f9cb5c93 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -12,6 +12,7 @@ Singleton { // Theme settings property string currentThemeName: "blue" + property string customThemeFile: "" property real topBarTransparency: 0.75 property real topBarWidgetTransparency: 0.85 property real popupTransparency: 0.92 @@ -65,6 +66,7 @@ Singleton { property string systemDefaultIconTheme: "" property bool qt5ctAvailable: false property bool qt6ctAvailable: false + property bool gtkAvailable: false property bool useOSLogo: false property string osLogoColorOverride: "" property real osLogoBrightness: 0.5 @@ -126,6 +128,7 @@ Singleton { } else { currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue" } + customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "" topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency @@ -288,6 +291,7 @@ Singleton { function saveSettings() { settingsFile.setText(JSON.stringify({ "currentThemeName": currentThemeName, + "customThemeFile": customThemeFile, "topBarTransparency": topBarTransparency, "topBarWidgetTransparency": topBarWidgetTransparency, "popupTransparency": popupTransparency, @@ -459,6 +463,11 @@ Singleton { saveSettings() } + function setCustomThemeFile(filePath) { + customThemeFile = filePath + saveSettings() + } + function setTopBarTransparency(transparency) { topBarTransparency = transparency saveSettings() @@ -808,11 +817,17 @@ Singleton { function setGtkThemingEnabled(enabled) { gtkThemingEnabled = enabled saveSettings() + if (enabled && typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } } function setQtThemingEnabled(enabled) { qtThemingEnabled = enabled saveSettings() + if (enabled && typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } } function setShowDock(enabled) { @@ -967,7 +982,7 @@ Singleton { Process { id: qtToolsDetectionProcess - command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'"] + command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] running: false stdout: StdioCollector { @@ -980,6 +995,8 @@ Singleton { qt5ctAvailable = line.split(':')[1] === 'true' else if (line.startsWith('qt6ct:')) qt6ctAvailable = line.split(':')[1] === 'true' + else if (line.startsWith('gtk:')) + gtkAvailable = line.split(':')[1] === 'true' } } } diff --git a/Common/Theme.qml b/Common/Theme.qml index e917af74..904d0a75 100644 --- a/Common/Theme.qml +++ b/Common/Theme.qml @@ -1,24 +1,23 @@ pragma Singleton pragma ComponentBehavior: Bound +import QtCore import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.UPower -import Qt.labs.platform +import qs.Services import "StockThemes.js" as StockThemes Singleton { id: root - // Theme selection property string currentTheme: "blue" property bool isLightMode: false readonly property string dynamic: "dynamic" readonly property bool isDynamicTheme: !StockThemes.isStockTheme(currentTheme) - // Dynamic color extraction properties readonly property string homeDir: { const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString() return url.startsWith("file://") ? url.substring(7) : url @@ -31,14 +30,14 @@ Singleton { readonly property string wallpaperPath: typeof SessionData !== "undefined" ? SessionData.wallpaperPath : "" property bool matugenAvailable: false - property bool gtkThemingEnabled: false - property bool qtThemingEnabled: false + property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false + property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false property bool systemThemeGenerationInProgress: false property var matugenColors: ({}) property bool extractionRequested: false property int colorUpdateTrigger: 0 + property var customThemeData: null - // Helper function to get matugen colors (unified from Colors.qml) function getMatugenColor(path, fallback) { colorUpdateTrigger const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" @@ -51,9 +50,10 @@ Singleton { return cur || fallback } - // Current theme data readonly property var currentThemeData: { - if (isDynamicTheme) { + if (currentTheme === "custom") { + return customThemeData || StockThemes.getThemeByName("blue", isLightMode) + } else if (currentTheme === dynamic) { return { primary: getMatugenColor("primary", "#42a5f5"), primaryText: getMatugenColor("on_primary", "#ffffff"), @@ -68,14 +68,16 @@ Singleton { backgroundText: getMatugenColor("on_background", "#e3e8ef"), outline: getMatugenColor("outline", "#8e918f"), surfaceContainer: getMatugenColor("surface_container", "#1e2023"), - surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f") + surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f"), + error: "#F2B8B5", + warning: "#FF9800", + info: "#2196F3" } } else { return StockThemes.getThemeByName(currentTheme, isLightMode) } } - // Core color properties (unified from both Theme.qml and Colors.qml) property color primary: currentThemeData.primary property color primaryText: currentThemeData.primaryText property color primaryContainer: currentThemeData.primaryContainer @@ -91,12 +93,10 @@ Singleton { property color surfaceContainer: currentThemeData.surfaceContainer property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh - // Additional semantic colors - property color error: "#F2B8B5" - property color warning: "#FF9800" - property color info: "#2196F3" + property color error: currentThemeData.error || "#F2B8B5" + property color warning: currentThemeData.warning || "#FF9800" + property color info: currentThemeData.info || "#2196F3" - // Interaction states property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12) property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08) property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16) @@ -125,7 +125,6 @@ Singleton { property color shadowMedium: Qt.rgba(0, 0, 0, 0.08) property color shadowStrong: Qt.rgba(0, 0, 0, 0.3) - // Animation and timing property int shortDuration: 150 property int mediumDuration: 300 property int longDuration: 500 @@ -133,7 +132,6 @@ Singleton { property int standardEasing: Easing.OutCubic property int emphasizedEasing: Easing.OutQuart - // Layout and sizing property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12 property real spacingXS: 4 property real spacingS: 8 @@ -149,29 +147,26 @@ Singleton { property real iconSizeSmall: 16 property real iconSizeLarge: 32 - // Transparency settings property real panelTransparency: 0.85 property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85 property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92 - // Theme switching API function switchTheme(themeName, savePrefs = true) { if (themeName === dynamic) { - if (StockThemes.isStockTheme(currentTheme)) { - // Switching from stock to dynamic, restore old theme - restoreSystemThemes() - } currentTheme = dynamic extractColors() - } else { - if (!StockThemes.isStockTheme(currentTheme)) { - // Switching from dynamic to stock - restoreSystemThemes() + } else if (themeName === "custom") { + currentTheme = "custom" + if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) { + loadCustomThemeFromFile(SettingsData.customThemeFile) } + } else { currentTheme = themeName } if (savePrefs && typeof SettingsData !== "undefined") SettingsData.setTheme(currentTheme) + + generateSystemThemesFromCurrentTheme() } function toggleLightMode(savePrefs = true) { @@ -190,15 +185,33 @@ Singleton { } function getThemeColors(themeName) { + if (themeName === "custom" && customThemeData) { + return customThemeData + } return StockThemes.getThemeByName(themeName, isLightMode) } + function loadCustomTheme(themeData) { + if (themeData.dark || themeData.light) { + const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" + const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light + customThemeData = selectedTheme + } else { + customThemeData = themeData + } + + generateSystemThemesFromCurrentTheme() + } + + function loadCustomThemeFromFile(filePath) { + customThemeFileView.path = filePath + } + property alias availableThemeNames: root._availableThemeNames readonly property var _availableThemeNames: StockThemes.getAllThemeNames() property string currentThemeName: currentTheme - // Background helper functions function popupBackground() { return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency) } @@ -223,7 +236,6 @@ Singleton { return popupTransparency } - // Utility functions function isColorDark(c) { return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5 } @@ -305,7 +317,6 @@ Singleton { } } - // Dynamic color extraction (merged from Colors.qml) function extractColors() { extractionRequested = true if (matugenAvailable) @@ -321,6 +332,10 @@ Singleton { generateSystemThemes() } } + + if (currentTheme === "custom" && customThemeFileView.path) { + customThemeFileView.reload() + } } function generateSystemThemes() { @@ -337,19 +352,30 @@ Singleton { systemThemeGenerator.running = true } - function restoreSystemThemes() { - if (!shellDir) return + function generateSystemThemesFromCurrentTheme() { + if (!isDynamicTheme) + return + + if (systemThemeGenerationInProgress) + return + + if (!matugenAvailable || !wallpaperPath) + return const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false" const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false" const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false" - systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming] - systemThemeRestoreProcess.running = true + if (gtkTheming === "false" && qtTheming === "false") + return + + systemThemeGenerationInProgress = true + systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming] + systemThemeGenerator.running = true } - // JSON extraction helper + function extractJsonFromText(text) { if (!text) return null @@ -400,7 +426,6 @@ Singleton { return null } - // Process definitions for dynamic theming Process { id: matugenCheck command: ["which", "matugen"] @@ -505,30 +530,7 @@ Singleton { } } - Process { - id: systemThemeRestoreProcess - running: false - stdout: StdioCollector { - id: restoreThemeStdout - } - - stderr: StdioCollector { - id: restoreThemeStderr - } - - onExited: exitCode => { - if (typeof ToastService !== "undefined") { - if (exitCode === 0) { - ToastService.showInfo("System themes restored to default") - } else { - ToastService.showWarning("Failed to restore system themes: " + restoreThemeStderr.text) - } - } - } - } - - // Generate app configs function generateAppConfigs() { if (!matugenColors || !matugenColors.colors) { return @@ -623,4 +625,32 @@ Singleton { if (typeof SessionData !== "undefined") SessionData.isLightModeChanged.connect(root.onLightModeChanged) } + + FileView { + id: customThemeFileView + watchChanges: true + + function parseAndLoadTheme() { + try { + var themeData = JSON.parse(customThemeFileView.text()) + loadCustomTheme(themeData) + } catch (e) { + ToastService.showError("Invalid JSON format: " + e.message) + } + } + + onLoaded: { + parseAndLoadTheme() + } + + onFileChanged: { + customThemeFileView.reload() + } + + onLoadFailed: function(error) { + if (typeof ToastService !== "undefined") { + ToastService.showError("Failed to read theme file: " + error) + } + } + } } \ No newline at end of file diff --git a/Modals/FileBrowserModal.qml b/Modals/FileBrowserModal.qml index c8d21ba6..6d89c1e5 100644 --- a/Modals/FileBrowserModal.qml +++ b/Modals/FileBrowserModal.qml @@ -19,15 +19,17 @@ DankModal { StandardPaths.HomeLocation) property string currentPath: "" property var fileExtensions: ["*.*"] + property alias filterExtensions: fileBrowserModal.fileExtensions property string browserTitle: "Select File" property string browserIcon: "folder_open" property string browserType: "generic" // "wallpaper" or "profile" for last path memory + property bool showHiddenFiles: false FolderListModel { id: folderModel showDirsFirst: true showDotAndDotDot: false - showHidden: false + showHidden: fileBrowserModal.showHiddenFiles nameFilters: fileExtensions showFiles: true showDirs: true diff --git a/Modules/Settings/ThemeColorsTab.qml b/Modules/Settings/ThemeColorsTab.qml index d23f3aa7..a4571950 100644 --- a/Modules/Settings/ThemeColorsTab.qml +++ b/Modules/Settings/ThemeColorsTab.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Controls +import Quickshell.Io import qs.Common +import qs.Modals import qs.Services import qs.Widgets @@ -179,7 +181,6 @@ Item { Row { spacing: Theme.spacingM - anchors.horizontalCenter: parent.horizontalCenter Repeater { model: Theme.availableThemeNames.slice(0, 5) @@ -248,7 +249,6 @@ Item { Row { spacing: Theme.spacingM - anchors.horizontalCenter: parent.horizontalCenter Repeater { model: Theme.availableThemeNames.slice(5, 10) @@ -320,41 +320,44 @@ Item { height: Theme.spacingM } - Rectangle { - width: 120 - height: 40 - radius: 20 + Row { anchors.horizontalCenter: parent.horizontalCenter - color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus === "matugen_missing") - return Qt.rgba(Theme.error.r, - Theme.error.g, - Theme.error.b, 0.12) - else - return Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - } - border.color: { - if (ToastService.wallpaperErrorStatus === "error" - || ToastService.wallpaperErrorStatus === "matugen_missing") - return Qt.rgba(Theme.error.r, - Theme.error.g, - Theme.error.b, 0.5) - else if (Theme.isDynamicTheme) - return Theme.primary - else - return Theme.outline - } - border.width: Theme.isDynamicTheme ? 2 : 1 - scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1) + spacing: Theme.spacingL - Row { - anchors.centerIn: parent - spacing: Theme.spacingS + Rectangle { + width: 120 + height: 40 + radius: 20 + color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus === "matugen_missing") + return Qt.rgba(Theme.error.r, + Theme.error.g, + Theme.error.b, 0.12) + else + return Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + } + border.color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus === "matugen_missing") + return Qt.rgba(Theme.error.r, + Theme.error.g, + Theme.error.b, 0.5) + else if (Theme.currentThemeName === "dynamic") + return Theme.primary + else + return Theme.outline + } + border.width: (Theme.currentThemeName === "dynamic") ? 2 : 1 + scale: (Theme.currentThemeName === "dynamic") ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1) - DankIcon { + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { name: { if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus @@ -474,6 +477,86 @@ Item { } } } + + Rectangle { + width: 120 + height: 40 + radius: 20 + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: (Theme.currentThemeName === "custom") ? Theme.primary : Theme.outline + border.width: (Theme.currentThemeName === "custom") ? 2 : 1 + scale: (Theme.currentThemeName === "custom") ? 1.1 : (customMouseArea.containsMouse ? 1.02 : 1) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "folder_open" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Custom" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: customMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + fileBrowserModal.open() + } + } + + Rectangle { + width: customTooltipText.contentWidth + Theme.spacingM * 2 + height: customTooltipText.contentHeight + Theme.spacingS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: customMouseArea.containsMouse && (Theme.currentThemeName !== "custom") + + StyledText { + id: customTooltipText + text: "Load custom theme from JSON file" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, 250) + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } // Close Row } } } @@ -625,7 +708,7 @@ Item { StyledText { id: warningText - text: "Changing these settings will manipulate GTK and Qt configurations on the system" + text: "Changing these settings will manipulate GTK and Qt configurations on the system, requires \"Auto\" theme" font.pixelSize: Theme.fontSizeSmall color: Theme.warning wrapMode: Text.WordWrap @@ -732,9 +815,9 @@ Item { DankToggle { width: parent.width text: "Theme GTK Applications" - description: Theme.gtkThemingEnabled ? "File managers, text editors, and system dialogs will match your theme" : "GTK theming not available (install gsettings)" - enabled: Theme.gtkThemingEnabled - checked: Theme.gtkThemingEnabled + description: SettingsData.gtkAvailable ? "File managers, text editors, and system dialogs will match your theme" : "GTK theming not available (install gsettings)" + enabled: SettingsData.gtkAvailable + checked: SettingsData.gtkAvailable && SettingsData.gtkThemingEnabled onToggled: function (checked) { SettingsData.setGtkThemingEnabled(checked) @@ -744,9 +827,9 @@ Item { DankToggle { width: parent.width text: "Theme Qt Applications" - description: Theme.qtThemingEnabled ? "Qt applications will match your theme colors" : "Qt theming not available (install qt5ct or qt6ct)" - enabled: Theme.qtThemingEnabled - checked: Theme.qtThemingEnabled + description: (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) ? "Qt applications will match your theme colors" : "Qt theming not available (install qt5ct or qt6ct)" + enabled: (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) + checked: (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) && SettingsData.qtThemingEnabled onToggled: function (checked) { SettingsData.setQtThemingEnabled(checked) @@ -756,4 +839,24 @@ Item { } } } + + FileBrowserModal { + id: fileBrowserModal + browserTitle: "Select Custom Theme" + filterExtensions: ["*.json"] + showHiddenFiles: true + + function selectCustomTheme() { + shouldBeVisible = true + } + + onFileSelected: function(filePath) { + // Save the custom theme file path and switch to custom theme + if (filePath.endsWith(".json")) { + SettingsData.setCustomThemeFile(filePath) + Theme.switchTheme("custom") + close() + } + } + } } diff --git a/Modules/Toast.qml b/Modules/Toast.qml index a97a71f6..36d9a2b6 100644 --- a/Modules/Toast.qml +++ b/Modules/Toast.qml @@ -66,9 +66,7 @@ PanelWindow { } } - width: shouldBeVisible ? - (ToastService.hasDetails ? 380 : messageText.implicitWidth + Theme.iconSize + Theme.spacingM * 3 + Theme.spacingL * 2) : - frozenWidth + width: shouldBeVisible ? (ToastService.hasDetails ? 380 : 350) : frozenWidth height: toastContent.height + Theme.spacingL * 2 anchors.horizontalCenter: parent.horizontalCenter y: Theme.barHeight - 4 + SettingsData.topBarSpacing + 2 @@ -132,10 +130,14 @@ PanelWindow { anchors.left: statusIcon.right anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter + anchors.right: ToastService.hasDetails ? expandButton.left : closeButton.left + anchors.rightMargin: Theme.spacingM wrapMode: Text.NoWrap + elide: Text.ElideRight } DankActionButton { + id: expandButton iconName: toast.expanded ? "expand_less" : "expand_more" iconSize: Theme.iconSize iconColor: Theme.background diff --git a/docs/CUSTOM_THEMES.md b/docs/CUSTOM_THEMES.md new file mode 100644 index 00000000..7bc38708 --- /dev/null +++ b/docs/CUSTOM_THEMES.md @@ -0,0 +1,127 @@ +# Custom Themes + +This guide covers creating custom themes for DankMaterialShell. You can define your own color schemes by creating theme files that the shell can load. + +## Theme Structure + +Themes are defined using the same structure as the built-in themes. Each theme must specify a complete set of Material Design 3 colors that work together harmoniously. + +### Required Core Colors + +These are the essential colors that define your theme's appearance: + +```json +{ + "dark": { + "name": "Cyberpunk Electric Dark", + "primary": "#00FFCC", + "primaryText": "#000000", + "primaryContainer": "#00CC99", + "secondary": "#FF4DFF", + "surface": "#0F0F0F", + "surfaceText": "#E0FFE0", + "surfaceVariant": "#1F2F1F", + "surfaceVariantText": "#CCFFCC", + "surfaceTint": "#00FFCC", + "background": "#000000", + "backgroundText": "#F0FFF0", + "outline": "#80FF80", + "surfaceContainer": "#1A2B1A", + "surfaceContainerHigh": "#264026", + "error": "#FF0066", + "warning": "#CCFF00", + "info": "#00FFCC" + }, + "light": { + "name": "Cyberpunk Electric Light", + "primary": "#00B899", + "primaryText": "#FFFFFF", + "primaryContainer": "#66FFDD", + "secondary": "#CC00CC", + "surface": "#F0FFF0", + "surfaceText": "#1F2F1F", + "surfaceVariant": "#E6FFE6", + "surfaceVariantText": "#2D4D2D", + "surfaceTint": "#00B899", + "background": "#FFFFFF", + "backgroundText": "#000000", + "outline": "#4DCC4D", + "surfaceContainer": "#F5FFF5", + "surfaceContainerHigh": "#EBFFEB", + "error": "#B3004D", + "warning": "#99CC00", + "info": "#00B899" + } +} +``` + +You can the colors at the top level if you do not want "dark" and "light" variants. + +## Example Themes + +There are example themes you can start from: + +- [Cyberpunk Electric](theme_cyberpunk_electric.json) - Neon green and magenta cyberpunk aesthetic +- [Hotline Miami](theme_hotline_miami.json) - Retro 80s inspired hot pink and blue +- [Miami Vice](theme_miami_vice.json) - Classic teal and pink vice aesthetic +- [Synthwave Electric](theme_synthwave_electric.json) - Electric purple and cyan synthwave vibes + +### Color Definitions + +**Primary Colors** +- `primary` - Main accent color used for buttons, highlights, and active states +- `primaryText` - Text color that contrasts well with primary background +- `primaryContainer` - Darker/lighter variant of primary for containers + +**Secondary Colors** +- `secondary` - Supporting accent color for variety and hierarchy +- `surfaceTint` - Tint color applied to surfaces, usually derived from primary + +**Surface Colors** +- `surface` - Default surface color for cards, panels, etc. +- `surfaceText` - Primary text color on surface backgrounds +- `surfaceVariant` - Alternate surface color for subtle differentiation +- `surfaceVariantText` - Text color for surfaceVariant backgrounds +- `surfaceContainer` - Container surface color, slightly different from surface +- `surfaceContainerHigh` - Elevated container color for layered interfaces + +**Background Colors** +- `background` - Main background color for the entire interface +- `backgroundText` - Text color for background areas + +**Outline Colors** +- `outline` - Border and divider color for subtle boundaries + +## Optional Properties + +While the core colors above are required, you can also customize these optional properties: + +### Semantic Colors +```json +{ + "error": "#f44336", + "warning": "#ff9800", + "info": "#2196f3" +} +``` + +- `error` - Used for error states, delete buttons, and critical warnings +- `warning` - Used for warning states and caution indicators +- `info` - Used for informational states and neutral indicators + +## Setting Custom Theme + +In settings -> Theme & Colors you can choose "Custom" to choose a path to your theme. + +You can also edit `~/.config/DankMaterialShell/settings.json` manually + +```json +{ + "currentThemeName": "custom", + "customThemeFile": "/path/to/mytheme.json" +} +``` + +### Reactivity + +Editing the custom theme file will auto-update the shell if it's the current theme. \ No newline at end of file diff --git a/docs/theme_cyberpunk_electric.json b/docs/theme_cyberpunk_electric.json new file mode 100644 index 00000000..c2448720 --- /dev/null +++ b/docs/theme_cyberpunk_electric.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Cyberpunk Electric Dark", + "primary": "#00FFCC", + "primaryText": "#000000", + "primaryContainer": "#00CC99", + "secondary": "#FF4DFF", + "surface": "#0F0F0F", + "surfaceText": "#E0FFE0", + "surfaceVariant": "#1F2F1F", + "surfaceVariantText": "#CCFFCC", + "surfaceTint": "#00FFCC", + "background": "#000000", + "backgroundText": "#F0FFF0", + "outline": "#80FF80", + "surfaceContainer": "#1A2B1A", + "surfaceContainerHigh": "#264026", + "error": "#FF0066", + "warning": "#CCFF00", + "info": "#00FFCC" + }, + "light": { + "name": "Cyberpunk Electric Light", + "primary": "#00B899", + "primaryText": "#FFFFFF", + "primaryContainer": "#66FFDD", + "secondary": "#CC00CC", + "surface": "#F0FFF0", + "surfaceText": "#1F2F1F", + "surfaceVariant": "#E6FFE6", + "surfaceVariantText": "#2D4D2D", + "surfaceTint": "#00B899", + "background": "#FFFFFF", + "backgroundText": "#000000", + "outline": "#4DCC4D", + "surfaceContainer": "#F5FFF5", + "surfaceContainerHigh": "#EBFFEB", + "error": "#B3004D", + "warning": "#99CC00", + "info": "#00B899" + } +} \ No newline at end of file diff --git a/docs/theme_hotline_miami.json b/docs/theme_hotline_miami.json new file mode 100644 index 00000000..60bad5eb --- /dev/null +++ b/docs/theme_hotline_miami.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Hotline Miami Dark", + "primary": "#FF0080", + "primaryText": "#FFFFFF", + "primaryContainer": "#CC0066", + "secondary": "#00FF80", + "surface": "#0D0D0D", + "surfaceText": "#F0F0F0", + "surfaceVariant": "#1A0F1A", + "surfaceVariantText": "#E0E0E0", + "surfaceTint": "#FF0080", + "background": "#000000", + "backgroundText": "#FFFFFF", + "outline": "#8000FF", + "surfaceContainer": "#1A0D1A", + "surfaceContainerHigh": "#260F26", + "error": "#FF4080", + "warning": "#FFFF00", + "info": "#00FF80" + }, + "light": { + "name": "Hotline Miami Light", + "primary": "#CC0066", + "primaryText": "#FFFFFF", + "primaryContainer": "#FF80B3", + "secondary": "#00CC66", + "surface": "#FFF0FF", + "surfaceText": "#1A0F1A", + "surfaceVariant": "#F0E6F0", + "surfaceVariantText": "#2D1A2D", + "surfaceTint": "#CC0066", + "background": "#FFFFFF", + "backgroundText": "#0D0D0D", + "outline": "#6600CC", + "surfaceContainer": "#F5F0F5", + "surfaceContainerHigh": "#EBE0EB", + "error": "#B30040", + "warning": "#B3B300", + "info": "#00B359" + } +} \ No newline at end of file diff --git a/docs/theme_miami_vice.json b/docs/theme_miami_vice.json new file mode 100644 index 00000000..e3c07aa6 --- /dev/null +++ b/docs/theme_miami_vice.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Miami Vice Dark", + "primary": "#00FFFF", + "primaryText": "#000000", + "primaryContainer": "#00CCCC", + "secondary": "#FF1493", + "surface": "#0A0A0F", + "surfaceText": "#E0E0FF", + "surfaceVariant": "#1A1A2E", + "surfaceVariantText": "#C0C0FF", + "surfaceTint": "#00FFFF", + "background": "#000008", + "backgroundText": "#F0F0FF", + "outline": "#4040FF", + "surfaceContainer": "#131325", + "surfaceContainerHigh": "#1F1F40", + "error": "#FF0080", + "warning": "#FFFF00", + "info": "#00FFFF" + }, + "light": { + "name": "Miami Vice Light", + "primary": "#0099CC", + "primaryText": "#FFFFFF", + "primaryContainer": "#00CCFF", + "secondary": "#CC0066", + "surface": "#F8F8FF", + "surfaceText": "#1A1A2E", + "surfaceVariant": "#E8E8FF", + "surfaceVariantText": "#2A2A4E", + "surfaceTint": "#0099CC", + "background": "#FFFFFF", + "backgroundText": "#0A0A2E", + "outline": "#6666CC", + "surfaceContainer": "#F0F0FF", + "surfaceContainerHigh": "#E0E0FF", + "error": "#CC0055", + "warning": "#CC9900", + "info": "#0099CC" + } +} \ No newline at end of file diff --git a/docs/theme_synthwave_electric.json b/docs/theme_synthwave_electric.json new file mode 100644 index 00000000..59817864 --- /dev/null +++ b/docs/theme_synthwave_electric.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Synthwave Electric Dark", + "primary": "#FF6600", + "primaryText": "#000000", + "primaryContainer": "#CC5200", + "secondary": "#0080FF", + "surface": "#0A0A15", + "surfaceText": "#E6F0FF", + "surfaceVariant": "#1A1A33", + "surfaceVariantText": "#CCE0FF", + "surfaceTint": "#FF6600", + "background": "#000008", + "backgroundText": "#F0F8FF", + "outline": "#4D80FF", + "surfaceContainer": "#151529", + "surfaceContainerHigh": "#212147", + "error": "#FF3366", + "warning": "#FFCC00", + "info": "#0080FF" + }, + "light": { + "name": "Synthwave Electric Light", + "primary": "#CC5200", + "primaryText": "#FFFFFF", + "primaryContainer": "#FF9966", + "secondary": "#0066CC", + "surface": "#FFF8F0", + "surfaceText": "#1A1A33", + "surfaceVariant": "#F0F0FF", + "surfaceVariantText": "#333366", + "surfaceTint": "#CC5200", + "background": "#FFFFFF", + "backgroundText": "#000008", + "outline": "#3366CC", + "surfaceContainer": "#F5F5FF", + "surfaceContainerHigh": "#EBEBFF", + "error": "#CC1A40", + "warning": "#CC9900", + "info": "#0066CC" + } +} \ No newline at end of file