From fa838ecdb1c36579a7a91ee7ad08a46c1829e815 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 15 Sep 2025 07:44:31 +0200 Subject: [PATCH] Cleaned up ColorSchemeTab, added program checks, added firefox template Matugen: added firefox (pywalfox) template SidePanelToggle: use ProgramCheckerService for gpu-screen-recorder ColorSchemeTab: use NCollapsible for matugen templates, use ProgramCheckerService to detect available programs (for matugen templates) NCollapsible: create collapsible category --- Assets/Matugen/Matugen.qml | 6 + Assets/Matugen/templates/pywalfox.json | 22 ++ Commons/Settings.qml | 1 + Modules/Bar/Widgets/SidePanelToggle.qml | 2 +- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 293 ++++++++++-------- Services/ProgramCheckerService.qml | 118 +++++++ Services/ScreenRecorderService.qml | 27 +- Widgets/NCollapsible.qml | 193 ++++++++++++ 8 files changed, 514 insertions(+), 148 deletions(-) create mode 100644 Assets/Matugen/templates/pywalfox.json create mode 100644 Services/ProgramCheckerService.qml create mode 100644 Widgets/NCollapsible.qml diff --git a/Assets/Matugen/Matugen.qml b/Assets/Matugen/Matugen.qml index 992740a..009349e 100644 --- a/Assets/Matugen/Matugen.qml +++ b/Assets/Matugen/Matugen.qml @@ -70,6 +70,12 @@ Singleton { lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"') lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"') } + if (Settings.data.matugen.pywalfox) { + lines.push("\n[templates.pywalfox]") + lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/pywalfox.json"') + lines.push('output_path = "~/.cache/wal/colors.json"') + lines.push('post_hook = "pywalfox update"') + } return lines.join("\n") + "\n" } diff --git a/Assets/Matugen/templates/pywalfox.json b/Assets/Matugen/templates/pywalfox.json new file mode 100644 index 0000000..a2a9087 --- /dev/null +++ b/Assets/Matugen/templates/pywalfox.json @@ -0,0 +1,22 @@ +{ + "wallpaper": "{{image}}", + "alpha": "100", + "colors": { + "color0": "{{colors.background.default.hex}}", + "color1": "", + "color2": "", + "color3": "", + "color4": "", + "color5": "", + "color6": "", + "color7": "", + "color8": "", + "color9": "", + "color10": "{{colors.primary.default.hex}}", + "color11": "", + "color12": "", + "color13": "{{colors.surface_bright.default.hex}}", + "color14": "", + "color15": "{{colors.on_surface.default.hex}}" + } + } \ No newline at end of file diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 622b92a..f143435 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -449,6 +449,7 @@ Singleton { property bool foot: false property bool fuzzel: false property bool vesktop: false + property bool pywalfox: false property bool enableUserTemplates: false } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 4f448d0..c3347a8 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -39,7 +39,7 @@ NIconButton { colorFg: Color.mOnSurface colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary colorBorder: Color.transparent - colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent + colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent onClicked: PanelService.getPanel("sidePanel")?.toggle(this) onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 69f3c1c..d752639 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -318,146 +318,187 @@ ColumnLayout { visible: Settings.data.colorSchemes.useWallpaperColors } - // Matugen template toggles (moved from MatugenTab) + // Matugen template toggles organized by category ColumnLayout { - spacing: Style.marginL * scaling Layout.fillWidth: true visible: Settings.data.colorSchemes.useWallpaperColors + spacing: Style.marginL * scaling - ColumnLayout { - spacing: Style.marginS * scaling + // UI Components + NCollapsible { Layout.fillWidth: true + label: "UI" + description: "Desktop environment and UI toolkit theming." + defaultExpanded: false - NText { - text: "Matugen Templates" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NCheckbox { + label: "GTK 4 (libadwaita)" + description: "Write ~/.config/gtk-4.0/gtk.css" + checked: Settings.data.matugen.gtk4 + onToggled: checked => { + Settings.data.matugen.gtk4 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } } - NText { - text: "Select which external components Matugen should apply theming to." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - wrapMode: Text.WordWrap + NCheckbox { + label: "GTK 3" + description: "Write ~/.config/gtk-3.0/gtk.css" + checked: Settings.data.matugen.gtk3 + onToggled: checked => { + Settings.data.matugen.gtk3 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + + NCheckbox { + label: "Qt6ct" + description: "Write ~/.config/qt6ct/colors/noctalia.conf" + checked: Settings.data.matugen.qt6 + onToggled: checked => { + Settings.data.matugen.qt6 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + + NCheckbox { + label: "Qt5ct" + description: "Write ~/.config/qt5ct/colors/noctalia.conf" + checked: Settings.data.matugen.qt5 + onToggled: checked => { + Settings.data.matugen.qt5 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } } } - NCheckbox { - label: "GTK 4 (libadwaita)" - description: "Write ~/.config/gtk-4.0/gtk.css" - checked: Settings.data.matugen.gtk4 - onToggled: checked => { - Settings.data.matugen.gtk4 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "GTK 3" - description: "Write ~/.config/gtk-3.0/gtk.css" - checked: Settings.data.matugen.gtk3 - onToggled: checked => { - Settings.data.matugen.gtk3 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Qt6ct" - description: "Write ~/.config/qt6ct/colors/noctalia.conf" - checked: Settings.data.matugen.qt6 - onToggled: checked => { - Settings.data.matugen.qt6 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Qt5ct" - description: "Write ~/.config/qt5ct/colors/noctalia.conf" - checked: Settings.data.matugen.qt5 - onToggled: checked => { - Settings.data.matugen.qt5 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Kitty" - description: "Write ~/.config/kitty/themes/noctalia.conf and reload" - checked: Settings.data.matugen.kitty - onToggled: checked => { - Settings.data.matugen.kitty = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Ghostty" - description: "Write ~/.config/ghostty/themes/noctalia and reload" - checked: Settings.data.matugen.ghostty - onToggled: checked => { - Settings.data.matugen.ghostty = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Foot" - description: "Write ~/.config/foot/themes/noctalia and reload" - checked: Settings.data.matugen.foot - onToggled: checked => { - Settings.data.matugen.foot = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Fuzzel" - description: "Write ~/.config/fuzzel/themes/noctalia and reload" - checked: Settings.data.matugen.fuzzel - onToggled: checked => { - Settings.data.matugen.fuzzel = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Vesktop" - description: "Write ~/.config/vesktop/themes/noctalia.theme.css" - checked: Settings.data.matugen.vesktop - onToggled: checked => { - Settings.data.matugen.vesktop = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NDivider { + // Terminal Emulators + NCollapsible { Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling - Layout.bottomMargin: Style.marginM * scaling + label: "Terminal" + description: "Terminal emulator theming." + defaultExpanded: false + + NCheckbox { + label: "Kitty" + description: ProgramCheckerService.kittyAvailable ? "Write ~/.config/kitty/themes/noctalia.conf and reload" : "Requires kitty terminal to be installed" + checked: Settings.data.matugen.kitty + enabled: ProgramCheckerService.kittyAvailable + opacity: ProgramCheckerService.kittyAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.kittyAvailable) { + Settings.data.matugen.kitty = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Ghostty" + description: ProgramCheckerService.ghosttyAvailable ? "Write ~/.config/ghostty/themes/noctalia and reload" : "Requires ghostty terminal to be installed" + checked: Settings.data.matugen.ghostty + enabled: ProgramCheckerService.ghosttyAvailable + opacity: ProgramCheckerService.ghosttyAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.ghosttyAvailable) { + Settings.data.matugen.ghostty = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Foot" + description: ProgramCheckerService.footAvailable ? "Write ~/.config/foot/themes/noctalia and reload" : "Requires foot terminal to be installed" + checked: Settings.data.matugen.foot + enabled: ProgramCheckerService.footAvailable + opacity: ProgramCheckerService.footAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.footAvailable) { + Settings.data.matugen.foot = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } } - NCheckbox { - label: "User Templates" - description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml" - checked: Settings.data.matugen.enableUserTemplates - onToggled: checked => { - Settings.data.matugen.enableUserTemplates = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } + // Applications + NCollapsible { + Layout.fillWidth: true + label: "Programs" + description: "Application-specific theming." + defaultExpanded: false + + NCheckbox { + label: "Fuzzel" + description: ProgramCheckerService.fuzzelAvailable ? "Write ~/.config/fuzzel/themes/noctalia and reload" : "Requires fuzzel launcher to be installed" + checked: Settings.data.matugen.fuzzel + enabled: ProgramCheckerService.fuzzelAvailable + opacity: ProgramCheckerService.fuzzelAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.fuzzelAvailable) { + Settings.data.matugen.fuzzel = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Vesktop" + description: ProgramCheckerService.vesktopAvailable ? "Write ~/.config/vesktop/themes/noctalia.theme.css" : "Requires vesktop Discord client to be installed" + checked: Settings.data.matugen.vesktop + enabled: ProgramCheckerService.vesktopAvailable + opacity: ProgramCheckerService.vesktopAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.vesktopAvailable) { + Settings.data.matugen.vesktop = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Pywalfox (Firefox)" + description: ProgramCheckerService.pywalfoxAvailable ? "Write ~/.cache/wal/colors.json and run pywalfox update" : "Requires pywalfox package to be installed" + checked: Settings.data.matugen.pywalfox + enabled: ProgramCheckerService.pywalfoxAvailable + opacity: ProgramCheckerService.pywalfoxAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.pywalfoxAvailable) { + Settings.data.matugen.pywalfox = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + } + + // Miscellaneous + NCollapsible { + Layout.fillWidth: true + label: "Misc" + description: "Additional configuration options." + defaultExpanded: false + + NCheckbox { + label: "User Templates" + description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml" + checked: Settings.data.matugen.enableUserTemplates + onToggled: checked => { + Settings.data.matugen.enableUserTemplates = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } } } } diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml new file mode 100644 index 0000000..f8e4fa9 --- /dev/null +++ b/Services/ProgramCheckerService.qml @@ -0,0 +1,118 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Service to check if various programs are available on the system +Singleton { + id: root + + // Program availability properties + property bool matugenAvailable: false + property bool pywalfoxAvailable: false + property bool kittyAvailable: false + property bool ghosttyAvailable: false + property bool footAvailable: false + property bool fuzzelAvailable: false + property bool vesktopAvailable: false + property bool gpuScreenRecorderAvailable: false + + // Signal emitted when all checks are complete + signal checksCompleted + + // Programs to check - maps property names to commands + readonly property var programsToCheck: ({ + "matugenAvailable": ["which", "matugen"], + "pywalfoxAvailable": ["which", "pywalfox"], + "kittyAvailable": ["which", "kitty"], + "ghosttyAvailable": ["which", "ghostty"], + "footAvailable": ["which", "foot"], + "fuzzelAvailable": ["which", "fuzzel"], + "vesktopAvailable": ["which", "vesktop"], + "gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"] + }) + + // Internal tracking + property int completedChecks: 0 + property int totalChecks: Object.keys(programsToCheck).length + + // Single reusable Process object + Process { + id: checker + running: false + + property string currentProperty: "" + + onExited: function (exitCode) { + // Set the availability property + root[currentProperty] = (exitCode === 0) + + // Stop the process to free resources + running = false + + // Track completion + root.completedChecks++ + + // Check next program or emit completion signal + if (root.completedChecks >= root.totalChecks) { + root.checksCompleted() + } else { + root.checkNextProgram() + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Queue of programs to check + property var checkQueue: [] + property int currentCheckIndex: 0 + + // Function to check the next program in the queue + function checkNextProgram() { + if (currentCheckIndex >= checkQueue.length) + return + + var propertyName = checkQueue[currentCheckIndex] + var command = programsToCheck[propertyName] + + checker.currentProperty = propertyName + checker.command = command + checker.running = true + + currentCheckIndex++ + } + + // Function to run all program checks + function checkAllPrograms() { + // Reset state + completedChecks = 0 + currentCheckIndex = 0 + checkQueue = Object.keys(programsToCheck) + + // Start first check + if (checkQueue.length > 0) { + checkNextProgram() + } + } + + // Function to check a specific program + function checkProgram(programProperty) { + if (!programsToCheck.hasOwnProperty(programProperty)) { + Logger.warn("ProgramChecker", "Unknown program property:", programProperty) + return + } + + checker.currentProperty = programProperty + checker.command = programsToCheck[programProperty] + checker.running = true + } + + // Initialize checks when service is created + Component.onCompleted: { + checkAllPrograms() + } +} diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 553c2fa..8a5512b 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -13,16 +13,13 @@ Singleton { property bool isRecording: false property bool isPending: false property string outputPath: "" - property bool isAvailable: false + property bool isAvailable: ProgramCheckerService.gpuScreenRecorderAvailable - Component.onCompleted: { - checkAvailability() - } - - function checkAvailability() { - // Detect native or Flatpak gpu-screen-recorder - availabilityCheckProcess.command = ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"] - availabilityCheckProcess.running = true + // Update availability when ProgramCheckerService completes its checks + Connections { + target: ProgramCheckerService + function onChecksCompleted() {// Availability is now automatically updated via property binding + } } // Start or Stop recording @@ -101,18 +98,6 @@ Singleton { } } - // Availability check process - Process { - id: availabilityCheckProcess - command: ["sh", "-c", "true"] - onExited: function (exitCode, exitStatus) { - // exitCode 0 means available, non-zero means unavailable - root.isAvailable = (exitCode === 0) - } - stdout: StdioCollector {} - stderr: StdioCollector {} - } - Timer { id: pendingTimer interval: 2000 // Wait 2 seconds to see if process stays alive diff --git a/Widgets/NCollapsible.qml b/Widgets/NCollapsible.qml new file mode 100644 index 0000000..1403cf3 --- /dev/null +++ b/Widgets/NCollapsible.qml @@ -0,0 +1,193 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + property string label: "" + property string description: "" + property bool expanded: false + property bool defaultExpanded: false + property real contentSpacing: Style.marginM * scaling + signal toggled(bool expanded) + + Layout.fillWidth: true + spacing: 0 + + // Default property to accept children + default property alias content: contentLayout.children + + // Header with clickable area + Rectangle { + id: headerContainer + Layout.fillWidth: true + Layout.preferredHeight: headerContent.implicitHeight + (Style.marginL * scaling * 2) + + // Material 3 style background + color: root.expanded ? Color.mSecondary : Color.mSurfaceVariant + radius: Style.radiusL * scaling + + // Subtle border + border.color: root.expanded ? Color.mOnSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Smooth color transitions + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + MouseArea { + id: headerArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: { + root.expanded = !root.expanded + root.toggled(root.expanded) + } + + // Hover effect overlay + Rectangle { + anchors.fill: parent + color: headerArea.containsMouse ? Color.mOnSurface : Color.transparent + opacity: headerArea.containsMouse ? 0.08 : 0 + radius: headerContainer.radius // Reference the container's radius directly + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + RowLayout { + id: headerContent + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // Expand/collapse icon with rotation animation + NIcon { + id: chevronIcon + icon: "chevron-right" + font.pointSize: Style.fontSizeL * scaling + color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + + rotation: root.expanded ? 90 : 0 + Behavior on rotation { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + // Header text content - properly contained + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginXXS * scaling + + NText { + text: root.label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightSemiBold + color: root.expanded ? Color.mOnSecondary : Color.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + NText { + text: root.description + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightRegular + color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: root.description !== "" + opacity: 0.87 + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + } + } + } + + // Collapsible content with Material 3 styling + Rectangle { + id: contentContainer + Layout.fillWidth: true + Layout.topMargin: Style.marginS * scaling + + visible: root.expanded + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Dynamic height based on content + Layout.preferredHeight: visible ? contentLayout.implicitHeight + (Style.marginL * scaling * 2) : 0 + + // Smooth height animation + Behavior on Layout.preferredHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Content layout + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: root.contentSpacing + + // Clip content during animation + clip: true + } + + // Fade in animation for content + opacity: root.expanded ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + // Initialize expanded state + Component.onCompleted: { + root.expanded = root.defaultExpanded + } +}