Files
noctalia-shell/Services/Theming/ColorSchemeService.qml
T

293 lines
10 KiB
QML

pragma Singleton
import Qt.labs.folderlistmodel
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Theming
import qs.Services.UI
Singleton {
id: root
property var schemes: []
property bool scanning: false
property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme"
property string downloadedSchemesDirectory: Settings.configDir + "colorschemes"
property string colorsJsonFilePath: Settings.configDir + "colors.json"
// Last successfully parsed predefined scheme JSON (full object). Used to refresh app templates
// on wallpaper changes without re-running applyScheme (avoids rewriting colors.json when unchanged).
property var lastPredefinedSchemeData: null
readonly property string gtkRefreshScript: Quickshell.shellDir + "/Scripts/python/src/theming/gtk-refresh.py"
// prefer-light/prefer-dark only; GTK template post_hook still runs full gtk-refresh.
function pushSystemColorScheme() {
if (!Settings.data.colorSchemes.syncGsettings)
return;
if (TemplateProcessor.isTemplateEnabled("gtk"))
return;
const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light";
Quickshell.execDetached(["python3", gtkRefreshScript, "--appearance-only", mode]);
}
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
Logger.d("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);
}
root.pushSystemColorScheme();
// Toast: dark/light mode switched
const enabled = !!Settings.data.colorSchemes.darkMode;
const label = enabled ? I18n.tr("tooltips.switch-to-dark-mode") : I18n.tr("tooltips.switch-to-light-mode");
const description = I18n.tr("common.enabled");
ToastService.showNotice(label, description, "dark-mode");
}
}
// --------------------------------
function init() {
// does nothing but ensure the singleton is created
// do not remove
Logger.i("ColorScheme", "Service started");
loadColorSchemes();
}
function loadColorSchemes() {
Logger.d("ColorScheme", "Load colorScheme");
scanning = true;
schemes = [];
// Use find command to locate all scheme.json files in both directories
// First ensure the downloaded schemes directory exists
Quickshell.execDetached(["mkdir", "-p", downloadedSchemesDirectory]);
// Find in both preinstalled and downloaded directories
findProcess.command = ["find", "-L", schemesDirectory, downloadedSchemesDirectory, "-mindepth", "2", "-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";
} else if (schemeName === "Rosepine") {
return "Rose Pine";
}
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";
} else if (schemeName === "Rose Pine") {
schemeName = "Rosepine";
}
// Check preinstalled directory first, then downloaded directory
var preinstalledPath = schemesDirectory + "/" + schemeName + "/" + schemeName + ".json";
var downloadedPath = downloadedSchemesDirectory + "/" + schemeName + "/" + schemeName + ".json";
// Try to find the scheme in the loaded schemes list to determine which directory it's in
for (var i = 0; i < schemes.length; i++) {
if (schemes[i].indexOf("/" + schemeName + "/") !== -1 || schemes[i].indexOf("/" + schemeName + ".json") !== -1) {
return schemes[i];
}
}
// Fallback: prefer preinstalled, then downloaded
return preinstalledPath;
}
function applyScheme(nameOrPath) {
// Force reload by bouncing the path
var filePath = resolveSchemePath(nameOrPath);
schemeReader.path = "";
schemeReader.path = filePath;
}
function setPredefinedScheme(schemeName) {
Logger.i("ColorScheme", "Attempting to set predefined scheme to:", schemeName);
var resolvedPath = resolveSchemePath(schemeName);
var basename = getBasename(schemeName);
// Check if the scheme actually exists in the loaded schemes list
var schemeExists = false;
for (var i = 0; i < schemes.length; i++) {
if (getBasename(schemes[i]) === basename) {
schemeExists = true;
break;
}
}
if (schemeExists) {
Settings.data.colorSchemes.predefinedScheme = basename;
applyScheme(schemeName);
ToastService.showNotice(I18n.tr("panels.color-scheme.title"), basename, "settings-color-scheme");
} else {
Logger.e("ColorScheme", "Scheme not found:", schemeName);
ToastService.showError(I18n.tr("panels.color-scheme.title"), `'${basename}' ` + I18n.tr("common.not-found"));
}
}
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.d("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.e("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);
lastPredefinedSchemeData = data;
Logger.i("ColorScheme", "Applying color scheme:", getBasename(path));
// Generate templates for predefined color schemes
if (hasEnabledTemplates() || Settings.data.templates.enableUserTheming) {
AppThemeService.generateFromPredefinedScheme(data);
}
} catch (e) {
Logger.e("ColorScheme", "Failed to parse scheme JSON:", path, e);
}
}
}
// Check if any templates are enabled
function hasEnabledTemplates() {
const activeTemplates = Settings.data.templates.activeTemplates;
if (!activeTemplates || activeTemplates.length === 0) {
return false;
}
for (let i = 0; i < activeTemplates.length; i++) {
if (activeTemplates[i].enabled) {
return true;
}
}
return false;
}
// Writer to colors.json using a JsonAdapter for safety
FileView {
id: colorsWriter
path: colorsJsonFilePath
printErrors: false
onSaved:
// Logger.i("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: "#000000"
property color mOnError: "#000000"
property color mSurface: "#000000"
property color mOnSurface: "#000000"
property color mSurfaceVariant: "#000000"
property color mOnSurfaceVariant: "#000000"
property color mOutline: "#000000"
property color mShadow: "#000000"
property color mHover: "#000000"
property color mOnHover: "#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);
out.mHover = pick(obj, "mHover", "hover", out.mHover);
out.mOnHover = pick(obj, "mOnHover", "onHover", out.mOnHover);
// Force a rewrite by updating the path
colorsWriter.path = "";
colorsWriter.path = colorsJsonFilePath;
colorsWriter.writeAdapter();
}
}