Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
clsty
2025-10-21 14:51:46 +08:00
48 changed files with 1499 additions and 1135 deletions
+7 -5
View File
@@ -58,7 +58,9 @@ 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)
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
@@ -67,9 +69,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 +249,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
+1
View File
@@ -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
+2 -1
View File
@@ -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: {
@@ -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)
}
}
}
@@ -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(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh"])
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
@@ -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"
@@ -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 {
@@ -211,6 +212,7 @@ Singleton {
property bool showKeyboardToggle: true
property bool showDarkModeToggle: true
property bool showPerformanceProfileToggle: false
property bool showScreenRecord: false
}
property JsonObject tray: JsonObject {
property bool monochromeIcons: true
@@ -244,6 +246,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
}
@@ -364,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 {
@@ -399,12 +407,12 @@ Singleton {
property JsonObject android: JsonObject {
property int columns: 5
property list<var> 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" }
]
}
}
@@ -417,13 +425,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
@@ -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)
}
@@ -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) :
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
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();
}
@@ -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();
}
@@ -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 {
@@ -165,7 +165,7 @@ Slider {
TrackDot {
required property real modelData
value: modelData
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent?.verticalCenter
}
}
}
@@ -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
}
}
@@ -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)
@@ -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
}
}
}
}
}
@@ -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<point> 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
}
}
}
}
@@ -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
}
}
@@ -0,0 +1,568 @@
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.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"
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"
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<point> points: []
property var mouseButton: null
property var imageRegions: []
readonly property list<var> 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<var> 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) => {
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 });
}
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 "activity_zone"
break;
case RegionSelection.SelectionMode.Circle:
return "gesture"
break;
default:
return "activity_zone"
}
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])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
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])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
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])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
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"
}
}
}
}
}
@@ -0,0 +1,80 @@
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
function dismiss() {
GlobalStates.regionSelectorOpen = false
}
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
Variants {
model: Quickshell.screens
delegate: Loader {
id: regionSelectorLoader
required property var modelData
active: GlobalStates.regionSelectorOpen
sourceComponent: RegionSelection {
screen: regionSelectorLoader.modelData
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
if (Config.options.search.imageSearch.useCircleSelection) {
root.selectionMode = RegionSelection.SelectionMode.Circle
} else {
root.selectionMode = RegionSelection.SelectionMode.RectCorners
}
GlobalStates.regionSelectorOpen = true
}
IpcHandler {
target: "region"
function screenshot() {
root.screenshot()
}
function search() {
root.search()
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: root.screenshot()
}
GlobalShortcut {
name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
}
}
@@ -0,0 +1,71 @@
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
required property color colBackground
required property color colForeground
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: 4
Rectangle {
id: regionLabelBackground
property real verticalPadding: 5
property real horizontalPadding: 10
radius: 10
color: regionRect.colBackground
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: regionRect.colForeground
}
}
}
}
@@ -225,6 +225,17 @@ ContentPage {
}
}
}
ConfigRow {
uniform: true
ConfigSwitch {
buttonIcon: "videocam"
text: Translation.tr("Record")
checked: Config.options.bar.utilButtons.showScreenRecord
onCheckedChanged: {
Config.options.bar.utilButtons.showScreenRecord = checked;
}
}
}
}
ContentSection {
@@ -113,6 +113,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 {
@@ -145,6 +159,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 {
@@ -240,6 +255,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")
@@ -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") }
]
}
}
}
}
@@ -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
}
}
}
@@ -13,14 +13,17 @@ import "./quickToggles/"
import "./quickToggles/classicStyle/"
import "./wifiNetworks/"
import "./bluetoothDevices/"
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
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;
}
}
}
@@ -57,7 +62,8 @@ Item {
SystemButtonRow {
Layout.fillHeight: false
Layout.margins: 10
Layout.fillWidth: true
// Layout.margins: 10
Layout.topMargin: 5
Layout.bottomMargin: 0
}
@@ -102,53 +108,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,42 +187,68 @@ 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;
}
}
}
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"
@@ -9,4 +9,6 @@ Rectangle {
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
}
@@ -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
@@ -101,6 +102,8 @@ AbstractQuickPanel {
spacing: root.spacing
onOpenWifiDialog: root.openWifiDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog()
}
}
}
@@ -129,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
@@ -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")
}
}
@@ -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}` : "")
)
)
}
}
@@ -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")
}
}
@@ -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()
@@ -65,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
}
@@ -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 {
@@ -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<var> appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> 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
}
}
@@ -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<PwNode> 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
}
}
}
}
}
}
}
}
}
@@ -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
}
}
}
-578
View File
@@ -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<var> 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<var> 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"
}
}
}
}
}
}
}
+23 -1
View File
@@ -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);
}
}
@@ -8,49 +8,79 @@ 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)
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
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"),
"-u", "critical",
"-a", "Shell"
])
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),
Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend),
"-u", "critical",
"-a", "Shell"
]);
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 (!root.available || !isFullAndCharging) return;
Quickshell.execDetached([
"notify-send",
Translation.tr("Battery full"),
Translation.tr("Please unplug the charger"),
"-a", "Shell"
]);
if (root.soundEnabled) Audio.playSystemSound("complete");
}
onIsPluggedInChanged: {
if (!root.available || !root.soundEnabled) return;
if (isPluggedIn) {
Audio.playSystemSound("power-plug")
} else {
Audio.playSystemSound("power-unplug")
}
}
}
@@ -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;
+3
View File
@@ -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 {} }
+2 -3
View File
@@ -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
}
+4 -2
View File
@@ -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 <set> (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;;
+9 -2
View File
@@ -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;;
*)
@@ -3,21 +3,17 @@
EAPI=8
inherit cmake
inherit cmake git-r3
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
EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git"
EGIT_COMMIT="00858812f25b748d08b075a0d284093685fa3ffd"
KEYWORDS="~amd64 ~arm64 ~x86"
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"
@@ -38,21 +34,27 @@ RDEPEND="
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
)
dev-cpp/cli11
dev-build/ninja
dev-build/cmake
dev-vcs/git
virtual/pkgconfig
breakpad? ( dev-util/breakpad )
"
src_configure(){
@@ -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
"
+1 -1
View File
@@ -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"
+4
View File
@@ -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
app-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
+1 -1
View File
@@ -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