mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Persist theme preference, dynamic auto theme with matugen
This commit is contained in:
140
Common/Colors.qml
Normal file
140
Common/Colors.qml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Qt.labs.platform // ← gives us StandardPaths
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/* ──────────────── basic state ──────────────── */
|
||||||
|
signal colorsUpdated()
|
||||||
|
|
||||||
|
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
|
readonly property string homeDir: _homeUrl.startsWith("file://")
|
||||||
|
? _homeUrl.substring(7)
|
||||||
|
: _homeUrl
|
||||||
|
readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper"
|
||||||
|
|
||||||
|
property bool matugenAvailable: false
|
||||||
|
property string matugenJson: ""
|
||||||
|
property var matugenColors: ({})
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("Colors.qml → home =", homeDir)
|
||||||
|
matugenCheck.running = true // kick off the chain
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────── availability checks ──────────────── */
|
||||||
|
Process {
|
||||||
|
id: matugenCheck
|
||||||
|
command: ["which", "matugen"]
|
||||||
|
onExited: (code) => {
|
||||||
|
matugenAvailable = (code === 0)
|
||||||
|
console.log("Matugen in PATH:", matugenAvailable)
|
||||||
|
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
console.warn("Matugen missing → dynamic theme disabled")
|
||||||
|
Theme.rootObj.wallpaperErrorStatus = "matugen_missing"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileChecker.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileChecker // exists & readable?
|
||||||
|
command: ["test", "-r", wallpaperPath]
|
||||||
|
onExited: (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
matugenProcess.running = true
|
||||||
|
} else {
|
||||||
|
console.error("code", code)
|
||||||
|
console.error("Wallpaper not found:", wallpaperPath)
|
||||||
|
Theme.rootObj.showWallpaperError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────── matugen invocation ──────────────── */
|
||||||
|
Process {
|
||||||
|
id: matugenProcess
|
||||||
|
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
|
||||||
|
|
||||||
|
/* ── grab stdout as a stream ── */
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: matugenCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
const out = matugenCollector.text
|
||||||
|
if (!out.length) {
|
||||||
|
console.error("matugen produced zero bytes\nstderr:", matugenProcess.stderr)
|
||||||
|
Theme.rootObj.showWallpaperError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
root.matugenJson = out
|
||||||
|
root.matugenColors = JSON.parse(out)
|
||||||
|
root.colorsUpdated()
|
||||||
|
Theme.rootObj.wallpaperErrorStatus = ""
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JSON parse failed:", e)
|
||||||
|
Theme.rootObj.showWallpaperError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* grab stderr too, so we can print it above */
|
||||||
|
stderr: StdioCollector { id: matugenErr }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────── public helper ──────────────── */
|
||||||
|
function extractColors() {
|
||||||
|
if (matugenAvailable)
|
||||||
|
fileChecker.running = true
|
||||||
|
else
|
||||||
|
matugenCheck.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatugenColor(path, fallback) {
|
||||||
|
let cur = matugenColors?.colors?.dark
|
||||||
|
for (const part of path.split(".")) {
|
||||||
|
if (!cur || typeof cur !== "object" || !(part in cur))
|
||||||
|
return fallback
|
||||||
|
cur = cur[part]
|
||||||
|
}
|
||||||
|
return cur || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────── color properties (MD3) ──────────────── */
|
||||||
|
property color primary: getMatugenColor("primary", "#42a5f5")
|
||||||
|
property color secondary: getMatugenColor("secondary", "#8ab4f8")
|
||||||
|
property color tertiary: getMatugenColor("tertiary", "#bb86fc")
|
||||||
|
property color tertiaryContainer: getMatugenColor("tertiary_container", "#3700b3")
|
||||||
|
property color error: getMatugenColor("error", "#cf6679")
|
||||||
|
property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea")
|
||||||
|
|
||||||
|
/* backgrounds */
|
||||||
|
property color bg: getMatugenColor("background", "#1a1c1e")
|
||||||
|
property color surface: getMatugenColor("surface", "#1a1c1e")
|
||||||
|
property color surfaceContainer: getMatugenColor("surface_container", "#1e2023")
|
||||||
|
property color surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f")
|
||||||
|
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f")
|
||||||
|
|
||||||
|
/* text */
|
||||||
|
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
|
||||||
|
property color primaryText: getMatugenColor("on_primary", "#ffffff")
|
||||||
|
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5")
|
||||||
|
|
||||||
|
/* containers & misc */
|
||||||
|
property color primaryContainer: getMatugenColor("primary_container", "#1976d2")
|
||||||
|
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
|
||||||
|
property color outline: getMatugenColor("outline", "#8e918f")
|
||||||
|
|
||||||
|
/* legacy aliases */
|
||||||
|
property color accentHi: primary
|
||||||
|
property color accentLo: secondary
|
||||||
|
|
||||||
|
function isColorDark(c) {
|
||||||
|
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Common/Prefs.qml
Normal file
49
Common/Prefs.qml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Qt.labs.settings
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias themeIndex: settings.themeIndex
|
||||||
|
property alias themeIsDynamic: settings.themeIsDynamic
|
||||||
|
|
||||||
|
Settings {
|
||||||
|
id: settings
|
||||||
|
category: "theme"
|
||||||
|
|
||||||
|
// 0-9 = built-in static themes, 10 = Auto (dynamic)
|
||||||
|
property int themeIndex: 0
|
||||||
|
property bool themeIsDynamic: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme when component is ready
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("Prefs Component.onCompleted - themeIndex:", settings.themeIndex, "isDynamic:", settings.themeIsDynamic)
|
||||||
|
Qt.callLater(applyStoredTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStoredTheme() {
|
||||||
|
console.log("Applying stored theme:", settings.themeIndex, settings.themeIsDynamic)
|
||||||
|
|
||||||
|
// Make sure Theme is available
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.switchTheme(settings.themeIndex, settings.themeIsDynamic, false) // Don't save during startup
|
||||||
|
} else {
|
||||||
|
// Try again in a moment
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.switchTheme(settings.themeIndex, settings.themeIsDynamic, false) // Don't save during startup
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(index, isDynamic) {
|
||||||
|
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic)
|
||||||
|
settings.themeIndex = index
|
||||||
|
settings.themeIsDynamic = isDynamic
|
||||||
|
console.log("Prefs saved - themeIndex:", settings.themeIndex, "isDynamic:", settings.themeIsDynamic)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,45 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Reference to the main shell root for calling functions
|
||||||
|
property var rootObj: null
|
||||||
|
|
||||||
|
// Apply saved theme on startup
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("Theme Component.onCompleted")
|
||||||
|
|
||||||
|
// Connect to Colors signal
|
||||||
|
if (typeof Colors !== "undefined") {
|
||||||
|
Colors.colorsUpdated.connect(root.onColorsUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (typeof Prefs !== "undefined") {
|
||||||
|
console.log("Theme applying saved preferences:", Prefs.themeIndex, Prefs.themeIsDynamic)
|
||||||
|
switchTheme(Prefs.themeIndex, Prefs.themeIsDynamic, false) // Don't save during startup
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle successful color extraction
|
||||||
|
function onColorsUpdated() {
|
||||||
|
console.log("Colors updated successfully - switching to dynamic theme")
|
||||||
|
currentThemeIndex = 10
|
||||||
|
isDynamicTheme = true
|
||||||
|
console.log("Dynamic theme activated. Theme.primary should now be:", primary)
|
||||||
|
|
||||||
|
// Save preference after successful switch
|
||||||
|
if (typeof Prefs !== "undefined") {
|
||||||
|
Prefs.setTheme(currentThemeIndex, isDynamicTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Theme definitions with complete Material 3 expressive color palettes
|
// Theme definitions with complete Material 3 expressive color palettes
|
||||||
property var themes: [
|
property var themes: [
|
||||||
{
|
{
|
||||||
@@ -179,33 +214,50 @@ QtObject {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// Current theme index
|
// Current theme index (10 = Auto/Dynamic)
|
||||||
property int currentThemeIndex: 0
|
property int currentThemeIndex: 0
|
||||||
|
property bool isDynamicTheme: false
|
||||||
|
|
||||||
// Function to switch themes
|
// Function to switch themes
|
||||||
function switchTheme(themeIndex) {
|
function switchTheme(themeIndex, isDynamic = false, savePrefs = true) {
|
||||||
if (themeIndex >= 0 && themeIndex < themes.length) {
|
console.log("Theme.switchTheme called:", themeIndex, isDynamic, "savePrefs:", savePrefs)
|
||||||
|
|
||||||
|
if (isDynamic && themeIndex === 10) {
|
||||||
|
console.log("Attempting to switch to dynamic theme - checking colors first")
|
||||||
|
|
||||||
|
// Don't change theme yet - wait for color extraction to succeed
|
||||||
|
if (typeof Colors !== "undefined") {
|
||||||
|
console.log("Calling Colors.extractColors()")
|
||||||
|
Colors.extractColors()
|
||||||
|
} else {
|
||||||
|
console.error("Colors singleton not available")
|
||||||
|
}
|
||||||
|
} else if (themeIndex >= 0 && themeIndex < themes.length) {
|
||||||
currentThemeIndex = themeIndex
|
currentThemeIndex = themeIndex
|
||||||
// Simple persistence - store in a property
|
isDynamicTheme = false
|
||||||
// In a real application, you might use Qt.labs.settings or another persistence mechanism
|
}
|
||||||
|
|
||||||
|
// Save preference (unless this is a startup restoration)
|
||||||
|
if (savePrefs && typeof Prefs !== "undefined") {
|
||||||
|
Prefs.setTheme(currentThemeIndex, isDynamicTheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic color properties that change based on current theme
|
// Dynamic color properties that change based on current theme
|
||||||
property color primary: themes[currentThemeIndex].primary
|
property color primary: isDynamicTheme ? Colors.accentHi : (currentThemeIndex < themes.length ? themes[currentThemeIndex].primary : themes[0].primary)
|
||||||
property color primaryText: themes[currentThemeIndex].primaryText
|
property color primaryText: isDynamicTheme ? Colors.primaryText : (currentThemeIndex < themes.length ? themes[currentThemeIndex].primaryText : themes[0].primaryText)
|
||||||
property color primaryContainer: themes[currentThemeIndex].primaryContainer
|
property color primaryContainer: isDynamicTheme ? Colors.primaryContainer : (currentThemeIndex < themes.length ? themes[currentThemeIndex].primaryContainer : themes[0].primaryContainer)
|
||||||
property color secondary: themes[currentThemeIndex].secondary
|
property color secondary: isDynamicTheme ? Colors.accentLo : (currentThemeIndex < themes.length ? themes[currentThemeIndex].secondary : themes[0].secondary)
|
||||||
property color surface: themes[currentThemeIndex].surface
|
property color surface: isDynamicTheme ? Colors.surface : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surface : themes[0].surface)
|
||||||
property color surfaceText: themes[currentThemeIndex].surfaceText
|
property color surfaceText: isDynamicTheme ? Colors.surfaceText : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceText : themes[0].surfaceText)
|
||||||
property color surfaceVariant: themes[currentThemeIndex].surfaceVariant
|
property color surfaceVariant: isDynamicTheme ? Colors.surfaceVariant : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceVariant : themes[0].surfaceVariant)
|
||||||
property color surfaceVariantText: themes[currentThemeIndex].surfaceVariantText
|
property color surfaceVariantText: isDynamicTheme ? Colors.surfaceVariantText : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceVariantText : themes[0].surfaceVariantText)
|
||||||
property color surfaceTint: themes[currentThemeIndex].surfaceTint
|
property color surfaceTint: isDynamicTheme ? Colors.surfaceTint : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceTint : themes[0].surfaceTint)
|
||||||
property color background: themes[currentThemeIndex].background
|
property color background: isDynamicTheme ? Colors.background : (currentThemeIndex < themes.length ? themes[currentThemeIndex].background : themes[0].background)
|
||||||
property color backgroundText: themes[currentThemeIndex].backgroundText
|
property color backgroundText: isDynamicTheme ? Colors.backgroundText : (currentThemeIndex < themes.length ? themes[currentThemeIndex].backgroundText : themes[0].backgroundText)
|
||||||
property color outline: themes[currentThemeIndex].outline
|
property color outline: isDynamicTheme ? Colors.outline : (currentThemeIndex < themes.length ? themes[currentThemeIndex].outline : themes[0].outline)
|
||||||
property color surfaceContainer: themes[currentThemeIndex].surfaceContainer
|
property color surfaceContainer: isDynamicTheme ? Colors.surfaceContainer : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceContainer : themes[0].surfaceContainer)
|
||||||
property color surfaceContainerHigh: themes[currentThemeIndex].surfaceContainerHigh
|
property color surfaceContainerHigh: isDynamicTheme ? Colors.surfaceContainerHigh : (currentThemeIndex < themes.length ? themes[currentThemeIndex].surfaceContainerHigh : themes[0].surfaceContainerHigh)
|
||||||
|
|
||||||
// Static colors that don't change with themes
|
// Static colors that don't change with themes
|
||||||
property color archBlue: "#1793D1"
|
property color archBlue: "#1793D1"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
module Common
|
module Common
|
||||||
|
|
||||||
singleton Theme 1.0 Theme.qml
|
singleton Theme 1.0 Theme.qml
|
||||||
|
singleton Colors 1.0 Colors.qml
|
||||||
|
singleton Prefs 1.0 Prefs.qml
|
||||||
Utilities 1.0 Utilities.js
|
Utilities 1.0 Utilities.js
|
||||||
@@ -6,7 +6,7 @@ Column {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Current Theme: " + Theme.themes[Theme.currentThemeIndex].name
|
text: "Current Theme: " + (Theme.isDynamicTheme ? "Auto" : (Theme.currentThemeIndex < Theme.themes.length ? Theme.themes[Theme.currentThemeIndex].name : "Blue"))
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -16,6 +16,9 @@ Column {
|
|||||||
// Theme description
|
// Theme description
|
||||||
Text {
|
Text {
|
||||||
text: {
|
text: {
|
||||||
|
if (Theme.isDynamicTheme) {
|
||||||
|
return "Wallpaper-based dynamic colors"
|
||||||
|
}
|
||||||
var descriptions = [
|
var descriptions = [
|
||||||
"Material blue inspired by modern interfaces",
|
"Material blue inspired by modern interfaces",
|
||||||
"Deep blue inspired by material 3",
|
"Deep blue inspired by material 3",
|
||||||
@@ -57,9 +60,9 @@ Column {
|
|||||||
radius: 16
|
radius: 16
|
||||||
color: Theme.themes[index].primary
|
color: Theme.themes[index].primary
|
||||||
border.color: Theme.outline
|
border.color: Theme.outline
|
||||||
border.width: Theme.currentThemeIndex === index ? 2 : 1
|
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
|
||||||
|
|
||||||
scale: Theme.currentThemeIndex === index ? 1.1 : 1.0
|
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1.0
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -103,7 +106,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Theme.switchTheme(index)
|
Theme.switchTheme(index, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,5 +183,135 @@ Column {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto theme button - prominent oval below the grid
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: 20
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
color: {
|
||||||
|
if (root.wallpaperErrorStatus === "error" || root.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 (root.wallpaperErrorStatus === "error" || root.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
|
||||||
|
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
if (root.wallpaperErrorStatus === "error" || root.wallpaperErrorStatus === "matugen_missing") return "error"
|
||||||
|
else return "palette"
|
||||||
|
}
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: {
|
||||||
|
if (root.wallpaperErrorStatus === "error" || root.wallpaperErrorStatus === "matugen_missing") return Theme.error
|
||||||
|
else return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Theme.iconFontWeight
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
if (root.wallpaperErrorStatus === "error") return "Error"
|
||||||
|
else if (root.wallpaperErrorStatus === "matugen_missing") return "No matugen"
|
||||||
|
else return "Auto"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: {
|
||||||
|
if (root.wallpaperErrorStatus === "error" || root.wallpaperErrorStatus === "matugen_missing") return Theme.error
|
||||||
|
else return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1.0)
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: autoMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Theme.switchTheme(10, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip for Auto button
|
||||||
|
Rectangle {
|
||||||
|
width: autoTooltipText.contentWidth + Theme.spacingM * 2
|
||||||
|
height: autoTooltipText.contentHeight + Theme.spacingS * 2
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: Theme.cornerRadiusSmall
|
||||||
|
anchors.bottom: parent.top
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: autoMouseArea.containsMouse && (!Theme.isDynamicTheme || root.wallpaperErrorStatus === "error" || root.wallpaperErrorStatus === "matugen_missing")
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: autoTooltipText
|
||||||
|
text: {
|
||||||
|
if (root.wallpaperErrorStatus === "error") {
|
||||||
|
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper"
|
||||||
|
} else if (root.wallpaperErrorStatus === "matugen_missing") {
|
||||||
|
return "Install matugen package for dynamic themes"
|
||||||
|
} else {
|
||||||
|
return "Dynamic wallpaper-based colors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: (root.wallpaperErrorStatus === "error" || root.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: Math.min(implicitWidth, 250)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
scripts/README-dynamic-theme.md
Normal file
73
scripts/README-dynamic-theme.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Dynamic Theme Setup
|
||||||
|
|
||||||
|
This setup adds wallpaper-aware "Auto" theme support to your Quickshell + Niri environment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Install the required tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required for Material-You palette generation
|
||||||
|
cargo install matugen
|
||||||
|
|
||||||
|
# Required for JSON processing (usually pre-installed)
|
||||||
|
sudo pacman -S jq # Arch Linux
|
||||||
|
# or: sudo apt install jq # Ubuntu/Debian
|
||||||
|
|
||||||
|
# Background setters (choose one)
|
||||||
|
sudo pacman -S swaybg # Simple and reliable
|
||||||
|
# or: cargo install swww # Smoother transitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Initial wallpaper setup:**
|
||||||
|
```bash
|
||||||
|
# Set your initial wallpaper
|
||||||
|
./scripts/set-wallpaper.sh /path/to/your/wallpaper.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Enable Niri color integration (optional):**
|
||||||
|
Add this line to your `~/.config/niri/config.kdl`:
|
||||||
|
```kdl
|
||||||
|
!include "generated_colors.kdl"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Enable Auto theme:**
|
||||||
|
Open Control Center → Theme Picker → Click the gradient "Auto" button
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Change wallpaper and auto-update theme:
|
||||||
|
```bash
|
||||||
|
./scripts/set-wallpaper.sh /new/wallpaper.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual theme switching:
|
||||||
|
- Use the Control Center theme picker
|
||||||
|
- Preferences persist across restarts
|
||||||
|
- Auto theme requires wallpaper symlink to exist
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Color extraction:** `Colors.qml` uses Quickshell's ColorQuantizer to extract dominant colors from the wallpaper symlink
|
||||||
|
2. **Persistence:** `Prefs.qml` stores your theme choice using PersistentProperties
|
||||||
|
3. **Dynamic switching:** `Theme.qml` switches between static themes and wallpaper colors
|
||||||
|
4. **Auto-reload:** Quickshell's file watching automatically reloads when the wallpaper symlink changes
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Dynamic theme requires wallpaper setup!" error
|
||||||
|
Run the setup command:
|
||||||
|
```bash
|
||||||
|
./scripts/set-wallpaper.sh /path/to/your/wallpaper.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Colors don't update when changing wallpaper
|
||||||
|
- Make sure you're using the script, not manually changing files
|
||||||
|
- The symlink at `~/quickshell/current_wallpaper` must exist
|
||||||
|
|
||||||
|
### Niri colors don't change
|
||||||
|
- Ensure `!include "generated_colors.kdl"` is in your config.kdl
|
||||||
|
- Check that matugen and jq are installed
|
||||||
|
- Look for `~/.config/niri/generated_colors.kdl`
|
||||||
78
scripts/set-wallpaper.sh
Executable file
78
scripts/set-wallpaper.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
img=$1
|
||||||
|
|
||||||
|
QS_DIR="$HOME/quickshell"
|
||||||
|
mkdir -p "$QS_DIR"
|
||||||
|
LINK="$QS_DIR/current_wallpaper"
|
||||||
|
ln -sf -- "$img" "$LINK"
|
||||||
|
swaybg -m fill -i "$LINK" & disown
|
||||||
|
|
||||||
|
json="$(matugen image "$img" --json hex)"
|
||||||
|
|
||||||
|
get() { jq -r "$1" <<<"$json"; }
|
||||||
|
|
||||||
|
bg=$(get '.colors.dark.background')
|
||||||
|
fg=$(get '.colors.dark.on_background')
|
||||||
|
primary=$(get '.colors.dark.primary')
|
||||||
|
secondary=$(get '.colors.dark.secondary')
|
||||||
|
tertiary=$(get '.colors.dark.tertiary')
|
||||||
|
tertiary_ctr=$(get '.colors.dark.tertiary_container')
|
||||||
|
error=$(get '.colors.dark.error')
|
||||||
|
inverse=$(get '.colors.dark.inverse_primary')
|
||||||
|
|
||||||
|
bg_b=$(get '.colors.light.background')
|
||||||
|
fg_b=$(get '.colors.light.on_background')
|
||||||
|
primary_b=$(get '.colors.light.primary')
|
||||||
|
secondary_b=$(get '.colors.light.secondary')
|
||||||
|
tertiary_b=$(get '.colors.light.tertiary')
|
||||||
|
tertiary_ctr_b=$(get '.colors.light.tertiary_container')
|
||||||
|
error_b=$(get '.colors.light.error')
|
||||||
|
inverse_b=$(get '.colors.light.inverse_primary')
|
||||||
|
|
||||||
|
cat >"$QS_DIR/generated_niri_colors.kdl" <<EOF
|
||||||
|
// AUTO-GENERATED on $(date)
|
||||||
|
layout {
|
||||||
|
border {
|
||||||
|
active-color "$primary"
|
||||||
|
inactive-color "$secondary"
|
||||||
|
}
|
||||||
|
focus-ring {
|
||||||
|
active-color "$inverse"
|
||||||
|
}
|
||||||
|
background-color "$bg"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo "→ Niri colours: $QS_DIR/generated_niri_colors.kdl"
|
||||||
|
|
||||||
|
cat >"$QS_DIR/generated_ghostty_colors.conf" <<EOF
|
||||||
|
# AUTO-GENERATED on $(date)
|
||||||
|
|
||||||
|
background = $bg
|
||||||
|
foreground = $fg
|
||||||
|
cursor-color = $inverse
|
||||||
|
selection-background = $secondary
|
||||||
|
selection-foreground = #ffffff
|
||||||
|
|
||||||
|
palette = 0=$bg
|
||||||
|
palette = 1=$error
|
||||||
|
palette = 2=$tertiary
|
||||||
|
palette = 3=$secondary
|
||||||
|
palette = 4=$primary
|
||||||
|
palette = 5=$tertiary_ctr
|
||||||
|
palette = 6=$inverse
|
||||||
|
palette = 7=$fg
|
||||||
|
palette = 8=$bg_b
|
||||||
|
palette = 9=$error_b
|
||||||
|
palette = 10=$tertiary_b
|
||||||
|
palette = 11=$secondary_b
|
||||||
|
palette = 12=$primary_b
|
||||||
|
palette = 13=$tertiary_ctr_b
|
||||||
|
palette = 14=$inverse_b
|
||||||
|
palette = 15=$fg_b
|
||||||
|
EOF
|
||||||
|
echo "→ Ghostty theme: $QS_DIR/generated_ghostty_colors.conf"
|
||||||
|
echo " (use in ghostty: theme = $QS_DIR/generated_ghostty_colors.conf )"
|
||||||
|
|
||||||
|
niri msg action do-screen-transition --delay-ms 100
|
||||||
26
shell.qml
26
shell.qml
@@ -18,6 +18,11 @@ import "Common/Utilities.js" as Utils
|
|||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Make root accessible to Theme singleton for error handling
|
||||||
|
Theme.rootObj = root
|
||||||
|
}
|
||||||
|
|
||||||
property bool calendarVisible: false
|
property bool calendarVisible: false
|
||||||
property bool showTrayMenu: false
|
property bool showTrayMenu: false
|
||||||
property real trayMenuX: 0
|
property real trayMenuX: 0
|
||||||
@@ -73,6 +78,9 @@ ShellRoot {
|
|||||||
property string wifiConnectionStatus: ""
|
property string wifiConnectionStatus: ""
|
||||||
property bool wifiAutoRefreshEnabled: false
|
property bool wifiAutoRefreshEnabled: false
|
||||||
|
|
||||||
|
// Wallpaper error status
|
||||||
|
property string wallpaperErrorStatus: ""
|
||||||
|
|
||||||
// Notification action handling - ALWAYS invoke action if exists
|
// Notification action handling - ALWAYS invoke action if exists
|
||||||
function handleNotificationClick(notifObj) {
|
function handleNotificationClick(notifObj) {
|
||||||
console.log("Handling notification click for:", notifObj.appName)
|
console.log("Handling notification click for:", notifObj.appName)
|
||||||
@@ -167,6 +175,24 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wallpaper Error Status Timer
|
||||||
|
Timer {
|
||||||
|
id: wallpaperErrorTimer
|
||||||
|
interval: 5000 // 5 seconds
|
||||||
|
running: false
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
root.wallpaperErrorStatus = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show wallpaper error
|
||||||
|
function showWallpaperError() {
|
||||||
|
console.log("showWallpaperError called - setting error status")
|
||||||
|
root.wallpaperErrorStatus = "error"
|
||||||
|
wallpaperErrorTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
// Notification Server
|
// Notification Server
|
||||||
NotificationServer {
|
NotificationServer {
|
||||||
id: notificationServer
|
id: notificationServer
|
||||||
|
|||||||
Reference in New Issue
Block a user