From 2b104435dc40d0ac31198396ea22b6683e553d67 Mon Sep 17 00:00:00 2001 From: Sola Date: Mon, 29 Sep 2025 07:59:42 +0800 Subject: [PATCH 01/32] fix overview scaling issue --- .../ii/modules/overview/OverviewWidget.qml | 2 +- .../ii/modules/overview/OverviewWindow.qml | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.config/quickshell/ii/modules/overview/OverviewWidget.qml b/.config/quickshell/ii/modules/overview/OverviewWidget.qml index 0487c6025..1d854f016 100644 --- a/.config/quickshell/ii/modules/overview/OverviewWidget.qml +++ b/.config/quickshell/ii/modules/overview/OverviewWidget.qml @@ -167,7 +167,7 @@ Item { scale: root.scale availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceHeight: root.workspaceImplicitHeight - widgetMonitorId: root.monitor.id + widgetMonitor: HyprlandData.monitors.find(m => m.id == root.monitor.id) windowData: windowByAddress[address] property bool atInitPosition: (initX == x && initY == y) diff --git a/.config/quickshell/ii/modules/overview/OverviewWindow.qml b/.config/quickshell/ii/modules/overview/OverviewWindow.qml index b2d39cfea..d55eeedf7 100644 --- a/.config/quickshell/ii/modules/overview/OverviewWindow.qml +++ b/.config/quickshell/ii/modules/overview/OverviewWindow.qml @@ -17,14 +17,30 @@ Item { // Window property var availableWorkspaceWidth property var availableWorkspaceHeight property bool restrictToWorkspace: true - property real initX: Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset - property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real widthRatio: { + const widgetWidth = widgetMonitor.transform & 1 ? widgetMonitor.height : widgetMonitor.width; + const monitorWidth = monitorData.transform & 1 ? monitorData.height : monitorData.width; + (widgetWidth * monitorData.scale) / (monitorWidth * widgetMonitor.scale); + } + property real heightRatio: { + const widgetHeight = widgetMonitor.transform & 1 ? widgetMonitor.width : widgetMonitor.height; + const monitorHeight = monitorData.transform & 1 ? monitorData.width : monitorData.height; + (widgetHeight * monitorData.scale) / (monitorHeight * widgetMonitor.scale); + } + property real initX: { + Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * widthRatio * root.scale, 0) + xOffset; + } + + property real initY: { + Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * heightRatio * root.scale, 0) + yOffset; + } property real xOffset: 0 property real yOffset: 0 - property int widgetMonitorId: 0 - - property var targetWindowWidth: windowData?.size[0] * scale - property var targetWindowHeight: windowData?.size[1] * scale + property var widgetMonitor + property int widgetMonitorId: widgetMonitor.id + + property var targetWindowWidth: windowData?.size[0] * scale * widthRatio + property var targetWindowHeight: windowData?.size[1] * scale * heightRatio property bool hovered: false property bool pressed: false @@ -35,11 +51,11 @@ Item { // Window property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth property bool indicateXWayland: windowData?.xwayland ?? false - + x: initX y: initY - width: windowData?.size[0] * root.scale - height: windowData?.size[1] * root.scale + width: targetWindowWidth + height: targetWindowHeight opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4 layer.enabled: true @@ -90,7 +106,7 @@ Item { // Window // console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) // console.log("Scale:", root.monitorData.scale) // console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale) - return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale; + return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio); } // mipmap: true Layout.alignment: Qt.AlignHCenter @@ -107,4 +123,4 @@ Item { // Window } } } -} \ No newline at end of file +} From 91955ef66c6f095fbc90c31044e98c01984e35bb Mon Sep 17 00:00:00 2001 From: Cleboost Date: Thu, 16 Oct 2025 22:02:00 +0200 Subject: [PATCH 02/32] Add screen recording button and config toggle --- .../quickshell/ii/modules/bar/UtilButtons.qml | 16 ++++++++++++++++ .../quickshell/ii/modules/common/Config.qml | 1 + .../quickshell/ii/modules/settings/BarConfig.qml | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml index 98ee4f6cd..6e4a917f2 100644 --- a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml +++ b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -36,6 +36,22 @@ Item { } } + Loader { + active: Config.options.bar.utilButtons.showScreenRecord + visible: Config.options.bar.utilButtons.showScreenRecord + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: Quickshell.execDetached(["/bin/bash", "/home/cleboost/.config/hypr/hyprland/scripts/record.sh", "--fullscreen-sound"]) + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 1 + text: "videocam" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + Loader { active: Config.options.bar.utilButtons.showColorPicker visible: Config.options.bar.utilButtons.showColorPicker diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index a9289d83b..39e7ed31a 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -211,6 +211,7 @@ Singleton { property bool showKeyboardToggle: true property bool showDarkModeToggle: true property bool showPerformanceProfileToggle: false + property bool showScreenRecord: true } property JsonObject tray: JsonObject { property bool monochromeIcons: true diff --git a/dots/.config/quickshell/ii/modules/settings/BarConfig.qml b/dots/.config/quickshell/ii/modules/settings/BarConfig.qml index 01a1d9ee9..817592ff6 100644 --- a/dots/.config/quickshell/ii/modules/settings/BarConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/BarConfig.qml @@ -225,6 +225,17 @@ ContentPage { } } } + ConfigRow { + uniform: true + ConfigSwitch { + buttonIcon: "videocam" + text: Translation.tr("Screen recording") + checked: Config.options.bar.utilButtons.showScreenRecord + onCheckedChanged: { + Config.options.bar.utilButtons.showScreenRecord = checked; + } + } + } } ContentSection { From 5fda1cdc612505ab8aa66b4e126857876272bf23 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Thu, 16 Oct 2025 22:07:29 +0200 Subject: [PATCH 03/32] fix: Invoke record script via bash -c with ~ expansion --- dots/.config/quickshell/ii/modules/bar/UtilButtons.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml index 6e4a917f2..65fa161e3 100644 --- a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml +++ b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -41,7 +41,7 @@ Item { visible: Config.options.bar.utilButtons.showScreenRecord sourceComponent: CircleUtilButton { Layout.alignment: Qt.AlignVCenter - onClicked: Quickshell.execDetached(["/bin/bash", "/home/cleboost/.config/hypr/hyprland/scripts/record.sh", "--fullscreen-sound"]) + onClicked: Quickshell.execDetached(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound"]) MaterialSymbol { horizontalAlignment: Qt.AlignHCenter fill: 1 From 37fd19fc9a6bccb6efc23bd0887db8c7403ed854 Mon Sep 17 00:00:00 2001 From: 0blivi0nis <182329535+0blivi0nis@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:03:14 -0700 Subject: [PATCH 04/32] =?UTF-8?q?=E2=9C=A8=20feat:=20sound=20alerts=20for?= =?UTF-8?q?=20battery=20and=20pomodoro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quickshell/ii/modules/common/Config.qml | 8 +++- .../ii/modules/settings/GeneralConfig.qml | 38 +++++++++++++++++ dots/.config/quickshell/ii/services/Audio.qml | 24 ++++++++++- .../quickshell/ii/services/Battery.qml | 42 ++++++++++++++++++- .../quickshell/ii/services/TimerService.qml | 7 ++-- 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index a9289d83b..f06ca2a50 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -244,6 +244,7 @@ Singleton { property JsonObject battery: JsonObject { property int low: 20 property int critical: 5 + property int full: 101 property bool automaticSuspend: true property int suspend: 3 } @@ -395,13 +396,18 @@ Singleton { } } + property JsonObject sounds: JsonObject { + property bool battery: false + property bool pomodoro: false + property string theme: "freedesktop" + } + property JsonObject time: JsonObject { // https://doc.qt.io/qt-6/qtime.html#toString property string format: "hh:mm" property string shortDateFormat: "dd/MM" property string dateFormat: "ddd, dd/MM" property JsonObject pomodoro: JsonObject { - property string alertSound: "" property int breakTime: 300 property int cyclesBeforeLongBreak: 4 property int focus: 1500 diff --git a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml index 565758468..2e53a757a 100644 --- a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml @@ -112,6 +112,20 @@ ContentPage { } } } + ConfigRow { + uniform: true + ConfigSpinBox { + icon: "charger" + text: Translation.tr("Full warning") + value: Config.options.battery.full + from: 0 + to: 101 + stepSize: 5 + onValueChanged: { + Config.options.battery.full = value; + } + } + } } ContentSection { @@ -239,6 +253,30 @@ ContentPage { } } + ContentSection { + icon: "notification_sound" + title: Translation.tr("Sounds") + ConfigRow { + uniform: true + ConfigSwitch { + buttonIcon: "battery_android_full" + text: Translation.tr("Battery") + checked: Config.options.sounds.battery + onCheckedChanged: { + Config.options.sounds.battery = checked; + } + } + ConfigSwitch { + buttonIcon: "av_timer" + text: Translation.tr("Pomodoro") + checked: Config.options.sounds.pomodoro + onCheckedChanged: { + Config.options.sounds.pomodoro = checked; + } + } + } + } + ContentSection { icon: "nest_clock_farsight_analog" title: Translation.tr("Time") diff --git a/dots/.config/quickshell/ii/services/Audio.qml b/dots/.config/quickshell/ii/services/Audio.qml index 43bc61b4d..4ce6521c6 100644 --- a/dots/.config/quickshell/ii/services/Audio.qml +++ b/dots/.config/quickshell/ii/services/Audio.qml @@ -15,6 +15,7 @@ Singleton { property PwNode sink: Pipewire.defaultAudioSink property PwNode source: Pipewire.defaultAudioSource readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so... + property string audioTheme: Config.options.sounds.theme signal sinkProtectionTriggered(string reason); @@ -49,7 +50,28 @@ Singleton { } lastVolume = sink.audio.volume; } - } + function playSystemSound(soundName) { + const ogaPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.oga`; + const oggPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.ogg`; + + // Try playing .oga first + let command = [ + "ffplay", + "-nodisp", + "-autoexit", + ogaPath + ]; + Quickshell.execDetached(command); + + // Also try playing .ogg (ffplay will just fail silently if file doesn't exist) + command = [ + "ffplay", + "-nodisp", + "-autoexit", + oggPath + ]; + Quickshell.execDetached(command); + } } diff --git a/dots/.config/quickshell/ii/services/Battery.qml b/dots/.config/quickshell/ii/services/Battery.qml index 0a19b70f1..8bee59770 100644 --- a/dots/.config/quickshell/ii/services/Battery.qml +++ b/dots/.config/quickshell/ii/services/Battery.qml @@ -18,10 +18,12 @@ Singleton { property bool isLow: available && (percentage <= Config.options.battery.low / 100) property bool isCritical: available && (percentage <= Config.options.battery.critical / 100) property bool isSuspending: available && (percentage <= Config.options.battery.suspend / 100) + property bool isFull: available && (percentage >= Config.options.battery.full / 100) property bool isLowAndNotCharging: isLow && !isCharging property bool isCriticalAndNotCharging: isCritical && !isCharging property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging + property bool isFullAndCharging: isFull && isCharging property real energyRate: UPower.displayDevice.changeRate property real timeToEmpty: UPower.displayDevice.timeToEmpty @@ -35,17 +37,28 @@ Singleton { "-u", "critical", "-a", "Shell" ]) + + if (available && Config.options.sounds.battery) { + if (isLowAndNotCharging) { + Audio.playSystemSound("dialog-warning") + } + } } onIsCriticalAndNotChargingChanged: { if (available && isCriticalAndNotCharging) Quickshell.execDetached([ "notify-send", Translation.tr("Critically low battery"), - Translation.tr("Please charge!\nAutomatic suspend triggers at %1").arg(Config.options.battery.suspend), + Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend), "-u", "critical", "-a", "Shell" ]); - + + if (available && Config.options.sounds.battery) { + if (isCriticalAndNotCharging) { + Audio.playSystemSound("suspend-error") + } + } } onIsSuspendingAndNotChargingChanged: { @@ -53,4 +66,29 @@ Singleton { Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]); } } + + onIsFullAndChargingChanged: { + if (available && isFullAndCharging) Quickshell.execDetached([ + "notify-send", + Translation.tr("Battery full"), + Translation.tr("Please unplug the charger"), + "-a", "Shell" + ]); + + if (available && Config.options.sounds.battery) { + if (isFullAndCharging) { + Audio.playSystemSound("complete") + } + } + } + + onIsPluggedInChanged: { + if (available && Config.options.sounds.battery) { + if (isPluggedIn) { + Audio.playSystemSound("power-plug") + } else { + Audio.playSystemSound("power-unplug") + } + } + } } diff --git a/dots/.config/quickshell/ii/services/TimerService.qml b/dots/.config/quickshell/ii/services/TimerService.qml index a75893681..69e5fe1c1 100644 --- a/dots/.config/quickshell/ii/services/TimerService.qml +++ b/dots/.config/quickshell/ii/services/TimerService.qml @@ -1,6 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound +import qs.services import qs.modules.common import Quickshell @@ -17,7 +18,6 @@ Singleton { property int breakTime: Config.options.time.pomodoro.breakTime property int longBreakTime: Config.options.time.pomodoro.longBreak property int cyclesBeforeLongBreak: Config.options.time.pomodoro.cyclesBeforeLongBreak - property string alertSound: Config.options.time.pomodoro.alertSound property bool pomodoroRunning: Persistent.states.timer.pomodoro.running property bool pomodoroBreak: Persistent.states.timer.pomodoro.isBreak @@ -64,8 +64,9 @@ Singleton { } Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]); - if (alertSound) - Quickshell.execDetached(["ffplay", "-nodisp", "-autoexit", alertSound]); + if (Config.options.sounds.pomodoro) { + Audio.playSystemSound("alarm-clock-elapsed") + } if (!pomodoroBreak) { Persistent.states.timer.pomodoro.cycle = (Persistent.states.timer.pomodoro.cycle + 1) % root.cyclesBeforeLongBreak; From f123e90392c6726cd09a0292c4cdeca9a5ab81a6 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:27:20 +0200 Subject: [PATCH 05/32] resourcePopup: fix unqualified access --- .../quickshell/ii/modules/bar/ResourcesPopup.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/bar/ResourcesPopup.qml b/dots/.config/quickshell/ii/modules/bar/ResourcesPopup.qml index 7c54a751a..1cac240d8 100644 --- a/dots/.config/quickshell/ii/modules/bar/ResourcesPopup.qml +++ b/dots/.config/quickshell/ii/modules/bar/ResourcesPopup.qml @@ -80,17 +80,17 @@ StyledPopup { ResourceItem { icon: "clock_loader_60" label: Translation.tr("Used:") - value: formatKB(ResourceUsage.memoryUsed) + value: root.formatKB(ResourceUsage.memoryUsed) } ResourceItem { icon: "check_circle" label: Translation.tr("Free:") - value: formatKB(ResourceUsage.memoryFree) + value: root.formatKB(ResourceUsage.memoryFree) } ResourceItem { icon: "empty_dashboard" label: Translation.tr("Total:") - value: formatKB(ResourceUsage.memoryTotal) + value: root.formatKB(ResourceUsage.memoryTotal) } } } @@ -109,17 +109,17 @@ StyledPopup { ResourceItem { icon: "clock_loader_60" label: Translation.tr("Used:") - value: formatKB(ResourceUsage.swapUsed) + value: root.formatKB(ResourceUsage.swapUsed) } ResourceItem { icon: "check_circle" label: Translation.tr("Free:") - value: formatKB(ResourceUsage.swapFree) + value: root.formatKB(ResourceUsage.swapFree) } ResourceItem { icon: "empty_dashboard" label: Translation.tr("Total:") - value: formatKB(ResourceUsage.swapTotal) + value: root.formatKB(ResourceUsage.swapTotal) } } } From f81d316ad462a5c4785f9008225ce4e01cd9207b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:29:24 +0200 Subject: [PATCH 06/32] roundcorner: use switch fallthrough for fewer dupe returns --- .../quickshell/ii/modules/common/widgets/RoundCorner.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml b/dots/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml index 785e01bce..f833a78d6 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml @@ -45,15 +45,15 @@ Item { pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting startX: switch (root.corner) { - case RoundCorner.CornerEnum.TopLeft: return 0; - case RoundCorner.CornerEnum.TopRight: return root.implicitSize; + case RoundCorner.CornerEnum.TopLeft: case RoundCorner.CornerEnum.BottomLeft: return 0; + case RoundCorner.CornerEnum.TopRight: case RoundCorner.CornerEnum.BottomRight: return root.implicitSize; } startY: switch (root.corner) { - case RoundCorner.CornerEnum.TopLeft: return 0; + case RoundCorner.CornerEnum.TopLeft: case RoundCorner.CornerEnum.TopRight: return 0; - case RoundCorner.CornerEnum.BottomLeft: return root.implicitSize; + case RoundCorner.CornerEnum.BottomLeft: case RoundCorner.CornerEnum.BottomRight: return root.implicitSize; } PathAngleArc { From 233b4c78ab89b7a36e353f9a462c02f5a3a3163f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:11:02 +0200 Subject: [PATCH 07/32] settings: add api key note for ai translation --- dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml index 2ce3ef652..fb4010a01 100644 --- a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml @@ -145,6 +145,7 @@ ContentPage { } ContentSubsection { title: Translation.tr("Generate translation with Gemini") + tooltip: Translation.tr("You'll need to enter your Gemini API key first.\nType /key on the sidebar for instructions.") ConfigRow { MaterialTextArea { From f3e4773811829c68ea00f545434f6432295b96fc Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:51:05 +0200 Subject: [PATCH 08/32] utilbuttons: record button: allow region selection --- dots/.config/quickshell/ii/modules/bar/UtilButtons.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml index 65fa161e3..2930329c1 100644 --- a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml +++ b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -41,7 +41,7 @@ Item { visible: Config.options.bar.utilButtons.showScreenRecord sourceComponent: CircleUtilButton { Layout.alignment: Qt.AlignVCenter - onClicked: Quickshell.execDetached(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound"]) + onClicked: Quickshell.execDetached(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh"]) MaterialSymbol { horizontalAlignment: Qt.AlignHCenter fill: 1 From 7b1fa1246f6cfc448c62f4c1aaf46c084ce832bf Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:51:46 +0200 Subject: [PATCH 09/32] bar: record util button off by default --- dots/.config/quickshell/ii/modules/common/Config.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 39e7ed31a..9faf32532 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -211,7 +211,7 @@ Singleton { property bool showKeyboardToggle: true property bool showDarkModeToggle: true property bool showPerformanceProfileToggle: false - property bool showScreenRecord: true + property bool showScreenRecord: false } property JsonObject tray: JsonObject { property bool monochromeIcons: true From 8798b4e826d8cc52ffd275a971261c229163e059 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:53:23 +0200 Subject: [PATCH 10/32] settings: make bar screen record util button toggle name shorter --- dots/.config/quickshell/ii/modules/settings/BarConfig.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dots/.config/quickshell/ii/modules/settings/BarConfig.qml b/dots/.config/quickshell/ii/modules/settings/BarConfig.qml index 817592ff6..e12e51291 100644 --- a/dots/.config/quickshell/ii/modules/settings/BarConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/BarConfig.qml @@ -229,7 +229,7 @@ ContentPage { uniform: true ConfigSwitch { buttonIcon: "videocam" - text: Translation.tr("Screen recording") + text: Translation.tr("Record") checked: Config.options.bar.utilButtons.showScreenRecord onCheckedChanged: { Config.options.bar.utilButtons.showScreenRecord = checked; From c65aea86c68e6a966dd50d3a842cca20a6ab7777 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:17:23 +0200 Subject: [PATCH 11/32] qs: use more neutral default pallete --- .../ii/modules/common/Appearance.qml | 99 +++++++++---------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/Appearance.qml b/dots/.config/quickshell/ii/modules/common/Appearance.qml index d873e3841..f4b42cac0 100644 --- a/dots/.config/quickshell/ii/modules/common/Appearance.qml +++ b/dots/.config/quickshell/ii/modules/common/Appearance.qml @@ -39,62 +39,57 @@ Singleton { property real contentTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency : 0 m3colors: QtObject { - property bool darkmode: false + property bool darkmode: true property bool transparent: false - property color m3primary_paletteKeyColor: "#91689E" - property color m3secondary_paletteKeyColor: "#837186" - property color m3tertiary_paletteKeyColor: "#9D6A67" - property color m3neutral_paletteKeyColor: "#7C757B" - property color m3neutral_variant_paletteKeyColor: "#7D747D" - property color m3background: "#161217" - property color m3onBackground: "#EAE0E7" - property color m3surface: "#161217" - property color m3surfaceDim: "#161217" - property color m3surfaceBright: "#3D373D" - property color m3surfaceContainerLowest: "#110D12" - property color m3surfaceContainerLow: "#1F1A1F" - property color m3surfaceContainer: "#231E23" - property color m3surfaceContainerHigh: "#2D282E" - property color m3surfaceContainerHighest: "#383339" - property color m3onSurface: "#EAE0E7" - property color m3surfaceVariant: "#4C444D" - property color m3onSurfaceVariant: "#CFC3CD" - property color m3inverseSurface: "#EAE0E7" - property color m3inverseOnSurface: "#342F34" - property color m3outline: "#988E97" - property color m3outlineVariant: "#4C444D" + property color m3background: "#141313" + property color m3onBackground: "#e6e1e1" + property color m3surface: "#141313" + property color m3surfaceDim: "#141313" + property color m3surfaceBright: "#3a3939" + property color m3surfaceContainerLowest: "#0f0e0e" + property color m3surfaceContainerLow: "#1c1b1c" + property color m3surfaceContainer: "#201f20" + property color m3surfaceContainerHigh: "#2b2a2a" + property color m3surfaceContainerHighest: "#363435" + property color m3onSurface: "#e6e1e1" + property color m3surfaceVariant: "#49464a" + property color m3onSurfaceVariant: "#cbc5ca" + property color m3inverseSurface: "#e6e1e1" + property color m3inverseOnSurface: "#313030" + property color m3outline: "#948f94" + property color m3outlineVariant: "#49464a" property color m3shadow: "#000000" property color m3scrim: "#000000" - property color m3surfaceTint: "#E5B6F2" - property color m3primary: "#E5B6F2" - property color m3onPrimary: "#452152" - property color m3primaryContainer: "#5D386A" - property color m3onPrimaryContainer: "#F9D8FF" - property color m3inversePrimary: "#775084" - property color m3secondary: "#D5C0D7" - property color m3onSecondary: "#392C3D" - property color m3secondaryContainer: "#534457" - property color m3onSecondaryContainer: "#F2DCF3" - property color m3tertiary: "#F5B7B3" - property color m3onTertiary: "#4C2523" - property color m3tertiaryContainer: "#BA837F" - property color m3onTertiaryContainer: "#000000" - property color m3error: "#FFB4AB" + property color m3surfaceTint: "#cbc4cb" + property color m3primary: "#cbc4cb" + property color m3onPrimary: "#322f34" + property color m3primaryContainer: "#2d2a2f" + property color m3onPrimaryContainer: "#bcb6bc" + property color m3inversePrimary: "#615d63" + property color m3secondary: "#cac5c8" + property color m3onSecondary: "#323032" + property color m3secondaryContainer: "#4d4b4d" + property color m3onSecondaryContainer: "#ece6e9" + property color m3tertiary: "#d1c3c6" + property color m3onTertiary: "#372e30" + property color m3tertiaryContainer: "#31292b" + property color m3onTertiaryContainer: "#c1b4b7" + property color m3error: "#ffb4ab" property color m3onError: "#690005" - property color m3errorContainer: "#93000A" - property color m3onErrorContainer: "#FFDAD6" - property color m3primaryFixed: "#F9D8FF" - property color m3primaryFixedDim: "#E5B6F2" - property color m3onPrimaryFixed: "#2E0A3C" - property color m3onPrimaryFixedVariant: "#5D386A" - property color m3secondaryFixed: "#F2DCF3" - property color m3secondaryFixedDim: "#D5C0D7" - property color m3onSecondaryFixed: "#241727" - property color m3onSecondaryFixedVariant: "#514254" - property color m3tertiaryFixed: "#FFDAD7" - property color m3tertiaryFixedDim: "#F5B7B3" - property color m3onTertiaryFixed: "#331110" - property color m3onTertiaryFixedVariant: "#663B39" + property color m3errorContainer: "#93000a" + property color m3onErrorContainer: "#ffdad6" + property color m3primaryFixed: "#e7e0e7" + property color m3primaryFixedDim: "#cbc4cb" + property color m3onPrimaryFixed: "#1d1b1f" + property color m3onPrimaryFixedVariant: "#49454b" + property color m3secondaryFixed: "#e6e1e4" + property color m3secondaryFixedDim: "#cac5c8" + property color m3onSecondaryFixed: "#1d1b1d" + property color m3onSecondaryFixedVariant: "#484648" + property color m3tertiaryFixed: "#eddfe1" + property color m3tertiaryFixedDim: "#d1c3c6" + property color m3onTertiaryFixed: "#211a1c" + property color m3onTertiaryFixedVariant: "#4e4447" property color m3success: "#B5CCBA" property color m3onSuccess: "#213528" property color m3successContainer: "#374B3E" From 65557dfb3d462f9de488fbda576a2558e85f12c9 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:37:10 +0200 Subject: [PATCH 12/32] fix sound plays every time low and charge state changes instead of just when it goes to low --- .../quickshell/ii/services/Battery.qml | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/dots/.config/quickshell/ii/services/Battery.qml b/dots/.config/quickshell/ii/services/Battery.qml index 8bee59770..b07bd5305 100644 --- a/dots/.config/quickshell/ii/services/Battery.qml +++ b/dots/.config/quickshell/ii/services/Battery.qml @@ -8,12 +8,14 @@ import QtQuick import Quickshell.Io Singleton { + id: root property bool available: UPower.displayDevice.isLaptopBattery property var chargeState: UPower.displayDevice.state property bool isCharging: chargeState == UPowerDeviceState.Charging property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge property real percentage: UPower.displayDevice?.percentage ?? 1 readonly property bool allowAutomaticSuspend: Config.options.battery.automaticSuspend + readonly property bool soundEnabled: Config.options.sounds.battery property bool isLow: available && (percentage <= Config.options.battery.low / 100) property bool isCritical: available && (percentage <= Config.options.battery.critical / 100) @@ -30,7 +32,8 @@ Singleton { property real timeToFull: UPower.displayDevice.timeToFull onIsLowAndNotChargingChanged: { - if (available && isLowAndNotCharging) Quickshell.execDetached([ + if (!root.available || !isLowAndNotCharging) return; + Quickshell.execDetached([ "notify-send", Translation.tr("Low battery"), Translation.tr("Consider plugging in your device"), @@ -38,15 +41,12 @@ Singleton { "-a", "Shell" ]) - if (available && Config.options.sounds.battery) { - if (isLowAndNotCharging) { - Audio.playSystemSound("dialog-warning") - } - } + if (root.soundEnabled) Audio.playSystemSound("dialog-warning"); } onIsCriticalAndNotChargingChanged: { - if (available && isCriticalAndNotCharging) Quickshell.execDetached([ + if (!root.available || !isCriticalAndNotCharging) return; + Quickshell.execDetached([ "notify-send", Translation.tr("Critically low battery"), Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend), @@ -54,41 +54,33 @@ Singleton { "-a", "Shell" ]); - if (available && Config.options.sounds.battery) { - if (isCriticalAndNotCharging) { - Audio.playSystemSound("suspend-error") - } - } + if (root.soundEnabled) Audio.playSystemSound("suspend-error"); } onIsSuspendingAndNotChargingChanged: { - if (available && isSuspendingAndNotCharging) { + if (root.available && isSuspendingAndNotCharging) { Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]); } } onIsFullAndChargingChanged: { - if (available && isFullAndCharging) Quickshell.execDetached([ + if (!root.available || !isFullAndCharging) return; + Quickshell.execDetached([ "notify-send", Translation.tr("Battery full"), Translation.tr("Please unplug the charger"), "-a", "Shell" ]); - if (available && Config.options.sounds.battery) { - if (isFullAndCharging) { - Audio.playSystemSound("complete") - } - } + if (root.soundEnabled) Audio.playSystemSound("complete"); } onIsPluggedInChanged: { - if (available && Config.options.sounds.battery) { - if (isPluggedIn) { - Audio.playSystemSound("power-plug") - } else { - Audio.playSystemSound("power-unplug") - } + if (!root.available || !root.soundEnabled) return; + if (isPluggedIn) { + Audio.playSystemSound("power-plug") + } else { + Audio.playSystemSound("power-unplug") } } } From fec23cab8d34188ad198bc480aa0a9ed82032a56 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:58:45 +0200 Subject: [PATCH 13/32] right sidebar: move audio controls to dialogs --- .../quickshell/ii/modules/common/Config.qml | 1 + .../modules/common/widgets/StyledSlider.qml | 2 +- .../widgets/WindowDialogSectionHeader.qml | 13 + .../sidebarRight/CenterWidgetGroup.qml | 68 +---- .../sidebarRight/SidebarRightContent.qml | 99 ++++--- .../quickToggles/AbstractQuickPanel.qml | 2 + .../quickToggles/AndroidQuickPanel.qml | 2 + .../androidStyle/AndroidAudioToggle.qml | 8 +- .../androidStyle/AndroidBluetoothToggle.qml | 5 +- .../androidStyle/AndroidMicToggle.qml | 8 +- .../AndroidToggleDelegateChooser.qml | 14 +- .../sidebarRight/volumeMixer/VolumeDialog.qml | 136 +++++++++ .../sidebarRight/volumeMixer/VolumeMixer.qml | 275 ------------------ .../volumeMixer/VolumeMixerEntry.qml | 3 +- 14 files changed, 248 insertions(+), 388 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSectionHeader.qml create mode 100644 dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialog.qml delete mode 100644 dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index b1cb46437..9f8fb6360 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -139,6 +139,7 @@ Singleton { property string networkEthernet: "kcmshell6 kcm_networkmanagement" property string taskManager: "plasma-systemmonitor --page-name Processes" property string terminal: "kitty -1" // This is only for shell actions + property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"` } property JsonObject background: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml index 99e7b8d6b..5473d4628 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -165,7 +165,7 @@ Slider { TrackDot { required property real modelData value: modelData - anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenter: parent?.verticalCenter } } } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSectionHeader.qml b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSectionHeader.qml new file mode 100644 index 000000000..10f641c66 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSectionHeader.qml @@ -0,0 +1,13 @@ +import QtQuick +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets + +StyledText { + text: "Section" + font { + pixelSize: Appearance.font.pixelSize.large + family: Appearance.font.family.title + } +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml b/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml index 35b185ef6..85d3b823a 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml @@ -13,70 +13,8 @@ Rectangle { radius: Appearance.rounding.normal color: Appearance.colors.colLayer1 - property int selectedTab: 0 - property var tabButtonList: [ - {"icon": "notifications", "name": Translation.tr("Notifications")}, - {"icon": "volume_up", "name": Translation.tr("Audio")} - ] - - Keys.onPressed: (event) => { - if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) { - if (event.key === Qt.Key_PageDown) { - root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1) - } else if (event.key === Qt.Key_PageUp) { - root.selectedTab = Math.max(root.selectedTab - 1, 0) - } - event.accepted = true; - } - if (event.modifiers === Qt.ControlModifier) { - if (event.key === Qt.Key_Tab) { - root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length - } else if (event.key === Qt.Key_Backtab) { - root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length - } - event.accepted = true; - } - } - - ColumnLayout { - anchors.margins: 5 + NotificationList { anchors.fill: parent - spacing: 0 - - PrimaryTabBar { - id: tabBar - tabButtonList: root.tabButtonList - externalTrackedTab: root.selectedTab - - function onCurrentIndexChanged(currentIndex) { - root.selectedTab = currentIndex - } - } - - SwipeView { - id: swipeView - Layout.topMargin: 5 - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 10 - currentIndex: root.selectedTab - onCurrentIndexChanged: { - tabBar.enableIndicatorAnimation = true - root.selectedTab = currentIndex - } - - clip: true - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: swipeView.width - height: swipeView.height - radius: Appearance.rounding.small - } - } - - NotificationList {} - VolumeMixer {} - } + anchors.margins: 5 } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 316c4eb5e..1eee4d335 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -13,6 +13,7 @@ import "./quickToggles/" import "./quickToggles/classicStyle/" import "./wifiNetworks/" import "./bluetoothDevices/" +import "./volumeMixer/" Item { id: root @@ -21,6 +22,8 @@ Item { property string settingsQmlPath: Quickshell.shellPath("settings.qml") property bool showWifiDialog: false property bool showBluetoothDialog: false + property bool showAudioOutputDialog: false + property bool showAudioInputDialog: false property bool editMode: false Connections { @@ -29,6 +32,8 @@ Item { if (!GlobalStates.sidebarRightOpen) { root.showWifiDialog = false; root.showBluetoothDialog = false; + root.showAudioOutputDialog = false; + root.showAudioInputDialog = false; } } } @@ -102,53 +107,71 @@ Item { } } - onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true; - Loader { + ToggleDialog { id: wifiDialogLoader - anchors.fill: parent - - active: root.showWifiDialog || item.visible - onActiveChanged: { - if (active) { - item.show = true; - item.forceActiveFocus(); - } - } - - sourceComponent: WifiDialog { - onDismiss: { - show = false - root.showWifiDialog = false - } - onVisibleChanged: { - if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false; - } + shownPropertyString: "showWifiDialog" + dialog: WifiDialog {} + onShownChanged: { + if (!shown) return; + Network.enableWifi(); + Network.rescanWifi(); } } - onShowBluetoothDialogChanged: { - if (showBluetoothDialog) bluetoothDialogLoader.active = true; - else Bluetooth.defaultAdapter.discovering = false; - } - Loader { + ToggleDialog { id: bluetoothDialogLoader + shownPropertyString: "showBluetoothDialog" + dialog: BluetoothDialog {} + onShownChanged: { + if (!shown) { + Bluetooth.defaultAdapter.discovering = false; + } else { + Bluetooth.defaultAdapter.enabled = true; + Bluetooth.defaultAdapter.discovering = true; + } + + } + } + + ToggleDialog { + id: audioOutputDialogLoader + shownPropertyString: "showAudioOutputDialog" + dialog: VolumeDialog { + isSink: true + } + } + + ToggleDialog { + id: audioInputDialogLoader + shownPropertyString: "showAudioInputDialog" + dialog: VolumeDialog { + isSink: false + } + } + + component ToggleDialog: Loader { + id: toggleDialogLoader + required property string shownPropertyString + property alias dialog: toggleDialogLoader.sourceComponent + readonly property bool shown: root[shownPropertyString] anchors.fill: parent - active: root.showBluetoothDialog || item.visible + onShownChanged: if (shown) toggleDialogLoader.active = true; + active: shown onActiveChanged: { if (active) { item.show = true; item.forceActiveFocus(); } } - - sourceComponent: BluetoothDialog { - onDismiss: { - show = false - root.showBluetoothDialog = false + Connections { + target: toggleDialogLoader.item + function onDismiss() { + toggleDialogLoader.item.show = false + root[toggleDialogLoader.shownPropertyString] = false; } - onVisibleChanged: { - if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false; + function onVisibleChanged() { + if (!toggleDialogLoader.item.visible && !root[toggleDialogLoader.shownPropertyString]) toggleDialogLoader.active = false; } } } @@ -163,15 +186,17 @@ Item { Connections { target: quickPanelImplLoader.item function onOpenWifiDialog() { - Network.enableWifi(); - Network.rescanWifi(); root.showWifiDialog = true; } function onOpenBluetoothDialog() { - Bluetooth.defaultAdapter.enabled = true; - Bluetooth.defaultAdapter.discovering = true; root.showBluetoothDialog = true; } + function onOpenAudioOutputDialog() { + root.showAudioOutputDialog = true; + } + function onOpenAudioInputDialog() { + root.showAudioInputDialog = true; + } } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml index 9bc9d44de..6f2861409 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml @@ -9,4 +9,6 @@ Rectangle { signal openWifiDialog() signal openBluetoothDialog() + signal openAudioOutputDialog() + signal openAudioInputDialog() } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml index fab308d7e..5aa95ad35 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml @@ -101,6 +101,8 @@ AbstractQuickPanel { spacing: root.spacing onOpenWifiDialog: root.openWifiDialog() onOpenBluetoothDialog: root.openBluetoothDialog() + onOpenAudioOutputDialog: root.openAudioOutputDialog() + onOpenAudioInputDialog: root.openAudioInputDialog() } } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidAudioToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidAudioToggle.qml index bfe8fe1d0..3fff4d620 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidAudioToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidAudioToggle.qml @@ -8,7 +8,7 @@ import Quickshell AndroidQuickToggleButton { id: root - name: Translation.tr("Audio") + name: Translation.tr("Audio output") statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted") toggled: !Audio.sink?.audio?.muted buttonIcon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up" @@ -16,7 +16,11 @@ AndroidQuickToggleButton { Audio.sink.audio.muted = !Audio.sink.audio.muted } + altAction: () => { + root.openMenu() + } + StyledToolTip { - text: Translation.tr("Audio") + text: Translation.tr("Audio output | Right-click for volume mixer & device selector") } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidBluetoothToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidBluetoothToggle.qml index 9d5a3d81a..3b4f8fa8d 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidBluetoothToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidBluetoothToggle.qml @@ -17,11 +17,14 @@ AndroidQuickToggleButton { onClicked: { Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled } + altAction: () => { + root.openMenu() + } StyledToolTip { text: Translation.tr("%1 | Right-click to configure").arg( (BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth")) + (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "") - ) + ) } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMicToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMicToggle.qml index 9930458e3..40f06f0a4 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMicToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMicToggle.qml @@ -8,7 +8,7 @@ import Quickshell AndroidQuickToggleButton { id: root - name: Translation.tr("Microphone") + name: Translation.tr("Audio input") statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted") toggled: !Audio.source?.audio?.muted buttonIcon: Audio.source?.audio?.muted ? "mic_off" : "mic" @@ -16,7 +16,11 @@ AndroidQuickToggleButton { Audio.source.audio.muted = !Audio.source.audio.muted } + altAction: () => { + root.openMenu() + } + StyledToolTip { - text: Translation.tr("Microphone") + text: Translation.tr("Audio input | Right-click for volume mixer & device selector") } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml index 5a64753b7..2cfaa9585 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml @@ -7,8 +7,6 @@ import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth -import "./androidStyle/" - DelegateChooser { id: root property bool editMode: false @@ -18,6 +16,8 @@ DelegateChooser { required property int startingIndex signal openWifiDialog() signal openBluetoothDialog() + signal openAudioOutputDialog() + signal openAudioInputDialog() role: "type" @@ -32,7 +32,7 @@ DelegateChooser { baseCellHeight: root.baseCellHeight cellSpacing: root.spacing cellSize: modelData.size - altAction: () => { + onOpenMenu: { root.openWifiDialog() } } } @@ -48,7 +48,7 @@ DelegateChooser { baseCellHeight: root.baseCellHeight cellSpacing: root.spacing cellSize: modelData.size - altAction: () => { + onOpenMenu: { root.openBluetoothDialog() } } } @@ -181,6 +181,9 @@ DelegateChooser { baseCellHeight: root.baseCellHeight cellSpacing: root.spacing cellSize: modelData.size + onOpenMenu: { + root.openAudioInputDialog() + } } } DelegateChoice { roleValue: "audio"; AndroidAudioToggle { @@ -194,6 +197,9 @@ DelegateChooser { baseCellHeight: root.baseCellHeight cellSpacing: root.spacing cellSize: modelData.size + onOpenMenu: { + root.openAudioOutputDialog() + } } } DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle { diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialog.qml b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialog.qml new file mode 100644 index 000000000..58a7af71a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialog.qml @@ -0,0 +1,136 @@ +pragma ComponentBehavior: Bound +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +WindowDialog { + id: root + property bool isSink: true + function correctType(node) { + return (node.isSink === root.isSink) && node.audio + } + readonly property list appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list but it breaks ScriptModel + return root.correctType(node) && node.isStream + }) + readonly property bool hasApps: appPwNodes.length > 0 + backgroundHeight: 700 + + WindowDialogTitle { + text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input") + } + + WindowDialogSectionHeader { + visible: root.hasApps + text: Translation.tr("Applications") + } + + WindowDialogSeparator { + visible: root.hasApps + Layout.topMargin: -22 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + } + + DialogSectionListView { + visible: root.hasApps + Layout.fillHeight: true + + model: ScriptModel { + values: root.appPwNodes + } + delegate: VolumeMixerEntry { + anchors { + left: parent?.left + right: parent?.right + } + required property var modelData + node: modelData + } + } + + WindowDialogSectionHeader { + text: Translation.tr("Devices") + } + + WindowDialogSeparator { + Layout.topMargin: -22 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + } + + DialogSectionListView { + Layout.fillHeight: !root.hasApps + Layout.preferredHeight: 180 + + model: ScriptModel { + values: Pipewire.nodes.values.filter(node => { + return root.correctType(node) && !node.isStream + }) + } + delegate: StyledRadioButton { + id: radioButton + required property var modelData + anchors { + left: parent?.left + right: parent?.right + } + + description: modelData.description + checked: modelData.id === (root.isSink ? Pipewire.preferredDefaultAudioSink?.id : Pipewire.preferredDefaultAudioSource?.id) + + onCheckedChanged: { + if (!checked) return; + if (root.isSink) { + Pipewire.preferredDefaultAudioSink = modelData + } else { + Pipewire.preferredDefaultAudioSource = modelData + } + } + } + } + + WindowDialogSeparator { + Layout.leftMargin: 0 + Layout.rightMargin: 0 + } + + WindowDialogButtonRow { + DialogButton { + buttonText: Translation.tr("Details") + onClicked: { + Quickshell.execDetached(["bash", "-c", `${Config.options.apps.volumeMixer}`]); + GlobalStates.sidebarRightOpen = false; + } + } + + Item { + Layout.fillWidth: true + } + + DialogButton { + buttonText: Translation.tr("Done") + onClicked: root.dismiss() + } + } + + component DialogSectionListView: StyledListView { + Layout.fillWidth: true + Layout.topMargin: -22 + Layout.bottomMargin: -16 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + topMargin: 12 + bottomMargin: 12 + leftMargin: 20 + rightMargin: 20 + + clip: true + spacing: 4 + animateAppearance: false + } +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml deleted file mode 100644 index a7e7ccc53..000000000 --- a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml +++ /dev/null @@ -1,275 +0,0 @@ -import qs.modules.common -import qs.modules.common.widgets -import qs.services -import Qt5Compat.GraphicalEffects -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Pipewire - - -Item { - id: root - property bool showDeviceSelector: false - property bool deviceSelectorInput - property int dialogMargins: 16 - property PwNode selectedDevice - readonly property list appPwNodes: Pipewire.nodes.values.filter((node) => { - // return node.type == "21" // Alternative, not as clean - return node.isSink && node.isStream - }) - - function showDeviceSelectorDialog(input: bool) { - root.selectedDevice = null - root.showDeviceSelector = true - root.deviceSelectorInput = input - } - - Keys.onPressed: (event) => { - // Close dialog on pressing Esc if open - if (event.key === Qt.Key_Escape && root.showDeviceSelector) { - root.showDeviceSelector = false - event.accepted = true; - } - } - - ColumnLayout { - anchors.fill: parent - Item { - Layout.fillWidth: true - Layout.fillHeight: true - StyledListView { - id: listView - model: root.appPwNodes - clip: true - anchors { - fill: parent - topMargin: 10 - bottomMargin: 10 - } - spacing: 6 - - delegate: VolumeMixerEntry { - // Layout.fillWidth: true - anchors { - left: parent.left - right: parent.right - leftMargin: 10 - rightMargin: 10 - } - required property var modelData - node: modelData - } - } - - // Placeholder when list is empty - Item { - anchors.fill: listView - - visible: opacity > 0 - opacity: (root.appPwNodes.length === 0) ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Appearance.animation.menuDecel.duration - easing.type: Appearance.animation.menuDecel.type - } - } - - ColumnLayout { - anchors.centerIn: parent - spacing: 5 - - MaterialSymbol { - Layout.alignment: Qt.AlignHCenter - iconSize: 55 - color: Appearance.m3colors.m3outline - text: "brand_awareness" - } - StyledText { - Layout.alignment: Qt.AlignHCenter - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.m3colors.m3outline - horizontalAlignment: Text.AlignHCenter - text: Translation.tr("No audio source") - } - } - } - } - - // Device selector - RowLayout { - id: deviceSelectorRowLayout - Layout.fillWidth: true - Layout.fillHeight: false - uniformCellSizes: true - - AudioDeviceSelectorButton { - Layout.fillWidth: true - input: false - downAction: () => root.showDeviceSelectorDialog(input) - } - AudioDeviceSelectorButton { - Layout.fillWidth: true - input: true - downAction: () => root.showDeviceSelectorDialog(input) - } - } - } - - // Device selector dialog - Item { - anchors.fill: parent - z: 9999 - - visible: opacity > 0 - opacity: root.showDeviceSelector ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve - } - } - - Rectangle { // Scrim - id: scrimOverlay - anchors.fill: parent - radius: Appearance.rounding.small - color: Appearance.colors.colScrim - MouseArea { - hoverEnabled: true - anchors.fill: parent - preventStealing: true - propagateComposedEvents: false - } - } - - Rectangle { // The dialog - id: dialog - color: Appearance.colors.colSurfaceContainerHigh - radius: Appearance.rounding.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: 30 - implicitHeight: dialogColumnLayout.implicitHeight - - ColumnLayout { - id: dialogColumnLayout - anchors.fill: parent - spacing: 16 - - StyledText { - id: dialogTitle - Layout.topMargin: dialogMargins - Layout.leftMargin: dialogMargins - Layout.rightMargin: dialogMargins - Layout.alignment: Qt.AlignLeft - color: Appearance.m3colors.m3onSurface - font.pixelSize: Appearance.font.pixelSize.larger - text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device") - } - - Rectangle { - color: Appearance.m3colors.m3outline - implicitHeight: 1 - Layout.fillWidth: true - Layout.leftMargin: dialogMargins - Layout.rightMargin: dialogMargins - } - - StyledFlickable { - id: dialogFlickable - Layout.fillWidth: true - clip: true - implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight) - - contentHeight: devicesColumnLayout.implicitHeight - - ColumnLayout { - id: devicesColumnLayout - anchors.fill: parent - Layout.fillWidth: true - spacing: 0 - - Repeater { - model: ScriptModel { - values: Pipewire.nodes.values.filter(node => { - return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio - }) - } - - // This could and should be refractored, but all data becomes null when passed wtf - delegate: StyledRadioButton { - id: radioButton - required property var modelData - Layout.leftMargin: root.dialogMargins - Layout.rightMargin: root.dialogMargins - Layout.fillWidth: true - - description: modelData.description - checked: modelData.id === Pipewire.defaultAudioSink?.id - - Connections { - target: root - function onShowDeviceSelectorChanged() { - if(!root.showDeviceSelector) return; - radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id) - } - } - - onCheckedChanged: { - if (checked) { - root.selectedDevice = modelData - } - } - } - } - Item { - implicitHeight: dialogMargins - } - } - } - - Rectangle { - color: Appearance.m3colors.m3outline - implicitHeight: 1 - Layout.fillWidth: true - Layout.leftMargin: dialogMargins - Layout.rightMargin: dialogMargins - } - - RowLayout { - id: dialogButtonsRowLayout - Layout.bottomMargin: dialogMargins - Layout.leftMargin: dialogMargins - Layout.rightMargin: dialogMargins - Layout.alignment: Qt.AlignRight - - DialogButton { - buttonText: Translation.tr("Cancel") - onClicked: { - root.showDeviceSelector = false - } - } - DialogButton { - buttonText: Translation.tr("OK") - onClicked: { - root.showDeviceSelector = false - if (root.selectedDevice) { - if (root.deviceSelectorInput) { - Pipewire.preferredDefaultAudioSource = root.selectedDevice - } else { - Pipewire.preferredDefaultAudioSink = root.selectedDevice - } - } - } - } - } - } - } - } - -} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml index 2f605fafe..503f83af0 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml @@ -21,7 +21,7 @@ Item { spacing: 6 Image { - property real size: slider.height * 0.9 + property real size: 36 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter visible: source != "" sourceSize.width: size @@ -57,6 +57,7 @@ Item { id: slider value: root.node.audio.volume onMoved: root.node.audio.volume = value + configuration: StyledSlider.Configuration.S } } } From 991abd4c1cbb2e3fef02cb6926a1fe6c6938f8d3 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:03:15 +0200 Subject: [PATCH 14/32] update default quick toggles --- dots/.config/quickshell/ii/modules/common/Config.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 9f8fb6360..44b262b96 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -402,12 +402,12 @@ Singleton { property JsonObject android: JsonObject { property int columns: 5 property list toggles: [ - { type: "network", size: 2 }, - { type: "bluetooth", size: 2 }, - { type: "idleInhibitor", size: 1 }, - { type: "easyEffects", size: 1 }, - { type: "nightLight", size: 2 }, - { type: "darkMode", size: 2 } + { "size": 2, "type": "network" }, + { "size": 2, "type": "bluetooth" }, + { "size": 1, "type": "idleInhibitor" }, + { "size": 1, "type": "mic" }, + { "size": 2, "type": "audio" }, + { "size": 2, "type": "nightLight" } ] } } From 96605fb0febb78ebe31905ea1adf8fb5d56e0f5d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:15:27 +0200 Subject: [PATCH 15/32] make notifs dismissable in both directions --- .../common/widgets/NotificationGroup.qml | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index ed14d75b7..1fa21a1a5 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -24,18 +24,19 @@ MouseArea { // Notification group area property real dragConfirmThreshold: 70 // Drag further to discard notification property real dismissOvershoot: 20 // Account for gaps and bouncy animations - property var qmlParent: root.parent.parent // There's something between this and the parent ListView - property var parentDragIndex: qmlParent.dragIndex - property var parentDragDistance: qmlParent.dragDistance + property var qmlParent: root?.parent?.parent // There's something between this and the parent ListView + property var parentDragIndex: qmlParent?.dragIndex + property var parentDragDistance: qmlParent?.dragDistance property var dragIndexDiff: Math.abs(parentDragIndex - index) - property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) : + property real xOffset: dragIndexDiff == 0 ? parentDragDistance : parentDragDistance > dragConfirmThreshold ? 0 : - dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) : - dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0 + dragIndexDiff == 1 ? (parentDragDistance * 0.3) : + dragIndexDiff == 2 ? (parentDragDistance * 0.1) : 0 - function destroyWithAnimation() { + function destroyWithAnimation(left = false) { root.qmlParent.resetDrag() background.anchors.leftMargin = background.anchors.leftMargin; // Break binding + destroyAnimation.left = left; destroyAnimation.running = true; } @@ -52,12 +53,13 @@ MouseArea { // Notification group area SequentialAnimation { // Drag finish animation id: destroyAnimation + property bool left: true running: false NumberAnimation { target: background.anchors property: "leftMargin" - to: root.width + root.dismissOvershoot + to: (root.width + root.dismissOvershoot) * (destroyAnimation.left ? -1 : 1) duration: Appearance.animation.elementMove.duration easing.type: Appearance.animation.elementMove.type easing.bezierCurve: Appearance.animation.elementMove.bezierCurve @@ -102,8 +104,8 @@ MouseArea { // Notification group area } onDragReleased: (diffX, diffY) => { - if (diffX > root.dragConfirmThreshold) - root.destroyWithAnimation(); + if (Math.abs(diffX) > root.dragConfirmThreshold) + root.destroyWithAnimation(diffX < 0); else dragManager.resetDrag(); } From ba0f2248d8222b6d5b5d280c178d4e9d04673805 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:20:06 +0200 Subject: [PATCH 16/32] make notif items also draggable to left --- .../common/widgets/NotificationGroup.qml | 2 +- .../common/widgets/NotificationItem.qml | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index 1fa21a1a5..6e8e1cecc 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -29,7 +29,7 @@ MouseArea { // Notification group area property var parentDragDistance: qmlParent?.dragDistance property var dragIndexDiff: Math.abs(parentDragIndex - index) property real xOffset: dragIndexDiff == 0 ? parentDragDistance : - parentDragDistance > dragConfirmThreshold ? 0 : + Math.abs(parentDragDistance) > dragConfirmThreshold ? 0 : dragIndexDiff == 1 ? (parentDragDistance * 0.3) : dragIndexDiff == 2 ? (parentDragDistance * 0.1) : 0 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml index 0c0fa40a4..9175be110 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml @@ -24,10 +24,10 @@ Item { // Notification item area property var parentDragIndex: qmlParent?.dragIndex ?? -1 property var parentDragDistance: qmlParent?.dragDistance ?? 0 property var dragIndexDiff: Math.abs(parentDragIndex - index) - property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) : - parentDragDistance > dragConfirmThreshold ? 0 : - dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) : - dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0 + property real xOffset: dragIndexDiff == 0 ? parentDragDistance : + Math.abs(parentDragDistance) > dragConfirmThreshold ? 0 : + dragIndexDiff == 1 ? (parentDragDistance * 0.3) : + dragIndexDiff == 2 ? (parentDragDistance * 0.1) : 0 implicitHeight: background.implicitHeight @@ -53,9 +53,10 @@ Item { // Notification item area return processedBody } - function destroyWithAnimation() { + function destroyWithAnimation(left = false) { root.qmlParent.resetDrag() background.anchors.leftMargin = background.anchors.leftMargin; // Break binding + destroyAnimation.left = left; destroyAnimation.running = true; } @@ -67,12 +68,13 @@ Item { // Notification item area SequentialAnimation { // Drag finish animation id: destroyAnimation + property bool left: true running: false NumberAnimation { target: background.anchors property: "leftMargin" - to: root.width + root.dismissOvershoot + to: (root.width + root.dismissOvershoot) * (destroyAnimation.left ? -1 : 1) duration: Appearance.animation.elementMove.duration easing.type: Appearance.animation.elementMove.type easing.bezierCurve: Appearance.animation.elementMove.bezierCurve @@ -107,8 +109,8 @@ Item { // Notification item area } onDragReleased: (diffX, diffY) => { - if (diffX > root.dragConfirmThreshold) - root.destroyWithAnimation(); + if (Math.abs(diffX) > root.dragConfirmThreshold) + root.destroyWithAnimation(diffX < 0); else dragManager.resetDrag(); } From 1d51cc3388cd67304a02c284b950ac21853e79f4 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:43:25 +0200 Subject: [PATCH 17/32] keybinds: make office keybind match windows, allow record when locked --- dots/.config/hypr/hyprland/keybinds.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index c389df352..0c20d7d3b 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -67,9 +67,9 @@ bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file # Recording stuff -bindd = Super+Alt, R, Record region (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound) -bindd = Ctrl+Alt, R, Record screen (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound) -bindd = Super+Shift+Alt, R, Record screen (with sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound) +bindl = Super+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound) +bindl = Ctrl+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound) +bindl = Super+Shift+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound) # AI bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text @@ -247,7 +247,7 @@ bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "${TERMINAL}" "kitty -1 fish -c yazi" # File manager bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" "command -v micro && kitty -1 micro" # Code editor -bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software +bind = Ctrl+Super+Shift+Alt, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" "libreoffice" # Office software bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app From af65c39c87d217ef5334e65b0fb181dc9b507ae4 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:13:01 +0200 Subject: [PATCH 18/32] make screen snip built into the shell so it's faster (#2113) --- dots/.config/quickshell/ii/GlobalStates.qml | 3 +- .../modules/regionSelector/RegionSelector.qml | 595 ++++++++++++++++++ dots/.config/quickshell/ii/screenshot.qml | 578 ----------------- dots/.config/quickshell/ii/shell.qml | 3 + 4 files changed, 600 insertions(+), 579 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml delete mode 100644 dots/.config/quickshell/ii/screenshot.qml diff --git a/dots/.config/quickshell/ii/GlobalStates.qml b/dots/.config/quickshell/ii/GlobalStates.qml index 507f5d191..5cee09d96 100644 --- a/dots/.config/quickshell/ii/GlobalStates.qml +++ b/dots/.config/quickshell/ii/GlobalStates.qml @@ -18,13 +18,14 @@ Singleton { property bool osdVolumeOpen: false property bool oskOpen: false property bool overviewOpen: false - property bool wallpaperSelectorOpen: false + property bool regionSelectorOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false property bool sessionOpen: false property bool superDown: false property bool superReleaseMightTrigger: true + property bool wallpaperSelectorOpen: false property bool workspaceShowNumbers: false onSidebarRightOpenChanged: { diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml new file mode 100644 index 000000000..6eecc365e --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml @@ -0,0 +1,595 @@ +pragma ComponentBehavior: Bound +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Hyprland + +Scope { + id: root + property string screenshotDir: Directories.screenshotTemp + property color overlayColor: "#77111111" + property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) + property color genericContentForeground: "#ddffffff" + property color selectionBorderColor: "#ddf1f1f1" + property color selectionFillColor: "#33ffffff" + property color windowBorderColor: "#dda0c0da" + property color windowFillColor: "#22a0c0da" + property color imageBorderColor: "#ddf1d1ff" + property color imageFillColor: "#33f1d1ff" + property color onBorderColor: "#ff000000" + property real standardRounding: 4 + readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { + // Sort floating=true windows before others + if (a.floating === b.floating) return 0; + return a.floating ? -1 : 1; + }) + readonly property var layers: HyprlandData.layers + readonly property real falsePositivePreventionRatio: 0.5 + + function dismiss() { + GlobalStates.regionSelectorOpen = false + } + + component TargetRegion: Rectangle { + id: regionRect + property bool showIcon: false + property bool targeted: false + property color borderColor + property color fillColor: "transparent" + property string text: "" + property real textPadding: 10 + z: 2 + color: fillColor + border.color: borderColor + border.width: targeted ? 3 : 1 + radius: root.standardRounding + + Rectangle { + id: regionLabelBackground + property real verticalPadding: 5 + property real horizontalPadding: 10 + radius: 10 + color: root.genericContentColor + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + anchors { + top: parent.top + left: parent.left + topMargin: regionRect.textPadding + leftMargin: regionRect.textPadding + } + implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2 + implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2 + Row { + id: regionInfoRow + anchors.centerIn: parent + spacing: 4 + + Loader { + id: regionIconLoader + active: regionRect.showIcon + visible: active + sourceComponent: IconImage { + implicitSize: Appearance.font.pixelSize.larger + source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing") + } + } + + StyledText { + id: regionText + text: regionRect.text + color: root.genericContentForeground + } + } + } + } + + Variants { + model: Quickshell.screens + delegate: Loader { + id: regionSelectorLoader + required property var modelData + active: GlobalStates.regionSelectorOpen + + sourceComponent: PanelWindow { + id: panelWindow + screen: regionSelectorLoader.modelData + visible: false + WlrLayershell.namespace: "quickshell:regionSelector" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + + readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen) + readonly property real monitorScale: hyprlandMonitor.scale + readonly property real monitorOffsetX: hyprlandMonitor.x + readonly property real monitorOffsetY: hyprlandMonitor.y + property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0 + property string screenshotPath: `${root.screenshotDir}/image-${screen.name}` + property real dragStartX: 0 + property real dragStartY: 0 + property real draggingX: 0 + property real draggingY: 0 + property real dragDiffX: 0 + property real dragDiffY: 0 + property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0) + property bool dragging: false + property var mouseButton: null + property var imageRegions: [] + readonly property list windowRegions: filterWindowRegionsByLayers( + root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId), + panelWindow.layerRegions + ).map(window => { + return { + at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY], + size: [window.size[0], window.size[1]], + class: window.class, + title: window.title, + } + }) + readonly property list layerRegions: { + const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name] + const topLayers = layersOfThisMonitor?.levels["2"] + if (!topLayers) return []; + const nonBarTopLayers = topLayers + .filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock"))) + .map(layer => { + return { + at: [layer.x, layer.y], + size: [layer.w, layer.h], + namespace: layer.namespace, + } + }) + const offsetAdjustedLayers = nonBarTopLayers.map(layer => { + return { + at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY], + size: layer.size, + namespace: layer.namespace, + } + }); + return offsetAdjustedLayers; + } + + property real targetedRegionX: -1 + property real targetedRegionY: -1 + property real targetedRegionWidth: 0 + property real targetedRegionHeight: 0 + + function intersectionOverUnion(regionA, regionB) { + // region: { at: [x, y], size: [w, h] } + const ax1 = regionA.at[0], ay1 = regionA.at[1]; + const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1]; + const bx1 = regionB.at[0], by1 = regionB.at[1]; + const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1]; + + const interX1 = Math.max(ax1, bx1); + const interY1 = Math.max(ay1, by1); + const interX2 = Math.min(ax2, bx2); + const interY2 = Math.min(ay2, by2); + + const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1); + const areaA = (ax2 - ax1) * (ay2 - ay1); + const areaB = (bx2 - bx1) * (by2 - by1); + const unionArea = areaA + areaB - interArea; + + return unionArea > 0 ? interArea / unionArea : 0; + } + + function filterOverlappingImageRegions(regions) { + let keep = []; + let removed = new Set(); + for (let i = 0; i < regions.length; ++i) { + if (removed.has(i)) continue; + let regionA = regions[i]; + for (let j = i + 1; j < regions.length; ++j) { + if (removed.has(j)) continue; + let regionB = regions[j]; + if (intersectionOverUnion(regionA, regionB) > 0) { + // Compare areas + let areaA = regionA.size[0] * regionA.size[1]; + let areaB = regionB.size[0] * regionB.size[1]; + if (areaA <= areaB) { + removed.add(j); + } else { + removed.add(i); + } + } + } + } + for (let i = 0; i < regions.length; ++i) { + if (!removed.has(i)) keep.push(regions[i]); + } + return keep; + } + + function filterWindowRegionsByLayers(windowRegions, layerRegions) { + return windowRegions.filter(windowRegion => { + for (let i = 0; i < layerRegions.length; ++i) { + if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0) + return false; + } + return true; + }); + } + + function filterImageRegions(regions, windowRegions, threshold = 0.1) { + // Remove image regions that overlap too much with any window region + let filtered = regions.filter(region => { + for (let i = 0; i < windowRegions.length; ++i) { + if (intersectionOverUnion(region, windowRegions[i]) > threshold) + return false; + } + return true; + }); + // Remove overlapping image regions, keep only the smaller one + return filterOverlappingImageRegions(filtered); + } + + function updateTargetedRegion(x, y) { + // Image regions + const clickedRegion = panelWindow.imageRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedRegion) { + panelWindow.targetedRegionX = clickedRegion.at[0]; + panelWindow.targetedRegionY = clickedRegion.at[1]; + panelWindow.targetedRegionWidth = clickedRegion.size[0]; + panelWindow.targetedRegionHeight = clickedRegion.size[1]; + return; + } + + // Layer regions + const clickedLayer = panelWindow.layerRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedLayer) { + panelWindow.targetedRegionX = clickedLayer.at[0]; + panelWindow.targetedRegionY = clickedLayer.at[1]; + panelWindow.targetedRegionWidth = clickedLayer.size[0]; + panelWindow.targetedRegionHeight = clickedLayer.size[1]; + return; + } + + // Window regions + const clickedWindow = panelWindow.windowRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedWindow) { + panelWindow.targetedRegionX = clickedWindow.at[0]; + panelWindow.targetedRegionY = clickedWindow.at[1]; + panelWindow.targetedRegionWidth = clickedWindow.size[0]; + panelWindow.targetedRegionHeight = clickedWindow.size[1]; + return; + } + + panelWindow.targetedRegionX = -1; + panelWindow.targetedRegionY = -1; + panelWindow.targetedRegionWidth = 0; + panelWindow.targetedRegionHeight = 0; + } + + property real regionWidth: Math.abs(draggingX - dragStartX) + property real regionHeight: Math.abs(draggingY - dragStartY) + property real regionX: Math.min(dragStartX, draggingX) + property real regionY: Math.min(dragStartY, draggingY) + + Process { + id: screenshotProcess + running: true + command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(panelWindow.screen.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`] + onExited: (exitCode, exitStatus) => { + panelWindow.visible = true; + imageDetectionProcess.running = true; + } + } + + Process { + id: imageDetectionProcess + command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh ` + + `--hyprctl ` + + `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' ` + + `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} ` + + `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `] + stdout: StdioCollector { + id: imageDimensionCollector + onStreamFinished: { + imageRegions = filterImageRegions( + JSON.parse(imageDimensionCollector.text), + panelWindow.windowRegions + ); + } + } + } + + Process { + id: snipProc + function snip() { + if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) { + console.warn("Invalid region size, skipping snip."); + root.dismiss(); + } + snipProc.startDetached(); + root.dismiss(); + } + command: ["bash", "-c", + `magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} ` + + `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - ` + + `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`] + } + + ScreencopyView { + anchors.fill: parent + live: false + captureSource: panelWindow.screen + + focus: panelWindow.visible + Keys.onPressed: (event) => { // Esc to close + if (event.key === Qt.Key_Escape) { + root.dismiss(); + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.CrossCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + + // Controls + onPressed: mouse => { + panelWindow.dragStartX = mouse.x; + panelWindow.dragStartY = mouse.y; + panelWindow.draggingX = mouse.x; + panelWindow.draggingY = mouse.y; + panelWindow.dragging = true; + panelWindow.mouseButton = mouse.button; + } + onReleased: mouse => { + // Detect if it was a click + + // Image regions + if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) { + if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) { + panelWindow.regionX = panelWindow.targetedRegionX; + panelWindow.regionY = panelWindow.targetedRegionY; + panelWindow.regionWidth = panelWindow.targetedRegionWidth; + panelWindow.regionHeight = panelWindow.targetedRegionHeight; + } + } + snipProc.snip(); + } + onPositionChanged: mouse => { + if (panelWindow.dragging) { + panelWindow.draggingX = mouse.x; + panelWindow.draggingY = mouse.y; + panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX; + panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY; + } + panelWindow.updateTargetedRegion(mouse.x, mouse.y); + } + + // Overlay to darken screen + Rectangle { // Base + id: darkenOverlay + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: panelWindow.regionX - darkenOverlay.border.width + topMargin: panelWindow.regionY - darkenOverlay.border.width + } + width: panelWindow.regionWidth + darkenOverlay.border.width * 2 + height: panelWindow.regionHeight + darkenOverlay.border.width * 2 + color: "transparent" + // border.color: root.selectionBorderColor + border.color: root.overlayColor + border.width: Math.max(panelWindow.width, panelWindow.height) + radius: root.standardRounding + } + Rectangle { + id: selectionBorder + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: panelWindow.regionX + topMargin: panelWindow.regionY + } + width: panelWindow.regionWidth + height: panelWindow.regionHeight + color: "transparent" + border.color: root.selectionBorderColor + border.width: 2 + // radius: root.standardRounding + radius: 0 // TODO: figure out how to make the overlay thing work with rounding + } + StyledText { + z: 2 + anchors { + top: selectionBorder.bottom + right: selectionBorder.right + margins: 8 + } + color: root.selectionBorderColor + text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}` + } + + // Instructions + Rectangle { + z: 9999 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2 + } + + opacity: panelWindow.dragging ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + color: root.genericContentColor + radius: 10 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + implicitWidth: instructionsRow.implicitWidth + 10 * 2 + implicitHeight: instructionsRow.implicitHeight + 5 * 2 + + Row { + id: instructionsRow + anchors.centerIn: parent + spacing: 4 + MaterialSymbol { + id: screenshotRegionIcon + // anchors.centerIn: parent + iconSize: Appearance.font.pixelSize.larger + text: "screenshot_region" + color: root.genericContentForeground + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit") + color: root.genericContentForeground + } + } + } + + // Window regions + Repeater { + model: ScriptModel { + values: panelWindow.windowRegions + } + delegate: TargetRegion { + z: 2 + required property var modelData + showIcon: true + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.class}` + radius: Appearance.rounding.windowRounding + } + } + + // Layer regions + Repeater { + model: ScriptModel { + values: panelWindow.layerRegions + } + delegate: TargetRegion { + z: 3 + required property var modelData + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.namespace}` + radius: Appearance.rounding.windowRounding + } + } + + // Image regions + Repeater { + model: ScriptModel { + values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : [] + } + delegate: TargetRegion { + z: 4 + required property var modelData + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.imageBorderColor + fillColor: targeted ? root.imageFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: "Content region" + } + } + } + } + } + } + } + + function screenshot() { + GlobalStates.regionSelectorOpen = true + } + + IpcHandler { + target: "region" + + function screenshot() { + root.screenshot() + } + } + + GlobalShortcut { + name: "regionScreenshot" + description: "Takes a screenshot of the selected region" + + onPressed: { + root.screenshot() + } + } +} diff --git a/dots/.config/quickshell/ii/screenshot.qml b/dots/.config/quickshell/ii/screenshot.qml deleted file mode 100644 index 499409daf..000000000 --- a/dots/.config/quickshell/ii/screenshot.qml +++ /dev/null @@ -1,578 +0,0 @@ -//@ pragma UseQApplication -//@ pragma Env QS_NO_RELOAD_POPUP=1 -//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic -//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 - -// Adjust this to make it smaller or larger -//@ pragma Env QT_SCALE_FACTOR=1 - -pragma ComponentBehavior: "Bound" -import qs.services -import qs.modules.common -import qs.modules.common.widgets -import qs.modules.common.functions -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland - -ShellRoot { - id: root - property string screenshotDir: Directories.screenshotTemp - property color overlayColor: "#77111111" - property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) - property color genericContentForeground: "#ddffffff" - property color selectionBorderColor: "#ddf1f1f1" - property color selectionFillColor: "#33ffffff" - property color windowBorderColor: "#dda0c0da" - property color windowFillColor: "#22a0c0da" - property color imageBorderColor: "#ddf1d1ff" - property color imageFillColor: "#33f1d1ff" - property color onBorderColor: "#ff000000" - property real standardRounding: 4 - readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { - // Sort floating=true windows before others - if (a.floating === b.floating) return 0; - return a.floating ? -1 : 1; - }) - readonly property var layers: HyprlandData.layers - readonly property real falsePositivePreventionRatio: 0.5 - - // Force initialization of some singletons - Component.onCompleted: { - MaterialThemeLoader.reapplyTheme(); - } - - component TargetRegion: Rectangle { - id: regionRect - property bool showIcon: false - property bool targeted: false - property color borderColor - property color fillColor: "transparent" - property string text: "" - property real textPadding: 10 - z: 2 - color: fillColor - border.color: borderColor - border.width: targeted ? 3 : 1 - radius: root.standardRounding - - Rectangle { - id: regionLabelBackground - property real verticalPadding: 5 - property real horizontalPadding: 10 - radius: 10 - color: root.genericContentColor - border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant - anchors { - top: parent.top - left: parent.left - topMargin: regionRect.textPadding - leftMargin: regionRect.textPadding - } - implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2 - implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2 - Row { - id: regionInfoRow - anchors.centerIn: parent - spacing: 4 - - Loader { - id: regionIconLoader - active: regionRect.showIcon - visible: active - sourceComponent: IconImage { - implicitSize: Appearance.font.pixelSize.larger - source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing") - } - } - - StyledText { - id: regionText - text: regionRect.text - color: root.genericContentForeground - } - } - } - } - - Variants { - model: Quickshell.screens - - PanelWindow { - id: panelWindow - required property var modelData - readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(modelData) - readonly property real monitorScale: hyprlandMonitor.scale - readonly property real monitorOffsetX: hyprlandMonitor.x - readonly property real monitorOffsetY: hyprlandMonitor.y - property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0 - property string screenshotPath: `${root.screenshotDir}/image-${modelData.name}` - property real dragStartX: 0 - property real dragStartY: 0 - property real draggingX: 0 - property real draggingY: 0 - property real dragDiffX: 0 - property real dragDiffY: 0 - property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0) - property bool dragging: false - property var mouseButton: null - property var imageRegions: [] - readonly property list windowRegions: filterWindowRegionsByLayers( - root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId), - panelWindow.layerRegions - ).map(window => { - return { - at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY], - size: [window.size[0], window.size[1]], - class: window.class, - title: window.title, - } - }) - readonly property list layerRegions: { - const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name] - const topLayers = layersOfThisMonitor?.levels["2"] - if (!topLayers) return []; - const nonBarTopLayers = topLayers - .filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock"))) - .map(layer => { - return { - at: [layer.x, layer.y], - size: [layer.w, layer.h], - namespace: layer.namespace, - } - }) - const offsetAdjustedLayers = nonBarTopLayers.map(layer => { - return { - at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY], - size: layer.size, - namespace: layer.namespace, - } - }); - return offsetAdjustedLayers; - } - - property real targetedRegionX: -1 - property real targetedRegionY: -1 - property real targetedRegionWidth: 0 - property real targetedRegionHeight: 0 - - function intersectionOverUnion(regionA, regionB) { - // region: { at: [x, y], size: [w, h] } - const ax1 = regionA.at[0], ay1 = regionA.at[1]; - const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1]; - const bx1 = regionB.at[0], by1 = regionB.at[1]; - const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1]; - - const interX1 = Math.max(ax1, bx1); - const interY1 = Math.max(ay1, by1); - const interX2 = Math.min(ax2, bx2); - const interY2 = Math.min(ay2, by2); - - const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1); - const areaA = (ax2 - ax1) * (ay2 - ay1); - const areaB = (bx2 - bx1) * (by2 - by1); - const unionArea = areaA + areaB - interArea; - - return unionArea > 0 ? interArea / unionArea : 0; - } - - function filterOverlappingImageRegions(regions) { - let keep = []; - let removed = new Set(); - for (let i = 0; i < regions.length; ++i) { - if (removed.has(i)) continue; - let regionA = regions[i]; - for (let j = i + 1; j < regions.length; ++j) { - if (removed.has(j)) continue; - let regionB = regions[j]; - if (intersectionOverUnion(regionA, regionB) > 0) { - // Compare areas - let areaA = regionA.size[0] * regionA.size[1]; - let areaB = regionB.size[0] * regionB.size[1]; - if (areaA <= areaB) { - removed.add(j); - } else { - removed.add(i); - } - } - } - } - for (let i = 0; i < regions.length; ++i) { - if (!removed.has(i)) keep.push(regions[i]); - } - return keep; - } - - function filterWindowRegionsByLayers(windowRegions, layerRegions) { - return windowRegions.filter(windowRegion => { - for (let i = 0; i < layerRegions.length; ++i) { - if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0) - return false; - } - return true; - }); - } - - function filterImageRegions(regions, windowRegions, threshold = 0.1) { - // Remove image regions that overlap too much with any window region - let filtered = regions.filter(region => { - for (let i = 0; i < windowRegions.length; ++i) { - if (intersectionOverUnion(region, windowRegions[i]) > threshold) - return false; - } - return true; - }); - // Remove overlapping image regions, keep only the smaller one - return filterOverlappingImageRegions(filtered); - } - - function updateTargetedRegion(x, y) { - // Image regions - const clickedRegion = panelWindow.imageRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedRegion) { - panelWindow.targetedRegionX = clickedRegion.at[0]; - panelWindow.targetedRegionY = clickedRegion.at[1]; - panelWindow.targetedRegionWidth = clickedRegion.size[0]; - panelWindow.targetedRegionHeight = clickedRegion.size[1]; - return; - } - - // Layer regions - const clickedLayer = panelWindow.layerRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedLayer) { - panelWindow.targetedRegionX = clickedLayer.at[0]; - panelWindow.targetedRegionY = clickedLayer.at[1]; - panelWindow.targetedRegionWidth = clickedLayer.size[0]; - panelWindow.targetedRegionHeight = clickedLayer.size[1]; - return; - } - - // Window regions - const clickedWindow = panelWindow.windowRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedWindow) { - panelWindow.targetedRegionX = clickedWindow.at[0]; - panelWindow.targetedRegionY = clickedWindow.at[1]; - panelWindow.targetedRegionWidth = clickedWindow.size[0]; - panelWindow.targetedRegionHeight = clickedWindow.size[1]; - return; - } - - panelWindow.targetedRegionX = -1; - panelWindow.targetedRegionY = -1; - panelWindow.targetedRegionWidth = 0; - panelWindow.targetedRegionHeight = 0; - } - - property real regionWidth: Math.abs(draggingX - dragStartX) - property real regionHeight: Math.abs(draggingY - dragStartY) - property real regionX: Math.min(dragStartX, draggingX) - property real regionY: Math.min(dragStartY, draggingY) - - visible: false - screen: modelData - WlrLayershell.namespace: "quickshell:screenshot" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - exclusionMode: ExclusionMode.Ignore - anchors { - left: true - right: true - top: true - bottom: true - } - - Process { - id: screenshotProcess - running: true - command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(modelData.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`] - onExited: (exitCode, exitStatus) => { - panelWindow.visible = true; - imageDetectionProcess.running = true; - } - } - - Process { - id: imageDetectionProcess - command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh ` - + `--hyprctl ` - + `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' ` - + `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} ` - + `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `] - stdout: StdioCollector { - id: imageDimensionCollector - onStreamFinished: { - imageRegions = filterImageRegions( - JSON.parse(imageDimensionCollector.text), - panelWindow.windowRegions - ); - } - } - } - - Process { - id: snipProc - function snip() { - if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) { - console.warn("Invalid region size, skipping snip."); - Qt.quit(); - } - snipProc.startDetached(); - Qt.quit(); - } - command: ["bash", "-c", - `magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} ` - + `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - ` - + `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`] - } - - ScreencopyView { - anchors.fill: parent - live: false - captureSource: modelData - - focus: panelWindow.visible - Keys.onPressed: (event) => { // Esc to close - if (event.key === Qt.Key_Escape) { - Qt.quit(); - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.CrossCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - - // Controls - onPressed: mouse => { - panelWindow.dragStartX = mouse.x; - panelWindow.dragStartY = mouse.y; - panelWindow.draggingX = mouse.x; - panelWindow.draggingY = mouse.y; - panelWindow.dragging = true; - panelWindow.mouseButton = mouse.button; - } - onReleased: mouse => { - // Detect if it was a click - - // Image regions - if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) { - if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) { - panelWindow.regionX = panelWindow.targetedRegionX; - panelWindow.regionY = panelWindow.targetedRegionY; - panelWindow.regionWidth = panelWindow.targetedRegionWidth; - panelWindow.regionHeight = panelWindow.targetedRegionHeight; - } - } - snipProc.snip(); - } - onPositionChanged: mouse => { - if (panelWindow.dragging) { - panelWindow.draggingX = mouse.x; - panelWindow.draggingY = mouse.y; - panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX; - panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY; - } - panelWindow.updateTargetedRegion(mouse.x, mouse.y); - } - - // Overlay to darken screen - Rectangle { // Base - id: darkenOverlay - z: 1 - anchors { - left: parent.left - top: parent.top - leftMargin: panelWindow.regionX - darkenOverlay.border.width - topMargin: panelWindow.regionY - darkenOverlay.border.width - } - width: panelWindow.regionWidth + darkenOverlay.border.width * 2 - height: panelWindow.regionHeight + darkenOverlay.border.width * 2 - color: "transparent" - // border.color: root.selectionBorderColor - border.color: root.overlayColor - border.width: Math.max(panelWindow.width, panelWindow.height) - radius: root.standardRounding - } - Rectangle { - id: selectionBorder - z: 1 - anchors { - left: parent.left - top: parent.top - leftMargin: panelWindow.regionX - topMargin: panelWindow.regionY - } - width: panelWindow.regionWidth - height: panelWindow.regionHeight - color: "transparent" - border.color: root.selectionBorderColor - border.width: 2 - // radius: root.standardRounding - radius: 0 // TODO: figure out how to make the overlay thing work with rounding - } - StyledText { - z: 2 - anchors { - top: selectionBorder.bottom - right: selectionBorder.right - margins: 8 - } - color: root.selectionBorderColor - text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}` - } - - // Instructions - Rectangle { - z: 9999 - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2 - } - - opacity: panelWindow.dragging ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - color: root.genericContentColor - radius: 10 - border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant - implicitWidth: instructionsRow.implicitWidth + 10 * 2 - implicitHeight: instructionsRow.implicitHeight + 5 * 2 - - Row { - id: instructionsRow - anchors.centerIn: parent - spacing: 4 - MaterialSymbol { - id: screenshotRegionIcon - // anchors.centerIn: parent - iconSize: Appearance.font.pixelSize.larger - text: "screenshot_region" - color: root.genericContentForeground - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit") - color: root.genericContentForeground - } - } - } - - // Window regions - Repeater { - model: ScriptModel { - values: panelWindow.windowRegions - } - delegate: TargetRegion { - z: 2 - required property var modelData - showIcon: true - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: `${modelData.class}` - radius: Appearance.rounding.windowRounding - } - } - - // Layer regions - Repeater { - model: ScriptModel { - values: panelWindow.layerRegions - } - delegate: TargetRegion { - z: 3 - required property var modelData - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: `${modelData.namespace}` - radius: Appearance.rounding.windowRounding - } - } - - // Image regions - Repeater { - model: ScriptModel { - values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : [] - } - delegate: TargetRegion { - z: 4 - required property var modelData - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.imageBorderColor - fillColor: targeted ? root.imageFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: "Content region" - } - } - } - } - } - } -} diff --git a/dots/.config/quickshell/ii/shell.qml b/dots/.config/quickshell/ii/shell.qml index 942bb282e..1dd1627f4 100644 --- a/dots/.config/quickshell/ii/shell.qml +++ b/dots/.config/quickshell/ii/shell.qml @@ -19,6 +19,7 @@ import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/onScreenKeyboard/" import "./modules/overview/" +import "./modules/regionSelector/" import "./modules/screenCorners/" import "./modules/sessionScreen/" import "./modules/sidebarLeft/" @@ -45,6 +46,7 @@ ShellRoot { property bool enableOnScreenDisplay: true property bool enableOnScreenKeyboard: true property bool enableOverview: true + property bool enableRegionSelector: true property bool enableReloadPopup: true property bool enableScreenCorners: true property bool enableSessionScreen: true @@ -74,6 +76,7 @@ ShellRoot { LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOverview; component: Overview {} } + LazyLoader { active: enableRegionSelector; component: RegionSelector {} } LazyLoader { active: enableReloadPopup; component: ReloadPopup {} } LazyLoader { active: enableScreenCorners; component: ScreenCorners {} } LazyLoader { active: enableSessionScreen; component: SessionScreen {} } From 21b3cca54a343ce12a94f578f8e88d94e32d4e60 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:13:54 +0200 Subject: [PATCH 19/32] hyprland: update keybinds+rules for previous commit --- dots/.config/hypr/hyprland/keybinds.conf | 3 ++- dots/.config/hypr/hyprland/rules.conf | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 0c20d7d3b..38fc05caf 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -58,7 +58,8 @@ bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $ # Screenshot, Record, OCR, Color picker, Clipboard history bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback) bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) -bindd = Super+Shift, S, Screen snip, exec, qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip +bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip +bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback) # OCR bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] # Color picker diff --git a/dots/.config/hypr/hyprland/rules.conf b/dots/.config/hypr/hyprland/rules.conf index e4283b4cb..9cb877f7c 100644 --- a/dots/.config/hypr/hyprland/rules.conf +++ b/dots/.config/hypr/hyprland/rules.conf @@ -141,6 +141,7 @@ layerrule = noanim, quickshell:lockWindowPusher layerrule = animation fade, quickshell:notificationPopup layerrule = noanim, quickshell:overview layerrule = animation slide bottom, quickshell:osk +layerrule = noanim, quickshell:regionSelector layerrule = noanim, quickshell:screenshot layerrule = blur, quickshell:session layerrule = noanim, quickshell:session From 4ea7401190ad744e6cf105d98fed5cc34c92c08a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:03:03 +0200 Subject: [PATCH 20/32] circle to search --- dots/.config/hypr/hyprland/keybinds.conf | 1 + .../quickshell/ii/modules/common/Config.qml | 2 + .../regionSelector/CircleSelectionDetails.qml | 48 ++ .../RectCornersSelectionDetails.qml | 84 +++ .../regionSelector/RegionSelection.qml | 563 +++++++++++++++++ .../modules/regionSelector/RegionSelector.qml | 565 +----------------- .../modules/regionSelector/TargetRegion.qml | 69 +++ 7 files changed, 790 insertions(+), 542 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml create mode 100644 dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml create mode 100644 dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml create mode 100644 dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 38fc05caf..74343d85b 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -60,6 +60,7 @@ bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call T bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback) +bind = Super+Shift, A, global, quickshell:regionSearch # Circle to Search # OCR bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] # Color picker diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 44b262b96..f93171584 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -355,6 +355,8 @@ Singleton { property JsonObject search: JsonObject { property int nonAppResultDelay: 30 // This prevents lagging when typing property string engineBaseUrl: "https://www.google.com/search?q=" + property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url=" + property string fileUploadApiEndpoint: "https://uguu.se/upload" property list excludedSites: ["quora.com", "facebook.com"] property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird. property JsonObject prefix: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml new file mode 100644 index 000000000..bea353d1f --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml @@ -0,0 +1,48 @@ +import qs.modules.common.widgets +import QtQuick +import QtQuick.Shapes +import Quickshell + +Item { + id: root + required property color color + required property color overlayColor + required property list points + property int strokeWidth: 10 + + function updatePoints() { + if (!root.dragging) return; + root.points.push({ x: root.mouseX, y: root.mouseY }); + } + + Rectangle { + id: darkenOverlay + z: 1 + anchors.fill: parent + color: root.overlayColor + } + + Shape { + id: shape + z: 2 + anchors.fill: parent + layer.enabled: true + layer.smooth: true + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: shapePath + strokeWidth: root.strokeWidth + pathHints: ShapePath.PathLinear + fillColor: "transparent" + strokeColor: root.color + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + + PathPolyline { + path: root.points + } + } + } + +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml b/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml new file mode 100644 index 000000000..b4e417c8d --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml @@ -0,0 +1,84 @@ +import qs.modules.common.widgets +import QtQuick + +Item { + id: root + required property real regionX + required property real regionY + required property real regionWidth + required property real regionHeight + required property real mouseX + required property real mouseY + required property color color + required property color overlayColor + + // Overlay to darken screen + // Base dark overlay around region + Rectangle { + id: darkenOverlay + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: root.regionX - darkenOverlay.border.width + topMargin: root.regionY - darkenOverlay.border.width + } + width: root.regionWidth + darkenOverlay.border.width * 2 + height: root.regionHeight + darkenOverlay.border.width * 2 + color: "transparent" + border.color: root.overlayColor + border.width: Math.max(root.width, root.height) + } + + // Selection border + Rectangle { + id: selectionBorder + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: root.regionX + topMargin: root.regionY + } + width: root.regionWidth + height: root.regionHeight + color: "transparent" + border.color: root.color + border.width: 2 + // radius: root.standardRounding + radius: 0 // TODO: figure out how to make the overlay thing work with rounding + } + + StyledText { + z: 2 + anchors { + top: selectionBorder.bottom + right: selectionBorder.right + margins: 8 + } + color: root.color + text: `${Math.round(root.regionWidth)} x ${Math.round(root.regionHeight)}` + } + + // Coord lines + Rectangle { // Vertical + z: 2 + x: root.mouseX + anchors { + top: parent.top + bottom: parent.bottom + } + width: 1 + color: root.color + } + Rectangle { // Horizontal + z: 2 + y: root.mouseY + anchors { + left: parent.left + right: parent.right + } + height: 1 + color: root.color + } +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml new file mode 100644 index 000000000..32549fbc0 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -0,0 +1,563 @@ +pragma ComponentBehavior: Bound +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Hyprland + +PanelWindow { + id: root + visible: false + WlrLayershell.namespace: "quickshell:regionSelector" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + + // TODO: Ask: sidebar AI; Ocr: tesseract + enum SnipAction { Copy, Edit, Search } + enum SelectionMode { RectCorners, Circle } + property var action: RegionSelection.SnipAction.Copy + property var selectionMode: RegionSelection.SelectionMode.RectCorners + signal dismiss() + + property string screenshotDir: Directories.screenshotTemp + property string imageSearchEngineBaseUrl: Config.options.search.imageSearchEngineBaseUrl + property string fileUploadApiEndpoint: Config.options.search.fileUploadApiEndpoint + property color overlayColor: "#77111111" + property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) + property color genericContentForeground: "#ddffffff" + property color selectionBorderColor: "#ddf1f1f1" + property color selectionFillColor: "#33ffffff" + property color windowBorderColor: "#dda0c0da" + property color windowFillColor: "#22a0c0da" + property color imageBorderColor: "#ddf1d1ff" + property color imageFillColor: "#33f1d1ff" + property color onBorderColor: "#ff000000" + property real standardRounding: 4 + readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { + // Sort floating=true windows before others + if (a.floating === b.floating) return 0; + return a.floating ? -1 : 1; + }) + readonly property var layers: HyprlandData.layers + readonly property real falsePositivePreventionRatio: 0.5 + + readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen) + readonly property real monitorScale: hyprlandMonitor.scale + readonly property real monitorOffsetX: hyprlandMonitor.x + readonly property real monitorOffsetY: hyprlandMonitor.y + property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0 + property string screenshotPath: `${root.screenshotDir}/image-${screen.name}` + property real dragStartX: 0 + property real dragStartY: 0 + property real draggingX: 0 + property real draggingY: 0 + property real dragDiffX: 0 + property real dragDiffY: 0 + property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0) + property bool dragging: false + property list points: [] + property var mouseButton: null + property var imageRegions: [] + readonly property list windowRegions: filterWindowRegionsByLayers( + root.windows.filter(w => w.workspace.id === root.activeWorkspaceId), + root.layerRegions + ).map(window => { + return { + at: [window.at[0] - root.monitorOffsetX, window.at[1] - root.monitorOffsetY], + size: [window.size[0], window.size[1]], + class: window.class, + title: window.title, + } + }) + readonly property list layerRegions: { + const layersOfThisMonitor = root.layers[root.hyprlandMonitor.name] + const topLayers = layersOfThisMonitor?.levels["2"] + if (!topLayers) return []; + const nonBarTopLayers = topLayers + .filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock"))) + .map(layer => { + return { + at: [layer.x, layer.y], + size: [layer.w, layer.h], + namespace: layer.namespace, + } + }) + const offsetAdjustedLayers = nonBarTopLayers.map(layer => { + return { + at: [layer.at[0] - root.monitorOffsetX, layer.at[1] - root.monitorOffsetY], + size: layer.size, + namespace: layer.namespace, + } + }); + return offsetAdjustedLayers; + } + + property real targetedRegionX: -1 + property real targetedRegionY: -1 + property real targetedRegionWidth: 0 + property real targetedRegionHeight: 0 + + function intersectionOverUnion(regionA, regionB) { + // region: { at: [x, y], size: [w, h] } + const ax1 = regionA.at[0], ay1 = regionA.at[1]; + const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1]; + const bx1 = regionB.at[0], by1 = regionB.at[1]; + const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1]; + + const interX1 = Math.max(ax1, bx1); + const interY1 = Math.max(ay1, by1); + const interX2 = Math.min(ax2, bx2); + const interY2 = Math.min(ay2, by2); + + const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1); + const areaA = (ax2 - ax1) * (ay2 - ay1); + const areaB = (bx2 - bx1) * (by2 - by1); + const unionArea = areaA + areaB - interArea; + + return unionArea > 0 ? interArea / unionArea : 0; + } + + function filterOverlappingImageRegions(regions) { + let keep = []; + let removed = new Set(); + for (let i = 0; i < regions.length; ++i) { + if (removed.has(i)) continue; + let regionA = regions[i]; + for (let j = i + 1; j < regions.length; ++j) { + if (removed.has(j)) continue; + let regionB = regions[j]; + if (intersectionOverUnion(regionA, regionB) > 0) { + // Compare areas + let areaA = regionA.size[0] * regionA.size[1]; + let areaB = regionB.size[0] * regionB.size[1]; + if (areaA <= areaB) { + removed.add(j); + } else { + removed.add(i); + } + } + } + } + for (let i = 0; i < regions.length; ++i) { + if (!removed.has(i)) keep.push(regions[i]); + } + return keep; + } + + function filterWindowRegionsByLayers(windowRegions, layerRegions) { + return windowRegions.filter(windowRegion => { + for (let i = 0; i < layerRegions.length; ++i) { + if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0) + return false; + } + return true; + }); + } + + function filterImageRegions(regions, windowRegions, threshold = 0.1) { + // Remove image regions that overlap too much with any window region + let filtered = regions.filter(region => { + for (let i = 0; i < windowRegions.length; ++i) { + if (intersectionOverUnion(region, windowRegions[i]) > threshold) + return false; + } + return true; + }); + // Remove overlapping image regions, keep only the smaller one + return filterOverlappingImageRegions(filtered); + } + + function updateTargetedRegion(x, y) { + // Image regions + const clickedRegion = root.imageRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedRegion) { + root.targetedRegionX = clickedRegion.at[0]; + root.targetedRegionY = clickedRegion.at[1]; + root.targetedRegionWidth = clickedRegion.size[0]; + root.targetedRegionHeight = clickedRegion.size[1]; + return; + } + + // Layer regions + const clickedLayer = root.layerRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedLayer) { + root.targetedRegionX = clickedLayer.at[0]; + root.targetedRegionY = clickedLayer.at[1]; + root.targetedRegionWidth = clickedLayer.size[0]; + root.targetedRegionHeight = clickedLayer.size[1]; + return; + } + + // Window regions + const clickedWindow = root.windowRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedWindow) { + root.targetedRegionX = clickedWindow.at[0]; + root.targetedRegionY = clickedWindow.at[1]; + root.targetedRegionWidth = clickedWindow.size[0]; + root.targetedRegionHeight = clickedWindow.size[1]; + return; + } + + root.targetedRegionX = -1; + root.targetedRegionY = -1; + root.targetedRegionWidth = 0; + root.targetedRegionHeight = 0; + } + + property real regionWidth: Math.abs(draggingX - dragStartX) + property real regionHeight: Math.abs(draggingY - dragStartY) + property real regionX: Math.min(dragStartX, draggingX) + property real regionY: Math.min(dragStartY, draggingY) + + Process { + id: screenshotProcess + running: true + command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`] + onExited: (exitCode, exitStatus) => { + root.visible = true; + imageDetectionProcess.running = true; + } + } + + Process { + id: imageDetectionProcess + command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh ` + + `--hyprctl ` + + `--image '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' ` + + `--max-width ${Math.round(root.screen.width * root.falsePositivePreventionRatio)} ` + + `--max-height ${Math.round(root.screen.height * root.falsePositivePreventionRatio)} `] + stdout: StdioCollector { + id: imageDimensionCollector + onStreamFinished: { + imageRegions = filterImageRegions( + JSON.parse(imageDimensionCollector.text), + root.windowRegions + ); + } + } + } + + function snip() { + // Validity check + if (root.regionWidth <= 0 || root.regionHeight <= 0) { + console.warn("[Region Selector] Invalid region size, skipping snip."); + root.dismiss(); + } + + // Adjust action + if (root.action === RegionSelection.SnipAction.Copy || root.action === RegionSelection.SnipAction.Edit) { + root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy; + } + + // Set command for action + const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} ` + + `-crop ${root.regionWidth * root.monitorScale}x${root.regionHeight * root.monitorScale}+${root.regionX * root.monitorScale}+${root.regionY * root.monitorScale}` + const cropToStdout = `${cropBase} -` + const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'` + const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'` + const uploadAndGetUrl = (filePath) => { + return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'` + } + switch (root.action) { + case RegionSelection.SnipAction.Copy: + snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`] + break; + case RegionSelection.SnipAction.Edit: + snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`] + break; + case RegionSelection.SnipAction.Search: + snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})"`] + break; + default: + console.warn("[Region Selector] Unknown snip action, skipping snip."); + root.dismiss(); + return; + } + + // Image post-processing + snipProc.startDetached(); + root.dismiss(); + } + + Process { + id: snipProc + } + + ScreencopyView { + anchors.fill: parent + live: false + captureSource: root.screen + + focus: root.visible + Keys.onPressed: (event) => { // Esc to close + if (event.key === Qt.Key_Escape) { + root.dismiss(); + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.CrossCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + + // Controls + onPressed: (mouse) => { + root.dragStartX = mouse.x; + root.dragStartY = mouse.y; + root.draggingX = mouse.x; + root.draggingY = mouse.y; + root.dragging = true; + root.mouseButton = mouse.button; + } + onReleased: (mouse) => { + // Circle dragging? + if (root.selectionMode === RegionSelection.SelectionMode.Circle) { + const maxX = Math.max(...root.points.map(p => p.x)); + const minX = Math.min(...root.points.map(p => p.x)); + const maxY = Math.max(...root.points.map(p => p.y)); + const minY = Math.min(...root.points.map(p => p.y)); + root.regionX = minX; + root.regionY = minY; + root.regionWidth = maxX - minX; + root.regionHeight = maxY - minY; + } + // Detect if it was a click -> Try to select targeted region + if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) { + if (root.targetedRegionX >= 0 && root.targetedRegionY >= 0) { + root.regionX = root.targetedRegionX; + root.regionY = root.targetedRegionY; + root.regionWidth = root.targetedRegionWidth; + root.regionHeight = root.targetedRegionHeight; + } + } + root.snip(); + } + onPositionChanged: (mouse) => { + if (!root.dragging) return; + root.draggingX = mouse.x; + root.draggingY = mouse.y; + root.dragDiffX = mouse.x - root.dragStartX; + root.dragDiffY = mouse.y - root.dragStartY; + root.points.push({ x: mouse.x, y: mouse.y }); + root.updateTargetedRegion(mouse.x, mouse.y); + } + + Loader { + z: 2 + anchors.fill: parent + active: root.selectionMode === RegionSelection.SelectionMode.RectCorners + sourceComponent: RectCornersSelectionDetails { + regionX: root.regionX + regionY: root.regionY + regionWidth: root.regionWidth + regionHeight: root.regionHeight + mouseX: mouseArea.mouseX + mouseY: mouseArea.mouseY + color: root.selectionBorderColor + overlayColor: root.overlayColor + } + } + + Loader { + z: 2 + anchors.fill: parent + active: root.selectionMode === RegionSelection.SelectionMode.Circle + sourceComponent: CircleSelectionDetails { + color: root.selectionBorderColor + overlayColor: root.overlayColor + points: root.points + } + } + + // Instructions + Rectangle { + z: 9999 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2 + } + + opacity: root.dragging ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + color: root.genericContentColor + radius: 10 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + implicitWidth: instructionsRow.implicitWidth + 10 * 2 + implicitHeight: instructionsRow.implicitHeight + 5 * 2 + + Row { + id: instructionsRow + anchors.centerIn: parent + spacing: 4 + MaterialSymbol { + id: screenshotRegionIcon + // anchors.centerIn: parent + iconSize: Appearance.font.pixelSize.larger + text: switch(root.selectionMode) { + case RegionSelection.SelectionMode.RectCorners: + return "crop_free" + break; + case RegionSelection.SelectionMode.Circle: + return "gesture" + break; + default: + return "crop_free" + } + color: root.genericContentForeground + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: { + var instructionText = ""; + var actionText = ""; + if (root.selectionMode === RegionSelection.SelectionMode.RectCorners) { + instructionText = Translation.tr("Drag or click a region"); + } else if (root.selectionMode === RegionSelection.SelectionMode.Circle) { + instructionText = Translation.tr("Circle"); + } + switch (root.action) { + case RegionSelection.SnipAction.Copy: + case RegionSelection.SnipAction.Edit: + actionText = Translation.tr(" | LMB: Copy • RMB: Edit"); + break; + case RegionSelection.SnipAction.Search: + actionText = Translation.tr(" to search"); + break; + default: + actionText = ""; + } + return instructionText + actionText; + } + color: root.genericContentForeground + } + } + } + + // Window regions + Repeater { + model: ScriptModel { + values: root.windowRegions + } + delegate: TargetRegion { + z: 2 + required property var modelData + showIcon: true + targeted: !root.draggedAway && + (root.targetedRegionX === modelData.at[0] + && root.targetedRegionY === modelData.at[1] + && root.targetedRegionWidth === modelData.size[0] + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.class}` + radius: Appearance.rounding.windowRounding + } + } + + // Layer regions + Repeater { + model: ScriptModel { + values: root.layerRegions + } + delegate: TargetRegion { + z: 3 + required property var modelData + targeted: !root.draggedAway && + (root.targetedRegionX === modelData.at[0] + && root.targetedRegionY === modelData.at[1] + && root.targetedRegionWidth === modelData.size[0] + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.namespace}` + radius: Appearance.rounding.windowRounding + } + } + + // Image regions + Repeater { + model: ScriptModel { + values: Config.options.screenshotTool.showContentRegions ? root.imageRegions : [] + } + delegate: TargetRegion { + z: 4 + required property var modelData + targeted: !root.draggedAway && + (root.targetedRegionX === modelData.at[0] + && root.targetedRegionY === modelData.at[1] + && root.targetedRegionWidth === modelData.size[0] + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.imageBorderColor + fillColor: targeted ? root.imageFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: "Content region" + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml index 6eecc365e..336c9fd6b 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml @@ -16,83 +16,13 @@ import Quickshell.Hyprland Scope { id: root - property string screenshotDir: Directories.screenshotTemp - property color overlayColor: "#77111111" - property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) - property color genericContentForeground: "#ddffffff" - property color selectionBorderColor: "#ddf1f1f1" - property color selectionFillColor: "#33ffffff" - property color windowBorderColor: "#dda0c0da" - property color windowFillColor: "#22a0c0da" - property color imageBorderColor: "#ddf1d1ff" - property color imageFillColor: "#33f1d1ff" - property color onBorderColor: "#ff000000" - property real standardRounding: 4 - readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { - // Sort floating=true windows before others - if (a.floating === b.floating) return 0; - return a.floating ? -1 : 1; - }) - readonly property var layers: HyprlandData.layers - readonly property real falsePositivePreventionRatio: 0.5 function dismiss() { GlobalStates.regionSelectorOpen = false } - component TargetRegion: Rectangle { - id: regionRect - property bool showIcon: false - property bool targeted: false - property color borderColor - property color fillColor: "transparent" - property string text: "" - property real textPadding: 10 - z: 2 - color: fillColor - border.color: borderColor - border.width: targeted ? 3 : 1 - radius: root.standardRounding - - Rectangle { - id: regionLabelBackground - property real verticalPadding: 5 - property real horizontalPadding: 10 - radius: 10 - color: root.genericContentColor - border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant - anchors { - top: parent.top - left: parent.left - topMargin: regionRect.textPadding - leftMargin: regionRect.textPadding - } - implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2 - implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2 - Row { - id: regionInfoRow - anchors.centerIn: parent - spacing: 4 - - Loader { - id: regionIconLoader - active: regionRect.showIcon - visible: active - sourceComponent: IconImage { - implicitSize: Appearance.font.pixelSize.larger - source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing") - } - } - - StyledText { - id: regionText - text: regionRect.text - color: root.genericContentForeground - } - } - } - } + property var action: RegionSelection.SnipAction.Copy + property var selectionMode: RegionSelection.SelectionMode.RectCorners Variants { model: Quickshell.screens @@ -101,478 +31,24 @@ Scope { required property var modelData active: GlobalStates.regionSelectorOpen - sourceComponent: PanelWindow { - id: panelWindow + sourceComponent: RegionSelection { screen: regionSelectorLoader.modelData - visible: false - WlrLayershell.namespace: "quickshell:regionSelector" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - exclusionMode: ExclusionMode.Ignore - anchors { - left: true - right: true - top: true - bottom: true - } - - readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen) - readonly property real monitorScale: hyprlandMonitor.scale - readonly property real monitorOffsetX: hyprlandMonitor.x - readonly property real monitorOffsetY: hyprlandMonitor.y - property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0 - property string screenshotPath: `${root.screenshotDir}/image-${screen.name}` - property real dragStartX: 0 - property real dragStartY: 0 - property real draggingX: 0 - property real draggingY: 0 - property real dragDiffX: 0 - property real dragDiffY: 0 - property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0) - property bool dragging: false - property var mouseButton: null - property var imageRegions: [] - readonly property list windowRegions: filterWindowRegionsByLayers( - root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId), - panelWindow.layerRegions - ).map(window => { - return { - at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY], - size: [window.size[0], window.size[1]], - class: window.class, - title: window.title, - } - }) - readonly property list layerRegions: { - const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name] - const topLayers = layersOfThisMonitor?.levels["2"] - if (!topLayers) return []; - const nonBarTopLayers = topLayers - .filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock"))) - .map(layer => { - return { - at: [layer.x, layer.y], - size: [layer.w, layer.h], - namespace: layer.namespace, - } - }) - const offsetAdjustedLayers = nonBarTopLayers.map(layer => { - return { - at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY], - size: layer.size, - namespace: layer.namespace, - } - }); - return offsetAdjustedLayers; - } - - property real targetedRegionX: -1 - property real targetedRegionY: -1 - property real targetedRegionWidth: 0 - property real targetedRegionHeight: 0 - - function intersectionOverUnion(regionA, regionB) { - // region: { at: [x, y], size: [w, h] } - const ax1 = regionA.at[0], ay1 = regionA.at[1]; - const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1]; - const bx1 = regionB.at[0], by1 = regionB.at[1]; - const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1]; - - const interX1 = Math.max(ax1, bx1); - const interY1 = Math.max(ay1, by1); - const interX2 = Math.min(ax2, bx2); - const interY2 = Math.min(ay2, by2); - - const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1); - const areaA = (ax2 - ax1) * (ay2 - ay1); - const areaB = (bx2 - bx1) * (by2 - by1); - const unionArea = areaA + areaB - interArea; - - return unionArea > 0 ? interArea / unionArea : 0; - } - - function filterOverlappingImageRegions(regions) { - let keep = []; - let removed = new Set(); - for (let i = 0; i < regions.length; ++i) { - if (removed.has(i)) continue; - let regionA = regions[i]; - for (let j = i + 1; j < regions.length; ++j) { - if (removed.has(j)) continue; - let regionB = regions[j]; - if (intersectionOverUnion(regionA, regionB) > 0) { - // Compare areas - let areaA = regionA.size[0] * regionA.size[1]; - let areaB = regionB.size[0] * regionB.size[1]; - if (areaA <= areaB) { - removed.add(j); - } else { - removed.add(i); - } - } - } - } - for (let i = 0; i < regions.length; ++i) { - if (!removed.has(i)) keep.push(regions[i]); - } - return keep; - } - - function filterWindowRegionsByLayers(windowRegions, layerRegions) { - return windowRegions.filter(windowRegion => { - for (let i = 0; i < layerRegions.length; ++i) { - if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0) - return false; - } - return true; - }); - } - - function filterImageRegions(regions, windowRegions, threshold = 0.1) { - // Remove image regions that overlap too much with any window region - let filtered = regions.filter(region => { - for (let i = 0; i < windowRegions.length; ++i) { - if (intersectionOverUnion(region, windowRegions[i]) > threshold) - return false; - } - return true; - }); - // Remove overlapping image regions, keep only the smaller one - return filterOverlappingImageRegions(filtered); - } - - function updateTargetedRegion(x, y) { - // Image regions - const clickedRegion = panelWindow.imageRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedRegion) { - panelWindow.targetedRegionX = clickedRegion.at[0]; - panelWindow.targetedRegionY = clickedRegion.at[1]; - panelWindow.targetedRegionWidth = clickedRegion.size[0]; - panelWindow.targetedRegionHeight = clickedRegion.size[1]; - return; - } - - // Layer regions - const clickedLayer = panelWindow.layerRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedLayer) { - panelWindow.targetedRegionX = clickedLayer.at[0]; - panelWindow.targetedRegionY = clickedLayer.at[1]; - panelWindow.targetedRegionWidth = clickedLayer.size[0]; - panelWindow.targetedRegionHeight = clickedLayer.size[1]; - return; - } - - // Window regions - const clickedWindow = panelWindow.windowRegions.find(region => { - return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; - }); - if (clickedWindow) { - panelWindow.targetedRegionX = clickedWindow.at[0]; - panelWindow.targetedRegionY = clickedWindow.at[1]; - panelWindow.targetedRegionWidth = clickedWindow.size[0]; - panelWindow.targetedRegionHeight = clickedWindow.size[1]; - return; - } - - panelWindow.targetedRegionX = -1; - panelWindow.targetedRegionY = -1; - panelWindow.targetedRegionWidth = 0; - panelWindow.targetedRegionHeight = 0; - } - - property real regionWidth: Math.abs(draggingX - dragStartX) - property real regionHeight: Math.abs(draggingY - dragStartY) - property real regionX: Math.min(dragStartX, draggingX) - property real regionY: Math.min(dragStartY, draggingY) - - Process { - id: screenshotProcess - running: true - command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(panelWindow.screen.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`] - onExited: (exitCode, exitStatus) => { - panelWindow.visible = true; - imageDetectionProcess.running = true; - } - } - - Process { - id: imageDetectionProcess - command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh ` - + `--hyprctl ` - + `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' ` - + `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} ` - + `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `] - stdout: StdioCollector { - id: imageDimensionCollector - onStreamFinished: { - imageRegions = filterImageRegions( - JSON.parse(imageDimensionCollector.text), - panelWindow.windowRegions - ); - } - } - } - - Process { - id: snipProc - function snip() { - if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) { - console.warn("Invalid region size, skipping snip."); - root.dismiss(); - } - snipProc.startDetached(); - root.dismiss(); - } - command: ["bash", "-c", - `magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} ` - + `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - ` - + `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`] - } - - ScreencopyView { - anchors.fill: parent - live: false - captureSource: panelWindow.screen - - focus: panelWindow.visible - Keys.onPressed: (event) => { // Esc to close - if (event.key === Qt.Key_Escape) { - root.dismiss(); - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.CrossCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - - // Controls - onPressed: mouse => { - panelWindow.dragStartX = mouse.x; - panelWindow.dragStartY = mouse.y; - panelWindow.draggingX = mouse.x; - panelWindow.draggingY = mouse.y; - panelWindow.dragging = true; - panelWindow.mouseButton = mouse.button; - } - onReleased: mouse => { - // Detect if it was a click - - // Image regions - if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) { - if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) { - panelWindow.regionX = panelWindow.targetedRegionX; - panelWindow.regionY = panelWindow.targetedRegionY; - panelWindow.regionWidth = panelWindow.targetedRegionWidth; - panelWindow.regionHeight = panelWindow.targetedRegionHeight; - } - } - snipProc.snip(); - } - onPositionChanged: mouse => { - if (panelWindow.dragging) { - panelWindow.draggingX = mouse.x; - panelWindow.draggingY = mouse.y; - panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX; - panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY; - } - panelWindow.updateTargetedRegion(mouse.x, mouse.y); - } - - // Overlay to darken screen - Rectangle { // Base - id: darkenOverlay - z: 1 - anchors { - left: parent.left - top: parent.top - leftMargin: panelWindow.regionX - darkenOverlay.border.width - topMargin: panelWindow.regionY - darkenOverlay.border.width - } - width: panelWindow.regionWidth + darkenOverlay.border.width * 2 - height: panelWindow.regionHeight + darkenOverlay.border.width * 2 - color: "transparent" - // border.color: root.selectionBorderColor - border.color: root.overlayColor - border.width: Math.max(panelWindow.width, panelWindow.height) - radius: root.standardRounding - } - Rectangle { - id: selectionBorder - z: 1 - anchors { - left: parent.left - top: parent.top - leftMargin: panelWindow.regionX - topMargin: panelWindow.regionY - } - width: panelWindow.regionWidth - height: panelWindow.regionHeight - color: "transparent" - border.color: root.selectionBorderColor - border.width: 2 - // radius: root.standardRounding - radius: 0 // TODO: figure out how to make the overlay thing work with rounding - } - StyledText { - z: 2 - anchors { - top: selectionBorder.bottom - right: selectionBorder.right - margins: 8 - } - color: root.selectionBorderColor - text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}` - } - - // Instructions - Rectangle { - z: 9999 - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2 - } - - opacity: panelWindow.dragging ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - color: root.genericContentColor - radius: 10 - border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant - implicitWidth: instructionsRow.implicitWidth + 10 * 2 - implicitHeight: instructionsRow.implicitHeight + 5 * 2 - - Row { - id: instructionsRow - anchors.centerIn: parent - spacing: 4 - MaterialSymbol { - id: screenshotRegionIcon - // anchors.centerIn: parent - iconSize: Appearance.font.pixelSize.larger - text: "screenshot_region" - color: root.genericContentForeground - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit") - color: root.genericContentForeground - } - } - } - - // Window regions - Repeater { - model: ScriptModel { - values: panelWindow.windowRegions - } - delegate: TargetRegion { - z: 2 - required property var modelData - showIcon: true - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: `${modelData.class}` - radius: Appearance.rounding.windowRounding - } - } - - // Layer regions - Repeater { - model: ScriptModel { - values: panelWindow.layerRegions - } - delegate: TargetRegion { - z: 3 - required property var modelData - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: `${modelData.namespace}` - radius: Appearance.rounding.windowRounding - } - } - - // Image regions - Repeater { - model: ScriptModel { - values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : [] - } - delegate: TargetRegion { - z: 4 - required property var modelData - targeted: !panelWindow.draggedAway && - (panelWindow.targetedRegionX === modelData.at[0] - && panelWindow.targetedRegionY === modelData.at[1] - && panelWindow.targetedRegionWidth === modelData.size[0] - && panelWindow.targetedRegionHeight === modelData.size[1]) - - opacity: panelWindow.draggedAway ? 0 : 1 - visible: opacity > 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - x: modelData.at[0] - y: modelData.at[1] - width: modelData.size[0] - height: modelData.size[1] - borderColor: root.imageBorderColor - fillColor: targeted ? root.imageFillColor : "transparent" - border.width: targeted ? 4 : 2 - text: "Content region" - } - } - } - } + onDismiss: root.dismiss() + action: root.action + selectionMode: root.selectionMode } } } function screenshot() { + root.action = RegionSelection.SnipAction.Copy + root.selectionMode = RegionSelection.SelectionMode.RectCorners + GlobalStates.regionSelectorOpen = true + } + + function search() { + root.action = RegionSelection.SnipAction.Search + root.selectionMode = RegionSelection.SelectionMode.Circle GlobalStates.regionSelectorOpen = true } @@ -582,14 +58,19 @@ Scope { function screenshot() { root.screenshot() } + function search() { + root.search() + } } GlobalShortcut { name: "regionScreenshot" description: "Takes a screenshot of the selected region" - - onPressed: { - root.screenshot() - } + onPressed: root.screenshot() + } + GlobalShortcut { + name: "regionSearch" + description: "Searches the selected region" + onPressed: root.search() } } diff --git a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml new file mode 100644 index 000000000..0fc2774fb --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Hyprland + +Rectangle { + id: regionRect + property bool showIcon: false + property bool targeted: false + property color borderColor + property color fillColor: "transparent" + property string text: "" + property real textPadding: 10 + z: 2 + color: fillColor + border.color: borderColor + border.width: targeted ? 3 : 1 + radius: root.standardRounding + + Rectangle { + id: regionLabelBackground + property real verticalPadding: 5 + property real horizontalPadding: 10 + radius: 10 + color: root.genericContentColor + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + anchors { + top: parent.top + left: parent.left + topMargin: regionRect.textPadding + leftMargin: regionRect.textPadding + } + implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2 + implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2 + Row { + id: regionInfoRow + anchors.centerIn: parent + spacing: 4 + + Loader { + id: regionIconLoader + active: regionRect.showIcon + visible: active + sourceComponent: IconImage { + implicitSize: Appearance.font.pixelSize.larger + source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing") + } + } + + StyledText { + id: regionText + text: regionRect.text + color: root.genericContentForeground + } + } + } +} \ No newline at end of file From cc605e24d93a3a23996f975658b5859e229dd40b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:09:27 +0200 Subject: [PATCH 21/32] region selector: fix target regions --- .../ii/modules/regionSelector/RegionSelection.qml | 9 +++++++-- .../ii/modules/regionSelector/TargetRegion.qml | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml index 32549fbc0..7f83c4e33 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -48,7 +48,6 @@ PanelWindow { property color imageBorderColor: "#ddf1d1ff" property color imageFillColor: "#33f1d1ff" property color onBorderColor: "#ff000000" - property real standardRounding: 4 readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { // Sort floating=true windows before others if (a.floating === b.floating) return 0; @@ -357,13 +356,13 @@ PanelWindow { root.snip(); } onPositionChanged: (mouse) => { + root.updateTargetedRegion(mouse.x, mouse.y); if (!root.dragging) return; root.draggingX = mouse.x; root.draggingY = mouse.y; root.dragDiffX = mouse.x - root.dragStartX; root.dragDiffY = mouse.y - root.dragStartY; root.points.push({ x: mouse.x, y: mouse.y }); - root.updateTargetedRegion(mouse.x, mouse.y); } Loader { @@ -478,6 +477,8 @@ PanelWindow { && root.targetedRegionWidth === modelData.size[0] && root.targetedRegionHeight === modelData.size[1]) + colBackground: root.genericContentColor + colForeground: root.genericContentForeground opacity: root.draggedAway ? 0 : 1 visible: opacity > 0 Behavior on opacity { @@ -510,6 +511,8 @@ PanelWindow { && root.targetedRegionWidth === modelData.size[0] && root.targetedRegionHeight === modelData.size[1]) + colBackground: root.genericContentColor + colForeground: root.genericContentForeground opacity: root.draggedAway ? 0 : 1 visible: opacity > 0 Behavior on opacity { @@ -542,6 +545,8 @@ PanelWindow { && root.targetedRegionWidth === modelData.size[0] && root.targetedRegionHeight === modelData.size[1]) + colBackground: root.genericContentColor + colForeground: root.genericContentForeground opacity: root.draggedAway ? 0 : 1 visible: opacity > 0 Behavior on opacity { diff --git a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml index 0fc2774fb..49a755ab0 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml @@ -16,6 +16,8 @@ import Quickshell.Hyprland Rectangle { id: regionRect + required property color colBackground + required property color colForeground property bool showIcon: false property bool targeted: false property color borderColor @@ -26,14 +28,14 @@ Rectangle { color: fillColor border.color: borderColor border.width: targeted ? 3 : 1 - radius: root.standardRounding + radius: 4 Rectangle { id: regionLabelBackground property real verticalPadding: 5 property real horizontalPadding: 10 radius: 10 - color: root.genericContentColor + color: regionRect.colBackground border.width: 1 border.color: Appearance.m3colors.m3outlineVariant anchors { @@ -62,7 +64,7 @@ Rectangle { StyledText { id: regionText text: regionRect.text - color: root.genericContentForeground + color: regionRect.colForeground } } } From 075a21a9db24969830fd6e0ff7b3c6d7a3495ebc Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:00:08 +0200 Subject: [PATCH 22/32] make google lens tool use normal region selection by default --- dots/.config/hypr/hyprland/keybinds.conf | 2 +- .../quickshell/ii/modules/common/Config.qml | 7 +++++-- .../ii/modules/regionSelector/RegionSelection.qml | 8 ++++---- .../ii/modules/regionSelector/RegionSelector.qml | 6 +++++- .../ii/modules/settings/ServicesConfig.qml | 14 ++++++++++++++ 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 74343d85b..469555874 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -60,7 +60,7 @@ bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call T bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback) -bind = Super+Shift, A, global, quickshell:regionSearch # Circle to Search +bind = Super+Shift, A, global, quickshell:regionSearch # Google Lens # OCR bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] # Color picker diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index f93171584..8f18bd0f6 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -355,8 +355,6 @@ Singleton { property JsonObject search: JsonObject { property int nonAppResultDelay: 30 // This prevents lagging when typing property string engineBaseUrl: "https://www.google.com/search?q=" - property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url=" - property string fileUploadApiEndpoint: "https://uguu.se/upload" property list excludedSites: ["quora.com", "facebook.com"] property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird. property JsonObject prefix: JsonObject { @@ -369,6 +367,11 @@ Singleton { property string shellCommand: "$" property string webSearch: "?" } + property JsonObject imageSearch: JsonObject { + property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url=" + property string fileUploadApiEndpoint: "https://uguu.se/upload" + property bool useCircleSelection: false + } } property JsonObject sidebar: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml index 7f83c4e33..b22ce4a58 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -36,8 +36,8 @@ PanelWindow { signal dismiss() property string screenshotDir: Directories.screenshotTemp - property string imageSearchEngineBaseUrl: Config.options.search.imageSearchEngineBaseUrl - property string fileUploadApiEndpoint: Config.options.search.fileUploadApiEndpoint + property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl + property string fileUploadApiEndpoint: Config.options.search.imageSearch.fileUploadApiEndpoint property color overlayColor: "#77111111" property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) property color genericContentForeground: "#ddffffff" @@ -424,13 +424,13 @@ PanelWindow { iconSize: Appearance.font.pixelSize.larger text: switch(root.selectionMode) { case RegionSelection.SelectionMode.RectCorners: - return "crop_free" + return "activity_zone" break; case RegionSelection.SelectionMode.Circle: return "gesture" break; default: - return "crop_free" + return "activity_zone" } color: root.genericContentForeground } diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml index 336c9fd6b..7cfd37349 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml @@ -48,7 +48,11 @@ Scope { function search() { root.action = RegionSelection.SnipAction.Search - root.selectionMode = RegionSelection.SelectionMode.Circle + if (Config.options.search.imageSearch.useCircleSelection) { + root.selectionMode = RegionSelection.SelectionMode.Circle + } else { + root.selectionMode = RegionSelection.SelectionMode.RectCorners + } GlobalStates.regionSelectorOpen = true } diff --git a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml index fae5bf4de..8b6c1ff2a 100644 --- a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -147,5 +147,19 @@ ContentPage { } } } + ContentSubsection { + title: Translation.tr("Google Lens") + + ConfigSelectionArray { + currentValue: Config.options.search.imageSearch.useCircleSelection ? "circle" : "rectangles" + onSelected: newValue => { + Config.options.search.imageSearch.useCircleSelection = (newValue === "circle"); + } + options: [ + { icon: "activity_zone", value: "rectangles", displayName: Translation.tr("Rectangular selection") }, + { icon: "gesture", value: "circle", displayName: Translation.tr("Circle to Search") } + ] + } + } } } From 54fe878580b4696a733d933b8fc0eac02696d978 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:25:23 +0200 Subject: [PATCH 23/32] right sidebar: containerize top row elements --- .../sidebarRight/SidebarRightContent.qml | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 1eee4d335..239377142 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -18,7 +18,7 @@ import "./volumeMixer/" Item { id: root property int sidebarWidth: Appearance.sizes.sidebarWidth - property int sidebarPadding: 12 + property int sidebarPadding: 10 property string settingsQmlPath: Quickshell.shellPath("settings.qml") property bool showWifiDialog: false property bool showBluetoothDialog: false @@ -62,7 +62,8 @@ Item { SystemButtonRow { Layout.fillHeight: false - Layout.margins: 10 + Layout.fillWidth: true + // Layout.margins: 10 Layout.topMargin: 5 Layout.bottomMargin: 0 } @@ -200,30 +201,54 @@ Item { } } - component SystemButtonRow: RowLayout { - spacing: 10 + component SystemButtonRow: Item { + implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight) - CustomIcon { - id: distroIcon - width: 25 - height: 25 - source: SystemInfo.distroIcon - colorize: true - color: Appearance.colors.colOnLayer0 - } - - StyledText { - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colOnLayer0 - text: Translation.tr("Up %1").arg(DateTime.uptime) - textFormat: Text.MarkdownText - } - - Item { - Layout.fillWidth: true + Rectangle { + id: uptimeContainer + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + color: Appearance.colors.colLayer1 + radius: height / 2 + implicitWidth: uptimeRow.implicitWidth + 24 + implicitHeight: uptimeRow.implicitHeight + 8 + + Row { + id: uptimeRow + anchors.centerIn: parent + spacing: 8 + CustomIcon { + id: distroIcon + anchors.verticalCenter: parent.verticalCenter + width: 25 + height: 25 + source: SystemInfo.distroIcon + colorize: true + color: Appearance.colors.colOnLayer0 + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer0 + text: Translation.tr("Up %1").arg(DateTime.uptime) + textFormat: Text.MarkdownText + } + } } ButtonGroup { + id: systemButtonsRow + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + } + color: Appearance.colors.colLayer1 + padding: 4 + QuickToggleButton { toggled: root.editMode visible: Config.options.sidebar.quickToggles.style === "android" From d5b1e9f40c14c632264b1406eab0d3ceff73e6b2 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:56:33 +0200 Subject: [PATCH 24/32] sidebar: quick toggles: smoother size change --- .../ii/modules/common/widgets/GroupButton.qml | 4 ++++ .../quickToggles/AndroidQuickPanel.qml | 17 ++++++++++------- .../androidStyle/AndroidQuickToggleButton.qml | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml index bc02e77e1..6ebe3341f 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml @@ -22,6 +22,8 @@ Button { property bool bounce: true property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2 property real baseHeight: contentItem.implicitHeight + verticalPadding * 2 + property bool enableImplicitWidthAnimation: true + property bool enableImplicitHeightAnimation: true property real clickedWidth: baseWidth + (isAtSide ? 10 : 20) property real clickedHeight: baseHeight property var parentGroup: root.parent @@ -61,10 +63,12 @@ Button { } Behavior on implicitWidth { + enabled: root.enableImplicitWidthAnimation animation: Appearance.animation.clickBounce.numberAnimation.createObject(this) } Behavior on implicitHeight { + enabled: root.enableImplicitHeightAnimation animation: Appearance.animation.clickBounce.numberAnimation.createObject(this) } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml index 5aa95ad35..eb7793fba 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml @@ -73,14 +73,14 @@ AbstractQuickPanel { Repeater { id: usedRowsRepeater model: ScriptModel { - values: root.toggleRows + values: Array(root.toggleRows.length) } delegate: ButtonGroup { id: toggleRow - required property var modelData required property int index + property var modelData: root.toggleRows[index] property int startingIndex: { - const rows = usedRowsRepeater.model.values; + const rows = root.toggleRows; let sum = 0; for (let i = 0; i < index; i++) { sum += rows[i].length; @@ -91,7 +91,8 @@ AbstractQuickPanel { Repeater { model: ScriptModel { - values: toggleRow.modelData + values: toggleRow?.modelData ?? [] + objectProp: "type" } delegate: AndroidToggleDelegateChooser { startingIndex: toggleRow.startingIndex @@ -131,16 +132,18 @@ AbstractQuickPanel { Repeater { model: ScriptModel { - values: root.unusedToggleRows + values: Array(root.unusedToggleRows.length) } delegate: ButtonGroup { id: unusedToggleRow - required property var modelData + required property int index + property var modelData: root.unusedToggleRows[index] spacing: root.spacing Repeater { model: ScriptModel { - values: unusedToggleRow.modelData + values: unusedToggleRow?.modelData ?? [] + objectProp: "type" } delegate: AndroidToggleDelegateChooser { startingIndex: -1 diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml index ee9d58f45..e7f6929e1 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml @@ -23,6 +23,21 @@ GroupButton { baseHeight: root.baseCellHeight property bool editMode: false + enableImplicitWidthAnimation: !editMode + enableImplicitHeightAnimation: !editMode + Behavior on baseWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + Behavior on baseHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + opacity: 0 + Component.onCompleted: { + opacity = 1 + } + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } signal openMenu() From fe3e5de51845e2c0c04ec5fdbbcc4eaaa1861d7f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:12:58 +0200 Subject: [PATCH 25/32] sidebar: quick toggles: larger icon size when 1 cell wide --- .../quickToggles/androidStyle/AndroidQuickToggleButton.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml index e7f6929e1..18848c21d 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml @@ -80,7 +80,7 @@ GroupButton { MaterialSymbol { anchors.centerIn: parent fill: root.toggled ? 1 : 0 - iconSize: Appearance.font.pixelSize.huge + iconSize: root.expandedSize ? 22 : 24 color: root.colIcon text: root.buttonIcon } From 854edba825df256d9fd4c9f7e621dfc67fa97665 Mon Sep 17 00:00:00 2001 From: jwihardi Date: Mon, 20 Oct 2025 18:42:59 -0400 Subject: [PATCH 26/32] added live quickshell ebuild with specific commit --- .../quickshell-9999.ebuild | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild diff --git a/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild b/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild new file mode 100644 index 000000000..c4b653bb3 --- /dev/null +++ b/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild @@ -0,0 +1,91 @@ +# Copyright 1999-2025 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit cmake + +DESCRIPTION="Toolkit for building desktop widgets using QtQuick" +HOMEPAGE="https://quickshell.org/" + +if [[ "${PV}" = *9999 ]]; then + inherit git-r3 + EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git" + EGIT_COMMIT="00858812f25b748d08b075a0d284093685fa3ffd" +else + SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" + KEYWORDS="~amd64" +fi + +LICENSE="LGPL-3" +SLOT="0" +# Upstream recommends leaving all build options enabled by default +IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth" + +RDEPEND=" + dev-qt/qtbase:6 + dev-qt/qtsvg:6 + jemalloc? ( dev-libs/jemalloc ) + wayland? ( + dev-libs/wayland + dev-qt/qtwayland:6 + ) + screencopy? ( + x11-libs/libdrm + media-libs/mesa + ) + X? ( x11-libs/libxcb ) + pipewire? ( media-video/pipewire ) + mpris? ( dev-qt/qtdbus ) + pam? ( sys-libs/pam ) + bluetooth? ( net-wireless/bluez ) + + + +" +DEPEND="${RDEPEND}" +BDEPEND=" + || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) + + dev-util/spirv-tools + dev-qt/qtshadertools:6 + wayland? ( + dev-util/wayland-scanner + dev-libs/wayland-protocols + ) + dev-cpp/cli11 + dev-build/ninja + dev-build/cmake + dev-vcs/git + virtual/pkgconfig + breakpad? ( dev-util/breakpad ) + +" + +src_configure(){ + mycmakeargs=( + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DDISTRIBUTOR="Gentoo GURU" + -DINSTALL_QML_PREFIX="lib64/qt6/qml" + -DCRASH_REPORTER=$(usex breakpad ON OFF) + -DUSE_JEMALLOC=$(usex jemalloc ON OFF) + -DSOCKETS=$(usex sockets ON OFF) + -DWAYLAND=$(usex wayland ON OFF) + -DWAYLAND_WLR_LAYERSHELL=$(usex layer-shell ON OFF) + -DWAYLAND_SESSION_LOCK=$(usex session-lock ON OFF) + -DWAYLAND_TOPLEVEL_MANAGEMENT=$(usex toplevel-management ON OFF) + -DSCREENCOPY=$(usex screencopy ON OFF) + -DX11=$(usex X ON OFF) + -DSERVICE_PIPEWIRE=$(usex pipewire ON OFF) + -DSERVICE_STATUS_NOTIFIER=$(usex tray ON OFF) + -DSERVICE_MPRIS=$(usex mpris ON OFF) + -DSERVICE_PAM=$(usex pam ON OFF) + -DHYPRLAND=$(usex hyprland ON OFF) + -DHYPRLAND_GLOBAL_SHORTCUTS=$(usex hyprland-global-shortcuts) + -DHYPRLAND_FOCUS_GRAB=$(usex hyprland-focus-grab) + -DI3=$(usex i3 ON OFF) + -DI3_IPC=$(usex i3-ipc ON OFF) + -DBLUETOOTH=$(usex bluetooth ON OFF) + ) + cmake_src_configure +} From bf96099e4696f8b3aa3764a071e55abfed62a439 Mon Sep 17 00:00:00 2001 From: jwihardi Date: Mon, 20 Oct 2025 19:14:38 -0400 Subject: [PATCH 27/32] fixed package name --- ...logical-impulse-quickshell-git-9999.ebuild | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild diff --git a/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild b/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild new file mode 100644 index 000000000..d5106355a --- /dev/null +++ b/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild @@ -0,0 +1,86 @@ +# Copyright 1999-2025 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit cmake git-r3 + +DESCRIPTION="Toolkit for building desktop widgets using QtQuick" +HOMEPAGE="https://quickshell.org/" + +EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git" +EGIT_COMMIT="00858812f25b748d08b075a0d284093685fa3ffd" + +KEYWORDS="~amd64 ~arm64 ~x86" +LICENSE="LGPL-3" +SLOT="0" +# Upstream recommends leaving all build options enabled by default +IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth" + +RDEPEND=" + dev-qt/qtbase:6 + dev-qt/qtsvg:6 + jemalloc? ( dev-libs/jemalloc ) + wayland? ( + dev-libs/wayland + dev-qt/qtwayland:6 + ) + screencopy? ( + x11-libs/libdrm + media-libs/mesa + ) + X? ( x11-libs/libxcb ) + pipewire? ( media-video/pipewire ) + mpris? ( dev-qt/qtdbus ) + pam? ( sys-libs/pam ) + bluetooth? ( net-wireless/bluez ) + + + +" +DEPEND="${RDEPEND}" +BDEPEND=" + || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) + + dev-util/spirv-tools + dev-qt/qtshadertools:6 + wayland? ( + dev-util/wayland-scanner + dev-libs/wayland-protocols + ) + dev-cpp/cli11 + dev-build/ninja + dev-build/cmake + dev-vcs/git + virtual/pkgconfig + breakpad? ( dev-util/breakpad ) + +" + +src_configure(){ + mycmakeargs=( + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DDISTRIBUTOR="Gentoo GURU" + -DINSTALL_QML_PREFIX="lib64/qt6/qml" + -DCRASH_REPORTER=$(usex breakpad ON OFF) + -DUSE_JEMALLOC=$(usex jemalloc ON OFF) + -DSOCKETS=$(usex sockets ON OFF) + -DWAYLAND=$(usex wayland ON OFF) + -DWAYLAND_WLR_LAYERSHELL=$(usex layer-shell ON OFF) + -DWAYLAND_SESSION_LOCK=$(usex session-lock ON OFF) + -DWAYLAND_TOPLEVEL_MANAGEMENT=$(usex toplevel-management ON OFF) + -DSCREENCOPY=$(usex screencopy ON OFF) + -DX11=$(usex X ON OFF) + -DSERVICE_PIPEWIRE=$(usex pipewire ON OFF) + -DSERVICE_STATUS_NOTIFIER=$(usex tray ON OFF) + -DSERVICE_MPRIS=$(usex mpris ON OFF) + -DSERVICE_PAM=$(usex pam ON OFF) + -DHYPRLAND=$(usex hyprland ON OFF) + -DHYPRLAND_GLOBAL_SHORTCUTS=$(usex hyprland-global-shortcuts) + -DHYPRLAND_FOCUS_GRAB=$(usex hyprland-focus-grab) + -DI3=$(usex i3 ON OFF) + -DI3_IPC=$(usex i3-ipc ON OFF) + -DBLUETOOTH=$(usex bluetooth ON OFF) + ) + cmake_src_configure +} From 4de08c438b08db6846376d6c436f623407930223 Mon Sep 17 00:00:00 2001 From: jwihardi <84292598+jwihardi@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:16:27 -0400 Subject: [PATCH 28/32] Delete sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild --- .../quickshell-9999.ebuild | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild diff --git a/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild b/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild deleted file mode 100644 index c4b653bb3..000000000 --- a/sdist/gentoo/illogical-impulse-quickshell-git/quickshell-9999.ebuild +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 1999-2025 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 - -EAPI=8 - -inherit cmake - -DESCRIPTION="Toolkit for building desktop widgets using QtQuick" -HOMEPAGE="https://quickshell.org/" - -if [[ "${PV}" = *9999 ]]; then - inherit git-r3 - EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git" - EGIT_COMMIT="00858812f25b748d08b075a0d284093685fa3ffd" -else - SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" - KEYWORDS="~amd64" -fi - -LICENSE="LGPL-3" -SLOT="0" -# Upstream recommends leaving all build options enabled by default -IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth" - -RDEPEND=" - dev-qt/qtbase:6 - dev-qt/qtsvg:6 - jemalloc? ( dev-libs/jemalloc ) - wayland? ( - dev-libs/wayland - dev-qt/qtwayland:6 - ) - screencopy? ( - x11-libs/libdrm - media-libs/mesa - ) - X? ( x11-libs/libxcb ) - pipewire? ( media-video/pipewire ) - mpris? ( dev-qt/qtdbus ) - pam? ( sys-libs/pam ) - bluetooth? ( net-wireless/bluez ) - - - -" -DEPEND="${RDEPEND}" -BDEPEND=" - || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) - - dev-util/spirv-tools - dev-qt/qtshadertools:6 - wayland? ( - dev-util/wayland-scanner - dev-libs/wayland-protocols - ) - dev-cpp/cli11 - dev-build/ninja - dev-build/cmake - dev-vcs/git - virtual/pkgconfig - breakpad? ( dev-util/breakpad ) - -" - -src_configure(){ - mycmakeargs=( - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DDISTRIBUTOR="Gentoo GURU" - -DINSTALL_QML_PREFIX="lib64/qt6/qml" - -DCRASH_REPORTER=$(usex breakpad ON OFF) - -DUSE_JEMALLOC=$(usex jemalloc ON OFF) - -DSOCKETS=$(usex sockets ON OFF) - -DWAYLAND=$(usex wayland ON OFF) - -DWAYLAND_WLR_LAYERSHELL=$(usex layer-shell ON OFF) - -DWAYLAND_SESSION_LOCK=$(usex session-lock ON OFF) - -DWAYLAND_TOPLEVEL_MANAGEMENT=$(usex toplevel-management ON OFF) - -DSCREENCOPY=$(usex screencopy ON OFF) - -DX11=$(usex X ON OFF) - -DSERVICE_PIPEWIRE=$(usex pipewire ON OFF) - -DSERVICE_STATUS_NOTIFIER=$(usex tray ON OFF) - -DSERVICE_MPRIS=$(usex mpris ON OFF) - -DSERVICE_PAM=$(usex pam ON OFF) - -DHYPRLAND=$(usex hyprland ON OFF) - -DHYPRLAND_GLOBAL_SHORTCUTS=$(usex hyprland-global-shortcuts) - -DHYPRLAND_FOCUS_GRAB=$(usex hyprland-focus-grab) - -DI3=$(usex i3 ON OFF) - -DI3_IPC=$(usex i3-ipc ON OFF) - -DBLUETOOTH=$(usex bluetooth ON OFF) - ) - cmake_src_configure -} From 9242b93558ae45d08d87b9cd5d42ef1a5b562faf Mon Sep 17 00:00:00 2001 From: jwihardi Date: Mon, 20 Oct 2025 20:35:43 -0400 Subject: [PATCH 29/32] quickshell, widgets, install, useflag (breakpad), keywords - updated --- ...cal-impulse-quickshell-git-9999-r1.ebuild} | 0 ...> illogical-impulse-widgets-1.0-r2.ebuild} | 7 +- .../quickshell-9999.ebuild | 84 ------------------- sdist/gentoo/install-deps.sh | 2 +- sdist/gentoo/keywords | 4 + sdist/gentoo/useflags | 2 +- 6 files changed, 11 insertions(+), 88 deletions(-) rename sdist/gentoo/illogical-impulse-quickshell-git/{illogical-impulse-quickshell-git-9999.ebuild => illogical-impulse-quickshell-git-9999-r1.ebuild} (100%) rename sdist/gentoo/illogical-impulse-widgets/{illogical-impulse-widgets-1.0-r1.ebuild => illogical-impulse-widgets-1.0-r2.ebuild} (82%) delete mode 100644 sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild diff --git a/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild b/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999-r1.ebuild similarity index 100% rename from sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999.ebuild rename to sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999-r1.ebuild diff --git a/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild b/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild similarity index 82% rename from sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild rename to sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild index e2b878a74..fe6109223 100644 --- a/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild +++ b/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild @@ -15,8 +15,11 @@ DEPEND="" RDEPEND=" gui-apps/fuzzel dev-libs/glib - gui-apps/quickshell + media-gfx/imagemagick + gui-apps/hypridle + gui-libs/hyprutils + gui-apps/hyprlock + gui-apps/hyprpicker app-i18n/translate-shell gui-apps/wlogout - media-gfx/imagemagick " diff --git a/sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild b/sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild deleted file mode 100644 index 89a092516..000000000 --- a/sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 1999-2025 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 - -EAPI=8 - -inherit cmake - -DESCRIPTION="Toolkit for building desktop widgets using QtQuick" -HOMEPAGE="https://quickshell.org/" - -if [[ "${PV}" = *9999 ]]; then - inherit git-r3 - EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git" -else - SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" -fi - -LICENSE="LGPL-3" -SLOT="0" -KEYWORDS="~amd64 ~arm64 ~x86" -# Upstream recommends leaving all build options enabled by default -IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth" - -RDEPEND=" - dev-qt/qtbase:6 - dev-qt/qtsvg:6 - jemalloc? ( dev-libs/jemalloc ) - wayland? ( - dev-libs/wayland - dev-qt/qtwayland:6 - ) - screencopy? ( - x11-libs/libdrm - media-libs/mesa - ) - X? ( x11-libs/libxcb ) - pipewire? ( media-video/pipewire ) - mpris? ( dev-qt/qtdbus ) - pam? ( sys-libs/pam ) - bluetooth? ( net-wireless/bluez ) -" -DEPEND="${RDEPEND}" -BDEPEND=" - || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) - dev-build/cmake - dev-build/ninja - virtual/pkgconfig - dev-cpp/cli11 - dev-util/spirv-tools - dev-qt/qtshadertools:6 - breakpad? ( dev-util/breakpad ) - wayland? ( - dev-util/wayland-scanner - dev-libs/wayland-protocols - ) -" - -src_configure(){ - mycmakeargs=( - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DDISTRIBUTOR="Gentoo GURU" - -DINSTALL_QML_PREFIX="lib64/qt6/qml" - -DCRASH_REPORTER=$(usex breakpad ON OFF) - -DUSE_JEMALLOC=$(usex jemalloc ON OFF) - -DSOCKETS=$(usex sockets ON OFF) - -DWAYLAND=$(usex wayland ON OFF) - -DWAYLAND_WLR_LAYERSHELL=$(usex layer-shell ON OFF) - -DWAYLAND_SESSION_LOCK=$(usex session-lock ON OFF) - -DWAYLAND_TOPLEVEL_MANAGEMENT=$(usex toplevel-management ON OFF) - -DSCREENCOPY=$(usex screencopy ON OFF) - -DX11=$(usex X ON OFF) - -DSERVICE_PIPEWIRE=$(usex pipewire ON OFF) - -DSERVICE_STATUS_NOTIFIER=$(usex tray ON OFF) - -DSERVICE_MPRIS=$(usex mpris ON OFF) - -DSERVICE_PAM=$(usex pam ON OFF) - -DHYPRLAND=$(usex hyprland ON OFF) - -DHYPRLAND_GLOBAL_SHORTCUTS=$(usex hyprland-global-shortcuts) - -DHYPRLAND_FOCUS_GRAB=$(usex hyprland-focus-grab) - -DI3=$(usex i3 ON OFF) - -DI3_IPC=$(usex i3-ipc ON OFF) - -DBLUETOOTH=$(usex bluetooth ON OFF) - ) - cmake_src_configure -} diff --git a/sdist/gentoo/install-deps.sh b/sdist/gentoo/install-deps.sh index 949bbf8eb..7be8f30a9 100644 --- a/sdist/gentoo/install-deps.sh +++ b/sdist/gentoo/install-deps.sh @@ -37,7 +37,7 @@ fi arch=$(portageq envvar ACCEPT_KEYWORDS) # Exclude hyprland, will deal with that separately -metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets}) +metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,quickshell-git,screencapture,toolkit,widgets}) ebuild_dir="/var/db/repos/localrepo" diff --git a/sdist/gentoo/keywords b/sdist/gentoo/keywords index 9716ace7a..e6043f0f7 100644 --- a/sdist/gentoo/keywords +++ b/sdist/gentoo/keywords @@ -9,6 +9,7 @@ app-misc/illogical-impulse-microtex-git app-misc/illogical-impulse-oneui4-icons-git app-misc/illogical-impulse-portal app-misc/illogical-impulse-python +gui-misc/illogical-impulse-quickshell-git app-misc/illogical-impulse-screencapture app-misc/illogical-impulse-toolkit app-misc/illogical-impulse-widgets @@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support ** gui-libs/hyprland-qtutils ** gui-wm/hyprland ** x11-libs/libxkbcommon +dev-util/breakpad +dev-libs/linux-syscall-support +dev-embedded/libdisasm diff --git a/sdist/gentoo/useflags b/sdist/gentoo/useflags index c3e12ed8e..e7c9755fc 100644 --- a/sdist/gentoo/useflags +++ b/sdist/gentoo/useflags @@ -111,7 +111,7 @@ sys-power/upower introspection gui-apps/fuzzel png svg dev-libs/glib dbus elf introspection mime xattr # ngl idk about nm-connection-editor. Works fine without -gui-apps/quickshell -X -i3 -i3-ipc -breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland +gui-apps/quickshell -X -i3 -i3-ipc breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland #app-i18n/translate-shell (nothing needed) #gui-apps/wlogout (no use flags) media-gfx/imagemagick xml From 3f030805d137d7fbdab08d981b0c94d206b497d9 Mon Sep 17 00:00:00 2001 From: jwihardi <84292598+jwihardi@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:15:35 -0400 Subject: [PATCH 30/32] gui-misc -> app-misc --- sdist/gentoo/keywords | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdist/gentoo/keywords b/sdist/gentoo/keywords index e6043f0f7..a10d1111e 100644 --- a/sdist/gentoo/keywords +++ b/sdist/gentoo/keywords @@ -9,7 +9,7 @@ app-misc/illogical-impulse-microtex-git app-misc/illogical-impulse-oneui4-icons-git app-misc/illogical-impulse-portal app-misc/illogical-impulse-python -gui-misc/illogical-impulse-quickshell-git +app-misc/illogical-impulse-quickshell-git app-misc/illogical-impulse-screencapture app-misc/illogical-impulse-toolkit app-misc/illogical-impulse-widgets From 3cd323cb1a9a7a96f9881aad85f267225a6f0d7a Mon Sep 17 00:00:00 2001 From: clsty Date: Tue, 21 Oct 2025 09:42:29 +0800 Subject: [PATCH 31/32] Add --skip-quickshell for install-files.sh --- sdata/options/install.sh | 6 ++++-- sdata/step/3.install-files.sh | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sdata/options/install.sh b/sdata/options/install.sh index 0093e106b..fa3f2b4e3 100644 --- a/sdata/options/install.sh +++ b/sdata/options/install.sh @@ -14,11 +14,12 @@ Options for install: --skip-allsetups Skip the whole process setting up permissions/services etc --skip-allfiles Skip the whole process copying configuration files -s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\" + --skip-quickshell Skip installing the config for Quickshell --skip-hyprland Skip installing the config for Hyprland --skip-fish Skip installing the config for Fish --skip-plasmaintg Skip installing plasma-browser-integration --skip-miscconf Skip copying the dirs and files to \".configs\" except for - AGS, Fish and Hyprland + Quickshell, Fish and Hyprland --exp-files Use experimental script for the third step copying files --fontset (Unavailable yet) Use a set of pre-defined font and config --via-nix (Unavailable yet) Use Nix to install dependencies @@ -32,7 +33,7 @@ cleancache(){ # `man getopt` to see more para=$(getopt \ -o hfk:cs \ - -l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \ + -l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-quickshell,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \ -n "$0" -- "$@") [ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1 ##################################################################################### @@ -64,6 +65,7 @@ while true ; do -s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;; --skip-hyprland) SKIP_HYPRLAND=true;shift;; --skip-fish) SKIP_FISH=true;shift;; + --skip-quickshell) SKIP_QUICKSHELL=true;shift;; --skip-miscconf) SKIP_MISCCONF=true;shift;; --skip-plasmaintg) SKIP_PLASMAINTG=true;shift;; --exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;; diff --git a/sdata/step/3.install-files.sh b/sdata/step/3.install-files.sh index 50b4b5692..a0d389570 100644 --- a/sdata/step/3.install-files.sh +++ b/sdata/step/3.install-files.sh @@ -84,11 +84,11 @@ esac # original dotfiles and new ones in the SAME DIRECTORY # (eg. in ~/.config/hypr) won't be mixed together -# MISC (For dots/.config/* but not fish, not Hyprland) +# MISC (For dots/.config/* but not quickshell, not fish, not Hyprland) case $SKIP_MISCCONF in true) sleep 0;; *) - for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do + for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'quickshell' ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do # i="dots/.config/$i" echo "[$0]: Found target: dots/.config/$i" if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/" @@ -98,6 +98,13 @@ case $SKIP_MISCCONF in ;; esac +case $SKIP_QUICKSHELL in + true) sleep 0;; + *) + warning_rsync; v rsync -av --delete dots/.config/quickshell/ "$XDG_CONFIG_HOME"/quickshell/ + ;; +esac + case $SKIP_FISH in true) sleep 0;; *) From 18c11899cbbc78d480838e97d165629f154de2fb Mon Sep 17 00:00:00 2001 From: "Celestial.y" Date: Tue, 21 Oct 2025 13:40:03 +0800 Subject: [PATCH 32/32] Update package-installers.sh --- sdata/lib/package-installers.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sdata/lib/package-installers.sh b/sdata/lib/package-installers.sh index c89d8009e..54da9833d 100644 --- a/sdata/lib/package-installers.sh +++ b/sdata/lib/package-installers.sh @@ -1,6 +1,5 @@ # This script depends on `functions.sh' . -# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. -# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. +# This script is not for direct execution, instead it should be sourced by other script. It does not need execution permission or shebang. # shellcheck shell=bash @@ -105,5 +104,5 @@ install-python-packages(){ x uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12 x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate x uv pip install -r sdata/uv/requirements.txt - x deactivate # We don't need the virtual environment anymore + x deactivate }