diff --git a/Modules/ControlCenter/DisplayTab.qml b/Modules/ControlCenter/DisplayTab.qml index b326ed6b..96323124 100644 --- a/Modules/ControlCenter/DisplayTab.qml +++ b/Modules/ControlCenter/DisplayTab.qml @@ -9,221 +9,248 @@ import qs.Services import qs.Widgets Item { - id: displayTab + id: displayTab - property var brightnessDebounceTimer + property var brightnessDebounceTimer - brightnessDebounceTimer: Timer { - property int pendingValue: 0 + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + contentWidth: width - interval: BrightnessService.ddcAvailable ? 500 : 50 - repeat: false - onTriggered: { - BrightnessService.setBrightnessInternal(pendingValue) - } - } + Column { + id: mainColumn - DankFlickable { - anchors.fill: parent - clip: true - contentHeight: mainColumn.height - contentWidth: width + width: parent.width + spacing: Theme.spacingL - Column { - id: mainColumn - width: parent.width - spacing: Theme.spacingL - - Loader { - width: parent.width - sourceComponent: brightnessComponent - } - - Loader { - width: parent.width - sourceComponent: settingsComponent - } - } - } - - Process { - id: nightModeEnableProcess - command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] - running: false - onExited: exitCode => { - if (exitCode !== 0) { - SettingsData.setNightModeEnabled(false) - } - } - } - - Process { - id: nightModeDisableProcess - command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"] - running: false - onExited: exitCode => { - if (exitCode !== 0) { - - } - } - } - - Component { - id: brightnessComponent - Column { - width: parent.width - spacing: Theme.spacingM - visible: BrightnessService.brightnessAvailable - - StyledText { - text: "Brightness" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankSlider { - width: parent.width - value: BrightnessService.brightnessLevel - leftIcon: "brightness_low" - rightIcon: "brightness_high" - enabled: BrightnessService.brightnessAvailable - onSliderValueChanged: function (newValue) { - brightnessDebounceTimer.pendingValue = newValue - brightnessDebounceTimer.restart() - } - onSliderDragFinished: function (finalValue) { - brightnessDebounceTimer.stop() - BrightnessService.setBrightnessInternal(finalValue) - } - } - - StyledText { - text: "using ddc - changes may take a moment to apply" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: BrightnessService.ddcAvailable - && !BrightnessService.laptopBacklightAvailable - anchors.horizontalCenter: parent.horizontalCenter - } - } - } - - Component { - id: settingsComponent - Column { - width: parent.width - spacing: Theme.spacingM - - StyledText { - text: "Display Settings" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - Row { - width: parent.width - spacing: Theme.spacingM - - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: SettingsData.nightModeEnabled ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: SettingsData.nightModeEnabled ? Theme.primary : "transparent" - border.width: SettingsData.nightModeEnabled ? 1 : 0 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: SettingsData.nightModeEnabled ? "nightlight" : "dark_mode" - size: Theme.iconSizeLarge - color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter + Loader { + width: parent.width + sourceComponent: brightnessComponent } + Loader { + width: parent.width + sourceComponent: settingsComponent + } + + } + + } + + Process { + id: nightModeEnableProcess + + command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] + running: false + onExited: (exitCode) => { + if (exitCode !== 0) + SettingsData.setNightModeEnabled(false); + + } + } + + Process { + id: nightModeDisableProcess + + command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"] + running: false + onExited: (exitCode) => { + if (exitCode !== 0) { + } + } + } + + Component { + id: brightnessComponent + + Column { + width: parent.width + spacing: Theme.spacingS + visible: BrightnessService.brightnessAvailable + StyledText { - text: "Night Mode" - font.pixelSize: Theme.fontSizeMedium - color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.horizontalCenter: parent.horizontalCenter + text: "Brightness" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium } - } - MouseArea { - id: nightModeToggle - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (SettingsData.nightModeEnabled) { - nightModeDisableProcess.running = true - SettingsData.setNightModeEnabled(false) - } else { - nightModeEnableProcess.running = true - SettingsData.setNightModeEnabled(true) - } + DankDropdown { + width: parent.width + height: 40 + visible: BrightnessService.devices.length > 1 + text: "Device" + description: "" + currentValue: BrightnessService.currentDevice + options: BrightnessService.devices.map(function(d) { + return d.name; + }) + optionIcons: BrightnessService.devices.map(function(d) { + if (d.class === "backlight") + return "desktop_windows"; + + if (d.name.includes("kbd")) + return "keyboard"; + + return "lightbulb"; + }) + onValueChanged: function(value) { + BrightnessService.setCurrentDevice(value); + } } - } + + DankSlider { + width: parent.width + value: BrightnessService.brightnessLevel + leftIcon: "brightness_low" + rightIcon: "brightness_high" + enabled: BrightnessService.brightnessAvailable + onSliderValueChanged: function(newValue) { + brightnessDebounceTimer.pendingValue = newValue; + brightnessDebounceTimer.restart(); + } + onSliderDragFinished: function(finalValue) { + brightnessDebounceTimer.stop(); + BrightnessService.setBrightnessInternal(finalValue, BrightnessService.currentDevice); + } + } + } - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: Theme.isLightMode ? Qt.rgba( - Theme.primary.r, Theme.primary.g, - Theme.primary.b, - 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: Theme.isLightMode ? Theme.primary : "transparent" - border.width: Theme.isLightMode ? 1 : 0 + } - Column { - anchors.centerIn: parent - spacing: Theme.spacingS + Component { + id: settingsComponent - DankIcon { - name: Theme.isLightMode ? "light_mode" : "palette" - size: Theme.iconSizeLarge - color: Theme.isLightMode ? Theme.primary : Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } + Column { + width: parent.width + spacing: Theme.spacingM StyledText { - text: Theme.isLightMode ? "Light Mode" : "Dark Mode" - font.pixelSize: Theme.fontSizeMedium - color: Theme.isLightMode ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.horizontalCenter: parent.horizontalCenter + text: "Display Settings" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium } - } - MouseArea { - id: lightModeToggle - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Theme.toggleLightMode() - } - } + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadius + color: SettingsData.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + border.color: SettingsData.nightModeEnabled ? Theme.primary : "transparent" + border.width: SettingsData.nightModeEnabled ? 1 : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: SettingsData.nightModeEnabled ? "nightlight" : "dark_mode" + size: Theme.iconSizeLarge + color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "Night Mode" + font.pixelSize: Theme.fontSizeMedium + color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + } + + MouseArea { + id: nightModeToggle + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (SettingsData.nightModeEnabled) { + nightModeDisableProcess.running = true; + SettingsData.setNightModeEnabled(false); + } else { + nightModeEnableProcess.running = true; + SettingsData.setNightModeEnabled(true); + } + } + } + + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadius + color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + border.color: Theme.isLightMode ? Theme.primary : "transparent" + border.width: Theme.isLightMode ? 1 : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: Theme.isLightMode ? "light_mode" : "palette" + size: Theme.iconSizeLarge + color: Theme.isLightMode ? Theme.primary : Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: Theme.isLightMode ? "Light Mode" : "Dark Mode" + font.pixelSize: Theme.fontSizeMedium + color: Theme.isLightMode ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + } + + MouseArea { + id: lightModeToggle + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Theme.toggleLightMode(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing } - } + } - } + } - } + + brightnessDebounceTimer: Timer { + property int pendingValue: 0 + + interval: 50 + repeat: false + onTriggered: { + BrightnessService.setBrightnessInternal(pendingValue, BrightnessService.currentDevice); + } + } + } diff --git a/README.md b/README.md index 635b6b7f..8ff72225 100644 --- a/README.md +++ b/README.md @@ -164,11 +164,11 @@ make && sudo make install ```bash # Arch Linux -pacman -S cava wl-clipboard cliphist ddcutil brightnessctl +pacman -S cava wl-clipboard cliphist brightnessctl paru -S matugen dgop-git # Fedora -sudo dnf install cava wl-clipboard ddcutil brightnessctl +sudo dnf install cava wl-clipboard brightnessctl sudo dnf copr enable wef/cliphist && sudo dnf install cliphist sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen ``` @@ -177,8 +177,7 @@ sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen - `dgop-git`: Ability to have system resource widgets, process list modal, and temperature monitoring. - `matugen`: Wallpaper-based dynamic theming -- `ddcutil`: External monitor brightness control -- `brightnessctl`: Laptop display brightness +- `brightnessctl`: Backlight and LED brightness control - `wl-clipboard`: Required for copying various elements to clipboard. - `cava`: Audio visualizer - `cliphist`: Clipboard history @@ -239,10 +238,11 @@ binds { spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "micmute"; } XF86MonBrightnessUp allow-when-locked=true { - spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "increment" "5"; + spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "increment" "5" ""; } + // You can override the default device for e.g. keyboards by adding the device name to the last param XF86MonBrightnessDown allow-when-locked=true { - spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5"; + spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5" ""; } } ``` diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 202bba00..ad14b89c 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -1,211 +1,207 @@ -pragma Singleton - -pragma ComponentBehavior - import QtQuick import Quickshell import Quickshell.Io +pragma Singleton +pragma ComponentBehavior Singleton { - id: root + id: root - property bool brightnessAvailable: laptopBacklightAvailable || ddcAvailable - property bool laptopBacklightAvailable: false - property bool ddcAvailable: false - property int brightnessLevel: 50 - property int maxBrightness: 100 - property int currentRawBrightness: 0 - property bool brightnessInitialized: false + property bool brightnessAvailable: devices.length > 0 + property var devices: [] + property string currentDevice: "" + property int brightnessLevel: 50 + property int maxBrightness: 100 + property bool brightnessInitialized: false - signal brightnessChanged + signal brightnessChanged() - function setBrightnessInternal(percentage) { - brightnessLevel = Math.max(1, Math.min(100, percentage)) - - if (laptopBacklightAvailable) { - laptopBrightnessProcess.command = ["brightnessctl", "set", brightnessLevel + "%"] - laptopBrightnessProcess.running = true - } else if (ddcAvailable) { - - Quickshell.execDetached( - ["ddcutil", "setvcp", "10", brightnessLevel.toString()]) + function setBrightnessInternal(percentage, device) { + const clampedValue = Math.max(1, Math.min(100, percentage)); + brightnessLevel = clampedValue; + if (device) + brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", clampedValue + "%"]; + else + brightnessSetProcess.command = ["brightnessctl", "set", clampedValue + "%"]; + brightnessSetProcess.running = true; } - } - function setBrightness(percentage) { - setBrightnessInternal(percentage) - brightnessChanged() - } - - Component.onCompleted: { - ddcAvailabilityChecker.running = true - laptopBacklightChecker.running = true - } - - onLaptopBacklightAvailableChanged: { - if (laptopBacklightAvailable && !brightnessInitialized) { - laptopBrightnessInitProcess.running = true + function setBrightness(percentage, device) { + setBrightnessInternal(percentage, device); + brightnessChanged(); } - } - onDdcAvailableChanged: { - if (ddcAvailable && !laptopBacklightAvailable && !brightnessInitialized) { - ddcBrightnessInitProcess.running = true + function setCurrentDevice(deviceName) { + if (currentDevice === deviceName) + return ; + + currentDevice = deviceName; + brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]; + brightnessGetProcess.running = true; } - } - Process { - id: ddcAvailabilityChecker - command: ["which", "ddcutil"] - onExited: function (exitCode) { - ddcAvailable = (exitCode === 0) + function refreshDevices() { + deviceListProcess.running = true; } - } - Process { - id: laptopBacklightChecker - command: ["brightnessctl", "--list"] - onExited: function (exitCode) { - laptopBacklightAvailable = (exitCode === 0) + Component.onCompleted: { + refreshDevices(); } - } - Process { - id: laptopBrightnessProcess - running: false + Process { + id: deviceListProcess - onExited: function (exitCode) { - if (exitCode !== 0) { - - } - } - } - - Process { - id: laptopBrightnessInitProcess - command: ["brightnessctl", "get"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.trim()) { - currentRawBrightness = parseInt(text.trim()) - laptopMaxBrightnessProcess.running = true + command: ["brightnessctl", "-m", "-l"] + onExited: function(exitCode) { + if (exitCode !== 0) { + console.warn("BrightnessService: Failed to list devices:", exitCode); + brightnessAvailable = false; + } } - } - } - onExited: function (exitCode) { - if (exitCode !== 0) { - console.warn("BrightnessService: Failed to read current brightness:", - exitCode) - } - } - } + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) { + console.warn("BrightnessService: No devices found"); + return ; + } + const lines = text.trim().split("\n"); + const newDevices = []; + for (const line of lines) { + const parts = line.split(","); + if (parts.length >= 5) + newDevices.push({ + "name": parts[0], + "class": parts[1], + "current": parseInt(parts[2]), + "percentage": parseInt(parts[3]), + "max": parseInt(parts[4]) + }); - Process { - id: laptopMaxBrightnessProcess - command: ["brightnessctl", "max"] - running: false + } + newDevices.sort((a, b) => { + if (a.class === "backlight" && b.class !== "backlight") + return -1; - stdout: StdioCollector { - onStreamFinished: { - if (text.trim()) { - maxBrightness = parseInt(text.trim()) - brightnessLevel = Math.round( - (currentRawBrightness / maxBrightness) * 100) - brightnessInitialized = true - console.log("BrightnessService: Initialized with brightness level:", - brightnessLevel + "%") + if (a.class !== "backlight" && b.class === "backlight") + return 1; + + return a.name.localeCompare(b.name); + }); + devices = newDevices; + if (devices.length > 0 && !currentDevice) + setCurrentDevice(devices[0].name); + + } } - } + } - onExited: function (exitCode) { - if (exitCode !== 0) { - console.warn("BrightnessService: Failed to read max brightness:", - exitCode) - } - } - } + Process { + id: brightnessSetProcess - Process { - id: ddcBrightnessInitProcess - command: ["ddcutil", "getvcp", "10", "--brief"] - running: false + running: false + onExited: function(exitCode) { + if (exitCode !== 0) + console.warn("BrightnessService: Failed to set brightness:", exitCode); - stdout: StdioCollector { - onStreamFinished: { - if (text.trim()) { - const parts = text.trim().split(" ") - if (parts.length >= 5) { - const current = parseInt(parts[3]) || 50 - const max = parseInt(parts[4]) || 100 - brightnessLevel = Math.round((current / max) * 100) - brightnessInitialized = true - } } - } } - onExited: function (exitCode) { - if (exitCode !== 0) { - if (!laptopBacklightAvailable) { - console.warn("BrightnessService: DDC brightness read failed:", - exitCode) + Process { + id: brightnessGetProcess + + running: false + onExited: function(exitCode) { + if (exitCode !== 0) + console.warn("BrightnessService: Failed to get brightness:", exitCode); + } - } - } - } - // IPC Handler for external control - IpcHandler { - target: "brightness" + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return ; - function set(percentage: string): string { - if (!root.brightnessAvailable) { - return "Brightness control not available" - } + const parts = text.trim().split(","); + if (parts.length >= 5) { + const current = parseInt(parts[2]); + const max = parseInt(parts[4]); + maxBrightness = max; + brightnessLevel = Math.round((current / max) * 100); + brightnessInitialized = true; + console.log("BrightnessService: Device", currentDevice, "brightness:", brightnessLevel + "%"); + } + } + } - const value = parseInt(percentage) - const clampedValue = Math.max(1, Math.min(100, value)) - root.setBrightness(clampedValue) - return "Brightness set to " + clampedValue + "%" } - function increment(step: string): string { - if (!root.brightnessAvailable) { - return "Brightness control not available" - } + // IPC Handler for external control + IpcHandler { + function set(percentage: string, device: string) : string { + if (!root.brightnessAvailable) + return "Brightness control not available"; - const currentLevel = root.brightnessLevel - const newLevel = Math.max(1, - Math.min(100, - currentLevel + parseInt(step || "10"))) - root.setBrightness(newLevel) - return "Brightness increased to " + newLevel + "%" + const value = parseInt(percentage); + const clampedValue = Math.max(1, Math.min(100, value)); + const targetDevice = device || ""; + root.setBrightness(clampedValue, targetDevice); + if (targetDevice) + return "Brightness set to " + clampedValue + "% on " + targetDevice; + else + return "Brightness set to " + clampedValue + "%"; + } + + function increment(step: string, device: string) : string { + if (!root.brightnessAvailable) + return "Brightness control not available"; + + const currentLevel = root.brightnessLevel; + const stepValue = parseInt(step || "10"); + const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue)); + const targetDevice = device || ""; + root.setBrightness(newLevel, targetDevice); + if (targetDevice) + return "Brightness increased to " + newLevel + "% on " + targetDevice; + else + return "Brightness increased to " + newLevel + "%"; + } + + function decrement(step: string, device: string) : string { + if (!root.brightnessAvailable) + return "Brightness control not available"; + + const currentLevel = root.brightnessLevel; + const stepValue = parseInt(step || "10"); + const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue)); + const targetDevice = device || ""; + root.setBrightness(newLevel, targetDevice); + if (targetDevice) + return "Brightness decreased to " + newLevel + "% on " + targetDevice; + else + return "Brightness decreased to " + newLevel + "%"; + } + + function status() : string { + if (!root.brightnessAvailable) + return "Brightness control not available"; + + return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"; + } + + function list() : string { + if (!root.brightnessAvailable) + return "No brightness devices available"; + + let result = "Available devices:\n"; + for (const device of root.devices) { + result += device.name + " (" + device.class + ")\n"; + } + return result; + } + + target: "brightness" } - function decrement(step: string): string { - if (!root.brightnessAvailable) { - return "Brightness control not available" - } - - const currentLevel = root.brightnessLevel - const newLevel = Math.max(1, - Math.min(100, - currentLevel - parseInt(step || "10"))) - root.setBrightness(newLevel) - return "Brightness decreased to " + newLevel + "%" - } - - function status(): string { - if (!root.brightnessAvailable) { - return "Brightness control not available" - } - - return "Brightness: " + root.brightnessLevel + "% (" - + (root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")" - } - } }