Files
noctalia-shell/Services/ColorSchemeService.qml
T
2025-10-06 14:51:43 -04:00

219 lines
7.4 KiB
QML

pragma Singleton
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
Singleton {
id: root
property var schemes: []
property bool scanning: false
property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme"
property string colorsJsonFilePath: Settings.configDir + "colors.json"
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
Logger.log("ColorScheme", "Detected dark mode change")
if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) {
// Re-apply current scheme to pick the right variant
applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
// Toast: dark/light mode switched
const enabled = !!Settings.data.colorSchemes.darkMode
const label = enabled ? "Dark mode" : "Light mode"
const description = enabled ? "Enabled" : "Enabled"
ToastService.showNotice(label, description)
}
}
// --------------------------------
function init() {
// does nothing but ensure the singleton is created
// do not remove
Logger.log("ColorScheme", "Service started")
loadColorSchemes()
}
function loadColorSchemes() {
Logger.log("ColorScheme", "Load colorScheme")
scanning = true
schemes = []
// Use find command to locate all scheme.json files
findProcess.command = ["find", schemesDirectory, "-name", "*.json", "-type", "f"]
findProcess.running = true
}
function getBasename(path) {
if (!path)
return ""
var chunks = path.split("/")
// Get the filename without extension
var filename = chunks[chunks.length - 1]
var schemeName = filename.replace(".json", "")
// Convert back to display names for special cases
if (schemeName === "Noctalia-default") {
return "Noctalia (default)"
} else if (schemeName === "Noctalia-legacy") {
return "Noctalia (legacy)"
} else if (schemeName === "Tokyo-Night") {
return "Tokyo Night"
}
return schemeName
}
function resolveSchemePath(nameOrPath) {
if (!nameOrPath)
return ""
if (nameOrPath.indexOf("/") !== -1) {
return nameOrPath
}
// Handle special cases for Noctalia schemes
var schemeName = nameOrPath.replace(".json", "")
if (schemeName === "Noctalia (default)") {
schemeName = "Noctalia-default"
} else if (schemeName === "Noctalia (legacy)") {
schemeName = "Noctalia-legacy"
} else if (schemeName === "Tokyo Night") {
schemeName = "Tokyo-Night"
}
return schemesDirectory + "/" + schemeName + "/" + schemeName + ".json"
}
function applyScheme(nameOrPath) {
// Force reload by bouncing the path
var filePath = resolveSchemePath(nameOrPath)
schemeReader.path = ""
schemeReader.path = filePath
}
Process {
id: findProcess
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
var output = stdout.text.trim()
var files = output.split('\n').filter(function (line) {
return line.length > 0
})
files.sort(function (a, b) {
var nameA = getBasename(a).toLowerCase()
var nameB = getBasename(b).toLowerCase()
return nameA.localeCompare(nameB)
})
schemes = files
scanning = false
Logger.log("ColorScheme", "Listed", schemes.length, "schemes")
// Normalize stored scheme to basename and re-apply if necessary
var stored = Settings.data.colorSchemes.predefinedScheme
if (stored) {
var basename = getBasename(stored)
if (basename !== stored) {
Settings.data.colorSchemes.predefinedScheme = basename
}
if (!Settings.data.colorSchemes.useWallpaperColors) {
applyScheme(basename)
}
}
} else {
Logger.error("ColorScheme", "Failed to find color scheme files")
schemes = []
scanning = false
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Internal loader to read a scheme file
FileView {
id: schemeReader
onLoaded: {
try {
var data = JSON.parse(text())
var variant = data
// If scheme provides dark/light variants, pick based on settings
if (data && (data.dark || data.light)) {
if (Settings.data.colorSchemes.darkMode) {
variant = data.dark || data.light
} else {
variant = data.light || data.dark
}
}
writeColorsToDisk(variant)
Logger.log("ColorScheme", "Applying color scheme:", getBasename(path))
// Generate Matugen templates if any are enabled and setting allows it
if (Settings.data.colorSchemes.generateTemplatesForPredefined && hasEnabledMatugenTemplates()) {
AppThemeService.generateFromPredefinedScheme(data)
}
} catch (e) {
Logger.error("ColorScheme", "Failed to parse scheme JSON:", path, e)
}
}
}
// Check if any Matugen templates are enabled
function hasEnabledMatugenTemplates() {
return Settings.data.templates.gtk || Settings.data.templates.qt || Settings.data.templates.kitty || Settings.data.templates.ghostty || Settings.data.templates.foot || Settings.data.templates.fuzzel || Settings.data.templates.vesktop || Settings.data.templates.pywalfox
}
// Writer to colors.json using a JsonAdapter for safety
FileView {
id: colorsWriter
path: colorsJsonFilePath
onSaved: {
// Logger.log("ColorScheme", "Colors saved")
}
JsonAdapter {
id: out
property color mPrimary: "#000000"
property color mOnPrimary: "#000000"
property color mSecondary: "#000000"
property color mOnSecondary: "#000000"
property color mTertiary: "#000000"
property color mOnTertiary: "#000000"
property color mError: "#ff0000"
property color mOnError: "#000000"
property color mSurface: "#ffffff"
property color mOnSurface: "#000000"
property color mSurfaceVariant: "#cccccc"
property color mOnSurfaceVariant: "#333333"
property color mOutline: "#444444"
property color mShadow: "#000000"
}
}
function writeColorsToDisk(obj) {
function pick(o, a, b, fallback) {
return (o && (o[a] || o[b])) || fallback
}
out.mPrimary = pick(obj, "mPrimary", "primary", out.mPrimary)
out.mOnPrimary = pick(obj, "mOnPrimary", "onPrimary", out.mOnPrimary)
out.mSecondary = pick(obj, "mSecondary", "secondary", out.mSecondary)
out.mOnSecondary = pick(obj, "mOnSecondary", "onSecondary", out.mOnSecondary)
out.mTertiary = pick(obj, "mTertiary", "tertiary", out.mTertiary)
out.mOnTertiary = pick(obj, "mOnTertiary", "onTertiary", out.mOnTertiary)
out.mError = pick(obj, "mError", "error", out.mError)
out.mOnError = pick(obj, "mOnError", "onError", out.mOnError)
out.mSurface = pick(obj, "mSurface", "surface", out.mSurface)
out.mOnSurface = pick(obj, "mOnSurface", "onSurface", out.mOnSurface)
out.mSurfaceVariant = pick(obj, "mSurfaceVariant", "surfaceVariant", out.mSurfaceVariant)
out.mOnSurfaceVariant = pick(obj, "mOnSurfaceVariant", "onSurfaceVariant", out.mOnSurfaceVariant)
out.mOutline = pick(obj, "mOutline", "outline", out.mOutline)
out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow)
// Force a rewrite by updating the path
colorsWriter.path = ""
colorsWriter.path = colorsJsonFilePath
colorsWriter.writeAdapter()
}
}