Merge branch 'end-4:main' into patch-1

This commit is contained in:
Yosuke Nishiyama
2025-10-22 23:26:44 +01:00
committed by GitHub
76 changed files with 1818 additions and 802 deletions
+167
View File
@@ -0,0 +1,167 @@
on:
issues:
types: [opened]
name: Close issues when the "ticked without reading" checkbox is checked
permissions:
issues: write
jobs:
detect-and-close:
runs-on: ubuntu-latest
steps:
- name: Detect checked "ticked without reading" checkbox, comment, close and lock
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_BODY: ${{ toJson(github.event.issue.body) }}
ISSUE_USER: ${{ github.event.issue.user.login }}
run: |
set -euo pipefail
# Normalize the JSON-encoded body into plain text
BODY=$(printf '%s' "$ISSUE_BODY" | sed -E 's/^"(.*)"$/\1/' | sed 's/\\"/"/g' | sed 's/\\n/\n/g')
echo "Checking issue #${ISSUE_NUMBER} for the target checked checkbox..."
# Use -- to stop option parsing so the leading - in the pattern isn't treated as an option
if printf '%s' "$BODY" | grep -Fiq -- "- [x] I've ticked the checkboxes without reading their contents"; then
echo "Target checkbox is checked. Proceeding to comment, close and lock the issue."
# --- Get issue node id via GraphQL ---
QUERY='query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { issue(number: $number) { id } } }'
GET_ID_PAYLOAD=$(jq -n --arg q "$QUERY" --arg owner "$OWNER" --arg name "$REPO" --argjson number "$ISSUE_NUMBER" '{query:$q, variables:{owner:$owner, name:$name, number:$number}}')
echo "GraphQL: fetching issue node id..."
RES=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$GET_ID_PAYLOAD" https://api.github.com/graphql)
echo "GraphQL response (get id):"
printf '%s\n' "$RES"
ISSUE_ID=$(printf '%s' "$RES" | jq -r '.data.repository.issue.id // empty')
if [ -z "$ISSUE_ID" ]; then
echo "Failed to get issue id from GraphQL response. Aborting."
exit 1
fi
echo "Issue node id: $ISSUE_ID"
# --- Post a comment to the issue ---
COMMENT_BODY="Hi @${ISSUE_USER} — I noticed you checked \"I've ticked the checkboxes without reading their contents\" in the issue template. To help others assist you effectively, please read the template and provide the requested diagnostic information (Step 2 & Step 3). I will close this issue now. If you create a new issue with the required information, we can re-evaluate. Thank you!"
MUT_ADD_COMMENT='mutation($id: ID!, $body: String!) { addComment(input: {subjectId: $id, body: $body}) { clientMutationId } }'
ADD_COMMENT_PAYLOAD=$(jq -n --arg q "$MUT_ADD_COMMENT" --arg id "$ISSUE_ID" --arg body "$COMMENT_BODY" '{query:$q, variables:{id:$id, body:$body}}')
echo "GraphQL: adding comment..."
RES_COMMENT=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$ADD_COMMENT_PAYLOAD" https://api.github.com/graphql)
echo "GraphQL response (add comment):"
printf '%s\n' "$RES_COMMENT"
ERR_COMMENT=$(printf '%s' "$RES_COMMENT" | jq -r '.errors[]?.message // empty')
if [ -n "$ERR_COMMENT" ]; then
echo "addComment error: $ERR_COMMENT"
exit 1
fi
echo "Comment posted."
# --- Attempt to close via GraphQL updateIssue ---
MUT_UPDATE_ISSUE='mutation($id: ID!) { updateIssue(input: {id: $id, state: CLOSED, stateReason: NOT_PLANNED}) { issue { number, state, stateReason } } }'
UPDATE_PAYLOAD=$(jq -n --arg q "$MUT_UPDATE_ISSUE" --arg id "$ISSUE_ID" '{query:$q, variables:{id:$id}}')
echo "GraphQL: updating issue (close with NOT_PLANNED)..."
RES_UPDATE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$UPDATE_PAYLOAD" https://api.github.com/graphql)
echo "GraphQL response (update issue):"
printf '%s\n' "$RES_UPDATE"
ERR_UPDATE=$(printf '%s' "$RES_UPDATE" | jq -r '.errors[]?.message // empty')
UPDATED_STATE=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.state // empty')
UPDATED_REASON=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.stateReason // empty')
CLOSED_OK=false
if [ -n "$ERR_UPDATE" ]; then
echo "GraphQL updateIssue returned errors: $ERR_UPDATE"
fi
if [ "$UPDATED_STATE" = "CLOSED" ]; then
echo "Issue closed via GraphQL: state=$UPDATED_STATE, stateReason=$UPDATED_REASON"
CLOSED_OK=true
else
echo "GraphQL update did not confirm the issue is closed. Falling back to REST API PATCH to ensure the issue is closed."
# REST fallback to close the issue with state_reason "not_planned"
REST_PAYLOAD=$(jq -n --arg state "closed" --arg sr "not_planned" '{state:$state, state_reason:$sr}')
echo "REST: PATCH /repos/$OWNER/$REPO/issues/$ISSUE_NUMBER payload: $REST_PAYLOAD"
RES_REST=$(curl -s -w "\n%{http_code}" -X PATCH \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-d "$REST_PAYLOAD" \
"https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")
HTTP_STATUS=$(printf '%s' "$RES_REST" | tail -n1)
RESP_BODY=$(printf '%s' "$RES_REST" | sed '$d')
echo "REST response body:"
printf '%s\n' "$RESP_BODY"
echo "REST HTTP status: $HTTP_STATUS"
if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then
CLOSED_STATE=$(printf '%s' "$RESP_BODY" | jq -r '.state // empty')
CLOSED_REASON=$(printf '%s' "$RESP_BODY" | jq -r '.state_reason // empty')
echo "Issue closed via REST: state=$CLOSED_STATE, state_reason=$CLOSED_REASON"
if [ "$CLOSED_STATE" = "closed" ]; then
CLOSED_OK=true
fi
else
echo "REST fallback failed to close the issue. See REST response above."
exit 1
fi
fi
# --- Attempt to lock the conversation (GraphQL first, then REST fallback) ---
if [ "$CLOSED_OK" = "true" ]; then
echo "Attempting to lock the conversation via GraphQL with reason NO_REASON..."
MUT_LOCK='mutation($id: ID!, $reason: LockReason) { lockLockable(input:{lockableId:$id, lockReason:$reason}) { clientMutationId } }'
LOCK_PAYLOAD=$(jq -n --arg q "$MUT_LOCK" --arg id "$ISSUE_ID" --arg reason "NO_REASON" '{query:$q, variables:{id:$id, reason:$reason}}')
RES_LOCK=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$LOCK_PAYLOAD" https://api.github.com/graphql)
echo "GraphQL response (lock):"
printf '%s\n' "$RES_LOCK"
LOCK_ERR=$(printf '%s' "$RES_LOCK" | jq -r '.errors[]?.message // empty')
if [ -n "$LOCK_ERR" ]; then
echo "GraphQL lockLockable returned errors: $LOCK_ERR"
echo "Falling back to REST API to lock the conversation (no explicit reason)."
# REST fallback to lock the issue (no lock_reason to indicate "no reason")
RES_REST_LOCK=$(curl -s -w "\n%{http_code}" -X PUT \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/lock" -d '{}')
HTTP_STATUS_LOCK=$(printf '%s' "$RES_REST_LOCK" | tail -n1)
RESP_BODY_LOCK=$(printf '%s' "$RES_REST_LOCK" | sed '$d')
echo "REST lock response body:"
printf '%s\n' "$RESP_BODY_LOCK"
echo "REST lock HTTP status: $HTTP_STATUS_LOCK"
if [ "$HTTP_STATUS_LOCK" -ge 200 ] && [ "$HTTP_STATUS_LOCK" -lt 300 ]; then
echo "Issue conversation locked via REST (no explicit reason)."
else
echo "REST fallback failed to lock the conversation. See REST response above."
exit 1
fi
else
echo "Lock via GraphQL succeeded (or returned no errors)."
fi
else
echo "Issue was not successfully closed; skipping lock."
fi
else
echo "Checkbox not present/checked. Nothing to do."
fi
+3 -2
View File
@@ -60,6 +60,7 @@ bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call T
bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip bind = Super+Shift, S, 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, 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 # OCR
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] 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 # Color picker
@@ -225,8 +226,8 @@ binde = Super, Equal, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
binde = Super, Minus, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out binde = Super, Minus, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
binde = Super, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in binde = Super, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
# Zoom with keypad # Zoom with keypad
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # Zoom out binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # [hidden] Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # [hidden] Zoom in
binde = Super, code:82, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out binde = Super, code:82, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in binde = Super, code:86, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="mdi-gentoo"
viewBox="0 0 20 20"
version="1.1"
sodipodi:docname="Pictogrammers-Material-Gentoo.svg"
width="20"
height="20"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="20.875"
inkscape:cx="9.508982"
inkscape:cy="9.9640719"
inkscape:window-width="1327"
inkscape:window-height="1068"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="mdi-gentoo" />
<path
d="m 8.2792823,-5.5983568e-4 c -0.35,0 -0.71,0.0299999957 -1.05,0.0999999957 -3.62,0.66 -6.17,3.79000004 -6.38,5.86000004 -0.11,1.01 0.44,1.77 0.74,2.1 0.81,0.91 2.44,1.6 3.48,2.1699998 -1.51,1.27 -2.2,1.91 -2.88,2.63 -1.02,1.07 -1.74,2.24 -1.74,3.09 0,0.27 -0.05,1.14 0.31,1.82 0.13,0.26 0.51,1.12 1.65,1.76 0.73,0.41 1.76,0.56 2.78,0.42 3.14,-0.45 7.3499997,-3.12 10.3599997,-5.6 1.91,-1.58 3.31,-3.12 3.71,-3.85 0.33,-0.6299998 0.37,-1.7199998 0.18,-2.4099998 -0.54,-1.95 -4.91,-5.94 -8.48,-7.54000004 -0.82,-0.37 -1.7599997,-0.54999999568 -2.6799997,-0.54999999568 m 1.06,2.91000003568 c 0.25,0 0.47,0.03 0.66,0.09 1.1499997,0.3 3.0799997,1.68 2.9099997,2.94 -0.23,1.66 -1.68,2.33 -3.3499997,2.09 -0.98,-0.13 -2.93,-1.23 -2.78,-3.14 0.11,-1.49 1.52,-1.99 2.56,-1.98 m -0.02,1.74 c -0.27,0 -0.48,0.06 -0.58,0.22 -0.47,0.72 -0.24,1.22 0.18,1.55 0.15,-0.38 1.7899997,0.03 1.8299997,0.37 1.42,-1.07 -0.39,-2.13 -1.4299997,-2.14 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+2 -1
View File
@@ -36,6 +36,7 @@ ApplicationWindow {
Component.onCompleted: { Component.onCompleted: {
Config.readWriteDelay = 0; Config.readWriteDelay = 0;
Config.blockWrites = true;
MaterialThemeLoader.reapplyTheme(); MaterialThemeLoader.reapplyTheme();
} }
@@ -90,8 +91,8 @@ ApplicationWindow {
} }
onClicked: { onClicked: {
Quickshell.execDetached(["killall", ...conflictGroup.programs]) Quickshell.execDetached(["killall", ...conflictGroup.programs])
conflictGroup.visible = false
conflictGroup.alwaysSelected() conflictGroup.alwaysSelected()
conflictGroup.visible = false
} }
} }
RippleButton { RippleButton {
@@ -13,7 +13,7 @@ import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import "./cookieClock" import qs.modules.background.cookieClock
Variants { Variants {
id: root id: root
@@ -9,8 +9,8 @@ import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import Quickshell.Io import Quickshell.Io
import "./dateIndicator" import qs.modules.background.cookieClock.dateIndicator
import "./minuteMarks" import qs.modules.background.cookieClock.minuteMarks
Item { Item {
id: root id: root
@@ -1,4 +1,4 @@
import "./weather" import qs.modules.bar.weather
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
@@ -25,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton { sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")]) onClicked: Hyprland.dispatch("global quickshell:regionScreenshot")
MaterialSymbol { MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
fill: 1 fill: 1
@@ -4,7 +4,7 @@ import qs.modules.common.widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "../" import qs.modules.bar
StyledPopup { StyledPopup {
id: root id: root
@@ -10,6 +10,7 @@ Singleton {
property alias options: configOptionsJsonAdapter property alias options: configOptionsJsonAdapter
property bool ready: false property bool ready: false
property int readWriteDelay: 50 // milliseconds property int readWriteDelay: 50 // milliseconds
property bool blockWrites: false
function setNestedValue(nestedKey, value) { function setNestedValue(nestedKey, value) {
let keys = nestedKey.split("."); let keys = nestedKey.split(".");
@@ -63,6 +64,7 @@ Singleton {
id: configFileView id: configFileView
path: root.filePath path: root.filePath
watchChanges: true watchChanges: true
blockWrites: root.blockWrites
onFileChanged: fileReloadTimer.restart() onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart() onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true onLoaded: root.ready = true
@@ -302,6 +304,9 @@ Singleton {
property string to: "06:30" // Format: "HH:mm", 24-hour time property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000 property int colorTemperature: 5000
} }
property JsonObject antiFlashbang: JsonObject {
property bool enable: false
}
} }
property JsonObject lock: JsonObject { property JsonObject lock: JsonObject {
@@ -349,6 +354,24 @@ Singleton {
property real columns: 5 property real columns: 5
} }
property JsonObject regionSelector: JsonObject {
property JsonObject targetRegions: JsonObject {
property bool windows: true
property bool layers: false
property bool content: true
property bool showLabel: false
property real opacity: 0.3
property real contentRegionOpacity: 0.8
}
property JsonObject rect: JsonObject {
property bool showAimLines: true
}
property JsonObject circle: JsonObject {
property int strokeWidth: 6
property int padding: 30
}
}
property JsonObject resources: JsonObject { property JsonObject resources: JsonObject {
property int updateInterval: 3000 property int updateInterval: 3000
} }
@@ -368,6 +391,10 @@ Singleton {
property string shellCommand: "$" property string shellCommand: "$"
property string webSearch: "?" property string webSearch: "?"
} }
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property bool useCircleSelection: false
}
} }
property JsonObject sidebar: JsonObject { property JsonObject sidebar: JsonObject {
@@ -454,10 +481,6 @@ Singleton {
property int arbitraryRaceConditionDelay: 20 // milliseconds property int arbitraryRaceConditionDelay: 20 // milliseconds
} }
property JsonObject screenshotTool: JsonObject {
property bool showContentRegions: true
}
property JsonObject workSafety: JsonObject { property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject { property JsonObject enable: JsonObject {
property bool wallpaper: true property bool wallpaper: true
@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import "./fuzzysort.js" as FuzzySort import "fuzzysort.js" as FuzzySort
/** /**
* Wrapper for FuzzySort to play nicely with Quickshell's imports * Wrapper for FuzzySort to play nicely with Quickshell's imports
@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import "./levendist.js" as Levendist import "levendist.js" as Levendist
/** /**
* Wrapper for levendist.js to play nicely with Quickshell's imports * Wrapper for levendist.js to play nicely with Quickshell's imports
@@ -7,6 +7,7 @@ import QtQuick.Controls
RippleButton { RippleButton {
id: root id: root
property string buttonIcon property string buttonIcon
property alias iconSize: iconWidget.iconSize
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: contentItem.implicitHeight + 8 * 2 implicitHeight: contentItem.implicitHeight + 8 * 2
@@ -17,6 +18,7 @@ RippleButton {
contentItem: RowLayout { contentItem: RowLayout {
spacing: 10 spacing: 10
OptionalMaterialSymbol { OptionalMaterialSymbol {
id: iconWidget
icon: root.buttonIcon icon: root.buttonIcon
opacity: root.enabled ? 1 : 0.4 opacity: root.enabled ? 1 : 0.4
iconSize: Appearance.font.pixelSize.larger iconSize: Appearance.font.pixelSize.larger
@@ -22,6 +22,8 @@ Button {
property bool bounce: true property bool bounce: true
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2 property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
property real baseHeight: contentItem.implicitHeight + verticalPadding * 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 clickedWidth: baseWidth + (isAtSide ? 10 : 20)
property real clickedHeight: baseHeight property real clickedHeight: baseHeight
property var parentGroup: root.parent property var parentGroup: root.parent
@@ -61,10 +63,12 @@ Button {
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.enableImplicitWidthAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this) animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
} }
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.enableImplicitHeightAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this) animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
} }
@@ -75,7 +79,9 @@ Button {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
property alias mouseArea: buttonMouseArea
MouseArea { MouseArea {
id: buttonMouseArea
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
required property string iconText
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
contentItem: Row {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.iconText
color: iconBtn.colText
}
StyledText {
visible: iconBtn.iconText.length > 0 && iconBtn.text.length > 0
anchors.verticalCenter: parent.verticalCenter
color: iconBtn.colText
text: iconBtn.text
}
}
}
@@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
implicitWidth: height
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.text
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
@@ -1,5 +1,5 @@
import qs.modules.common import qs.modules.common
import "./notification_utils.js" as NotificationUtils import "notification_utils.js" as NotificationUtils
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -1,7 +1,7 @@
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.functions import qs.modules.common.functions
import "./notification_utils.js" as NotificationUtils import "notification_utils.js" as NotificationUtils
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
@@ -73,12 +73,13 @@ Slider {
component TrackDot: Rectangle { component TrackDot: Rectangle {
required property real value required property real value
property real normalizedValue: (value - root.from) / (root.to - root.from)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
x: root.handleMargins + (value * root.effectiveDraggingWidth) - (root.trackDotSize / 2) x: root.handleMargins + (normalizedValue * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
width: root.trackDotSize width: root.trackDotSize
height: root.trackDotSize height: root.trackDotSize
radius: Appearance.rounding.full radius: Appearance.rounding.full
color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted color: normalizedValue > root.visualPosition ? root.dotColor : root.dotColorHighlighted
Behavior on color { Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -50,7 +50,7 @@ Rectangle {
property real targetY: root.height / 2 - root.backgroundHeight / 2 property real targetY: root.height / 2 - root.backgroundHeight / 2
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance) y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
implicitWidth: 350 implicitWidth: 350
implicitHeight: 0 implicitHeight: contentColumn.implicitHeight + dialogBackground.radius * 2
Behavior on implicitHeight { Behavior on implicitHeight {
NumberAnimation { NumberAnimation {
id: dialogBackgroundHeightAnimation id: dialogBackgroundHeightAnimation
@@ -0,0 +1,43 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
Column {
id: root
property alias text: sliderName.text
property alias from: sliderWidget.from
property alias to: sliderWidget.to
property alias value: sliderWidget.value
property alias tooltipContent: sliderWidget.tooltipContent
property alias stopIndicatorValues: sliderWidget.stopIndicatorValues
signal moved()
spacing: -2
ContentSubsectionLabel {
id: sliderName
visible: text?.length > 0
text: ""
anchors {
left: parent.left
right: parent.right
}
}
StyledSlider {
id: sliderWidget
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: leftMargin
}
configuration: StyledSlider.Configuration.S
onMoved: root.moved()
}
}
@@ -234,26 +234,26 @@ MouseArea {
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
} }
ActionToolbarIconButton { IconToolbarButton {
id: sleepButton id: sleepButton
onClicked: Session.suspend() onClicked: Session.suspend()
text: "dark_mode" text: "dark_mode"
} }
PasswordGuardedActionToolbarIconButton { PasswordGuardedIconToolbarButton {
id: powerButton id: powerButton
text: "power_settings_new" text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff targetAction: LockContext.ActionEnum.Poweroff
} }
PasswordGuardedActionToolbarIconButton { PasswordGuardedIconToolbarButton {
id: rebootButton id: rebootButton
text: "restart_alt" text: "restart_alt"
targetAction: LockContext.ActionEnum.Reboot targetAction: LockContext.ActionEnum.Reboot
} }
} }
component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton { component PasswordGuardedIconToolbarButton: IconToolbarButton {
id: guardedBtn id: guardedBtn
required property var targetAction required property var targetAction
@@ -273,24 +273,6 @@ MouseArea {
} }
} }
component ActionToolbarIconButton: ToolbarButton {
id: iconBtn
implicitWidth: height
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: iconBtn.text
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
component IconAndTextPair: Row { component IconAndTextPair: Row {
id: pair id: pair
required property string icon required property string icon
@@ -159,7 +159,13 @@ Scope {
} }
Item { // No player placeholder Item { // No player placeholder
Layout.fillWidth: true Layout.alignment: {
if (mediaControlsRoot.anchors.left) return Qt.AlignLeft;
if (mediaControlsRoot.anchors.right) return Qt.AlignRight;
return Qt.AlignHCenter;
}
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
visible: root.meaningfulPlayers.length === 0 visible: root.meaningfulPlayers.length === 0
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
@@ -2,7 +2,7 @@ import qs.services
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import "../" import qs.modules.onScreenDisplay
OsdValueIndicator { OsdValueIndicator {
id: root id: root
@@ -1,6 +1,6 @@
import qs.services import qs.services
import QtQuick import QtQuick
import "../" import qs.modules.onScreenDisplay
OsdValueIndicator { OsdValueIndicator {
id: osdValues id: osdValues
@@ -0,0 +1,49 @@
import qs.modules.common
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: Config.options.regionSelector.circle.strokeWidth
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,90 @@
import qs.modules.common
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
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
// 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
visible: root.showAimLines
opacity: 0.2
z: 2
x: root.mouseX
anchors {
top: parent.top
bottom: parent.bottom
}
width: 1
color: root.color
}
Rectangle { // Horizontal
visible: root.showAimLines
opacity: 0.2
z: 2
y: root.mouseY
anchors {
left: parent.left
right: parent.right
}
height: 1
color: root.color
}
}
@@ -0,0 +1,601 @@
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: "https://uguu.se/upload"
property color overlayColor: "#88111111"
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
property color genericContentForeground: "#ddffffff"
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary
property color brightTertiary: Appearance.m3colors.darkmode ? Appearance.colors.colTertiary : Qt.lighter(Appearance.colors.colPrimary)
property color selectionBorderColor: ColorUtils.mix(brightText, brightSecondary, 0.5)
property color selectionFillColor: "#33ffffff"
property color windowBorderColor: brightSecondary
property color windowFillColor: ColorUtils.transparentize(windowBorderColor, 0.85)
property color imageBorderColor: brightTertiary
property color imageFillColor: ColorUtils.transparentize(imageBorderColor, 0.85)
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 bool isCircleSelection: (root.selectionMode === RegionSelection.SelectionMode.Circle)
property bool enableWindowRegions: Config.options.regionSelector.targetRegions.windows && !isCircleSelection
property bool enableLayerRegions: Config.options.regionSelector.targetRegions.layers && !isCircleSelection
property bool enableContentRegions: Config.options.regionSelector.targetRegions.content
property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity
property bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity
property real targetedRegionX: -1
property real targetedRegionY: -1
property real targetedRegionWidth: 0
property real targetedRegionHeight: 0
function targetedRegionValid() {
return (root.targetedRegionX >= 0 && root.targetedRegionY >= 0)
}
function setRegionToTargeted() {
root.regionX = root.targetedRegionX;
root.regionY = root.targetedRegionY;
root.regionWidth = root.targetedRegionWidth;
root.regionHeight = root.targetedRegionHeight;
}
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();
}
// Clamp region to screen bounds
root.regionX = Math.max(0, Math.min(root.regionX, root.screen.width - root.regionWidth));
root.regionY = Math.max(0, Math.min(root.regionY, root.screen.height - root.regionHeight));
root.regionWidth = Math.max(0, Math.min(root.regionWidth, root.screen.width - root.regionX));
root.regionHeight = Math.max(0, Math.min(root.regionHeight, root.screen.height - root.regionY));
// 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 padding = Config.options.regionSelector.circle.padding + Config.options.regionSelector.circle.strokeWidth / 2;
const dragPoints = (root.points.length > 0) ? root.points : [{ x: mouseArea.mouseX, y: mouseArea.mouseY }];
const maxX = Math.max(...dragPoints.map(p => p.x));
const minX = Math.min(...dragPoints.map(p => p.x));
const maxY = Math.max(...dragPoints.map(p => p.y));
const minY = Math.min(...dragPoints.map(p => p.y));
root.regionX = minX - padding;
root.regionY = minY - padding;
root.regionWidth = maxX - minX + padding * 2;
root.regionHeight = maxY - minY + padding * 2;
if (root.targetedRegionValid() && imageRegions.find(region => {
return (region.at[0] === root.targetedRegionX
&& region.at[1] === root.targetedRegionY
&& region.size[0] === root.targetedRegionWidth
&& region.size[1] === root.targetedRegionHeight)
})) {
root.setRegionToTargeted();
}
}
// Detect if it was a click -> Try to select targeted region
else if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
if (root.targetedRegionValid()) {
root.setRegionToTargeted();
}
}
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
}
}
// Window regions
Repeater {
model: ScriptModel {
values: root.enableWindowRegions ? 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 : root.targetRegionOpacity
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.enableLayerRegions ? 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 : root.targetRegionOpacity
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
}
}
// Content regions
Repeater {
model: ScriptModel {
values: root.enableContentRegions ? 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 : root.contentRegionOpacity
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: Translation.tr("Content region")
}
}
// Options toolbar
Toolbar {
id: toolbar
z: 9999
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: -height
}
opacity: 0
Connections {
target: root
function onVisibleChanged() {
if (!visible) return;
toolbar.anchors.bottomMargin = 8;
toolbar.opacity = 1;
}
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
MaterialCookie {
Layout.fillHeight: true
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitSize: 36 // Intentionally smaller because this one is brighter than others
sides: 10
amplitude: implicitSize / 44
color: Appearance.colors.colPrimary
MaterialSymbol {
anchors.centerIn: parent
iconSize: 22
color: Appearance.colors.colOnPrimary
animateChange: true
text: switch (root.action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
return "content_cut";
case RegionSelection.SnipAction.Search:
return "image_search";
default:
return "";
}
}
}
IconAndTextToolbarButton {
iconText: "activity_zone"
text: Translation.tr("Rect")
toggled: root.selectionMode === RegionSelection.SelectionMode.RectCorners
onClicked: root.selectionMode = RegionSelection.SelectionMode.RectCorners
}
IconAndTextToolbarButton {
iconText: "gesture"
text: Translation.tr("Circle")
toggled: root.selectionMode === RegionSelection.SelectionMode.Circle
onClicked: root.selectionMode = RegionSelection.SelectionMode.Circle
}
IconToolbarButton {
text: "close"
colBackground: Appearance.colors.colLayer3
onClicked: root.dismiss();
}
}
}
}
}
@@ -16,83 +16,13 @@ import Quickshell.Hyprland
Scope { Scope {
id: root id: root
property string screenshotDir: Directories.screenshotTemp
property color overlayColor: "#77111111"
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
property color genericContentForeground: "#ddffffff"
property color selectionBorderColor: "#ddf1f1f1"
property color selectionFillColor: "#33ffffff"
property color windowBorderColor: "#dda0c0da"
property color windowFillColor: "#22a0c0da"
property color imageBorderColor: "#ddf1d1ff"
property color imageFillColor: "#33f1d1ff"
property color onBorderColor: "#ff000000"
property real standardRounding: 4
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating) return 0;
return a.floating ? -1 : 1;
})
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
function dismiss() { function dismiss() {
GlobalStates.regionSelectorOpen = false GlobalStates.regionSelectorOpen = false
} }
component TargetRegion: Rectangle { property var action: RegionSelection.SnipAction.Copy
id: regionRect property var selectionMode: RegionSelection.SelectionMode.RectCorners
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 { Variants {
model: Quickshell.screens model: Quickshell.screens
@@ -101,478 +31,28 @@ Scope {
required property var modelData required property var modelData
active: GlobalStates.regionSelectorOpen active: GlobalStates.regionSelectorOpen
sourceComponent: PanelWindow { sourceComponent: RegionSelection {
id: panelWindow
screen: regionSelectorLoader.modelData screen: regionSelectorLoader.modelData
visible: false onDismiss: root.dismiss()
WlrLayershell.namespace: "quickshell:regionSelector" action: root.action
WlrLayershell.layer: WlrLayer.Overlay selectionMode: root.selectionMode
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property real monitorOffsetX: hyprlandMonitor.x
readonly property real monitorOffsetY: hyprlandMonitor.y
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
property real dragStartX: 0
property real dragStartY: 0
property real draggingX: 0
property real draggingY: 0
property real dragDiffX: 0
property real dragDiffY: 0
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
property bool dragging: false
property var mouseButton: null
property var imageRegions: []
readonly property list<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)
Process {
id: screenshotProcess
running: true
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(panelWindow.screen.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`]
onExited: (exitCode, exitStatus) => {
panelWindow.visible = true;
imageDetectionProcess.running = true;
}
}
Process {
id: imageDetectionProcess
command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh `
+ `--hyprctl `
+ `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' `
+ `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} `
+ `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `]
stdout: StdioCollector {
id: imageDimensionCollector
onStreamFinished: {
imageRegions = filterImageRegions(
JSON.parse(imageDimensionCollector.text),
panelWindow.windowRegions
);
}
}
}
Process {
id: snipProc
function snip() {
if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) {
console.warn("Invalid region size, skipping snip.");
root.dismiss();
}
snipProc.startDetached();
root.dismiss();
}
command: ["bash", "-c",
`magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} `
+ `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - `
+ `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`]
}
ScreencopyView {
anchors.fill: parent
live: false
captureSource: panelWindow.screen
focus: panelWindow.visible
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
root.dismiss();
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.CrossCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
// Controls
onPressed: mouse => {
panelWindow.dragStartX = mouse.x;
panelWindow.dragStartY = mouse.y;
panelWindow.draggingX = mouse.x;
panelWindow.draggingY = mouse.y;
panelWindow.dragging = true;
panelWindow.mouseButton = mouse.button;
}
onReleased: mouse => {
// Detect if it was a click
// Image regions
if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) {
if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) {
panelWindow.regionX = panelWindow.targetedRegionX;
panelWindow.regionY = panelWindow.targetedRegionY;
panelWindow.regionWidth = panelWindow.targetedRegionWidth;
panelWindow.regionHeight = panelWindow.targetedRegionHeight;
}
}
snipProc.snip();
}
onPositionChanged: mouse => {
if (panelWindow.dragging) {
panelWindow.draggingX = mouse.x;
panelWindow.draggingY = mouse.y;
panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX;
panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY;
}
panelWindow.updateTargetedRegion(mouse.x, mouse.y);
}
// Overlay to darken screen
Rectangle { // Base
id: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: panelWindow.regionX - darkenOverlay.border.width
topMargin: panelWindow.regionY - darkenOverlay.border.width
}
width: panelWindow.regionWidth + darkenOverlay.border.width * 2
height: panelWindow.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
// border.color: root.selectionBorderColor
border.color: root.overlayColor
border.width: Math.max(panelWindow.width, panelWindow.height)
radius: root.standardRounding
}
Rectangle {
id: selectionBorder
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: panelWindow.regionX
topMargin: panelWindow.regionY
}
width: panelWindow.regionWidth
height: panelWindow.regionHeight
color: "transparent"
border.color: root.selectionBorderColor
border.width: 2
// radius: root.standardRounding
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
}
StyledText {
z: 2
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
margins: 8
}
color: root.selectionBorderColor
text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}`
}
// Instructions
Rectangle {
z: 9999
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2
}
opacity: panelWindow.dragging ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
color: root.genericContentColor
radius: 10
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
implicitWidth: instructionsRow.implicitWidth + 10 * 2
implicitHeight: instructionsRow.implicitHeight + 5 * 2
Row {
id: instructionsRow
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
id: screenshotRegionIcon
// anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
text: "screenshot_region"
color: root.genericContentForeground
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit")
color: root.genericContentForeground
}
}
}
// Window regions
Repeater {
model: ScriptModel {
values: panelWindow.windowRegions
}
delegate: TargetRegion {
z: 2
required property var modelData
showIcon: true
targeted: !panelWindow.draggedAway &&
(panelWindow.targetedRegionX === modelData.at[0]
&& panelWindow.targetedRegionY === modelData.at[1]
&& panelWindow.targetedRegionWidth === modelData.size[0]
&& panelWindow.targetedRegionHeight === modelData.size[1])
opacity: panelWindow.draggedAway ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
x: modelData.at[0]
y: modelData.at[1]
width: modelData.size[0]
height: modelData.size[1]
borderColor: root.windowBorderColor
fillColor: targeted ? root.windowFillColor : "transparent"
border.width: targeted ? 4 : 2
text: `${modelData.class}`
radius: Appearance.rounding.windowRounding
}
}
// Layer regions
Repeater {
model: ScriptModel {
values: panelWindow.layerRegions
}
delegate: TargetRegion {
z: 3
required property var modelData
targeted: !panelWindow.draggedAway &&
(panelWindow.targetedRegionX === modelData.at[0]
&& panelWindow.targetedRegionY === modelData.at[1]
&& panelWindow.targetedRegionWidth === modelData.size[0]
&& panelWindow.targetedRegionHeight === modelData.size[1])
opacity: panelWindow.draggedAway ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
x: modelData.at[0]
y: modelData.at[1]
width: modelData.size[0]
height: modelData.size[1]
borderColor: root.windowBorderColor
fillColor: targeted ? root.windowFillColor : "transparent"
border.width: targeted ? 4 : 2
text: `${modelData.namespace}`
radius: Appearance.rounding.windowRounding
}
}
// Image regions
Repeater {
model: ScriptModel {
values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : []
}
delegate: TargetRegion {
z: 4
required property var modelData
targeted: !panelWindow.draggedAway &&
(panelWindow.targetedRegionX === modelData.at[0]
&& panelWindow.targetedRegionY === modelData.at[1]
&& panelWindow.targetedRegionWidth === modelData.size[0]
&& panelWindow.targetedRegionHeight === modelData.size[1])
opacity: panelWindow.draggedAway ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
x: modelData.at[0]
y: modelData.at[1]
width: modelData.size[0]
height: modelData.size[1]
borderColor: root.imageBorderColor
fillColor: targeted ? root.imageFillColor : "transparent"
border.width: targeted ? 4 : 2
text: "Content region"
}
}
}
}
} }
} }
} }
function screenshot() { 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 GlobalStates.regionSelectorOpen = true
} }
@@ -582,14 +62,19 @@ Scope {
function screenshot() { function screenshot() {
root.screenshot() root.screenshot()
} }
function search() {
root.search()
}
} }
GlobalShortcut { GlobalShortcut {
name: "regionScreenshot" name: "regionScreenshot"
description: "Takes a screenshot of the selected region" description: "Takes a screenshot of the selected region"
onPressed: root.screenshot()
onPressed: { }
root.screenshot() GlobalShortcut {
} name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
} }
} }
@@ -0,0 +1,68 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Widgets
Rectangle {
id: root
required property color colBackground
required property color colForeground
property bool showLabel: Config.options.regionSelector.targetRegions.showLabel
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
Loader {
anchors {
top: parent.top
left: parent.left
topMargin: root.textPadding
leftMargin: root.textPadding
}
active: root.showLabel
sourceComponent: Rectangle {
property real verticalPadding: 5
property real horizontalPadding: 10
radius: 10
color: root.colBackground
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
Loader {
id: regionIconLoader
active: root.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(root.text), "image-missing")
}
}
StyledText {
id: regionText
text: root.text
color: root.colForeground
}
}
}
}
}
@@ -64,7 +64,6 @@ Scope {
id: sidebarCornerOpenInteractionLoader id: sidebarCornerOpenInteractionLoader
active: { active: {
if (!Config.options.sidebar.cornerOpen.enable) return false; if (!Config.options.sidebar.cornerOpen.enable) return false;
if (!Config.options.bar.vertical && Config.options.sidebar.cornerOpen.bottom == Config.options.bar.bottom) return false;
if (cornerPanelWindow.fullscreen) return false; if (cornerPanelWindow.fullscreen) return false;
return (Config.options.sidebar.cornerOpen.bottom == cornerWidget.isBottom); return (Config.options.sidebar.cornerOpen.bottom == cornerWidget.isBottom);
} }
@@ -574,6 +574,100 @@ ContentPage {
} }
} }
ContentSection {
icon: "screenshot_frame_2"
title: Translation.tr("Region selector (screen snipping/Google Lens)")
ContentSubsection {
title: Translation.tr("Hint target regions")
ConfigRow {
ConfigSwitch {
buttonIcon: "select_window"
text: Translation.tr('Windows')
checked: Config.options.regionSelector.targetRegions.windows
onCheckedChanged: {
Config.options.regionSelector.targetRegions.windows = checked;
}
}
ConfigSwitch {
buttonIcon: "right_panel_open"
text: Translation.tr('Layers')
checked: Config.options.regionSelector.targetRegions.layers
onCheckedChanged: {
Config.options.regionSelector.targetRegions.layers = checked;
}
}
ConfigSwitch {
buttonIcon: "nearby"
text: Translation.tr('Content')
checked: Config.options.regionSelector.targetRegions.content
onCheckedChanged: {
Config.options.regionSelector.targetRegions.content = checked;
}
StyledToolTip {
text: Translation.tr("Could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
}
}
}
}
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") }
]
}
}
ContentSubsection {
title: Translation.tr("Rectangular selection")
ConfigSwitch {
buttonIcon: "point_scan"
text: Translation.tr("Show aim lines")
checked: Config.options.regionSelector.rect.showAimLines
onCheckedChanged: {
Config.options.regionSelector.rect.showAimLines = checked;
}
}
}
ContentSubsection {
title: Translation.tr("Circle selection")
ConfigSpinBox {
icon: "eraser_size_3"
text: Translation.tr("Stroke width")
value: Config.options.regionSelector.circle.strokeWidth
from: 1
to: 20
stepSize: 1
onValueChanged: {
Config.options.regionSelector.circle.strokeWidth = value;
}
}
ConfigSpinBox {
icon: "screenshot_frame_2"
text: Translation.tr("Padding")
value: Config.options.regionSelector.circle.padding
from: 0
to: 100
stepSize: 5
onValueChanged: {
Config.options.regionSelector.circle.padding = value;
}
}
}
}
ContentSection { ContentSection {
icon: "side_navigation" icon: "side_navigation"
title: Translation.tr("Sidebars") title: Translation.tr("Sidebars")
@@ -848,23 +942,6 @@ ContentPage {
} }
} }
ContentSection {
icon: "screenshot_frame_2"
title: Translation.tr("Screenshot tool")
ConfigSwitch {
buttonIcon: "nearby"
text: Translation.tr('Show regions of potential interest')
checked: Config.options.screenshotTool.showContentRegions
onCheckedChanged: {
Config.options.screenshotTool.showContentRegions = checked;
}
StyledToolTip {
text: Translation.tr("Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
}
}
}
ContentSection { ContentSection {
icon: "wallpaper_slideshow" icon: "wallpaper_slideshow"
title: Translation.tr("Wallpaper selector") title: Translation.tr("Wallpaper selector")
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "./aiChat/" import qs.modules.sidebarLeft.aiChat
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "./anime/" import qs.modules.sidebarLeft.anime
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -10,7 +10,6 @@ import Quickshell.Hyprland
Scope { // Scope Scope { // Scope
id: root id: root
property int sidebarPadding: 15
property bool detach: false property bool detach: false
property Component contentComponent: SidebarLeftContent {} property Component contentComponent: SidebarLeftContent {}
property Item sidebarContent property Item sidebarContent
@@ -9,6 +9,7 @@ import Qt5Compat.GraphicalEffects
Item { Item {
id: root id: root
required property var scopeRoot required property var scopeRoot
property int sidebarPadding: 10
anchors.fill: parent anchors.fill: parent
property bool aiChatEnabled: Config.options.policies.ai !== 0 property bool aiChatEnabled: Config.options.policies.ai !== 0
property bool translatorEnabled: Config.options.sidebar.translator.enable property bool translatorEnabled: Config.options.sidebar.translator.enable
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "./translator/" import qs.modules.sidebarLeft.translator
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
@@ -3,8 +3,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "../" import qs.modules.sidebarLeft
import qs.services
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -1,9 +1,9 @@
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.services import qs.services
import "./calendar" import qs.modules.sidebarRight.calendar
import "./todo" import qs.modules.sidebarRight.todo
import "./pomodoro" import qs.modules.sidebarRight.pomodoro
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -1,8 +1,8 @@
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.services import qs.services
import "./notifications" import qs.modules.sidebarRight.notifications
import "./volumeMixer" import qs.modules.sidebarRight.volumeMixer
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -9,21 +9,24 @@ import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import Quickshell.Hyprland import Quickshell.Hyprland
import "./quickToggles/" import qs.modules.sidebarRight.quickToggles
import "./quickToggles/classicStyle/" import qs.modules.sidebarRight.quickToggles.classicStyle
import "./wifiNetworks/"
import "./bluetoothDevices/" import qs.modules.sidebarRight.bluetoothDevices
import "./volumeMixer/" import qs.modules.sidebarRight.nightLight
import qs.modules.sidebarRight.volumeMixer
import qs.modules.sidebarRight.wifiNetworks
Item { Item {
id: root id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12 property int sidebarPadding: 10
property string settingsQmlPath: Quickshell.shellPath("settings.qml") property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false
property bool showBluetoothDialog: false
property bool showAudioOutputDialog: false property bool showAudioOutputDialog: false
property bool showAudioInputDialog: false property bool showAudioInputDialog: false
property bool showBluetoothDialog: false
property bool showNightLightDialog: false
property bool showWifiDialog: false
property bool editMode: false property bool editMode: false
Connections { Connections {
@@ -62,7 +65,8 @@ Item {
SystemButtonRow { SystemButtonRow {
Layout.fillHeight: false Layout.fillHeight: false
Layout.margins: 10 Layout.fillWidth: true
// Layout.margins: 10
Layout.topMargin: 5 Layout.topMargin: 5
Layout.bottomMargin: 0 Layout.bottomMargin: 0
} }
@@ -108,18 +112,20 @@ Item {
} }
ToggleDialog { ToggleDialog {
id: wifiDialogLoader shownPropertyString: "showAudioOutputDialog"
shownPropertyString: "showWifiDialog" dialog: VolumeDialog {
dialog: WifiDialog {} isSink: true
onShownChanged: { }
if (!shown) return; }
Network.enableWifi();
Network.rescanWifi(); ToggleDialog {
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
} }
} }
ToggleDialog { ToggleDialog {
id: bluetoothDialogLoader
shownPropertyString: "showBluetoothDialog" shownPropertyString: "showBluetoothDialog"
dialog: BluetoothDialog {} dialog: BluetoothDialog {}
onShownChanged: { onShownChanged: {
@@ -129,23 +135,21 @@ Item {
Bluetooth.defaultAdapter.enabled = true; Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true; Bluetooth.defaultAdapter.discovering = true;
} }
} }
} }
ToggleDialog { ToggleDialog {
id: audioOutputDialogLoader shownPropertyString: "showNightLightDialog"
shownPropertyString: "showAudioOutputDialog" dialog: NightLightDialog {}
dialog: VolumeDialog {
isSink: true
}
} }
ToggleDialog { ToggleDialog {
id: audioInputDialogLoader shownPropertyString: "showWifiDialog"
shownPropertyString: "showAudioInputDialog" dialog: WifiDialog {}
dialog: VolumeDialog { onShownChanged: {
isSink: false if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
} }
} }
@@ -185,45 +189,72 @@ Item {
active: Config.options.sidebar.quickToggles.style === styleName active: Config.options.sidebar.quickToggles.style === styleName
Connections { Connections {
target: quickPanelImplLoader.item target: quickPanelImplLoader.item
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenAudioOutputDialog() { function onOpenAudioOutputDialog() {
root.showAudioOutputDialog = true; root.showAudioOutputDialog = true;
} }
function onOpenAudioInputDialog() { function onOpenAudioInputDialog() {
root.showAudioInputDialog = true; root.showAudioInputDialog = true;
} }
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenNightLightDialog() {
root.showNightLightDialog = true;
}
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
} }
} }
component SystemButtonRow: RowLayout { component SystemButtonRow: Item {
spacing: 10 implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
CustomIcon { Rectangle {
id: distroIcon id: uptimeContainer
width: 25 anchors {
height: 25 top: parent.top
source: SystemInfo.distroIcon bottom: parent.bottom
colorize: true left: parent.left
color: Appearance.colors.colOnLayer0 }
} color: Appearance.colors.colLayer1
radius: height / 2
implicitWidth: uptimeRow.implicitWidth + 24
implicitHeight: uptimeRow.implicitHeight + 8
StyledText { Row {
font.pixelSize: Appearance.font.pixelSize.normal id: uptimeRow
color: Appearance.colors.colOnLayer0 anchors.centerIn: parent
text: Translation.tr("Up %1").arg(DateTime.uptime) spacing: 8
textFormat: Text.MarkdownText CustomIcon {
} id: distroIcon
anchors.verticalCenter: parent.verticalCenter
Item { width: 25
Layout.fillWidth: true 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 { ButtonGroup {
id: systemButtonsRow
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
color: Appearance.colors.colLayer1
padding: 4
QuickToggleButton { QuickToggleButton {
toggled: root.editMode toggled: root.editMode
visible: Config.options.sidebar.quickToggles.style === "android" visible: Config.options.sidebar.quickToggles.style === "android"
@@ -1,7 +1,7 @@
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import "./calendar_layout.js" as CalendarLayout import "calendar_layout.js" as CalendarLayout
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -0,0 +1,157 @@
import qs
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 Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
WindowDialogTitle {
text: Translation.tr("Eye protection")
}
WindowDialogSectionHeader {
text: Translation.tr("Night Light")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: nightLightColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "lightbulb"
text: Translation.tr("Enable now")
checked: Hyprsunset.active
onCheckedChanged: {
Hyprsunset.toggle(checked)
}
}
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "night_sight_auto"
text: Translation.tr("Automatic")
checked: Config.options.light.night.automatic
onCheckedChanged: {
Config.options.light.night.automatic = checked;
}
}
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
text: Translation.tr("Color temperature")
from: 1000
to: 20000
stopIndicatorValues: [6000, to]
value: Config.options.light.night.colorTemperature
onMoved: Config.options.light.night.colorTemperature = value
tooltipContent: `${Math.round(value)}K`
}
}
WindowDialogSectionHeader {
text: Translation.tr("Anti-flashbang (experimental)")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: antiFlashbangColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "destruction"
text: Translation.tr("Enable")
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: {
Config.options.light.antiFlashbang.enable = checked;
}
StyledToolTip {
text: Translation.tr("Example use case: eroge on one workspace, dark Discord window on another")
}
}
}
WindowDialogSectionHeader {
text: Translation.tr("Brightness")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: brightnessColumn
Layout.topMargin: -16
Layout.fillWidth: true
Layout.fillHeight: true
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
// text: Translation.tr("Brightness")
value: root.brightnessMonitor.brightness
onMoved: root.brightnessMonitor.setBrightness(value)
}
}
WindowDialogButtonRow {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}
@@ -7,8 +7,9 @@ Rectangle {
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1 color: Appearance.colors.colLayer1
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog() signal openAudioOutputDialog()
signal openAudioInputDialog() signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
} }
@@ -6,30 +6,20 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import "./androidStyle/" import qs.modules.sidebarRight.quickToggles.androidStyle
AbstractQuickPanel { AbstractQuickPanel {
id: root id: root
property bool editMode: false property bool editMode: false
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
// Sizes
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
Behavior on implicitHeight { Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
} }
property real spacing: 6 property real spacing: 6
property real padding: 6 property real padding: 6
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
readonly property list<var> toggles: Config.options.sidebar.quickToggles.android.toggles
readonly property list<var> toggleRows: toggleRowsForList(toggles)
readonly property list<var> unusedToggles: {
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
return types.map(type => { return { type: type, size: 1 } })
}
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
readonly property real baseCellWidth: { readonly property real baseCellWidth: {
// This is the wrong calculation, but it looks correct in reality??? // This is the wrong calculation, but it looks correct in reality???
// (theoretically spacing should be multiplied by 1 column less) // (theoretically spacing should be multiplied by 1 column less)
@@ -38,6 +28,17 @@ AbstractQuickPanel {
} }
readonly property real baseCellHeight: 56 readonly property real baseCellHeight: 56
// Toggles
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
readonly property list<var> toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : []
readonly property list<var> toggleRows: toggleRowsForList(toggles)
readonly property list<var> unusedToggles: {
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
return types.map(type => { return { type: type, size: 1 } })
}
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
function toggleRowsForList(togglesList) { function toggleRowsForList(togglesList) {
var rows = []; var rows = [];
var row = []; var row = [];
@@ -73,14 +74,14 @@ AbstractQuickPanel {
Repeater { Repeater {
id: usedRowsRepeater id: usedRowsRepeater
model: ScriptModel { model: ScriptModel {
values: root.toggleRows values: Array(root.toggleRows.length)
} }
delegate: ButtonGroup { delegate: ButtonGroup {
id: toggleRow id: toggleRow
required property var modelData
required property int index required property int index
property var modelData: root.toggleRows[index]
property int startingIndex: { property int startingIndex: {
const rows = usedRowsRepeater.model.values; const rows = root.toggleRows;
let sum = 0; let sum = 0;
for (let i = 0; i < index; i++) { for (let i = 0; i < index; i++) {
sum += rows[i].length; sum += rows[i].length;
@@ -91,7 +92,8 @@ AbstractQuickPanel {
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: toggleRow.modelData values: toggleRow?.modelData ?? []
objectProp: "type"
} }
delegate: AndroidToggleDelegateChooser { delegate: AndroidToggleDelegateChooser {
startingIndex: toggleRow.startingIndex startingIndex: toggleRow.startingIndex
@@ -99,10 +101,11 @@ AbstractQuickPanel {
baseCellWidth: root.baseCellWidth baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight baseCellHeight: root.baseCellHeight
spacing: root.spacing spacing: root.spacing
onOpenWifiDialog: root.openWifiDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenAudioOutputDialog: root.openAudioOutputDialog() onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog() onOpenAudioInputDialog: root.openAudioInputDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenNightLightDialog: root.openNightLightDialog()
onOpenWifiDialog: root.openWifiDialog()
} }
} }
} }
@@ -131,16 +134,18 @@ AbstractQuickPanel {
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: root.unusedToggleRows values: Array(root.unusedToggleRows.length)
} }
delegate: ButtonGroup { delegate: ButtonGroup {
id: unusedToggleRow id: unusedToggleRow
required property var modelData required property int index
property var modelData: root.unusedToggleRows[index]
spacing: root.spacing spacing: root.spacing
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: unusedToggleRow.modelData values: unusedToggleRow?.modelData ?? []
objectProp: "type"
} }
delegate: AndroidToggleDelegateChooser { delegate: AndroidToggleDelegateChooser {
startingIndex: -1 startingIndex: -1
@@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Bluetooth import Quickshell.Bluetooth
import "./classicStyle/" import qs.modules.sidebarRight.quickToggles.classicStyle
AbstractQuickPanel { AbstractQuickPanel {
id: root id: root
@@ -19,7 +19,7 @@ AndroidQuickToggleButton {
} }
altAction: () => { altAction: () => {
Config.options.light.night.automatic = !Config.options.light.night.automatic root.openMenu()
} }
Component.onCompleted: { Component.onCompleted: {
@@ -27,7 +27,7 @@ AndroidQuickToggleButton {
} }
StyledToolTip { StyledToolTip {
text: Translation.tr("Night Light | Right-click to toggle Auto mode") text: Translation.tr("Night Light | Right-click to configure")
} }
} }
@@ -23,6 +23,21 @@ GroupButton {
baseHeight: root.baseCellHeight baseHeight: root.baseCellHeight
property bool editMode: false property bool editMode: false
enableImplicitWidthAnimation: !editMode && root.mouseArea.containsMouse
enableImplicitHeightAnimation: !editMode && root.mouseArea.containsMouse
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() signal openMenu()
@@ -65,7 +80,7 @@ GroupButton {
MaterialSymbol { MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
fill: root.toggled ? 1 : 0 fill: root.toggled ? 1 : 0
iconSize: Appearance.font.pixelSize.huge iconSize: root.expandedSize ? 22 : 24
color: root.colIcon color: root.colIcon
text: root.buttonIcon text: root.buttonIcon
} }
@@ -4,6 +4,7 @@ import qs.modules.common.widgets
import qs.services import qs.services
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
AndroidQuickToggleButton { AndroidQuickToggleButton {
id: root id: root
@@ -22,7 +23,7 @@ AndroidQuickToggleButton {
interval: 300 interval: 300
repeat: false repeat: false
onTriggered: { onTriggered: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")]) Hyprland.dispatch("global quickshell:regionScreenshot")
} }
} }
@@ -14,10 +14,11 @@ DelegateChooser {
required property real baseCellHeight required property real baseCellHeight
required property real spacing required property real spacing
required property int startingIndex required property int startingIndex
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog() signal openAudioOutputDialog()
signal openAudioInputDialog() signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
role: "type" role: "type"
@@ -90,6 +91,9 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing cellSpacing: root.spacing
cellSize: modelData.size cellSize: modelData.size
onOpenMenu: {
root.openNightLightDialog()
}
} } } }
DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle { DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle {
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "../" import qs.modules.sidebarRight.quickToggles
import qs import qs
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -14,7 +14,7 @@ GroupButton {
contentItem: MaterialSymbol { contentItem: MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
iconSize: 20 iconSize: 22
fill: toggled ? 1 : 0 fill: toggled ? 1 : 0
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1 color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services import qs.services
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "./../bar" as Bar import qs.modules.bar as Bar
MouseArea { MouseArea {
id: root id: root
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "../bar" as Bar import qs.modules.bar as Bar
MouseArea { MouseArea {
id: root id: root
@@ -8,7 +8,7 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "../bar" as Bar import qs.modules.bar as Bar
Item { // Bar content region Item { // Bar content region
id: root id: root
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services import qs.services
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "../bar" as Bar import qs.modules.bar as Bar
Item { Item {
id: root id: root
@@ -4,7 +4,7 @@ import qs.services
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import QtQuick.Layouts import QtQuick.Layouts
import "../bar" as Bar import qs.modules.bar as Bar
Item { // Full hitbox Item { // Full hitbox
id: root id: root
@@ -8,7 +8,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "../bar" as Bar import qs.modules.bar as Bar
MouseArea { MouseArea {
id: root id: root
@@ -316,7 +316,7 @@ MouseArea {
bottomMargin: 8 bottomMargin: 8
} }
ToolbarButton { IconToolbarButton {
implicitWidth: height implicitWidth: height
onClicked: { onClicked: {
Wallpapers.openFallbackPicker(root.useDarkMode); Wallpapers.openFallbackPicker(root.useDarkMode);
@@ -327,42 +327,27 @@ MouseArea {
GlobalStates.wallpaperSelectorOpen = false; GlobalStates.wallpaperSelectorOpen = false;
Config.options.wallpaperSelector.useSystemFileDialog = true Config.options.wallpaperSelector.useSystemFileDialog = true
} }
contentItem: MaterialSymbol { text: "open_in_new"
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "open_in_new"
iconSize: Appearance.font.pixelSize.larger
}
StyledToolTip { StyledToolTip {
text: Translation.tr("Use the system file picker instead\nRight-click to make this the default behavior") text: Translation.tr("Use the system file picker instead\nRight-click to make this the default behavior")
} }
} }
ToolbarButton { IconToolbarButton {
implicitWidth: height implicitWidth: height
onClicked: { onClicked: {
Wallpapers.randomFromCurrentFolder(); Wallpapers.randomFromCurrentFolder();
} }
contentItem: MaterialSymbol { text: "ifl"
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "ifl"
iconSize: Appearance.font.pixelSize.larger
}
StyledToolTip { StyledToolTip {
text: Translation.tr("Pick random from this folder") text: Translation.tr("Pick random from this folder")
} }
} }
ToolbarButton { IconToolbarButton {
implicitWidth: height implicitWidth: height
onClicked: root.useDarkMode = !root.useDarkMode onClicked: root.useDarkMode = !root.useDarkMode
contentItem: MaterialSymbol { text: root.useDarkMode ? "dark_mode" : "light_mode"
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: root.useDarkMode ? "dark_mode" : "light_mode"
iconSize: Appearance.font.pixelSize.larger
}
StyledToolTip { StyledToolTip {
text: Translation.tr("Click to toggle light/dark mode\n(applied when wallpaper is chosen)") text: Translation.tr("Click to toggle light/dark mode\n(applied when wallpaper is chosen)")
} }
@@ -403,17 +388,12 @@ MouseArea {
} }
} }
ToolbarButton { IconToolbarButton {
implicitWidth: height implicitWidth: height
onClicked: { onClicked: {
GlobalStates.wallpaperSelectorOpen = false; GlobalStates.wallpaperSelectorOpen = false;
} }
contentItem: MaterialSymbol { text: "close"
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "cancel_presentation"
iconSize: Appearance.font.pixelSize.larger
}
StyledToolTip { StyledToolTip {
text: Translation.tr("Cancel wallpaper selection") text: Translation.tr("Cancel wallpaper selection")
} }
+1 -1
View File
@@ -7,7 +7,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import QtQuick import QtQuick
import "./ai/" import qs.services.ai
/** /**
* Basic service to handle LLM chats. Supports Google's and OpenAI's API formats. * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
// From https://github.com/caelestia-dots/shell with modifications. // From https://github.com/caelestia-dots/shell with modifications.
// License: GPLv3 // License: GPLv3
import qs.modules.common
import qs.modules.common.functions
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -14,6 +16,7 @@ import QtQuick
*/ */
Singleton { Singleton {
id: root id: root
property real minimumBrightnessAllowed: 0.00001 // Setting to 0 would kind of turn off the screen. We don't want that.
signal brightnessChanged() signal brightnessChanged()
@@ -84,6 +87,8 @@ Singleton {
} }
property int rawMaxBrightness: 100 property int rawMaxBrightness: 100
property real brightness property real brightness
property real brightnessMultiplier: 1.0
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * brightnessMultiplier))
property bool ready: false property bool ready: false
onBrightnessChanged: { onBrightnessChanged: {
@@ -119,17 +124,23 @@ Singleton {
} }
function syncBrightness() { function syncBrightness() {
const rounded = Math.round(monitor.brightness * monitor.rawMaxBrightness); const brightnessValue = monitor.multipliedBrightness
const rounded = Math.round(brightnessValue * monitor.rawMaxBrightness);
setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "--class", "backlight", "s", rounded, "--quiet"]; setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "--class", "backlight", "s", rounded, "--quiet"];
setProc.startDetached(); setProc.startDetached();
} }
function setBrightness(value: real): void { function setBrightness(value: real): void {
value = Math.max(0.01, Math.min(1, value)); value = Math.max(root.minimumBrightnessAllowed, Math.min(1, value));
monitor.brightness = value; monitor.brightness = value;
setTimer.restart(); setTimer.restart();
} }
function setBrightnessMultiplier(value: real): void {
monitor.brightnessMultiplier = value;
setTimer.restart();
}
Component.onCompleted: { Component.onCompleted: {
initialize(); initialize();
} }
@@ -145,6 +156,61 @@ Singleton {
BrightnessMonitor {} BrightnessMonitor {}
} }
// Anti-flashbang
property string screenshotDir: "/tmp/quickshell/brightness/antiflashbang"
function brightnessMultiplierForLightness(x: real): real {
// 6.600135 + 216.360356 * e^(-0.0811129189x)
// Division by 100 is to normalize to [0, 1]
return (6.600135 + 216.360356 * Math.pow(Math.E, -0.0811129189 * x)) / 100.0;
}
Variants {
model: Quickshell.screens
Scope {
id: screenScope
required property var modelData
property string screenName: modelData.name
property string screenshotPath: `${root.screenshotDir}/screenshot-${screenName}.png`
Connections {
enabled: Config.options.light.antiFlashbang.enable
target: Hyprland
function onRawEvent(event) {
if (["workspacev2"].includes(event.name)) {
screenshotTimer.restart();
}
}
}
Timer {
id: screenshotTimer
interval: 700 // This is what I have for a Hyprland ws anim
onTriggered: {
screenshotProc.running = false;
screenshotProc.running = true;
}
}
Process {
id: screenshotProc
command: ["bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}'`
+ ` && grim -o '${StringUtils.shellSingleQuoteEscape(screenScope.screenName)}' '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}'`
+ ` && magick '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}' -colorspace Gray -format "%[fx:mean*100]" info:`
]
stdout: StdioCollector {
id: lightnessCollector
onStreamFinished: {
Quickshell.execDetached(["rm", screenScope.screenshotPath]); // Cleanup
const lightness = lightnessCollector.text
const newMultiplier = root.brightnessMultiplierForLightness(parseFloat(lightness))
Brightness.getMonitorForScreen(screenScope.modelData).setBrightnessMultiplier(newMultiplier)
}
}
}
}
}
// External trigger points
IpcHandler { IpcHandler {
target: "brightness" target: "brightness"
@@ -4,6 +4,7 @@ import QtQuick
import qs.modules.common import qs.modules.common
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland
/** /**
* Simple hyprsunset service with automatic mode. * Simple hyprsunset service with automatic mode.
@@ -111,18 +112,28 @@ Singleton {
} }
} }
function toggle() { function toggle(active = undefined) {
if (root.manualActive === undefined) { if (root.manualActive === undefined) {
root.manualActive = root.active; root.manualActive = root.active;
root.manualActiveHour = root.clockHour; root.manualActiveHour = root.clockHour;
root.manualActiveMinute = root.clockMinute; root.manualActiveMinute = root.clockMinute;
} }
root.manualActive = !root.manualActive; root.manualActive = active !== undefined ? active : !root.manualActive;
if (root.manualActive) { if (root.manualActive) {
root.enable(); root.enable();
} else { } else {
root.disable(); root.disable();
} }
} }
// Change temp
Connections {
target: Config.options.light.night
function onColorTemperatureChanged() {
if (!root.active) return;
Hyprland.dispatch(`hyprctl hyprsunset temperature ${Config.options.light.night.colorTemperature}`);
Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", `${Config.options.light.night.colorTemperature}`]);
}
}
} }
@@ -6,7 +6,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import "./network" import qs.services.network
/** /**
* Network service with nmcli. * Network service with nmcli.
@@ -70,6 +70,8 @@ Singleton {
case "debian": case "debian":
case "raspbian": case "raspbian":
case "kali": distroIcon = "debian-symbolic"; break; case "kali": distroIcon = "debian-symbolic"; break;
case "funtoo":
case "gentoo": distroIcon = "gentoo-symbolic"; break;
default: distroIcon = "linux-symbolic"; break; default: distroIcon = "linux-symbolic"; break;
} }
if (textOsRelease.toLowerCase().includes("nyarch")) { if (textOsRelease.toLowerCase().includes("nyarch")) {
@@ -79,7 +79,7 @@ Singleton {
// Special cases // Special cases
if (!text) return ""; if (!text) return "";
var key = text.toString(); var key = text.toString();
if (root.isLoading || (!root.translations.hasOwnProperty(key) && !root.generatedTranslations.hasOwnProperty(key))) if (root.isLoading || (!root?.translations?.hasOwnProperty(key) && !root?.generatedTranslations?.hasOwnProperty(key)))
return key; return key;
// Normal cases // Normal cases
+20 -20
View File
@@ -7,30 +7,30 @@
//@ pragma Env QT_SCALE_FACTOR=1 //@ pragma Env QT_SCALE_FACTOR=1
import "./modules/common/" import qs.modules.common
import "./modules/background/" import qs.modules.background
import "./modules/bar/" import qs.modules.bar
import "./modules/cheatsheet/" import qs.modules.cheatsheet
import "./modules/crosshair/" import qs.modules.crosshair
import "./modules/dock/" import qs.modules.dock
import "./modules/lock/" import qs.modules.lock
import "./modules/mediaControls/" import qs.modules.mediaControls
import "./modules/notificationPopup/" import qs.modules.notificationPopup
import "./modules/onScreenDisplay/" import qs.modules.onScreenDisplay
import "./modules/onScreenKeyboard/" import qs.modules.onScreenKeyboard
import "./modules/overview/" import qs.modules.overview
import "./modules/regionSelector/" import qs.modules.regionSelector
import "./modules/screenCorners/" import qs.modules.screenCorners
import "./modules/sessionScreen/" import qs.modules.sessionScreen
import "./modules/sidebarLeft/" import qs.modules.sidebarLeft
import "./modules/sidebarRight/" import qs.modules.sidebarRight
import "./modules/verticalBar/" import qs.modules.verticalBar
import "./modules/wallpaperSelector/" import qs.modules.wallpaperSelector
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import Quickshell import Quickshell
import "./services/" import qs.services
ShellRoot { ShellRoot {
// Enable/disable modules here. False = not loaded at all, so rest assured // Enable/disable modules here. False = not loaded at all, so rest assured
+2 -3
View File
@@ -1,6 +1,5 @@
# This script depends on `functions.sh' . # This script depends on `functions.sh' .
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. # This script is not for direct execution, instead it should be sourced by other script. It does 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.
# shellcheck shell=bash # 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 uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12
x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
x uv pip install -r sdata/uv/requirements.txt 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-allsetups Skip the whole process setting up permissions/services etc
--skip-allfiles Skip the whole process copying configuration files --skip-allfiles Skip the whole process copying configuration files
-s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\" -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-hyprland Skip installing the config for Hyprland
--skip-fish Skip installing the config for Fish --skip-fish Skip installing the config for Fish
--skip-plasmaintg Skip installing plasma-browser-integration --skip-plasmaintg Skip installing plasma-browser-integration
--skip-miscconf Skip copying the dirs and files to \".configs\" except for --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 --exp-files Use experimental script for the third step copying files
--fontset <set> (Unavailable yet) Use a set of pre-defined font and config --fontset <set> (Unavailable yet) Use a set of pre-defined font and config
--via-nix (Unavailable yet) Use Nix to install dependencies --via-nix (Unavailable yet) Use Nix to install dependencies
@@ -32,7 +33,7 @@ cleancache(){
# `man getopt` to see more # `man getopt` to see more
para=$(getopt \ para=$(getopt \
-o hfk:cs \ -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" -- "$@") -n "$0" -- "$@")
[ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1 [ $? != 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;; -s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;;
--skip-hyprland) SKIP_HYPRLAND=true;shift;; --skip-hyprland) SKIP_HYPRLAND=true;shift;;
--skip-fish) SKIP_FISH=true;shift;; --skip-fish) SKIP_FISH=true;shift;;
--skip-quickshell) SKIP_QUICKSHELL=true;shift;;
--skip-miscconf) SKIP_MISCCONF=true;shift;; --skip-miscconf) SKIP_MISCCONF=true;shift;;
--skip-plasmaintg) SKIP_PLASMAINTG=true;shift;; --skip-plasmaintg) SKIP_PLASMAINTG=true;shift;;
--exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;; --exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;;
+24 -11
View File
@@ -53,21 +53,27 @@ function ask_backup_configs(){
showfun backup_clashing_targets showfun backup_clashing_targets
printf "${STY_RED}" printf "${STY_RED}"
printf "Would you like to backup clashing dirs/files under \"$XDG_CONFIG_HOME\" and \"$XDG_DATA_HOME\" to \"$BACKUP_DIR\"?" printf "Would you like to backup clashing dirs/files under \"$XDG_CONFIG_HOME\" and \"$XDG_DATA_HOME\" to \"$BACKUP_DIR\"?"
read -p "[y/N] " backup_confirm
case $backup_confirm in
[yY][eE][sS]|[yY])
backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config"
backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share"
;;
*) echo "Skipping backup..." ;;
esac
printf "${STY_RST}" printf "${STY_RST}"
while true;do
echo " y = Yes, backup"
echo " n = No, skip to next"
local p; read -p "====> " p
case $p in
[yY]) echo -e "${STY_BLUE}OK, doing backup...${STY_RST}" ;local backup=true;break ;;
[nN]) echo -e "${STY_BLUE}Alright, skipping...${STY_RST}" ;local backup=false;break ;;
*) echo -e "${STY_RED}Please enter [y/n].${STY_RST}";;
esac
done
if $backup;then
backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config"
backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share"
fi
} }
##################################################################################### #####################################################################################
# In case some dirs does not exists # In case some dirs does not exists
v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME/quickshell $XDG_DATA_HOME
case $ask in case $ask in
false) sleep 0 ;; false) sleep 0 ;;
@@ -84,11 +90,11 @@ esac
# original dotfiles and new ones in the SAME DIRECTORY # original dotfiles and new ones in the SAME DIRECTORY
# (eg. in ~/.config/hypr) won't be mixed together # (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 case $SKIP_MISCCONF in
true) sleep 0;; 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" # i="dots/.config/$i"
echo "[$0]: Found target: 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/" if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/"
@@ -98,6 +104,13 @@ case $SKIP_MISCCONF in
;; ;;
esac esac
case $SKIP_QUICKSHELL in
true) sleep 0;;
*)
warning_rsync; v rsync -av --delete dots/.config/quickshell/ii/ "$XDG_CONFIG_HOME"/quickshell/ii/
;;
esac
case $SKIP_FISH in case $SKIP_FISH in
true) sleep 0;; true) sleep 0;;
*) *)
@@ -1,4 +1,4 @@
_commit='00858812f25b748d08b075a0d284093685fa3ffd' _commit='3e2ce40b18af943f9ba370ed73565e9f487663ef'
# Useful links: # Useful links:
# https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master # https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master
# https://aur.archlinux.org/packages/quickshell-git # https://aur.archlinux.org/packages/quickshell-git
@@ -3,21 +3,17 @@
EAPI=8 EAPI=8
inherit cmake inherit cmake git-r3
DESCRIPTION="Toolkit for building desktop widgets using QtQuick" DESCRIPTION="Toolkit for building desktop widgets using QtQuick"
HOMEPAGE="https://quickshell.org/" HOMEPAGE="https://quickshell.org/"
if [[ "${PV}" = *9999 ]]; then EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git"
inherit git-r3 EGIT_COMMIT="3e2ce40b18af943f9ba370ed73565e9f487663ef"
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
KEYWORDS="~amd64 ~arm64 ~x86"
LICENSE="LGPL-3" LICENSE="LGPL-3"
SLOT="0" SLOT="0"
KEYWORDS="~amd64 ~arm64 ~x86"
# Upstream recommends leaving all build options enabled by default # 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" 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 ) mpris? ( dev-qt/qtdbus )
pam? ( sys-libs/pam ) pam? ( sys-libs/pam )
bluetooth? ( net-wireless/bluez ) bluetooth? ( net-wireless/bluez )
" "
DEPEND="${RDEPEND}" DEPEND="${RDEPEND}"
BDEPEND=" BDEPEND="
|| ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* )
dev-build/cmake
dev-build/ninja
virtual/pkgconfig
dev-cpp/cli11
dev-util/spirv-tools dev-util/spirv-tools
dev-qt/qtshadertools:6 dev-qt/qtshadertools:6
breakpad? ( dev-util/breakpad )
wayland? ( wayland? (
dev-util/wayland-scanner dev-util/wayland-scanner
dev-libs/wayland-protocols dev-libs/wayland-protocols
) )
dev-cpp/cli11
dev-build/ninja
dev-build/cmake
dev-vcs/git
virtual/pkgconfig
breakpad? ( dev-util/breakpad )
" "
src_configure(){ src_configure(){
@@ -15,8 +15,11 @@ DEPEND=""
RDEPEND=" RDEPEND="
gui-apps/fuzzel gui-apps/fuzzel
dev-libs/glib 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 app-i18n/translate-shell
gui-apps/wlogout gui-apps/wlogout
media-gfx/imagemagick
" "
+1 -1
View File
@@ -37,7 +37,7 @@ fi
arch=$(portageq envvar ACCEPT_KEYWORDS) arch=$(portageq envvar ACCEPT_KEYWORDS)
# Exclude hyprland, will deal with that separately # 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" 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-oneui4-icons-git
app-misc/illogical-impulse-portal app-misc/illogical-impulse-portal
app-misc/illogical-impulse-python app-misc/illogical-impulse-python
app-misc/illogical-impulse-quickshell-git
app-misc/illogical-impulse-screencapture app-misc/illogical-impulse-screencapture
app-misc/illogical-impulse-toolkit app-misc/illogical-impulse-toolkit
app-misc/illogical-impulse-widgets app-misc/illogical-impulse-widgets
@@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support **
gui-libs/hyprland-qtutils ** gui-libs/hyprland-qtutils **
gui-wm/hyprland ** gui-wm/hyprland **
x11-libs/libxkbcommon 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 gui-apps/fuzzel png svg
dev-libs/glib dbus elf introspection mime xattr dev-libs/glib dbus elf introspection mime xattr
# ngl idk about nm-connection-editor. Works fine without # 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) #app-i18n/translate-shell (nothing needed)
#gui-apps/wlogout (no use flags) #gui-apps/wlogout (no use flags)
media-gfx/imagemagick xml media-gfx/imagemagick xml
+30 -1
View File
@@ -18,6 +18,9 @@ Syntax:
Subcommands: Subcommands:
install (Default) Install/Reinstall/Update illogical-impulse. install (Default) Install/Reinstall/Update illogical-impulse.
install-deps Run the install step \"1. Install dependencies\"
install-setups Run the install step \"2. Setup for permissions/services etc\"
install-files Run the install step \"3. Copying config files\"
exp-uninstall (Experimental) Uninstall illogical-impulse. exp-uninstall (Experimental) Uninstall illogical-impulse.
exp-update (Experimental) Update illogical-impulse without fully reinstall. exp-update (Experimental) Update illogical-impulse without fully reinstall.
help Show this help message. help Show this help message.
@@ -30,7 +33,8 @@ case $1 in
# Global help # Global help
help|--help|-h)showhelp_global;exit;; help|--help|-h)showhelp_global;exit;;
# Correct subcommand # Correct subcommand
install|exp-uninstall|exp-update)SCRIPT_SUBCOMMAND=$1;shift;; install|install-deps|install-setups|install-files|exp-uninstall|exp-update)
SCRIPT_SUBCOMMAND=$1;shift;;
# No subcommand # No subcommand
-*|"")SCRIPT_SUBCOMMAND=install;; -*|"")SCRIPT_SUBCOMMAND=install;;
# Wrong subcommand # Wrong subcommand
@@ -60,6 +64,31 @@ case ${SCRIPT_SUBCOMMAND} in
fi fi
fi fi
;; ;;
install-deps)
source ./sdata/options/install.sh
if [[ "${SKIP_ALLDEPS}" != true ]]; then
printf "${STY_CYAN}[$0]: 1. Install dependencies\n${STY_RST}"
source ./sdata/step/1.install-deps-selector.sh
fi
;;
install-setups)
source ./sdata/options/install.sh
if [[ "${SKIP_ALLSETUPS}" != true ]]; then
printf "${STY_CYAN}[$0]: 2. Setup for permissions/services etc\n${STY_RST}"
source ./sdata/step/2.install-setups-selector.sh
fi
;;
install-files)
source ./sdata/options/install.sh
if [[ "${SKIP_ALLFILES}" != true ]]; then
printf "${STY_CYAN}[$0]: 3. Copying config files\n${STY_RST}"
if [[ "${EXPERIMENTAL_FILES_SCRIPT}" == true ]]; then
source ./sdata/step/3.install-files.experimental.sh
else
source ./sdata/step/3.install-files.sh
fi
fi
;;
exp-uninstall) exp-uninstall)
source ./sdata/options/exp-uninstall.sh source ./sdata/options/exp-uninstall.sh
source ./sdata/step/exp-uninstall.sh source ./sdata/step/exp-uninstall.sh