1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00
Files
DankMaterialShell/quickshell/Modules/Settings/ThemeColorsTab.qml

2059 lines
104 KiB
QML

import QtCore
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: themeColorsTab
property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
property var installedRegistryThemes: []
property var templateDetection: ({})
property var cursorIncludeStatus: ({
"exists": false,
"included": false
})
property bool checkingCursorInclude: false
property bool fixingCursorInclude: false
function getCursorConfigPaths() {
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
switch (CompositorService.compositor) {
case "niri":
return {
"configFile": configDir + "/niri/config.kdl",
"cursorFile": configDir + "/niri/dms/cursor.kdl",
"grepPattern": 'include.*"dms/cursor.kdl"',
"includeLine": 'include "dms/cursor.kdl"'
};
case "hyprland":
return {
"configFile": configDir + "/hypr/hyprland.conf",
"cursorFile": configDir + "/hypr/dms/cursor.conf",
"grepPattern": 'source.*dms/cursor.conf',
"includeLine": "source = ./dms/cursor.conf"
};
case "dwl":
return {
"configFile": configDir + "/mango/config.conf",
"cursorFile": configDir + "/mango/dms/cursor.conf",
"grepPattern": 'source.*dms/cursor.conf',
"includeLine": "source=./dms/cursor.conf"
};
default:
return null;
}
}
function checkCursorIncludeStatus() {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
cursorIncludeStatus = {
"exists": false,
"included": false
};
return;
}
const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf";
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
checkingCursorInclude = true;
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
checkingCursorInclude = false;
if (exitCode !== 0) {
cursorIncludeStatus = {
"exists": false,
"included": false
};
return;
}
try {
cursorIncludeStatus = JSON.parse(output.trim());
} catch (e) {
cursorIncludeStatus = {
"exists": false,
"included": false
};
}
});
}
function fixCursorInclude() {
const paths = getCursorConfigPaths();
if (!paths)
return;
fixingCursorInclude = true;
const cursorDir = paths.cursorFile.substring(0, paths.cursorFile.lastIndexOf("/"));
const unixTime = Math.floor(Date.now() / 1000);
const backupFile = paths.configFile + ".backup" + unixTime;
Proc.runCommand("fix-cursor-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${cursorDir}" && ` + `touch "${paths.cursorFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
fixingCursorInclude = false;
if (exitCode !== 0)
return;
checkCursorIncludeStatus();
SettingsData.updateCompositorCursor();
});
}
function isTemplateDetected(templateId) {
if (!templateDetection || Object.keys(templateDetection).length === 0)
return true;
return templateDetection[templateId] !== false;
}
function getTemplateDescription(templateId, baseDescription) {
if (isTemplateDetected(templateId))
return baseDescription;
if (baseDescription)
return baseDescription + " · " + I18n.tr("Not detected");
return I18n.tr("Not detected");
}
function getTemplateDescriptionColor(templateId) {
if (isTemplateDetected(templateId))
return Theme.surfaceVariantText;
return Theme.warning;
}
Component.onCompleted: {
SettingsData.detectAvailableIconThemes();
SettingsData.detectAvailableCursorThemes();
if (DMSService.dmsAvailable)
DMSService.listInstalledThemes();
if (PopoutService.pendingThemeInstall)
Qt.callLater(() => showThemeBrowser());
templateCheckProcess.running = true;
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl)
checkCursorIncludeStatus();
}
Process {
id: templateCheckProcess
command: ["dms", "matugen", "check"]
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
const results = JSON.parse(text);
const detection = {};
for (const item of results) {
detection[item.id] = item.detected;
}
themeColorsTab.templateDetection = detection;
} catch (e) {
console.warn("ThemeColorsTab: Failed to parse template check:", e);
}
}
}
}
Connections {
target: DMSService
function onInstalledThemesReceived(themes) {
themeColorsTab.installedRegistryThemes = themes;
}
}
Connections {
target: PopoutService
function onPendingThemeInstallChanged() {
if (PopoutService.pendingThemeInstall)
showThemeBrowser();
}
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "theme"
tags: ["color", "palette", "theme", "appearance"]
title: I18n.tr("Theme Color")
settingKey: "themeColor"
iconName: "palette"
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
property string registryThemeName: {
if (Theme.currentThemeCategory !== "registry")
return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
return t.name;
}
return "";
}
text: {
if (Theme.currentTheme === Theme.dynamic)
return I18n.tr("Current Theme: %1", "current theme label").arg(I18n.tr("Dynamic", "dynamic theme name"));
if (Theme.currentThemeCategory === "registry" && registryThemeName)
return I18n.tr("Current Theme: %1", "current theme label").arg(registryThemeName);
return I18n.tr("Current Theme: %1", "current theme label").arg(Theme.getThemeColors(Theme.currentThemeName).name);
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (Theme.currentTheme === Theme.dynamic)
return I18n.tr("Material colors generated from wallpaper", "dynamic theme description");
if (Theme.currentThemeCategory === "registry")
return I18n.tr("Color theme from DMS registry", "registry theme description");
if (Theme.currentTheme === Theme.custom)
return I18n.tr("Custom theme loaded from JSON file", "custom theme description");
return I18n.tr("Material Design inspired color themes", "generic theme description");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: Math.min(parent.width, 400)
horizontalAlignment: Text.AlignHCenter
}
}
Column {
id: themeCategoryColumn
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
Item {
width: parent.width
height: themeCategoryGroup.implicitHeight
clip: true
DankButtonGroup {
id: themeCategoryGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 420 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 420 ? 44 : 64
textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
property int currentThemeIndex: {
if (isRegistryTheme)
return 3;
if (Theme.currentTheme === Theme.dynamic)
return 1;
if (Theme.currentThemeName === "custom")
return 2;
return 0;
}
property int pendingThemeIndex: -1
model: DMSService.dmsAvailable ? ["Generic", "Auto", "Custom", "Browse"] : ["Generic", "Auto", "Custom"]
currentIndex: currentThemeIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingThemeIndex = index;
}
onAnimationCompleted: {
if (pendingThemeIndex === -1)
return;
switch (pendingThemeIndex) {
case 0:
Theme.switchThemeCategory("generic", "blue");
break;
case 1:
if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError(I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error"));
else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error"));
else
Theme.switchThemeCategory("dynamic", Theme.dynamic);
break;
case 2:
Theme.switchThemeCategory("custom", "custom");
break;
case 3:
Theme.switchThemeCategory("registry", "");
break;
}
pendingThemeIndex = -1;
}
}
}
Item {
width: parent.width
height: genericColorGrid.implicitHeight + Math.ceil(genericColorGrid.dotSize * 0.05)
visible: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom"
Grid {
id: genericColorGrid
property var colorList: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
property int dotSize: parent.width < 300 ? 28 : 32
columns: Math.ceil(colorList.length / 2)
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: genericColorGrid.colorList
Rectangle {
required property string modelData
property string themeName: modelData
width: genericColorGrid.dotSize
height: genericColorGrid.dotSize
radius: width / 2
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2
height: nameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
StyledText {
id: nameText
text: Theme.getThemeColors(parent.parent.themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.switchTheme(parent.themeName)
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
Row {
width: parent.width
spacing: Theme.spacingM
StyledRect {
width: 120
height: 90
radius: Theme.cornerRadius
color: Theme.surfaceVariant
CachingImage {
anchors.fill: parent
anchors.margins: 1
imagePath: (Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")) ? Theme.wallpaperPath : ""
fillMode: Image.PreserveAspectCrop
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: autoWallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") ? Theme.wallpaperPath : "transparent"
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
}
Rectangle {
id: autoWallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
size: Theme.iconSizeLarge
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText
visible: !Theme.wallpaperPath
}
}
Column {
width: parent.width - 120 - Theme.spacingM
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: {
if (ToastService.wallpaperErrorStatus === "error")
return I18n.tr("Wallpaper Error", "wallpaper error status");
if (ToastService.wallpaperErrorStatus === "matugen_missing")
return I18n.tr("Matugen Missing", "matugen not found status");
if (Theme.wallpaperPath)
return Theme.wallpaperPath.split('/').pop();
return I18n.tr("No wallpaper selected", "no wallpaper status");
}
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
elide: Text.ElideMiddle
maximumLineCount: 1
width: parent.width
}
StyledText {
text: {
if (ToastService.wallpaperErrorStatus === "error")
return I18n.tr("Wallpaper processing failed", "wallpaper processing error");
if (ToastService.wallpaperErrorStatus === "matugen_missing")
return I18n.tr("Install matugen package for dynamic theming", "matugen installation hint");
if (Theme.wallpaperPath)
return Theme.wallpaperPath;
return I18n.tr("Dynamic colors from wallpaper", "dynamic colors description");
}
font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 2
width: parent.width
wrapMode: Text.WordWrap
}
}
}
SettingsDropdownRow {
tab: "theme"
tags: ["matugen", "palette", "algorithm", "dynamic"]
settingKey: "matugenScheme"
text: I18n.tr("Matugen Palette")
description: I18n.tr("Select the palette algorithm used for wallpaper-based colors")
options: cachedMatugenSchemes
currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label
enabled: Theme.matugenAvailable
opacity: enabled ? 1 : 0.4
onValueChanged: value => {
for (var i = 0; i < Theme.availableMatugenSchemes.length; i++) {
var option = Theme.availableMatugenSchemes[i];
if (option.label === value) {
SettingsData.setMatugenScheme(option.value);
break;
}
}
}
}
StyledText {
text: {
var scheme = Theme.getMatugenScheme(SettingsData.matugenScheme);
return scheme.description + " (" + scheme.value + ")";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: Theme.currentThemeName === "custom" && Theme.currentThemeCategory !== "registry"
Row {
width: parent.width
spacing: Theme.spacingM
DankActionButton {
buttonSize: 48
iconName: "folder_open"
iconSize: Theme.iconSize
backgroundColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
iconColor: Theme.primary
onClicked: fileBrowserModal.open()
}
Column {
width: parent.width - 48 - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SettingsData.customThemeFile ? SettingsData.customThemeFile.split('/').pop() : I18n.tr("No custom theme file", "no custom theme file status")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
elide: Text.ElideMiddle
maximumLineCount: 1
width: parent.width
}
StyledText {
text: SettingsData.customThemeFile || I18n.tr("Click to select a custom theme JSON file", "custom theme file hint")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
width: parent.width
}
}
}
}
Column {
id: registrySection
width: parent.width
spacing: Theme.spacingM
visible: Theme.currentThemeCategory === "registry"
Grid {
id: themeGrid
property int cardWidth: registrySection.width < 350 ? 100 : 140
property int cardHeight: registrySection.width < 350 ? 72 : 100
columns: Math.max(1, Math.floor((registrySection.width + spacing) / (cardWidth + spacing)))
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: themeColorsTab.installedRegistryThemes.length > 0
Repeater {
model: themeColorsTab.installedRegistryThemes
Rectangle {
id: themeCard
property bool isActive: Theme.currentThemeCategory === "registry" && Theme.currentThemeName === "custom" && SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((modelData.sourceDir || modelData.id) + "/theme.json")
property bool hasVariants: modelData.hasVariants || false
property var variants: modelData.variants || null
property string selectedVariant: hasVariants ? SettingsData.getRegistryThemeVariant(modelData.id, variants?.default || "") : ""
property string previewPath: {
const baseDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id);
const mode = Theme.isLightMode ? "light" : "dark";
if (hasVariants && selectedVariant)
return baseDir + "/preview-" + selectedVariant + "-" + mode + ".svg";
return baseDir + "/preview-" + mode + ".svg";
}
width: themeGrid.cardWidth
height: themeGrid.cardHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
border.color: isActive ? Theme.primary : Theme.outline
border.width: isActive ? 2 : 1
scale: isActive ? 1.03 : 1
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Image {
id: previewImage
anchors.fill: parent
anchors.margins: 2
source: "file://" + themeCard.previewPath
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
}
DankIcon {
anchors.centerIn: parent
name: "palette"
size: themeGrid.cardWidth < 120 ? 24 : 32
color: Theme.primary
visible: previewImage.status === Image.Error || previewImage.status === Image.Null
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: themeGrid.cardWidth < 120 ? 18 : 22
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.6)
StyledText {
anchors.centerIn: parent
text: modelData.name
font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall
color: "white"
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width - Theme.spacingXS * 2
horizontalAlignment: Text.AlignHCenter
}
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 16 : 20
height: width
radius: width / 2
color: Theme.primary
visible: themeCard.isActive
DankIcon {
anchors.centerIn: parent
name: "check"
size: themeGrid.cardWidth < 120 ? 10 : 14
color: Theme.surface
}
}
Rectangle {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 16 : 20
height: width
radius: width / 2
color: Theme.secondary
visible: themeCard.hasVariants && !deleteButton.visible
StyledText {
anchors.centerIn: parent
text: {
if (themeCard.variants?.type === "multi")
return themeCard.variants?.accents?.length || 0;
return themeCard.variants?.options?.length || 0;
}
font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 4 : Theme.fontSizeSmall - 2
color: Theme.surface
font.weight: Font.Bold
}
}
MouseArea {
id: cardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const themesDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes";
const themePath = themesDir + "/" + (modelData.sourceDir || modelData.id) + "/theme.json";
SettingsData.set("customThemeFile", themePath);
Theme.switchTheme("custom", true, true);
}
}
Rectangle {
id: deleteButton
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
width: themeGrid.cardWidth < 120 ? 18 : 24
height: width
radius: width / 2
color: deleteMouseArea.containsMouse ? Theme.error : Qt.rgba(0, 0, 0, 0.6)
opacity: cardMouseArea.containsMouse || deleteMouseArea.containsMouse ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
DankIcon {
anchors.centerIn: parent
name: "close"
size: themeGrid.cardWidth < 120 ? 10 : 14
color: "white"
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(modelData.name));
DMSService.uninstallTheme(modelData.id, response => {
if (response.error) {
ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error));
return;
}
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(modelData.name));
DMSService.listInstalledThemes();
});
}
}
}
}
}
}
Column {
id: variantSelector
width: parent.width
spacing: Theme.spacingS
visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0))
property string activeThemeId: {
if (Theme.currentThemeCategory !== "registry" || Theme.currentTheme !== "custom")
return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
return t.id;
}
return "";
}
property var activeThemeVariants: {
if (!activeThemeId)
return null;
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (t.id === activeThemeId && t.hasVariants)
return t.variants;
}
return null;
}
property bool isMultiVariant: activeThemeVariants?.type === "multi"
property string colorMode: Theme.isLightMode ? "light" : "dark"
property var multiDefaults: {
if (!isMultiVariant || !activeThemeVariants?.defaults)
return {};
return activeThemeVariants.defaults[colorMode] || activeThemeVariants.defaults.dark || {};
}
property var storedMulti: activeThemeId ? SettingsData.getRegistryThemeMultiVariant(activeThemeId, multiDefaults) : multiDefaults
property string selectedFlavor: storedMulti.flavor || multiDefaults.flavor || ""
property string selectedAccent: storedMulti.accent || multiDefaults.accent || ""
property var flavorOptions: {
if (!isMultiVariant || !activeThemeVariants?.flavors)
return [];
return activeThemeVariants.flavors.filter(f => f.mode === colorMode || f.mode === "both");
}
property var flavorNames: flavorOptions.map(f => f.name)
property int flavorIndex: {
for (var i = 0; i < flavorOptions.length; i++) {
if (flavorOptions[i].id === selectedFlavor)
return i;
}
return 0;
}
property string selectedVariant: activeThemeId ? SettingsData.getRegistryThemeVariant(activeThemeId, activeThemeVariants?.default || "") : ""
property var variantNames: {
if (!activeThemeVariants?.options)
return [];
return activeThemeVariants.options.map(v => v.name);
}
property int selectedIndex: {
if (!activeThemeVariants?.options || !selectedVariant)
return 0;
for (var i = 0; i < activeThemeVariants.options.length; i++) {
if (activeThemeVariants.options[i].id === selectedVariant)
return i;
}
return 0;
}
Item {
width: parent.width
height: flavorButtonGroup.implicitHeight
clip: true
visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1
DankButtonGroup {
id: flavorButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 400 ? 44 : 64
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: variantSelector.flavorNames
currentIndex: variantSelector.flavorIndex
selectionMode: "single"
onAnimationCompleted: {
if (currentIndex < 0 || currentIndex >= variantSelector.flavorOptions.length)
return;
const flavorId = variantSelector.flavorOptions[currentIndex]?.id;
if (!flavorId || flavorId === variantSelector.selectedFlavor)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, flavorId, variantSelector.selectedAccent);
}
}
}
Item {
width: parent.width
height: accentColorsGrid.implicitHeight
visible: variantSelector.isMultiVariant && variantSelector.activeThemeVariants?.accents?.length > 0
Grid {
id: accentColorsGrid
property int accentCount: variantSelector.activeThemeVariants?.accents?.length ?? 0
property int dotSize: parent.width < 300 ? 28 : 32
columns: accentCount > 0 ? Math.ceil(accentCount / 2) : 1
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: variantSelector.activeThemeVariants?.accents || []
Rectangle {
required property var modelData
required property int index
property string accentId: modelData.id
property bool isSelected: accentId === variantSelector.selectedAccent
width: accentColorsGrid.dotSize
height: accentColorsGrid.dotSize
radius: width / 2
color: modelData.color || Theme.primary
border.color: Theme.outline
border.width: isSelected ? 2 : 1
scale: isSelected ? 1.1 : 1
Rectangle {
width: accentNameText.contentWidth + Theme.spacingS * 2
height: accentNameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: accentMouseArea.containsMouse
StyledText {
id: accentNameText
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: accentMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (parent.isSelected)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
Item {
width: parent.width
height: variantButtonGroup.implicitHeight
clip: true
visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0
DankButtonGroup {
id: variantButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 400 ? 44 : 64
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: variantSelector.variantNames
currentIndex: variantSelector.selectedIndex
selectionMode: "single"
onAnimationCompleted: {
if (currentIndex < 0 || !variantSelector.activeThemeVariants?.options)
return;
const variantId = variantSelector.activeThemeVariants.options[currentIndex]?.id;
if (!variantId || variantId === variantSelector.selectedVariant)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeVariant(variantSelector.activeThemeId, variantId);
}
}
}
}
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
visible: themeColorsTab.installedRegistryThemes.length === 0
horizontalAlignment: Text.AlignHCenter
}
DankButton {
text: I18n.tr("Browse Themes", "browse themes button")
iconName: "store"
anchors.horizontalCenter: parent.horizontalCenter
onClicked: showThemeBrowser()
}
}
}
}
SettingsCard {
tab: "theme"
tags: ["light", "dark", "mode", "appearance"]
title: I18n.tr("Color Mode")
settingKey: "colorMode"
iconName: "contrast"
SettingsToggleRow {
tab: "theme"
tags: ["light", "dark", "mode"]
settingKey: "isLightMode"
text: I18n.tr("Light Mode")
description: I18n.tr("Use light theme instead of dark theme")
checked: SessionData.isLightMode
onToggled: checked => {
Theme.screenTransition();
Theme.setLightMode(checked);
}
}
}
SettingsCard {
tab: "theme"
tags: ["transparency", "opacity", "widget", "styling"]
title: I18n.tr("Widget Styling")
settingKey: "widgetStyling"
iconName: "opacity"
SettingsButtonGroupRow {
tab: "theme"
tags: ["widget", "style", "colorful", "default"]
settingKey: "widgetColorMode"
text: I18n.tr("Widget Style")
description: I18n.tr("Change bar appearance")
model: ["default", "colorful"]
currentIndex: SettingsData.widgetColorMode === "colorful" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("widgetColorMode", index === 1 ? "colorful" : "default");
}
}
SettingsButtonGroupRow {
tab: "theme"
tags: ["widget", "background", "color"]
settingKey: "widgetBackgroundColor"
text: I18n.tr("Widget Background Color")
description: I18n.tr("Choose the background color for widgets")
model: ["sth", "s", "sc", "sch"]
buttonHeight: 20
minButtonWidth: 32
buttonPadding: Theme.spacingS
checkIconSize: Theme.iconSizeSmall - 2
textSize: Theme.fontSizeSmall - 2
spacing: 1
currentIndex: {
switch (SettingsData.widgetBackgroundColor) {
case "sth":
return 0;
case "s":
return 1;
case "sc":
return 2;
case "sch":
return 3;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const colorOptions = ["sth", "s", "sc", "sch"];
SettingsData.set("widgetBackgroundColor", colorOptions[index]);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency"
text: I18n.tr("Popup Transparency")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => SettingsData.set("popupTransparency", newValue / 100)
}
SettingsSliderRow {
tab: "theme"
tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius"
text: I18n.tr("Corner Radius")
description: I18n.tr("0 = square corners")
value: SettingsData.cornerRadius
minimum: 0
maximum: 32
unit: "px"
defaultValue: 12
onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue)
}
}
SettingsCard {
tab: "theme"
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
title: I18n.tr("Niri Layout Overrides").replace("Niri", "niri")
settingKey: "niriLayout"
iconName: "crop_square"
visible: CompositorService.isNiri
SettingsToggleRow {
tab: "theme"
tags: ["niri", "gaps", "override"]
settingKey: "niriLayoutGapsOverrideEnabled"
text: I18n.tr("Override Gaps")
description: I18n.tr("Use custom gaps instead of bar spacing")
checked: SettingsData.niriLayoutGapsOverride >= 0
onToggled: checked => {
if (checked) {
const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
SettingsData.set("niriLayoutGapsOverride", currentGaps);
return;
}
SettingsData.set("niriLayoutGapsOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["niri", "gaps", "override"]
settingKey: "niriLayoutGapsOverride"
text: I18n.tr("Window Gaps")
description: I18n.tr("Space between windows")
visible: SettingsData.niriLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.niriLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("niriLayoutGapsOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["niri", "radius", "override"]
settingKey: "niriLayoutRadiusOverrideEnabled"
text: I18n.tr("Override Corner Radius")
description: I18n.tr("Use custom window radius instead of theme radius")
checked: SettingsData.niriLayoutRadiusOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("niriLayoutRadiusOverride", SettingsData.cornerRadius);
return;
}
SettingsData.set("niriLayoutRadiusOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["niri", "radius", "override"]
settingKey: "niriLayoutRadiusOverride"
text: I18n.tr("Window Corner Radius")
description: I18n.tr("Rounded corners for windows")
visible: SettingsData.niriLayoutRadiusOverride >= 0
value: Math.max(0, SettingsData.niriLayoutRadiusOverride)
minimum: 0
maximum: 100
unit: "px"
defaultValue: SettingsData.cornerRadius
onSliderValueChanged: newValue => SettingsData.set("niriLayoutRadiusOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["niri", "border", "override"]
settingKey: "niriLayoutBorderSizeEnabled"
text: I18n.tr("Override Border Size")
description: I18n.tr("Use custom border/focus-ring width")
checked: SettingsData.niriLayoutBorderSize >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("niriLayoutBorderSize", 2);
return;
}
SettingsData.set("niriLayoutBorderSize", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["niri", "border", "override"]
settingKey: "niriLayoutBorderSize"
text: I18n.tr("Border Size")
description: I18n.tr("Width of window border and focus ring")
visible: SettingsData.niriLayoutBorderSize >= 0
value: Math.max(0, SettingsData.niriLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
defaultValue: 2
onSliderValueChanged: newValue => SettingsData.set("niriLayoutBorderSize", newValue)
}
}
SettingsCard {
tab: "theme"
tags: ["hyprland", "layout", "gaps", "radius", "window", "border", "rounding"]
title: I18n.tr("Hyprland Layout Overrides")
settingKey: "hyprlandLayout"
iconName: "crop_square"
visible: CompositorService.isHyprland
SettingsToggleRow {
tab: "theme"
tags: ["hyprland", "gaps", "override"]
settingKey: "hyprlandLayoutGapsOverrideEnabled"
text: I18n.tr("Override Gaps")
description: I18n.tr("Use custom gaps instead of bar spacing")
checked: SettingsData.hyprlandLayoutGapsOverride >= 0
onToggled: checked => {
if (checked) {
const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
SettingsData.set("hyprlandLayoutGapsOverride", currentGaps);
return;
}
SettingsData.set("hyprlandLayoutGapsOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["hyprland", "gaps", "override"]
settingKey: "hyprlandLayoutGapsOverride"
text: I18n.tr("Window Gaps")
description: I18n.tr("Space between windows (gaps_in and gaps_out)")
visible: SettingsData.hyprlandLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutGapsOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["hyprland", "radius", "override", "rounding"]
settingKey: "hyprlandLayoutRadiusOverrideEnabled"
text: I18n.tr("Override Corner Radius")
description: I18n.tr("Use custom window rounding instead of theme radius")
checked: SettingsData.hyprlandLayoutRadiusOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("hyprlandLayoutRadiusOverride", SettingsData.cornerRadius);
return;
}
SettingsData.set("hyprlandLayoutRadiusOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["hyprland", "radius", "override", "rounding"]
settingKey: "hyprlandLayoutRadiusOverride"
text: I18n.tr("Window Rounding")
description: I18n.tr("Rounded corners for windows (decoration.rounding)")
visible: SettingsData.hyprlandLayoutRadiusOverride >= 0
value: Math.max(0, SettingsData.hyprlandLayoutRadiusOverride)
minimum: 0
maximum: 100
unit: "px"
defaultValue: SettingsData.cornerRadius
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutRadiusOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["hyprland", "border", "override"]
settingKey: "hyprlandLayoutBorderSizeEnabled"
text: I18n.tr("Override Border Size")
description: I18n.tr("Use custom border size")
checked: SettingsData.hyprlandLayoutBorderSize >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("hyprlandLayoutBorderSize", 2);
return;
}
SettingsData.set("hyprlandLayoutBorderSize", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["hyprland", "border", "override"]
settingKey: "hyprlandLayoutBorderSize"
text: I18n.tr("Border Size")
description: I18n.tr("Width of window border (general.border_size)")
visible: SettingsData.hyprlandLayoutBorderSize >= 0
value: Math.max(0, SettingsData.hyprlandLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
defaultValue: 2
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutBorderSize", newValue)
}
}
SettingsCard {
tab: "theme"
tags: ["mangowc", "mango", "dwl", "layout", "gaps", "radius", "window", "border"]
title: I18n.tr("MangoWC Layout Overrides")
settingKey: "mangoLayout"
iconName: "crop_square"
visible: CompositorService.isDwl
SettingsToggleRow {
tab: "theme"
tags: ["mangowc", "mango", "gaps", "override"]
settingKey: "mangoLayoutGapsOverrideEnabled"
text: I18n.tr("Override Gaps")
description: I18n.tr("Use custom gaps instead of bar spacing")
checked: SettingsData.mangoLayoutGapsOverride >= 0
onToggled: checked => {
if (checked) {
const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
SettingsData.set("mangoLayoutGapsOverride", currentGaps);
return;
}
SettingsData.set("mangoLayoutGapsOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["mangowc", "mango", "gaps", "override"]
settingKey: "mangoLayoutGapsOverride"
text: I18n.tr("Window Gaps")
description: I18n.tr("Space between windows (gappih/gappiv/gappoh/gappov)")
visible: SettingsData.mangoLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.mangoLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutGapsOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["mangowc", "mango", "radius", "override"]
settingKey: "mangoLayoutRadiusOverrideEnabled"
text: I18n.tr("Override Corner Radius")
description: I18n.tr("Use custom window radius instead of theme radius")
checked: SettingsData.mangoLayoutRadiusOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("mangoLayoutRadiusOverride", SettingsData.cornerRadius);
return;
}
SettingsData.set("mangoLayoutRadiusOverride", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["mangowc", "mango", "radius", "override"]
settingKey: "mangoLayoutRadiusOverride"
text: I18n.tr("Window Corner Radius")
description: I18n.tr("Rounded corners for windows (border_radius)")
visible: SettingsData.mangoLayoutRadiusOverride >= 0
value: Math.max(0, SettingsData.mangoLayoutRadiusOverride)
minimum: 0
maximum: 100
unit: "px"
defaultValue: SettingsData.cornerRadius
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutRadiusOverride", newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["mangowc", "mango", "border", "override"]
settingKey: "mangoLayoutBorderSizeEnabled"
text: I18n.tr("Override Border Size")
description: I18n.tr("Use custom border size")
checked: SettingsData.mangoLayoutBorderSize >= 0
onToggled: checked => {
if (checked) {
SettingsData.set("mangoLayoutBorderSize", 2);
return;
}
SettingsData.set("mangoLayoutBorderSize", -1);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["mangowc", "mango", "border", "override"]
settingKey: "mangoLayoutBorderSize"
text: I18n.tr("Border Size")
description: I18n.tr("Width of window border (borderpx)")
visible: SettingsData.mangoLayoutBorderSize >= 0
value: Math.max(0, SettingsData.mangoLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
defaultValue: 2
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutBorderSize", newValue)
}
}
SettingsCard {
tab: "theme"
tags: ["modal", "darken", "background", "overlay"]
title: I18n.tr("Modal Background")
settingKey: "modalBackground"
SettingsToggleRow {
tab: "theme"
tags: ["modal", "darken", "background", "overlay"]
settingKey: "modalDarkenBackground"
text: I18n.tr("Darken Modal Background")
description: I18n.tr("Show darkened overlay behind modal dialogs")
checked: SettingsData.modalDarkenBackground
onToggled: checked => SettingsData.set("modalDarkenBackground", checked)
}
}
SettingsCard {
tab: "theme"
tags: ["applications", "portal", "dark", "terminal"]
title: I18n.tr("Applications")
settingKey: "applications"
iconName: "terminal"
SettingsToggleRow {
tab: "theme"
tags: ["portal", "sync", "dark", "mode"]
settingKey: "syncModeWithPortal"
text: I18n.tr("Sync Mode with Portal")
description: I18n.tr("Sync dark mode with settings portals for system-wide theme hints")
checked: SettingsData.syncModeWithPortal
onToggled: checked => SettingsData.set("syncModeWithPortal", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["terminal", "dark", "always"]
settingKey: "terminalsAlwaysDark"
text: I18n.tr("Terminals - Always use Dark Theme")
description: I18n.tr("Force terminal applications to always use dark color schemes")
checked: SettingsData.terminalsAlwaysDark
onToggled: checked => SettingsData.set("terminalsAlwaysDark", checked)
}
}
SettingsCard {
tab: "theme"
tags: ["cursor", "mouse", "pointer", "theme", "size"]
title: I18n.tr("Cursor Theme")
settingKey: "cursorTheme"
iconName: "mouse"
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
Column {
width: parent.width
spacing: Theme.spacingM
StyledRect {
id: cursorWarningBox
width: parent.width
height: cursorWarningContent.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
readonly property bool showError: themeColorsTab.cursorIncludeStatus.exists && !themeColorsTab.cursorIncludeStatus.included
readonly property bool showSetup: !themeColorsTab.cursorIncludeStatus.exists && !themeColorsTab.cursorIncludeStatus.included
color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent"
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent"
border.width: 1
visible: (showError || showSetup) && !themeColorsTab.checkingCursorInclude
Row {
id: cursorWarningContent
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - (cursorFixButton.visible ? cursorFixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: cursorWarningBox.showSetup ? I18n.tr("Cursor Config Not Configured") : I18n.tr("Cursor Include Missing")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.warning
}
StyledText {
text: cursorWarningBox.showSetup ? I18n.tr("Click 'Setup' to create cursor config and add include to your compositor config.") : I18n.tr("dms/cursor config exists but is not included. Cursor settings won't apply.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankButton {
id: cursorFixButton
visible: cursorWarningBox.showError || cursorWarningBox.showSetup
text: themeColorsTab.fixingCursorInclude ? I18n.tr("Fixing...") : (cursorWarningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now"))
backgroundColor: Theme.warning
textColor: Theme.background
enabled: !themeColorsTab.fixingCursorInclude
anchors.verticalCenter: parent.verticalCenter
onClicked: themeColorsTab.fixCursorInclude()
}
}
}
SettingsDropdownRow {
tab: "theme"
tags: ["cursor", "mouse", "pointer", "theme"]
settingKey: "cursorTheme"
text: I18n.tr("Cursor Theme")
description: I18n.tr("Mouse pointer appearance")
currentValue: SettingsData.cursorSettings.theme
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 236
options: cachedCursorThemes
onValueChanged: value => {
SettingsData.setCursorTheme(value);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["cursor", "mouse", "pointer", "size"]
settingKey: "cursorSize"
text: I18n.tr("Cursor Size")
description: I18n.tr("Mouse pointer size in pixels")
value: SettingsData.cursorSettings.size
minimum: 12
maximum: 128
unit: "px"
defaultValue: 24
onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue)
}
SettingsToggleRow {
tab: "theme"
tags: ["cursor", "hide", "typing"]
settingKey: "cursorHideWhenTyping"
text: I18n.tr("Hide When Typing")
description: I18n.tr("Hide cursor when pressing keyboard keys")
visible: CompositorService.isNiri || CompositorService.isHyprland
checked: {
if (CompositorService.isNiri)
return SettingsData.cursorSettings.niri?.hideWhenTyping || false;
if (CompositorService.isHyprland)
return SettingsData.cursorSettings.hyprland?.hideOnKeyPress || false;
return false;
}
onToggled: checked => {
const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings));
if (CompositorService.isNiri) {
if (!updated.niri)
updated.niri = {};
updated.niri.hideWhenTyping = checked;
} else if (CompositorService.isHyprland) {
if (!updated.hyprland)
updated.hyprland = {};
updated.hyprland.hideOnKeyPress = checked;
}
SettingsData.set("cursorSettings", updated);
}
}
SettingsToggleRow {
tab: "theme"
tags: ["cursor", "hide", "touch"]
settingKey: "cursorHideOnTouch"
text: I18n.tr("Hide on Touch")
description: I18n.tr("Hide cursor when using touch input")
visible: CompositorService.isHyprland
checked: SettingsData.cursorSettings.hyprland?.hideOnTouch || false
onToggled: checked => {
const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings));
if (!updated.hyprland)
updated.hyprland = {};
updated.hyprland.hideOnTouch = checked;
SettingsData.set("cursorSettings", updated);
}
}
SettingsSliderRow {
tab: "theme"
tags: ["cursor", "hide", "timeout", "inactive"]
settingKey: "cursorHideAfterInactive"
text: I18n.tr("Auto-Hide Timeout")
description: I18n.tr("Hide cursor after inactivity (0 = disabled)")
value: {
if (CompositorService.isNiri)
return SettingsData.cursorSettings.niri?.hideAfterInactiveMs || 0;
if (CompositorService.isHyprland)
return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0;
if (CompositorService.isDwl)
return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0;
return 0;
}
minimum: 0
maximum: CompositorService.isNiri ? 5000 : 10
unit: CompositorService.isNiri ? "ms" : "s"
defaultValue: 0
onSliderValueChanged: newValue => {
const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings));
if (CompositorService.isNiri) {
if (!updated.niri)
updated.niri = {};
updated.niri.hideAfterInactiveMs = newValue;
} else if (CompositorService.isHyprland) {
if (!updated.hyprland)
updated.hyprland = {};
updated.hyprland.inactiveTimeout = newValue;
} else if (CompositorService.isDwl) {
if (!updated.dwl)
updated.dwl = {};
updated.dwl.cursorHideTimeout = newValue;
}
SettingsData.set("cursorSettings", updated);
}
}
}
}
SettingsCard {
tab: "theme"
tags: ["matugen", "templates", "theming"]
title: I18n.tr("Matugen Templates")
settingKey: "matugenTemplates"
iconName: "auto_awesome"
visible: Theme.matugenAvailable
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "user", "templates"]
settingKey: "runUserMatugenTemplates"
text: I18n.tr("Run User Templates")
description: ""
checked: SettingsData.runUserMatugenTemplates
onToggled: checked => SettingsData.set("runUserMatugenTemplates", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "dms", "templates"]
settingKey: "runDmsMatugenTemplates"
text: I18n.tr("Run DMS Templates")
description: ""
checked: SettingsData.runDmsMatugenTemplates
onToggled: checked => SettingsData.set("runDmsMatugenTemplates", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "gtk", "template"]
settingKey: "matugenTemplateGtk"
text: "GTK"
description: getTemplateDescription("gtk", "")
descriptionColor: getTemplateDescriptionColor("gtk")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateGtk
onToggled: checked => SettingsData.set("matugenTemplateGtk", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "niri", "template"]
settingKey: "matugenTemplateNiri"
text: "niri"
description: getTemplateDescription("niri", "")
descriptionColor: getTemplateDescriptionColor("niri")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateNiri
onToggled: checked => SettingsData.set("matugenTemplateNiri", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "hyprland", "template"]
settingKey: "matugenTemplateHyprland"
text: "Hyprland"
description: getTemplateDescription("hyprland", "")
descriptionColor: getTemplateDescriptionColor("hyprland")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateHyprland
onToggled: checked => SettingsData.set("matugenTemplateHyprland", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "mangowc", "template"]
settingKey: "matugenTemplateMangowc"
text: "mangowc"
description: getTemplateDescription("mangowc", "")
descriptionColor: getTemplateDescriptionColor("mangowc")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateMangowc
onToggled: checked => SettingsData.set("matugenTemplateMangowc", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "qt5ct", "template"]
settingKey: "matugenTemplateQt5ct"
text: "qt5ct"
description: getTemplateDescription("qt5ct", "")
descriptionColor: getTemplateDescriptionColor("qt5ct")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateQt5ct
onToggled: checked => SettingsData.set("matugenTemplateQt5ct", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "qt6ct", "template"]
settingKey: "matugenTemplateQt6ct"
text: "qt6ct"
description: getTemplateDescription("qt6ct", "")
descriptionColor: getTemplateDescriptionColor("qt6ct")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateQt6ct
onToggled: checked => SettingsData.set("matugenTemplateQt6ct", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "firefox", "template"]
settingKey: "matugenTemplateFirefox"
text: "Firefox"
description: getTemplateDescription("firefox", "")
descriptionColor: getTemplateDescriptionColor("firefox")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateFirefox
onToggled: checked => SettingsData.set("matugenTemplateFirefox", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "pywalfox", "template"]
settingKey: "matugenTemplatePywalfox"
text: "pywalfox"
description: getTemplateDescription("pywalfox", "")
descriptionColor: getTemplateDescriptionColor("pywalfox")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplatePywalfox
onToggled: checked => SettingsData.set("matugenTemplatePywalfox", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "zenbrowser", "template"]
settingKey: "matugenTemplateZenBrowser"
text: "zenbrowser"
description: getTemplateDescription("zenbrowser", "")
descriptionColor: getTemplateDescriptionColor("zenbrowser")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateZenBrowser
onToggled: checked => SettingsData.set("matugenTemplateZenBrowser", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "vesktop", "discord", "template"]
settingKey: "matugenTemplateVesktop"
text: "vesktop"
description: getTemplateDescription("vesktop", "")
descriptionColor: getTemplateDescriptionColor("vesktop")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateVesktop
onToggled: checked => SettingsData.set("matugenTemplateVesktop", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "equibop", "discord", "template"]
settingKey: "matugenTemplateEquibop"
text: "equibop"
description: getTemplateDescription("equibop", "")
descriptionColor: getTemplateDescriptionColor("equibop")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateEquibop
onToggled: checked => SettingsData.set("matugenTemplateEquibop", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "ghostty", "terminal", "template"]
settingKey: "matugenTemplateGhostty"
text: "Ghostty"
description: getTemplateDescription("ghostty", "")
descriptionColor: getTemplateDescriptionColor("ghostty")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateGhostty
onToggled: checked => SettingsData.set("matugenTemplateGhostty", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "kitty", "terminal", "template"]
settingKey: "matugenTemplateKitty"
text: "kitty"
description: getTemplateDescription("kitty", "")
descriptionColor: getTemplateDescriptionColor("kitty")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateKitty
onToggled: checked => SettingsData.set("matugenTemplateKitty", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "foot", "terminal", "template"]
settingKey: "matugenTemplateFoot"
text: "foot"
description: getTemplateDescription("foot", "")
descriptionColor: getTemplateDescriptionColor("foot")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateFoot
onToggled: checked => SettingsData.set("matugenTemplateFoot", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovim"
text: "neovim"
description: getTemplateDescription("nvim", "Requires lazy plugin manager")
descriptionColor: getTemplateDescriptionColor("nvim")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateNeovim
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "alacritty", "terminal", "template"]
settingKey: "matugenTemplateAlacritty"
text: "Alacritty"
description: getTemplateDescription("alacritty", "")
descriptionColor: getTemplateDescriptionColor("alacritty")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateAlacritty
onToggled: checked => SettingsData.set("matugenTemplateAlacritty", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "wezterm", "terminal", "template"]
settingKey: "matugenTemplateWezterm"
text: "WezTerm"
description: getTemplateDescription("wezterm", "")
descriptionColor: getTemplateDescriptionColor("wezterm")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateWezterm
onToggled: checked => SettingsData.set("matugenTemplateWezterm", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "dgop", "template"]
settingKey: "matugenTemplateDgop"
text: "dgop"
description: getTemplateDescription("dgop", "")
descriptionColor: getTemplateDescriptionColor("dgop")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateDgop
onToggled: checked => SettingsData.set("matugenTemplateDgop", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "kcolorscheme", "kde", "template"]
settingKey: "matugenTemplateKcolorscheme"
text: "KColorScheme"
description: getTemplateDescription("kcolorscheme", "")
descriptionColor: getTemplateDescriptionColor("kcolorscheme")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateKcolorscheme
onToggled: checked => SettingsData.set("matugenTemplateKcolorscheme", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "vscode", "code", "template"]
settingKey: "matugenTemplateVscode"
text: "VS Code"
description: getTemplateDescription("vscode", "")
descriptionColor: getTemplateDescriptionColor("vscode")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateVscode
onToggled: checked => SettingsData.set("matugenTemplateVscode", checked)
}
}
Rectangle {
width: parent.width
height: warningText.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "info"
size: Theme.iconSizeSmall
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: warningText
font.pixelSize: Theme.fontSizeSmall
text: I18n.tr("The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).")
wrapMode: Text.WordWrap
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
}
}
SettingsCard {
tab: "theme"
tags: ["icon", "theme", "system"]
title: I18n.tr("Icon Theme")
settingKey: "iconTheme"
SettingsDropdownRow {
tab: "theme"
tags: ["icon", "theme", "system"]
settingKey: "iconTheme"
text: I18n.tr("Icon Theme")
description: I18n.tr("DankShell & System Icons (requires restart)")
currentValue: SettingsData.iconTheme
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 236
options: cachedIconThemes
onValueChanged: value => {
SettingsData.setIconTheme(value);
if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" && Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") {
ToastService.showError("Missing Environment Variables", "You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.");
}
}
}
}
SettingsCard {
tab: "theme"
tags: ["system", "app", "theming", "gtk", "qt"]
title: I18n.tr("System App Theming")
settingKey: "systemAppTheming"
iconName: "extension"
visible: Theme.matugenAvailable
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "folder"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Apply GTK Colors")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.applyGtkColors()
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "settings"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Apply Qt Colors")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.applyQtColors()
}
}
}
StyledText {
text: I18n.tr(`Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href="https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming" style="text-decoration:none; color:${Theme.primary};">adw-gtk3</a> prior to applying GTK themes.`)
textFormat: Text.RichText
linkColor: Theme.primary
onLinkActivated: url => Qt.openUrlExternally(url)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
}
}
FileBrowserModal {
id: fileBrowserModal
browserTitle: I18n.tr("Select Custom Theme", "custom theme file browser title")
filterExtensions: ["*.json"]
showHiddenFiles: true
function selectCustomTheme() {
shouldBeVisible = true;
}
onFileSelected: function (filePath) {
if (filePath.endsWith(".json")) {
SettingsData.set("customThemeFile", filePath);
Theme.switchTheme("custom");
close();
}
}
}
LazyLoader {
id: themeBrowserLoader
active: false
ThemeBrowser {
id: themeBrowserItem
}
}
function showThemeBrowser() {
themeBrowserLoader.active = true;
if (themeBrowserLoader.item)
themeBrowserLoader.item.show();
}
}