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)
bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip
bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback)
bind = Super+Shift, A, global, quickshell:regionSearch # Google Lens
# OCR
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
# Color picker
@@ -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, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
# Zoom with keypad
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
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 # [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: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: {
Config.readWriteDelay = 0;
Config.blockWrites = true;
MaterialThemeLoader.reapplyTheme();
}
@@ -90,8 +91,8 @@ ApplicationWindow {
}
onClicked: {
Quickshell.execDetached(["killall", ...conflictGroup.programs])
conflictGroup.visible = false
conflictGroup.alwaysSelected()
conflictGroup.visible = false
}
}
RippleButton {
@@ -13,7 +13,7 @@ import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import "./cookieClock"
import qs.modules.background.cookieClock
Variants {
id: root
@@ -9,8 +9,8 @@ import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import "./dateIndicator"
import "./minuteMarks"
import qs.modules.background.cookieClock.dateIndicator
import qs.modules.background.cookieClock.minuteMarks
Item {
id: root
@@ -1,4 +1,4 @@
import "./weather"
import qs.modules.bar.weather
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -25,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
onClicked: Hyprland.dispatch("global quickshell:regionScreenshot")
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
@@ -4,7 +4,7 @@ import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import "../"
import qs.modules.bar
StyledPopup {
id: root
@@ -101,4 +101,4 @@ StyledPopup {
}
}
}
}
}
@@ -10,6 +10,7 @@ Singleton {
property alias options: configOptionsJsonAdapter
property bool ready: false
property int readWriteDelay: 50 // milliseconds
property bool blockWrites: false
function setNestedValue(nestedKey, value) {
let keys = nestedKey.split(".");
@@ -63,6 +64,7 @@ Singleton {
id: configFileView
path: root.filePath
watchChanges: true
blockWrites: root.blockWrites
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
@@ -302,6 +304,9 @@ Singleton {
property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000
}
property JsonObject antiFlashbang: JsonObject {
property bool enable: false
}
}
property JsonObject lock: JsonObject {
@@ -349,6 +354,24 @@ Singleton {
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 int updateInterval: 3000
}
@@ -368,6 +391,10 @@ Singleton {
property string shellCommand: "$"
property string webSearch: "?"
}
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property bool useCircleSelection: false
}
}
property JsonObject sidebar: JsonObject {
@@ -454,10 +481,6 @@ Singleton {
property int arbitraryRaceConditionDelay: 20 // milliseconds
}
property JsonObject screenshotTool: JsonObject {
property bool showContentRegions: true
}
property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject {
property bool wallpaper: true
@@ -1,6 +1,6 @@
pragma Singleton
import Quickshell
import "./fuzzysort.js" as FuzzySort
import "fuzzysort.js" as FuzzySort
/**
* Wrapper for FuzzySort to play nicely with Quickshell's imports
@@ -1,6 +1,6 @@
pragma Singleton
import Quickshell
import "./levendist.js" as Levendist
import "levendist.js" as Levendist
/**
* Wrapper for levendist.js to play nicely with Quickshell's imports
@@ -7,6 +7,7 @@ import QtQuick.Controls
RippleButton {
id: root
property string buttonIcon
property alias iconSize: iconWidget.iconSize
Layout.fillWidth: true
implicitHeight: contentItem.implicitHeight + 8 * 2
@@ -17,6 +18,7 @@ RippleButton {
contentItem: RowLayout {
spacing: 10
OptionalMaterialSymbol {
id: iconWidget
icon: root.buttonIcon
opacity: root.enabled ? 1 : 0.4
iconSize: Appearance.font.pixelSize.larger
@@ -22,6 +22,8 @@ Button {
property bool bounce: true
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
property real baseHeight: contentItem.implicitHeight + verticalPadding * 2
property bool enableImplicitWidthAnimation: true
property bool enableImplicitHeightAnimation: true
property real clickedWidth: baseWidth + (isAtSide ? 10 : 20)
property real clickedHeight: baseHeight
property var parentGroup: root.parent
@@ -61,10 +63,12 @@ Button {
}
Behavior on implicitWidth {
enabled: root.enableImplicitWidthAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
enabled: root.enableImplicitHeightAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
@@ -75,7 +79,9 @@ Button {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
property alias mouseArea: buttonMouseArea
MouseArea {
id: buttonMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
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 "./notification_utils.js" as NotificationUtils
import "notification_utils.js" as NotificationUtils
import Qt5Compat.GraphicalEffects
import QtQuick
import Quickshell
@@ -1,7 +1,7 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import "./notification_utils.js" as NotificationUtils
import "notification_utils.js" as NotificationUtils
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -73,12 +73,13 @@ Slider {
component TrackDot: Rectangle {
required property real value
property real normalizedValue: (value - root.from) / (root.to - root.from)
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
height: root.trackDotSize
radius: Appearance.rounding.full
color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted
color: normalizedValue > root.visualPosition ? root.dotColor : root.dotColorHighlighted
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -50,7 +50,7 @@ Rectangle {
property real targetY: root.height / 2 - root.backgroundHeight / 2
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
implicitWidth: 350
implicitHeight: 0
implicitHeight: contentColumn.implicitHeight + dialogBackground.radius * 2
Behavior on implicitHeight {
NumberAnimation {
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
}
ActionToolbarIconButton {
IconToolbarButton {
id: sleepButton
onClicked: Session.suspend()
text: "dark_mode"
}
PasswordGuardedActionToolbarIconButton {
PasswordGuardedIconToolbarButton {
id: powerButton
text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff
}
PasswordGuardedActionToolbarIconButton {
PasswordGuardedIconToolbarButton {
id: rebootButton
text: "restart_alt"
targetAction: LockContext.ActionEnum.Reboot
}
}
component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton {
component PasswordGuardedIconToolbarButton: IconToolbarButton {
id: guardedBtn
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 {
id: pair
required property string icon
@@ -159,7 +159,13 @@ Scope {
}
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
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
@@ -2,7 +2,7 @@ import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "../"
import qs.modules.onScreenDisplay
OsdValueIndicator {
id: root
@@ -1,6 +1,6 @@
import qs.services
import QtQuick
import "../"
import qs.modules.onScreenDisplay
OsdValueIndicator {
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 {
id: root
property string screenshotDir: Directories.screenshotTemp
property color overlayColor: "#77111111"
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
property color genericContentForeground: "#ddffffff"
property color selectionBorderColor: "#ddf1f1f1"
property color selectionFillColor: "#33ffffff"
property color windowBorderColor: "#dda0c0da"
property color windowFillColor: "#22a0c0da"
property color imageBorderColor: "#ddf1d1ff"
property color imageFillColor: "#33f1d1ff"
property color onBorderColor: "#ff000000"
property real standardRounding: 4
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating) return 0;
return a.floating ? -1 : 1;
})
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
function dismiss() {
GlobalStates.regionSelectorOpen = false
}
component TargetRegion: Rectangle {
id: regionRect
property bool showIcon: false
property bool targeted: false
property color borderColor
property color fillColor: "transparent"
property string text: ""
property real textPadding: 10
z: 2
color: fillColor
border.color: borderColor
border.width: targeted ? 3 : 1
radius: root.standardRounding
Rectangle {
id: regionLabelBackground
property real verticalPadding: 5
property real horizontalPadding: 10
radius: 10
color: root.genericContentColor
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
anchors {
top: parent.top
left: parent.left
topMargin: regionRect.textPadding
leftMargin: regionRect.textPadding
}
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
Loader {
id: regionIconLoader
active: regionRect.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing")
}
}
StyledText {
id: regionText
text: regionRect.text
color: root.genericContentForeground
}
}
}
}
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
Variants {
model: Quickshell.screens
@@ -101,478 +31,28 @@ Scope {
required property var modelData
active: GlobalStates.regionSelectorOpen
sourceComponent: PanelWindow {
id: panelWindow
sourceComponent: RegionSelection {
screen: regionSelectorLoader.modelData
visible: false
WlrLayershell.namespace: "quickshell:regionSelector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property real monitorOffsetX: hyprlandMonitor.x
readonly property real monitorOffsetY: hyprlandMonitor.y
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
property real dragStartX: 0
property real dragStartY: 0
property real draggingX: 0
property real draggingY: 0
property real dragDiffX: 0
property real dragDiffY: 0
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
property bool dragging: false
property var mouseButton: null
property var imageRegions: []
readonly property list<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"
}
}
}
}
onDismiss: root.dismiss()
action: root.action
selectionMode: root.selectionMode
}
}
}
function screenshot() {
root.action = RegionSelection.SnipAction.Copy
root.selectionMode = RegionSelection.SelectionMode.RectCorners
GlobalStates.regionSelectorOpen = true
}
function search() {
root.action = RegionSelection.SnipAction.Search
if (Config.options.search.imageSearch.useCircleSelection) {
root.selectionMode = RegionSelection.SelectionMode.Circle
} else {
root.selectionMode = RegionSelection.SelectionMode.RectCorners
}
GlobalStates.regionSelectorOpen = true
}
@@ -582,14 +62,19 @@ Scope {
function screenshot() {
root.screenshot()
}
function search() {
root.search()
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: {
root.screenshot()
}
onPressed: root.screenshot()
}
GlobalShortcut {
name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
}
}
@@ -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
active: {
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;
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 {
icon: "side_navigation"
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 {
icon: "wallpaper_slideshow"
title: Translation.tr("Wallpaper selector")
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./aiChat/"
import qs.modules.sidebarLeft.aiChat
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./anime/"
import qs.modules.sidebarLeft.anime
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -10,7 +10,6 @@ import Quickshell.Hyprland
Scope { // Scope
id: root
property int sidebarPadding: 15
property bool detach: false
property Component contentComponent: SidebarLeftContent {}
property Item sidebarContent
@@ -9,6 +9,7 @@ import Qt5Compat.GraphicalEffects
Item {
id: root
required property var scopeRoot
property int sidebarPadding: 10
anchors.fill: parent
property bool aiChatEnabled: Config.options.policies.ai !== 0
property bool translatorEnabled: Config.options.sidebar.translator.enable
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./translator/"
import qs.modules.sidebarLeft.translator
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -3,8 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../"
import qs.services
import qs.modules.sidebarLeft
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -287,4 +286,4 @@ Rectangle {
}
}
}
}
}
@@ -1,9 +1,9 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import "./calendar"
import "./todo"
import "./pomodoro"
import qs.modules.sidebarRight.calendar
import qs.modules.sidebarRight.todo
import qs.modules.sidebarRight.pomodoro
import QtQuick
import QtQuick.Layouts
@@ -248,4 +248,4 @@ Rectangle {
anchors.margins: 5
}
}
}
}
@@ -1,8 +1,8 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import "./notifications"
import "./volumeMixer"
import qs.modules.sidebarRight.notifications
import qs.modules.sidebarRight.volumeMixer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
@@ -9,21 +9,24 @@ import Quickshell
import Quickshell.Bluetooth
import Quickshell.Hyprland
import "./quickToggles/"
import "./quickToggles/classicStyle/"
import "./wifiNetworks/"
import "./bluetoothDevices/"
import "./volumeMixer/"
import qs.modules.sidebarRight.quickToggles
import qs.modules.sidebarRight.quickToggles.classicStyle
import qs.modules.sidebarRight.bluetoothDevices
import qs.modules.sidebarRight.nightLight
import qs.modules.sidebarRight.volumeMixer
import qs.modules.sidebarRight.wifiNetworks
Item {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12
property int sidebarPadding: 10
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false
property bool showBluetoothDialog: false
property bool showAudioOutputDialog: false
property bool showAudioInputDialog: false
property bool showBluetoothDialog: false
property bool showNightLightDialog: false
property bool showWifiDialog: false
property bool editMode: false
Connections {
@@ -62,7 +65,8 @@ Item {
SystemButtonRow {
Layout.fillHeight: false
Layout.margins: 10
Layout.fillWidth: true
// Layout.margins: 10
Layout.topMargin: 5
Layout.bottomMargin: 0
}
@@ -108,18 +112,20 @@ Item {
}
ToggleDialog {
id: wifiDialogLoader
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
}
ToggleDialog {
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
}
}
ToggleDialog {
id: bluetoothDialogLoader
shownPropertyString: "showBluetoothDialog"
dialog: BluetoothDialog {}
onShownChanged: {
@@ -129,23 +135,21 @@ Item {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
}
}
}
ToggleDialog {
id: audioOutputDialogLoader
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
shownPropertyString: "showNightLightDialog"
dialog: NightLightDialog {}
}
ToggleDialog {
id: audioInputDialogLoader
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
}
}
@@ -185,45 +189,72 @@ Item {
active: Config.options.sidebar.quickToggles.style === styleName
Connections {
target: quickPanelImplLoader.item
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenAudioOutputDialog() {
root.showAudioOutputDialog = true;
}
function onOpenAudioInputDialog() {
root.showAudioInputDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenNightLightDialog() {
root.showNightLightDialog = true;
}
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
}
}
component SystemButtonRow: RowLayout {
spacing: 10
component SystemButtonRow: Item {
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
CustomIcon {
id: distroIcon
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
Item {
Layout.fillWidth: true
Rectangle {
id: uptimeContainer
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
color: Appearance.colors.colLayer1
radius: height / 2
implicitWidth: uptimeRow.implicitWidth + 24
implicitHeight: uptimeRow.implicitHeight + 8
Row {
id: uptimeRow
anchors.centerIn: parent
spacing: 8
CustomIcon {
id: distroIcon
anchors.verticalCenter: parent.verticalCenter
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
}
}
ButtonGroup {
id: systemButtonsRow
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
color: Appearance.colors.colLayer1
padding: 4
QuickToggleButton {
toggled: root.editMode
visible: Config.options.sidebar.quickToggles.style === "android"
@@ -1,7 +1,7 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import "./calendar_layout.js" as CalendarLayout
import "calendar_layout.js" as CalendarLayout
import QtQuick
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
color: Appearance.colors.colLayer1
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
}
@@ -6,30 +6,20 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import "./androidStyle/"
import qs.modules.sidebarRight.quickToggles.androidStyle
AbstractQuickPanel {
id: root
property bool editMode: false
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 {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
property real spacing: 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: {
// This is the wrong calculation, but it looks correct in reality???
// (theoretically spacing should be multiplied by 1 column less)
@@ -38,6 +28,17 @@ AbstractQuickPanel {
}
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) {
var rows = [];
var row = [];
@@ -73,14 +74,14 @@ AbstractQuickPanel {
Repeater {
id: usedRowsRepeater
model: ScriptModel {
values: root.toggleRows
values: Array(root.toggleRows.length)
}
delegate: ButtonGroup {
id: toggleRow
required property var modelData
required property int index
property var modelData: root.toggleRows[index]
property int startingIndex: {
const rows = usedRowsRepeater.model.values;
const rows = root.toggleRows;
let sum = 0;
for (let i = 0; i < index; i++) {
sum += rows[i].length;
@@ -91,7 +92,8 @@ AbstractQuickPanel {
Repeater {
model: ScriptModel {
values: toggleRow.modelData
values: toggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: toggleRow.startingIndex
@@ -99,10 +101,11 @@ AbstractQuickPanel {
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
spacing: root.spacing
onOpenWifiDialog: root.openWifiDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenNightLightDialog: root.openNightLightDialog()
onOpenWifiDialog: root.openWifiDialog()
}
}
}
@@ -131,16 +134,18 @@ AbstractQuickPanel {
Repeater {
model: ScriptModel {
values: root.unusedToggleRows
values: Array(root.unusedToggleRows.length)
}
delegate: ButtonGroup {
id: unusedToggleRow
required property var modelData
required property int index
property var modelData: root.unusedToggleRows[index]
spacing: root.spacing
Repeater {
model: ScriptModel {
values: unusedToggleRow.modelData
values: unusedToggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: -1
@@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import "./classicStyle/"
import qs.modules.sidebarRight.quickToggles.classicStyle
AbstractQuickPanel {
id: root
@@ -19,7 +19,7 @@ AndroidQuickToggleButton {
}
altAction: () => {
Config.options.light.night.automatic = !Config.options.light.night.automatic
root.openMenu()
}
Component.onCompleted: {
@@ -27,7 +27,7 @@ AndroidQuickToggleButton {
}
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
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()
@@ -65,7 +80,7 @@ GroupButton {
MaterialSymbol {
anchors.centerIn: parent
fill: root.toggled ? 1 : 0
iconSize: Appearance.font.pixelSize.huge
iconSize: root.expandedSize ? 22 : 24
color: root.colIcon
text: root.buttonIcon
}
@@ -4,6 +4,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
AndroidQuickToggleButton {
id: root
@@ -22,7 +23,7 @@ AndroidQuickToggleButton {
interval: 300
repeat: false
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 spacing
required property int startingIndex
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
role: "type"
@@ -90,6 +91,9 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openNightLightDialog()
}
} }
DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle {
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../"
import qs.modules.sidebarRight.quickToggles
import qs
import QtQuick
import Quickshell
@@ -14,7 +14,7 @@ GroupButton {
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: 20
iconSize: 22
fill: toggled ? 1 : 0
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignHCenter
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "./../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -8,7 +8,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../bar" as Bar
import qs.modules.bar as Bar
Item { // Bar content region
id: root
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
Item {
id: root
@@ -4,7 +4,7 @@ import qs.services
import QtQuick
import QtQuick.Shapes
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
Item { // Full hitbox
id: root
@@ -8,7 +8,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import "../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -316,7 +316,7 @@ MouseArea {
bottomMargin: 8
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
Wallpapers.openFallbackPicker(root.useDarkMode);
@@ -327,42 +327,27 @@ MouseArea {
GlobalStates.wallpaperSelectorOpen = false;
Config.options.wallpaperSelector.useSystemFileDialog = true
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "open_in_new"
iconSize: Appearance.font.pixelSize.larger
}
text: "open_in_new"
StyledToolTip {
text: Translation.tr("Use the system file picker instead\nRight-click to make this the default behavior")
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
Wallpapers.randomFromCurrentFolder();
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "ifl"
iconSize: Appearance.font.pixelSize.larger
}
text: "ifl"
StyledToolTip {
text: Translation.tr("Pick random from this folder")
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: root.useDarkMode = !root.useDarkMode
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: root.useDarkMode ? "dark_mode" : "light_mode"
iconSize: Appearance.font.pixelSize.larger
}
text: root.useDarkMode ? "dark_mode" : "light_mode"
StyledToolTip {
text: Translation.tr("Click to toggle light/dark mode\n(applied when wallpaper is chosen)")
}
@@ -403,17 +388,12 @@ MouseArea {
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
GlobalStates.wallpaperSelectorOpen = false;
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "cancel_presentation"
iconSize: Appearance.font.pixelSize.larger
}
text: "close"
StyledToolTip {
text: Translation.tr("Cancel wallpaper selection")
}
+1 -1
View File
@@ -7,7 +7,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "./ai/"
import qs.services.ai
/**
* 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.
// License: GPLv3
import qs.modules.common
import qs.modules.common.functions
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
@@ -14,6 +16,7 @@ import QtQuick
*/
Singleton {
id: root
property real minimumBrightnessAllowed: 0.00001 // Setting to 0 would kind of turn off the screen. We don't want that.
signal brightnessChanged()
@@ -84,6 +87,8 @@ Singleton {
}
property int rawMaxBrightness: 100
property real brightness
property real brightnessMultiplier: 1.0
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * brightnessMultiplier))
property bool ready: false
onBrightnessChanged: {
@@ -119,17 +124,23 @@ Singleton {
}
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.startDetached();
}
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;
setTimer.restart();
}
function setBrightnessMultiplier(value: real): void {
monitor.brightnessMultiplier = value;
setTimer.restart();
}
Component.onCompleted: {
initialize();
}
@@ -145,6 +156,61 @@ Singleton {
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 {
target: "brightness"
@@ -4,6 +4,7 @@ import QtQuick
import qs.modules.common
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
/**
* Simple hyprsunset service with automatic mode.
@@ -111,18 +112,28 @@ Singleton {
}
}
function toggle() {
function toggle(active = undefined) {
if (root.manualActive === undefined) {
root.manualActive = root.active;
root.manualActiveHour = root.clockHour;
root.manualActiveMinute = root.clockMinute;
}
root.manualActive = !root.manualActive;
root.manualActive = active !== undefined ? active : !root.manualActive;
if (root.manualActive) {
root.enable();
} else {
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.Io
import QtQuick
import "./network"
import qs.services.network
/**
* Network service with nmcli.
@@ -70,6 +70,8 @@ Singleton {
case "debian":
case "raspbian":
case "kali": distroIcon = "debian-symbolic"; break;
case "funtoo":
case "gentoo": distroIcon = "gentoo-symbolic"; break;
default: distroIcon = "linux-symbolic"; break;
}
if (textOsRelease.toLowerCase().includes("nyarch")) {
@@ -79,7 +79,7 @@ Singleton {
// Special cases
if (!text) return "";
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;
// Normal cases
+20 -20
View File
@@ -7,30 +7,30 @@
//@ pragma Env QT_SCALE_FACTOR=1
import "./modules/common/"
import "./modules/background/"
import "./modules/bar/"
import "./modules/cheatsheet/"
import "./modules/crosshair/"
import "./modules/dock/"
import "./modules/lock/"
import "./modules/mediaControls/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/onScreenKeyboard/"
import "./modules/overview/"
import "./modules/regionSelector/"
import "./modules/screenCorners/"
import "./modules/sessionScreen/"
import "./modules/sidebarLeft/"
import "./modules/sidebarRight/"
import "./modules/verticalBar/"
import "./modules/wallpaperSelector/"
import qs.modules.common
import qs.modules.background
import qs.modules.bar
import qs.modules.cheatsheet
import qs.modules.crosshair
import qs.modules.dock
import qs.modules.lock
import qs.modules.mediaControls
import qs.modules.notificationPopup
import qs.modules.onScreenDisplay
import qs.modules.onScreenKeyboard
import qs.modules.overview
import qs.modules.regionSelector
import qs.modules.screenCorners
import qs.modules.sessionScreen
import qs.modules.sidebarLeft
import qs.modules.sidebarRight
import qs.modules.verticalBar
import qs.modules.wallpaperSelector
import QtQuick
import QtQuick.Window
import Quickshell
import "./services/"
import qs.services
ShellRoot {
// 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 is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file.
# This script is not for direct execution, instead it should be sourced by other script. It does not need execution permission or shebang.
# shellcheck shell=bash
@@ -105,5 +104,5 @@ install-python-packages(){
x uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12
x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
x uv pip install -r sdata/uv/requirements.txt
x deactivate # We don't need the virtual environment anymore
x deactivate
}
+4 -2
View File
@@ -14,11 +14,12 @@ Options for install:
--skip-allsetups Skip the whole process setting up permissions/services etc
--skip-allfiles Skip the whole process copying configuration files
-s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\"
--skip-quickshell Skip installing the config for Quickshell
--skip-hyprland Skip installing the config for Hyprland
--skip-fish Skip installing the config for Fish
--skip-plasmaintg Skip installing plasma-browser-integration
--skip-miscconf Skip copying the dirs and files to \".configs\" except for
AGS, Fish and Hyprland
Quickshell, Fish and Hyprland
--exp-files Use experimental script for the third step copying files
--fontset <set> (Unavailable yet) Use a set of pre-defined font and config
--via-nix (Unavailable yet) Use Nix to install dependencies
@@ -32,7 +33,7 @@ cleancache(){
# `man getopt` to see more
para=$(getopt \
-o hfk:cs \
-l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \
-l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-quickshell,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \
-n "$0" -- "$@")
[ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1
#####################################################################################
@@ -64,6 +65,7 @@ while true ; do
-s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;;
--skip-hyprland) SKIP_HYPRLAND=true;shift;;
--skip-fish) SKIP_FISH=true;shift;;
--skip-quickshell) SKIP_QUICKSHELL=true;shift;;
--skip-miscconf) SKIP_MISCCONF=true;shift;;
--skip-plasmaintg) SKIP_PLASMAINTG=true;shift;;
--exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;;
+24 -11
View File
@@ -53,21 +53,27 @@ function ask_backup_configs(){
showfun backup_clashing_targets
printf "${STY_RED}"
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}"
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
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
false) sleep 0 ;;
@@ -84,11 +90,11 @@ esac
# original dotfiles and new ones in the SAME DIRECTORY
# (eg. in ~/.config/hypr) won't be mixed together
# MISC (For dots/.config/* but not fish, not Hyprland)
# MISC (For dots/.config/* but not quickshell, not fish, not Hyprland)
case $SKIP_MISCCONF in
true) sleep 0;;
*)
for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'quickshell' ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
# i="dots/.config/$i"
echo "[$0]: Found target: dots/.config/$i"
if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/"
@@ -98,6 +104,13 @@ case $SKIP_MISCCONF in
;;
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
true) sleep 0;;
*)
@@ -1,4 +1,4 @@
_commit='00858812f25b748d08b075a0d284093685fa3ffd'
_commit='3e2ce40b18af943f9ba370ed73565e9f487663ef'
# Useful links:
# https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master
# https://aur.archlinux.org/packages/quickshell-git
@@ -3,21 +3,17 @@
EAPI=8
inherit cmake
inherit cmake git-r3
DESCRIPTION="Toolkit for building desktop widgets using QtQuick"
HOMEPAGE="https://quickshell.org/"
if [[ "${PV}" = *9999 ]]; then
inherit git-r3
EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git"
else
SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz"
fi
EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git"
EGIT_COMMIT="3e2ce40b18af943f9ba370ed73565e9f487663ef"
KEYWORDS="~amd64 ~arm64 ~x86"
LICENSE="LGPL-3"
SLOT="0"
KEYWORDS="~amd64 ~arm64 ~x86"
# Upstream recommends leaving all build options enabled by default
IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth"
@@ -38,21 +34,27 @@ RDEPEND="
mpris? ( dev-qt/qtdbus )
pam? ( sys-libs/pam )
bluetooth? ( net-wireless/bluez )
"
DEPEND="${RDEPEND}"
BDEPEND="
|| ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* )
dev-build/cmake
dev-build/ninja
virtual/pkgconfig
dev-cpp/cli11
dev-util/spirv-tools
dev-qt/qtshadertools:6
breakpad? ( dev-util/breakpad )
wayland? (
dev-util/wayland-scanner
dev-libs/wayland-protocols
)
dev-cpp/cli11
dev-build/ninja
dev-build/cmake
dev-vcs/git
virtual/pkgconfig
breakpad? ( dev-util/breakpad )
"
src_configure(){
@@ -15,8 +15,11 @@ DEPEND=""
RDEPEND="
gui-apps/fuzzel
dev-libs/glib
gui-apps/quickshell
media-gfx/imagemagick
gui-apps/hypridle
gui-libs/hyprutils
gui-apps/hyprlock
gui-apps/hyprpicker
app-i18n/translate-shell
gui-apps/wlogout
media-gfx/imagemagick
"
+1 -1
View File
@@ -37,7 +37,7 @@ fi
arch=$(portageq envvar ACCEPT_KEYWORDS)
# Exclude hyprland, will deal with that separately
metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets})
metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,quickshell-git,screencapture,toolkit,widgets})
ebuild_dir="/var/db/repos/localrepo"
+4
View File
@@ -9,6 +9,7 @@ app-misc/illogical-impulse-microtex-git
app-misc/illogical-impulse-oneui4-icons-git
app-misc/illogical-impulse-portal
app-misc/illogical-impulse-python
app-misc/illogical-impulse-quickshell-git
app-misc/illogical-impulse-screencapture
app-misc/illogical-impulse-toolkit
app-misc/illogical-impulse-widgets
@@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support **
gui-libs/hyprland-qtutils **
gui-wm/hyprland **
x11-libs/libxkbcommon
dev-util/breakpad
dev-libs/linux-syscall-support
dev-embedded/libdisasm
+1 -1
View File
@@ -111,7 +111,7 @@ sys-power/upower introspection
gui-apps/fuzzel png svg
dev-libs/glib dbus elf introspection mime xattr
# ngl idk about nm-connection-editor. Works fine without
gui-apps/quickshell -X -i3 -i3-ipc -breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland
gui-apps/quickshell -X -i3 -i3-ipc breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland
#app-i18n/translate-shell (nothing needed)
#gui-apps/wlogout (no use flags)
media-gfx/imagemagick xml
+30 -1
View File
@@ -18,6 +18,9 @@ Syntax:
Subcommands:
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-update (Experimental) Update illogical-impulse without fully reinstall.
help Show this help message.
@@ -30,7 +33,8 @@ case $1 in
# Global help
help|--help|-h)showhelp_global;exit;;
# 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
-*|"")SCRIPT_SUBCOMMAND=install;;
# Wrong subcommand
@@ -60,6 +64,31 @@ case ${SCRIPT_SUBCOMMAND} in
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)
source ./sdata/options/exp-uninstall.sh
source ./sdata/step/exp-uninstall.sh