21 Commits
Author SHA1 Message Date
zach c29b91cd26 Merge pull request 'Screenshot tools features' (#141) from screenshot-picker-improvements into main
Reviewed-on: #141
Reviewed-by: Inorishio <inorishio@gmail.com>
2026-08-11 21:59:32 +02:00
zach 06fb202e36 ctrl + c & ctrl + s shortcuts + screenshot destination dir env var
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 32s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 1m35s
Python / typecheck (pull_request) Failing after 1m12s
Rust / fmt (pull_request) Successful in 38s
Rust / build (pull_request) Successful in 1m34s
Rust / clippy (pull_request) Successful in 1m28s
Python / buildcheck (pull_request) Successful in 2m35s
C++ / clang-tidy (pull_request) Successful in 3m23s
2026-08-11 18:57:36 +02:00
zach ad5fda2718 Merge branch 'main' into screenshot-picker-improvements
C++ / fmt (pull_request) Successful in 7s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Successful in 29s
Python / lint (pull_request) Successful in 27s
Python / test (pull_request) Successful in 50s
Python / typecheck (pull_request) Failing after 48s
Rust / fmt (pull_request) Successful in 38s
C++ / build (pull_request) Successful in 3m0s
Rust / build (pull_request) Successful in 1m40s
Python / buildcheck (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Successful in 1m15s
C++ / clang-tidy (pull_request) Successful in 4m27s
2026-08-11 15:03:12 +02:00
zach 7482741e46 Merge pull request 'Status icons' (#142) from refactor/status-icons into main
Reviewed-on: #142
2026-08-11 15:03:04 +02:00
zach 2e4517b0a6 simpler model for status entries
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 16s
Python / fmt (pull_request) Successful in 29s
Python / lint (pull_request) Successful in 30s
Python / test (pull_request) Successful in 59s
Python / typecheck (pull_request) Failing after 1m7s
C++ / build (pull_request) Successful in 1m51s
Rust / fmt (pull_request) Successful in 40s
Rust / build (pull_request) Successful in 1m36s
Python / buildcheck (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Successful in 1m42s
C++ / clang-tidy (pull_request) Successful in 5m31s
2026-08-11 15:00:44 +02:00
zach d81808dd8f refactor: use entries directly in bar object instead of nested components + various fixes and additions to settings
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 23s
Python / lint (pull_request) Successful in 30s
Python / fmt (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m1s
Python / typecheck (pull_request) Failing after 52s
C++ / build (pull_request) Successful in 1m45s
Rust / fmt (pull_request) Successful in 57s
Rust / build (pull_request) Successful in 1m50s
Rust / clippy (pull_request) Successful in 1m44s
Python / buildcheck (pull_request) Successful in 2m29s
C++ / clang-tidy (pull_request) Successful in 3m31s
2026-08-11 14:05:57 +02:00
zach 47c842941f screenshot tools baked-in: drawing uses optimized qcanvaspainter, direct to clipboard, eliminate forking to swappy
C++ / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 21s
JS/TS / fmt (pull_request) Successful in 20s
Python / lint (pull_request) Successful in 31s
Python / fmt (pull_request) Successful in 33s
Python / typecheck (pull_request) Failing after 1m4s
Python / test (pull_request) Successful in 1m7s
C++ / build (pull_request) Successful in 1m48s
Rust / fmt (pull_request) Successful in 37s
Rust / build (pull_request) Successful in 1m42s
Python / buildcheck (pull_request) Successful in 2m33s
Rust / clippy (pull_request) Successful in 1m32s
C++ / clang-tidy (pull_request) Successful in 3m35s
2026-08-10 12:02:34 +02:00
zach 93c6ac251e Merge pull request 'pr and issue templates + typecheck ci python' (#138) from 135-issue-pr-templates into main
Reviewed-on: #138
Reviewed-by: zach <zach@brohn.se>
2026-07-10 18:19:23 +02:00
zach 1af6bf33ff Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 22s
Python / lint (pull_request) Successful in 31s
Python / fmt (pull_request) Successful in 34s
Python / test (pull_request) Successful in 59s
Python / typecheck (pull_request) Failing after 59s
C++ / build (pull_request) Successful in 2m4s
Rust / fmt (pull_request) Successful in 1m0s
Rust / build (pull_request) Successful in 1m53s
Python / buildcheck (pull_request) Successful in 2m41s
Rust / clippy (pull_request) Successful in 2m31s
C++ / clang-tidy (pull_request) Successful in 6m12s
2026-07-10 15:21:24 +02:00
zach 5eaebacbab remove unrelated directory 2026-07-10 15:00:02 +02:00
AramJonghu 4cb30647a9 hotfix(bugreport): adding optional input field; which init system?
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 15s
Python / lint (pull_request) Successful in 32s
Python / fmt (pull_request) Successful in 35s
Python / test (pull_request) Successful in 1m8s
Python / typecheck (pull_request) Failing after 1m10s
C++ / build (pull_request) Successful in 1m41s
Rust / build (pull_request) Successful in 1m44s
Rust / fmt (pull_request) Successful in 1m5s
Python / buildcheck (pull_request) Successful in 2m39s
Rust / clippy (pull_request) Successful in 2m3s
C++ / clang-tidy (pull_request) Successful in 3m42s
2026-07-10 12:07:27 +02:00
AramJonghu b3820ce1e0 Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 35s
Python / lint (pull_request) Successful in 32s
Python / typecheck (pull_request) Failing after 55s
Python / test (pull_request) Successful in 1m1s
C++ / build (pull_request) Successful in 1m53s
Rust / fmt (pull_request) Successful in 1m13s
Rust / build (pull_request) Successful in 1m48s
Python / buildcheck (pull_request) Successful in 2m43s
Rust / clippy (pull_request) Successful in 2m9s
C++ / clang-tidy (pull_request) Successful in 3m48s
2026-07-10 11:58:35 +02:00
zach d1e3897d02 fix: logo colors 2026-07-10 11:44:01 +02:00
zach 46e24f8543 add initial logo to about page + assets and readme 2026-07-10 11:32:09 +02:00
AramJonghu 1ff7c36723 Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 11s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 30s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 57s
Python / typecheck (pull_request) Failing after 52s
C++ / build (pull_request) Successful in 3m4s
Rust / fmt (pull_request) Successful in 1m1s
Rust / build (pull_request) Successful in 1m49s
Python / buildcheck (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m41s
C++ / clang-tidy (pull_request) Successful in 4m41s
2026-07-10 01:32:39 +02:00
AramJonghu f75fef3f55 chore(template): checklist adjustment
C++ / fmt (pull_request) Successful in 8s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 22s
Python / lint (pull_request) Successful in 32s
Python / fmt (pull_request) Successful in 35s
Python / test (pull_request) Successful in 1m4s
Python / typecheck (pull_request) Failing after 1m3s
C++ / build (pull_request) Successful in 1m55s
Rust / build (pull_request) Successful in 1m54s
Rust / fmt (pull_request) Successful in 1m16s
Python / buildcheck (pull_request) Successful in 2m46s
Rust / clippy (pull_request) Successful in 2m40s
C++ / clang-tidy (pull_request) Successful in 5m36s
2026-07-10 01:16:21 +02:00
AramJonghu 246ee67fb3 update(typecheck): added typecheck in workflow and pyproject
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 34s
Python / lint (pull_request) Successful in 31s
Python / test (pull_request) Successful in 59s
Python / typecheck (pull_request) Failing after 53s
C++ / build (pull_request) Successful in 1m56s
Rust / fmt (pull_request) Successful in 1m18s
Rust / build (pull_request) Successful in 1m50s
Rust / clippy (pull_request) Successful in 2m7s
Python / buildcheck (pull_request) Successful in 2m42s
C++ / clang-tidy (pull_request) Successful in 3m49s
2026-07-10 01:09:08 +02:00
AramJonghu 5b5f38a48d feat(templates): Added template yaml files 2026-07-10 01:09:01 +02:00
zach 1266166676 fix: text input field bounds 2026-07-09 14:20:06 +02:00
AramJonghu 8d4b910e23 Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 29s
Python / test (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 3m25s
Rust / fmt (pull_request) Successful in 1m12s
Python / buildcheck (pull_request) Successful in 2m51s
Rust / build (pull_request) Successful in 2m14s
Rust / clippy (pull_request) Successful in 1m52s
C++ / clang-tidy (pull_request) Successful in 6m7s
2026-07-08 22:14:09 +02:00
AramJonghu 5e78b3b492 init(new branch): New branch
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 1m3s
Lint & Format (Rust) / lint-format (pull_request) Successful in 2m36s
C++ / build (pull_request) Successful in 6m21s
2026-07-04 04:54:28 +02:00
46 changed files with 2480 additions and 745 deletions
+108
View File
@@ -0,0 +1,108 @@
name: Bug Report
about: Something broke
title: "bug: "
labels:
- bug
body:
- type: textarea
id: description
attributes:
label: What happened
placeholder: Describe the bug
validations:
required: true
- type: textarea
id: expected
attributes:
label: What should happen
placeholder: Expected behavior
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
render: shell
validations:
required: true
- type: dropdown
id: os
attributes:
label: OS
options:
- NixOS
- Arch
- Debian / Ubuntu
- Fedora
- Alpine
- Artix
- Gentoo
- Other
validations:
required: false
- type: input
id: other-os
attributes:
label: Other OS (if "Other" above)
placeholder: e.g. Void, Gentoo, FreeBSD
validations:
required: false
- type: dropdown
id: init
attributes:
label: Init system
description: Relevant for UWSM configuration
options:
- systemd
- non-systemd (openrc / runit / s6 / ...)
validations:
required: false
- type: input
id: other-init
attributes:
label: Other init system (if "non-systemd" above)
placeholder: e.g. openrc, runit, s6, dinit
validations:
required: false
- type: input
id: qt-version
attributes:
label: Qt version
placeholder: 6.11.1
validations:
required: false
- type: input
id: commit
attributes:
label: Branch / commit
placeholder: main / abc1234
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logs / backtrace / screenshots
render: shell
validations:
required: false
- type: textarea
id: workaround
attributes:
label: Found a workaround?
validations:
required: false
+51
View File
@@ -0,0 +1,51 @@
name: Feature Request
about: Idea or improvement
title: "feat: "
labels:
- enhancement
body:
- type: textarea
id: problem
attributes:
label: What's missing / annoying
placeholder: The use case or pain point
validations:
required: true
- type: textarea
id: solution
attributes:
label: How should it work
placeholder: Describe the desired behavior
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Other approaches considered
validations:
required: false
- type: dropdown
id: scope
attributes:
label: Area
options:
- Core / Daemon
- UI / Components
- Plugin system
- CMake / Build
- CI / Tooling
- Documentation
- Other
validations:
required: false
- type: textarea
id: context
attributes:
label: Mockups / references / notes
validations:
required: false
+59
View File
@@ -0,0 +1,59 @@
name: Pull Request
about: Submit changes
title: ""
labels: []
body:
- type: textarea
id: description
attributes:
label: What & why
placeholder: Describe the change and motivation
validations:
required: true
- type: input
id: issue
attributes:
label: Related issue
placeholder: "#123"
validations:
required: false
- type: dropdown
id: type
attributes:
label: Type
options:
- Bug fix
- New feature
- Refactor / style
- CI / build / tooling
- Documentation
validations:
required: true
- type: checkboxes
id: tested
attributes:
label: Tested
options:
- label: Build succeeds
- label: Existing tests pass
- label: Manually verified
- type: textarea
id: breaking
attributes:
label: Breaking changes?
placeholder: API/ABI breakage? Migration path?
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: CI passes (format, lint, typecheck)
- label: Clean commits
- label: No new warnings
- label: Docs updated if needed
+23
View File
@@ -81,6 +81,29 @@ jobs:
cd cli cd cli
python -m pytest tests/ -v python -m pytest tests/ -v
typecheck:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir basedpyright
- name: Type check
run: |
. .venv/bin/activate
basedpyright
buildcheck: buildcheck:
runs-on: alpine runs-on: alpine
container: node:26-alpine container: node:26-alpine
+1
View File
@@ -15,3 +15,4 @@ dist/
**/target/ **/target/
**/test-plugins/ **/test-plugins/
**/Charts/ **/Charts/
network-dev/
+1 -1
View File
@@ -35,7 +35,7 @@ CustomRect {
readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor
property real padding property real padding
readonly property alias pressed: stateLayer.pressed readonly property alias pressed: stateLayer.pressed
property real pressedRadius: Appearance.rounding.small property real pressedRadius: Appearance.rounding.smallest
readonly property alias radiusAnim: radiusAnim readonly property alias radiusAnim: radiusAnim
property bool radiusMorph: true property bool radiusMorph: true
property alias shapeMorph: stateLayer.shapeMorph property alias shapeMorph: stateLayer.shapeMorph
+35 -30
View File
@@ -21,8 +21,6 @@ TextFieldBase {
property bool isError property bool isError
property string leadingIcon property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0 readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.extraSmall * 2
property real outlineGapScale: activeFocus || text ? 1 : 0
property int radius: Appearance.rounding.small property int radius: Appearance.rounding.small
readonly property real smallFontScale: smallFontSize / font.pointSize readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Appearance.font.size.small property int smallFontSize: Appearance.font.size.small
@@ -57,11 +55,6 @@ TextFieldBase {
onClicked: root.focus = true onClicked: root.focus = true
} }
} }
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
onEditingFinished: { onEditingFinished: {
if (!valid) if (!valid)
@@ -103,7 +96,7 @@ TextFieldBase {
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text) when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
PropertyChanges { PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + -Appearance.spacing.extraSmall placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
placeholder.scale: root.smallFontScale placeholder.scale: root.smallFontScale
} }
@@ -136,7 +129,7 @@ TextFieldBase {
AnchorAnim { AnchorAnim {
duration: Appearance.anim.durations.expressiveEffects duration: Appearance.anim.durations.expressiveEffects
easing: Appearance.anim.curves.expressiveDefaultEffects easing.bezierCurve: Appearance.anim.curves.expressiveEffects
} }
} }
} }
@@ -146,12 +139,11 @@ TextFieldBase {
active: root.leadingIcon active: root.leadingIcon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Appearance.padding.larger anchors.leftMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.leadingIcon text: root.leadingIcon
} }
} }
@@ -161,12 +153,11 @@ TextFieldBase {
active: root.trailingIcon active: root.trailingIcon
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Appearance.padding.larger anchors.rightMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.trailingIcon text: root.trailingIcon
} }
} }
@@ -201,12 +192,24 @@ TextFieldBase {
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
id: path
readonly property real inset: strokeWidth / 2
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.small * 2
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
capStyle: ShapePath.RoundCap capStyle: ShapePath.RoundCap
fillColor: "transparent" fillColor: "transparent"
startX: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 + root.outlineGap * root.outlineGapScale startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
startY: path.inset
strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline) strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
strokeWidth: root.activeFocus ? 2 : 1 strokeWidth: root.activeFocus ? 2 : 1
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on strokeColor { Behavior on strokeColor {
CAnim { CAnim {
} }
@@ -217,54 +220,56 @@ TextFieldBase {
} }
PathLine { PathLine {
x: bg.width - root.clampedRadius x: bg.width - path.inset - root.clampedRadius
y: path.inset
} }
PathArc { PathArc {
radiusX: root.clampedRadius radiusX: root.clampedRadius
radiusY: root.clampedRadius radiusY: root.clampedRadius
x: bg.width x: bg.width - path.inset
y: root.clampedRadius y: path.inset + root.clampedRadius
} }
PathLine { PathLine {
x: bg.width x: bg.width - path.inset
y: bg.height - root.clampedRadius y: bg.height - path.inset - root.clampedRadius
} }
PathArc { PathArc {
radiusX: root.clampedRadius radiusX: root.clampedRadius
radiusY: root.clampedRadius radiusY: root.clampedRadius
x: bg.width - root.clampedRadius x: bg.width - path.inset - root.clampedRadius
y: bg.height y: bg.height - path.inset
} }
PathLine { PathLine {
x: root.clampedRadius x: path.inset + root.clampedRadius
y: bg.height y: bg.height - path.inset
} }
PathArc { PathArc {
radiusX: root.clampedRadius radiusX: root.clampedRadius
radiusY: root.clampedRadius radiusY: root.clampedRadius
x: 0 x: path.inset
y: bg.height - root.clampedRadius y: bg.height - path.inset - root.clampedRadius
} }
PathLine { PathLine {
x: 0 x: path.inset
y: root.clampedRadius y: path.inset + root.clampedRadius
} }
PathArc { PathArc {
radiusX: root.clampedRadius radiusX: root.clampedRadius
radiusY: root.clampedRadius radiusY: root.clampedRadius
x: root.clampedRadius x: path.inset + root.clampedRadius
y: 0 y: path.inset
} }
PathLine { PathLine {
x: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
y: path.inset
} }
} }
} }
+113 -7
View File
@@ -1,13 +1,119 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import QtQuick
Singleton { Singleton {
readonly property AppearanceConf.Anim anim: Config.appearance.anim property Anim anim: Anim {
readonly property AppearanceConf.Deform deform: Config.appearance.deform }
readonly property AppearanceConf.FontStuff font: Config.appearance.font property Deform deform: Deform {
readonly property AppearanceConf.Padding padding: Config.appearance.padding }
readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding property FontStuff font: FontStuff {
readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing }
readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency property Padding padding: Padding {
}
property Rounding rounding: Rounding {
}
property Spacing spacing: Spacing {
}
property Transparency transparency: Transparency {
}
component Anim: QtObject {
property AnimCurves curves: AnimCurves {
}
property AnimDurations durations: AnimDurations {
}
property real mediaGifSpeedAdjustment: Config.appearance.anim.mediaGifSpeedAdjustment
property real sessionGifSpeed: Config.appearance.anim.sessionGifSpeed
}
component AnimCurves: QtObject {
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
}
component AnimDurations: QtObject {
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
property int expressiveFastEffects: 150 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveSlowEffects: 300 * scale
property int extraLarge: 1000 * scale
property int large: 600 * scale
property int normal: 400 * scale
property real scale: 1
property int small: 200 * scale
}
component Deform: QtObject {
property real scale: Config.appearance.deform.scale
}
component FontFamily: QtObject {
property string clock: Config.appearance.font.family.clock
property string material: Config.appearance.font.family.material
property string mono: Config.appearance.font.family.mono
property string sans: Config.appearance.font.family.sans
}
component FontSize: QtObject {
property int extraLarge: 28 * scale
property int large: 18 * scale
property int larger: 16 * scale
property int medium: 14 * scale
property int normal: 13 * scale
property real scale: 1
property int small: 11 * scale
property int smaller: 12 * scale
}
component FontStuff: QtObject {
property FontFamily family: FontFamily {
}
property FontSize size: FontSize {
}
}
component Padding: QtObject {
property int extraLarge: 28 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int largeIncreased: 20 * scale
property int larger: 12 * scale
property int normal: 8 * scale
property real scale: 1
property int small: 5 * scale
property int smaller: 7 * scale
property int smallest: 2 * scale
}
component Rounding: QtObject {
property int extraSmall: 4 * scale
property int full: 1000 * scale
property int large: 24 * scale
property int medium: 16 * scale
property int normal: 18 * scale
property real scale: 1
property int small: 12 * scale
property int smallest: 8 * scale
}
component Spacing: QtObject {
property int extraSmall: 4 * scale
property int large: 20 * scale
property int larger: 16 * scale
property int normal: 12 * scale
property real scale: 1
property int small: 8 * scale
property int smaller: 10 * scale
}
component Transparency: QtObject {
property real base: Config.appearance.transparency.base
property bool enabled: Config.appearance.transparency.enabled
property real layers: Config.appearance.transparency.layers
}
} }
-81
View File
@@ -7,50 +7,13 @@ JsonObject {
} }
property FontStuff font: FontStuff { property FontStuff font: FontStuff {
} }
property Padding padding: Padding {
}
property Rounding rounding: Rounding {
}
property Spacing spacing: Spacing {
}
property Transparency transparency: Transparency { property Transparency transparency: Transparency {
} }
component Anim: JsonObject { component Anim: JsonObject {
property AnimCurves curves: AnimCurves {
}
property AnimDurations durations: AnimDurations {
}
property real mediaGifSpeedAdjustment: 300 property real mediaGifSpeedAdjustment: 300
property real sessionGifSpeed: 0.7 property real sessionGifSpeed: 0.7
} }
component AnimCurves: JsonObject {
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
}
component AnimDurations: JsonObject {
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
property int expressiveFastEffects: 150 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveSlowEffects: 300 * scale
property int extraLarge: 1000 * scale
property int large: 600 * scale
property int normal: 400 * scale
property real scale: 1
property int small: 200 * scale
}
component Deform: JsonObject { component Deform: JsonObject {
property real scale: 1 property real scale: 1
} }
@@ -60,53 +23,9 @@ JsonObject {
property string mono: "CaskaydiaCove NF" property string mono: "CaskaydiaCove NF"
property string sans: "Segoe UI Variable Text" property string sans: "Segoe UI Variable Text"
} }
component FontSize: JsonObject {
property int extraLarge: 28 * scale
property int large: 18 * scale
property int larger: 16 * scale
property int medium: 14 * scale
property int normal: 13 * scale
property real scale: 1
property int small: 11 * scale
property int smaller: 12 * scale
}
component FontStuff: JsonObject { component FontStuff: JsonObject {
property FontFamily family: FontFamily { property FontFamily family: FontFamily {
} }
property FontSize size: FontSize {
}
}
component Padding: JsonObject {
property int extraLarge: 28 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int largeIncreased: 20 * scale
property int larger: 12 * scale
property int normal: 8 * scale
property real scale: 1
property int small: 5 * scale
property int smaller: 7 * scale
property int smallest: 2 * scale
}
component Rounding: JsonObject {
property int extraSmall: 4 * scale
property int full: 1000 * scale
property int large: 24 * scale
property int medium: 16 * scale
property int normal: 18 * scale
property real scale: 1
property int small: 12 * scale
property int smallest: 8 * scale
}
component Spacing: JsonObject {
property int extraSmall: 4 * scale
property int large: 20 * scale
property int larger: 16 * scale
property int normal: 12 * scale
property real scale: 1
property int small: 8 * scale
property int smaller: 10 * scale
} }
component Transparency: JsonObject { component Transparency: JsonObject {
property real base: 0.85 property real base: 0.85
-7
View File
@@ -4,11 +4,4 @@ import qs.Config
JsonObject { JsonObject {
property bool enabled: true property bool enabled: true
property int wallFadeDuration: MaterialEasing.standardTime property int wallFadeDuration: MaterialEasing.standardTime
property real alignX: 0.5
property real alignY: 0.5
property real zoom: 1.0
property real sourceClipX: 0
property real sourceClipY: 0
property real sourceClipW: 0
property real sourceClipH: 0
} }
+31 -12
View File
@@ -40,6 +40,10 @@ JsonObject {
id: "tray", id: "tray",
enabled: true enabled: true
}, },
{
id: "statusIcons",
enabled: true
},
{ {
id: "network", id: "network",
enabled: false enabled: false
@@ -63,23 +67,38 @@ JsonObject {
} }
component Popouts: JsonObject { component Popouts: JsonObject {
property bool activeWindow: true property bool statusIcons: true
property bool audio: true
property bool clock: true
property bool network: true
property bool resources: true
property bool tray: true property bool tray: true
property bool upower: true
} }
component Tray: JsonObject { component Tray: JsonObject {
property bool recolorIcons: false property bool recolorIcons: false
property bool showAudio: true
property bool showBluetooth: false
property bool showMicrophone: true
property bool showNetwork: false
property bool showOnHover: false property bool showOnHover: false
property bool showPower: true property list<var> statusIcons: [
property bool showWifi: false {
id: "audio",
enabled: true
},
{
id: "microphone",
enabled: true
},
{
id: "bluetooth",
enabled: true
},
{
id: "network",
enabled: false
},
{
id: "wifi",
enabled: false
},
{
id: "power",
enabled: true
},
]
property int trayIconSize: 24 property int trayIconSize: 24
} }
} }
+1
View File
@@ -5,6 +5,7 @@ JsonObject {
property string mode: "manual" property string mode: "manual"
property real radius: 12.0 property real radius: 12.0
property bool rounding: false property bool rounding: false
property bool saveOnCopy: false
property bool shadow: true property bool shadow: true
property real shadow_blur: 22.0 property real shadow_blur: 22.0
property list<int> shadow_color: [0, 0, 0, 160] property list<int> shadow_color: [0, 0, 0, 160]
Regular → Executable
+162 -61
View File
@@ -7,9 +7,10 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers.Picker
import qs.Paths
MouseArea { Item {
id: root id: root
property list<var> clients: { property list<var> clients: {
@@ -29,9 +30,11 @@ MouseArea {
readonly property int cornerRadius: Hypr.options.decoration.rounding readonly property int cornerRadius: Hypr.options.decoration.rounding
property real ex: screen.width property real ex: screen.width
property real ey: screen.height property real ey: screen.height
property color inkColor: "#ff3b30"
required property LazyLoader loader required property LazyLoader loader
property string mode: "select"
property bool onClient property bool onClient
property real realBorderWidth: onClient ? (Hypr.options.general.border_size ?? 1) : 2 property real realBorderWidth: 2
property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0 property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0
property real rsx: Math.min(sx, ex) property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey) property real rsy: Math.min(sy, ey)
@@ -48,6 +51,12 @@ MouseArea {
property real sw: Math.abs(sx - ex) property real sw: Math.abs(sx - ex)
property real sx: 0 property real sx: 0
property real sy: 0 property real sy: 0
property string tool: "move"
function cancel(): void {
annotations.clearAll();
closeAnim.start();
}
function checkClientRects(x: real, y: real): void { function checkClientRects(x: real, y: real): void {
for (const client of clients) { for (const client of clients) {
@@ -71,24 +80,49 @@ MouseArea {
} }
} }
function save(): void { function enterEditMode(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`); mode = "edit";
tool = "move";
}
function save(copyToClipboard: bool): void {
const rounding = root.cornerRadius > 0; const rounding = root.cornerRadius > 0;
const shadow_blur = root.shadowRange / root.shadowRenderPower; const shadow_blur = root.shadowRange / root.shadowRenderPower;
const r = Math.floor(root.shadowColor.r * 256); const r = Math.floor(root.shadowColor.r * 256);
const g = Math.floor(root.shadowColor.g * 256); const g = Math.floor(root.shadowColor.g * 256);
const b = Math.floor(root.shadowColor.b * 256); const b = Math.floor(root.shadowColor.b * 256);
const a = Math.floor(root.shadowColor.a * 256); const a = Math.floor(root.shadowColor.a * 256);
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : []; const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
const cmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : ["swappy", "-f"]; const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path]));
overlay.visible = border.visible = false;
handles.visible = false;
toolbar.visible = false;
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
const finalize = finalPath => {
if (copyToClipboard) {
Quickshell.execDetached(["sh", "-c", `wl-copy --type image/png < '${finalPath.replace(/'/g, `'\\''`)}'`]);
}
if (!copyToClipboard || Config.screenshot.saveOnCopy) {
const dest = `${Paths.screenshots}/zshell-${Date.now()}.png`;
Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname '${dest}')" && cp '${path.replace(/'/g, `'\\''`)}' '${dest}'`]);
}
};
if (postCmd) {
Quickshell.execDetached(postCmd.concat([path]));
finalize(path);
} else {
finalize(path);
}
});
closeAnim.start(); closeAnim.start();
} }
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.CrossCursor
focus: true focus: true
hoverEnabled: true
opacity: 0 opacity: 0
Behavior on opacity { Behavior on opacity {
@@ -97,25 +131,25 @@ MouseArea {
} }
} }
Behavior on rsx { Behavior on rsx {
enabled: !root.pressed enabled: !selectArea.pressed && root.mode === "select"
ExAnim { ExAnim {
} }
} }
Behavior on rsy { Behavior on rsy {
enabled: !root.pressed enabled: !selectArea.pressed && root.mode === "select"
ExAnim { ExAnim {
} }
} }
Behavior on sh { Behavior on sh {
enabled: !root.pressed enabled: !selectArea.pressed && root.mode === "select"
ExAnim { ExAnim {
} }
} }
Behavior on sw { Behavior on sw {
enabled: !root.pressed enabled: !selectArea.pressed && root.mode === "select"
ExAnim { ExAnim {
} }
@@ -123,8 +157,7 @@ MouseArea {
Component.onCompleted: { Component.onCompleted: {
Hypr.extras.refreshOptions(); Hypr.extras.refreshOptions();
if (loader.freeze) screencopy.active = true;
clients = clients;
opacity = 1; opacity = 1;
@@ -144,48 +177,49 @@ MouseArea {
ey = screen.height / 2 + 100; ey = screen.height / 2 + 100;
} }
} }
Keys.onEscapePressed: closeAnim.start() Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start()
onClientsChanged: checkClientRects(mouseX, mouseY) Keys.onPressed: e => {
onPositionChanged: event => { if ((e.key == Qt.Key_C) && (e.modifiers & Qt.ControlModifier)) {
const x = event.x; save(true);
const y = event.y; e.accepted = true;
} else if ((e.key == Qt.Key_S) && (e.modifiers & Qt.ControlModifier)) {
if (pressed) { save(false);
onClient = false; e.accepted = true;
sx = ssx;
sy = ssy;
ex = x;
ey = y;
} else if (!saveTimer.running) {
checkClientRects(x, y);
} }
} }
onPressed: event => { onClientsChanged: {
ssx = event.x; if (root.mode === "select")
ssy = event.y; checkClientRects(selectArea.mouseX, selectArea.mouseY);
} }
onReleased: {
if (closeAnim.running)
return;
if (root.loader.freeze) { MouseArea {
saveTimer.start(); id: selectArea
} else {
overlay.visible = border.visible = false; anchors.fill: parent
screencopy.visible = false; cursorShape: Qt.CrossCursor
screencopy.active = true; enabled: root.mode === "select"
saveTimer.start(); hoverEnabled: true
visible: root.mode === "select"
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (pressed) {
root.onClient = false;
root.sx = root.ssx;
root.sy = root.ssy;
root.ex = x;
root.ey = y;
} else {
root.checkClientRects(x, y);
}
} }
} onPressed: event => {
root.ssx = event.x;
Timer { root.ssy = event.y;
id: saveTimer }
onReleased: root.enterEditMode()
interval: 25
repeat: false
running: false
onTriggered: root.save()
} }
SequentialAnimation { SequentialAnimation {
@@ -231,16 +265,38 @@ MouseArea {
} }
} }
Loader { Item {
id: screencopy id: captureLayer
active: root.loader.freeze
anchors.fill: parent anchors.fill: parent
asynchronous: true
sourceComponent: ScreencopyView { Loader {
captureSource: root.screen id: screencopy
paintCursor: false
active: false
anchors.fill: parent
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
}
}
AnnotationCanvas {
id: annotations
anchors.fill: parent
inkColor: root.inkColor
layer.enabled: true
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
layer.effect: MultiEffect {
maskEnabled: true
maskSource: selectionWrapper
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
} }
} }
@@ -252,7 +308,7 @@ MouseArea {
layer.enabled: true layer.enabled: true
opacity: 0.3 opacity: 0.3
radius: root.realRounding radius: root.realRounding
visible: screencopy.item.hasContent || !root.loader.freeze visible: screencopy.item?.hasContent ?? false
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskEnabled: true maskEnabled: true
@@ -290,7 +346,7 @@ MouseArea {
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: screencopy.item.hasContent || !root.loader.freeze visible: screencopy.item?.hasContent ?? false
x: selectionRect.x - root.realBorderWidth x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth y: selectionRect.y - root.realBorderWidth
@@ -300,6 +356,51 @@ MouseArea {
} }
} }
SelectionHandles {
id: handles
selH: root.sh
selW: root.sw
selX: root.rsx
selY: root.rsy
visible: root.mode === "edit" && root.tool === "move"
onBoundsChanged: (x, y, w, h) => {
root.sx = x;
root.sy = y;
root.ex = x + w;
root.ey = y + h;
}
onMoveBy: (dx, dy) => {
const w = root.sw, h = root.sh;
let nx = root.rsx + dx, ny = root.rsy + dy;
nx = Math.max(0, Math.min(nx, root.screen.width - w));
ny = Math.max(0, Math.min(ny, root.screen.height - h));
root.sx = nx;
root.sy = ny;
root.ex = nx + w;
root.ey = ny + h;
}
}
AnnotationToolbar {
id: toolbar
inkColor: root.inkColor
tool: root.tool
visible: root.mode === "edit"
x: Math.max(8, Math.min(root.rsx + root.sw / 2 - width / 2, root.screen.width - width - 8))
y: (root.rsy + root.sh + height + Appearance.padding.normal <= root.screen.height) ? root.rsy + root.sh + Appearance.padding.normal : (root.rsy - height - Appearance.padding.normal <= 0) ? root.rsy + root.sh - height - Appearance.padding.normal : root.rsy - height - Appearance.padding.normal
onCancelRequested: root.cancel()
onClearRequested: annotations.clearAll()
onColorSelected: c => root.inkColor = c
onCopyRequested: root.save(true)
onSaveRequested: root.save(false)
onToolSelected: t => root.tool = t
onUndoRequested: annotations.undo()
}
component ExAnim: Anim { component ExAnim: Anim {
duration: MaterialEasing.expressiveEffectsTime duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: MaterialEasing.expressiveEffects
+143
View File
@@ -0,0 +1,143 @@
pragma ComponentBehavior: Bound
import QtQuick
import ZShell.Internal
Item {
id: root
property color inkColor: "#ff3b30"
property real inkWidth: 4
property string tool: "none"
signal changed
function clearAll() {
committedCanvas.clear();
previewCanvas.clear();
changed();
}
function sampleShape(tool, x1, y1, x2, y2) {
const pts = [];
switch (tool) {
case "line":
pts.push([x1, y1], [x2, y2]);
break;
case "arrow":
{
const angle = Math.atan2(y2 - y1, x2 - x1);
const headLen = 14 + root.inkWidth * 2;
pts.push([x1, y1], [x2, y2]);
pts.push([x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6)]);
pts.push([x2, y2]);
pts.push([x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6)]);
break;
}
case "rect":
pts.push([x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]);
break;
case "ellipse":
{
const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
const rx = Math.abs(x2 - x1) / 2, ry = Math.abs(y2 - y1) / 2;
const perimeter = Math.PI * (3 * (rx + ry) - Math.sqrt((3 * rx + ry) * (rx + 3 * ry)));
const steps = Math.max(48, Math.min(256, Math.ceil(perimeter / 4)));
for (let i = 0; i <= steps; i++) {
const a = (i / steps) * Math.PI * 2;
pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
}
break;
}
}
return pts;
}
function undo() {
committedCanvas.undoLastGroup();
changed();
}
anchors.fill: parent
StrokeCanvas {
id: committedCanvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
StrokeCanvas {
id: previewCanvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
PointHandler {
id: drawHandler
property real startX
property real startY
property bool started: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.tool !== "none"
onActiveChanged: {
if (active)
return;
if (root.tool === "pen") {
committedCanvas.endStroke();
} else if (started) {
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
if (pts.length > 0) {
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
committedCanvas.endStroke();
}
previewCanvas.clear();
}
started = false;
root.changed();
}
onPointChanged: {
if (!active)
return;
if (point.pressedButtons & Qt.RightButton) {
root.clearAll();
return;
}
const x = point.position.x;
const y = point.position.y;
if (root.tool === "pen") {
if (!started) {
started = true;
committedCanvas.beginStroke(x, y);
return;
}
committedCanvas.appendPoint(x, y);
return;
}
if (!started) {
started = true;
startX = point.pressPosition.x;
startY = point.pressPosition.y;
}
const pts = root.sampleShape(root.tool, startX, startY, x, y);
if (pts.length > 0) {
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
}
}
}
}
+184
View File
@@ -0,0 +1,184 @@
pragma ComponentBehavior: Bound
import ZShell.Components
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
CustomRect {
id: root
readonly property var colorPalette: ["#ff3b30", "#ff9500", "#ffcc00", "#34c759", "#0a84ff", "#af52de", "#ffffff", "#000000"]
property color inkColor: "#ff3b30"
property string tool: "move"
readonly property var toolList: [
{
id: "move",
glyph: "arrows_output"
},
{
id: "pen",
glyph: "draw"
},
{
id: "line",
glyph: "diagonal_line"
},
{
id: "arrow",
glyph: "arrow_outward"
},
{
id: "rect",
glyph: "rectangle"
},
{
id: "ellipse",
glyph: "circle"
}
]
signal cancelRequested
signal clearRequested
signal colorSelected(color c)
signal copyRequested
signal saveRequested
signal toolSelected(string tool)
signal undoRequested
color: DynamicColors.palette.m3surface
height: row.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.full
width: row.implicitWidth + Appearance.padding.normal * 2
RowLayout {
id: row
anchors.centerIn: parent
spacing: 6
ButtonRow {
id: toolRow
spacing: Appearance.spacing.extraSmall
Repeater {
model: root.toolList
delegate: IconButton {
id: tool
required property var modelData
color: root.tool === modelData.id ? "#3584e4" : "transparent"
icon: tool.modelData.glyph
inactiveColor: root.tool === modelData.id ? DynamicColors.palette.m3primary : "transparent"
inactiveOnColor: root.tool === modelData.id ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: shapeMorph && pressed ? 12 : 0
onClicked: root.toolSelected(tool.modelData.id)
}
}
}
CustomRect {
Layout.fillHeight: true
Layout.preferredWidth: 1
color: DynamicColors.palette.m3outlineVariant
}
ButtonRow {
spacing: Appearance.spacing.extraSmall
Repeater {
model: root.colorPalette
delegate: IconButton {
readonly property real buttonSize: toolRow.implicitHeight - Appearance.padding.normal
required property color modelData
fillWidth: false
font.pointSize: 0
icon: ""
implicitHeight: buttonSize
implicitWidth: buttonSize
inactiveColor: modelData
inactiveOnColor: DynamicColors.on(modelData)
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.colorSelected(modelData)
}
}
}
CustomRect {
Layout.fillHeight: true
Layout.preferredWidth: 1
color: DynamicColors.palette.m3outlineVariant
}
ButtonRow {
spacing: Appearance.spacing.extraSmall
IconButton {
icon: "undo"
inactiveColor: "transparent"
inactiveOnColor: DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.undoRequested()
}
IconButton {
icon: "delete_history"
inactiveColor: "transparent"
inactiveOnColor: DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.clearRequested()
}
IconButton {
icon: "content_copy"
inactiveColor: "transparent"
inactiveOnColor: DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.copyRequested()
}
IconButton {
icon: "delete_forever"
inactiveColor: "transparent"
inactiveOnColor: DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.cancelRequested()
}
IconButton {
icon: "check"
inactiveColor: "transparent"
inactiveOnColor: DynamicColors.palette.m3onSurface
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.saveRequested()
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
property color handleBorder: "#3584e4"
property color handleFill: "white"
property real hs: 12
property real minSize: 24
required property real selH
required property real selW
required property real selX
required property real selY
signal boundsChanged(x: real, y: real, w: real, h: real)
signal dragFinished
signal moveBy(dx: real, dy: real)
height: selH
width: selW
x: selX
y: selY
DragHandler {
id: moveHandler
property real accumX: 0
property real accumY: 0
target: null
onActiveChanged: {
accumX = 0;
accumY = 0;
if (!active)
root.dragFinished();
}
onTranslationChanged: {
const t = moveHandler.translation;
root.moveBy(t.x - accumX, t.y - accumY);
accumX = t.x;
accumY = t.y;
}
}
Handle {
edge: "tl"
x: -root.hs / 2
y: -root.hs / 2
}
Handle {
edge: "t"
x: root.width / 2 - root.hs / 2
y: -root.hs / 2
}
Handle {
edge: "tr"
x: root.width - root.hs / 2
y: -root.hs / 2
}
Handle {
edge: "r"
x: root.width - root.hs / 2
y: root.height / 2 - root.hs / 2
}
Handle {
edge: "br"
x: root.width - root.hs / 2
y: root.height - root.hs / 2
}
Handle {
edge: "b"
x: root.width / 2 - root.hs / 2
y: root.height - root.hs / 2
}
Handle {
edge: "bl"
x: -root.hs / 2
y: root.height - root.hs / 2
}
Handle {
edge: "l"
x: -root.hs / 2
y: root.height / 2 - root.hs / 2
}
component Handle: Rectangle {
id: handle
required property string edge
border.color: root.handleBorder
border.width: 2
color: root.handleFill
height: root.hs
radius: root.hs / 2
width: root.hs
z: 10
HoverHandler {
cursorShape: {
switch (handle.edge) {
case "tl":
case "br":
return Qt.SizeFDiagCursor;
case "tr":
case "bl":
return Qt.SizeBDiagCursor;
case "t":
case "b":
return Qt.SizeVerCursor;
default:
return Qt.SizeHorCursor;
}
}
}
DragHandler {
id: dh
property real startH
property real startW
property real startX
property real startY
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
target: null
onActiveChanged: {
if (active) {
startX = root.x;
startY = root.y;
startW = root.width;
startH = root.height;
} else {
root.dragFinished();
}
}
onTranslationChanged: {
const delta = dh.translation;
const e = handle.edge;
let nx = startX, ny = startY, nw = startW, nh = startH;
if (e.includes("l")) {
nx = startX + delta.x;
nw = startW - delta.x;
}
if (e.includes("r")) {
nw = startW + delta.x;
}
if (e.includes("t")) {
ny = startY + delta.y;
nh = startH - delta.y;
}
if (e.includes("b")) {
nh = startH + delta.y;
}
if (nw < root.minSize) {
if (e.includes("l"))
nx = startX + startW - root.minSize;
nw = root.minSize;
}
if (nh < root.minSize) {
if (e.includes("t"))
ny = startY + startH - root.minSize;
nh = root.minSize;
}
root.boundsChanged(nx, ny, nw, nh);
}
}
}
}
+13 -1
View File
@@ -20,7 +20,19 @@ Singleton {
readonly property string timeStr: format("hh:mm:ss") readonly property string timeStr: format("hh:mm:ss")
function format(fmt: string): string { function format(fmt: string): string {
return Qt.formatDateTime(clock.date, fmt); return Qt.formatDateTime(clock.date, resolveFormat(fmt));
}
function resolveFormat(fmt: string): string {
if (!Config.services.useTwelveHourClock)
return fmt;
let f = fmt.replace(/hh/g, "h");
if (/h/.test(f) && !/AP|ap/.test(f))
f += " AP";
return f;
} }
SystemClock { SystemClock {
+68 -67
View File
@@ -12,17 +12,18 @@ import qs.Modules.Updates
RowLayout { RowLayout {
id: root id: root
required property bool fullscreen
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
required property bool fullscreen
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
function checkPopout(x: real): void { function checkPopout(x: real): void {
const ch = childAt(x, height / 2) as WrappedLoader; const ch = childAt(x, height / 2) as EntryWrapper;
const id = ch?.entryId;
if (!ch || ch?.id === "spacer") { if (!ch || id === undefined) {
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover)
popouts.hasCurrent = false; popouts.hasCurrent = false;
return; return;
@@ -31,32 +32,38 @@ RowLayout {
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings)
return; return;
if (ch.id === "tray") { if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const tray = ch.item; const items = (ch.item as StatusIcons).items;
const localPos = tray.mapFromItem(root, x, height / 2); const localX = mapToItem(items, x, 0).x;
const sub = tray.getHoveredSubItem(localPos.x, localPos.y); const icon = items.childAt(localX, items.height / 2);
if (sub) { if (icon) {
popouts.currentName = sub.id; popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
popouts.hasCurrent = true;
}
} else if (id === "tray" && Config.bar.popouts.tray && Config.bar.tray.showOnHover) {
const tray = ch.item as TrayIcons;
const layout = tray.layout;
const localX = mapToItem(layout, x, 0).x;
const trayItem = layout.childAt(localX, layout.height / 2);
if (trayItem) {
const idx = trayItem.index;
popouts.currentName = `traymenu${idx}`;
popouts.currentCenter = Qt.binding(() => { popouts.currentCenter = Qt.binding(() => {
const centerX = sub.item.mapToItem(root, sub.item.width / 2, 0).x; const it = tray.items.itemAt(idx);
return centerX; return it ? it.mapToItem(root, it.implicitWidth / 2, 0).x : popouts.currentCenter;
}); });
popouts.hasCurrent = true; popouts.hasCurrent = true;
return;
} }
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) if (!popouts.currentName.startsWith("traymenu"))
popouts.hasCurrent = false; popouts.hasCurrent = false;
} }
const id = ch.id;
const top = ch.x;
const item = ch.item;
const itemWidth = item.implicitWidth;
if (id === "updates") { if (id === "updates") {
popouts.currentName = "updates"; popouts.currentName = "updates";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.currentCenter = Qt.binding(() => ch.item.mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x);
popouts.hasCurrent = true; popouts.hasCurrent = true;
} }
} }
@@ -66,7 +73,9 @@ RowLayout {
Repeater { Repeater {
id: repeater id: repeater
model: Config.bar.entries model: ScriptModel {
values: Config.bar.entries.filter(e => e.enabled)
}
DelegateChooser { DelegateChooser {
role: "id" role: "id"
@@ -74,8 +83,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "hyprsunset" roleValue: "hyprsunset"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: HyprsunsetWidget { HyprsunsetWidget {
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -84,7 +93,7 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "spacer" roleValue: "spacer"
delegate: WrappedLoader { delegate: EntryWrapper {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
@@ -92,8 +101,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "workspaces" roleValue: "workspaces"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: Workspaces { Workspaces {
screen: root.screen screen: root.screen
visible: !root.fullscreen visible: !root.fullscreen
} }
@@ -103,8 +112,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "tray" roleValue: "tray"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: TrayWidget { TrayIcons {
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visible: !root.fullscreen visible: !root.fullscreen
@@ -112,11 +121,20 @@ RowLayout {
} }
} }
DelegateChoice {
roleValue: "statusIcons"
delegate: EntryWrapper {
StatusIcons {
}
}
}
DelegateChoice { DelegateChoice {
roleValue: "resources" roleValue: "resources"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: Resources { Resources {
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen
} }
@@ -126,8 +144,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "updates" roleValue: "updates"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: UpdatesWidget { UpdatesWidget {
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -136,8 +154,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "notifBell" roleValue: "notifBell"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: NotifBell { NotifBell {
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen
@@ -148,8 +166,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "clock" roleValue: "clock"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: Clock { Clock {
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
@@ -161,8 +179,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "activeWindow" roleValue: "activeWindow"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: WindowTitle { WindowTitle {
bar: root bar: root
visible: !root.fullscreen visible: !root.fullscreen
} }
@@ -172,8 +190,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "network" roleValue: "network"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: NetworkWidget { NetworkWidget {
} }
} }
} }
@@ -181,8 +199,8 @@ RowLayout {
DelegateChoice { DelegateChoice {
roleValue: "media" roleValue: "media"
delegate: WrappedLoader { delegate: EntryWrapper {
sourceComponent: MediaWidget { MediaWidget {
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -190,34 +208,17 @@ RowLayout {
} }
} }
component WrappedLoader: Loader { component EntryWrapper: Item {
required property bool enabled readonly property string entryId: modelData.id
required property string id
required property int index required property int index
default property Item item
function findFirstEnabled(): Item { required property var modelData
const count = repeater.count;
for (let i = 0; i < count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
function findLastEnabled(): Item {
for (let i = repeater.count - 1; i >= 0; i--) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 Layout.leftMargin: index === 0 ? root.vPadding : (entryId === "statusIcons") ? -root.spacing + Appearance.spacing.extraSmall : 0
Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 Layout.rightMargin: index === repeater.count - 1 ? root.vPadding : 0
active: enabled children: item
visible: enabled implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0
} }
} }
@@ -16,9 +16,15 @@ CustomRect {
bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
opacity: enabled && parent.enabled ? 1 : 0.5
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
Behavior on opacity {
Anim {
}
}
CustomRect { CustomRect {
id: highlight id: highlight
+249
View File
@@ -0,0 +1,249 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Blobs
import qs.Components
import qs.Config
Item {
id: root
property bool acceptAllowed: true
required property string acceptLabel
required property Component content
required property string header
property int horizontalContentMargin
required property string icon
required property string label
property bool open
property real openHeight: Math.min(rootParent.height * 0.8, 600)
property real openWidth: Math.min(rootParent.width * 0.8, 400)
required property Item rootParent
property bool separateContent
signal accepted
signal cancelled
function reparentWrapper(): void {
const newParent = open ? rootParent : root;
const pos = dialogWrapper.mapToItem(newParent, 0, 0);
dialogWrapper.parent = newParent;
dialogWrapper.x = pos.x;
dialogWrapper.y = pos.y;
}
Layout.fillWidth: true
implicitHeight: openButton.implicitHeight
z: open || dialogTransition.running ? 2 : 0
BlobGroup {
id: blobGroup
color: root.open ? DynamicColors.palette.m3surfaceContainerHighest : DynamicColors.tPalette.m3surfaceContainer
Behavior on color {
CAnim {
}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
parent: root.open ? root.rootParent : root
onClicked: root.open = false
}
Item {
id: dialogWrapper
height: openButton.implicitHeight
width: root.width
z: 1
states: State {
name: "open"
when: root.open
PropertyChanges {
backdrop.enabled: true
dialogBg.bottomLeftRadius: Appearance.rounding.large
dialogBg.bottomRightRadius: Appearance.rounding.large
dialogBg.radius: Appearance.rounding.large
dialogContent.opacity: 1
dialogWrapper.height: root.openHeight
dialogWrapper.width: root.openWidth
dialogWrapper.x: (root.rootParent.width - root.openWidth) / 2
dialogWrapper.y: (root.rootParent.height - root.openHeight) / 2
elevation.opacity: 1
openButton.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: root.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,radius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
bottomLeftRadius: dialogBg.bottomLeftRadius
bottomRightRadius: dialogBg.bottomRightRadius
level: 4
opacity: 0
radius: dialogBg.radius
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
}
BlobRect {
id: dialogBg
anchors.fill: parent
bottomLeftRadius: Appearance.rounding.large
bottomRightRadius: Appearance.rounding.large
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
radius: Appearance.rounding.extraSmall
}
RowButton {
id: openButton
anchors.left: parent.left
anchors.right: parent.right
color: "transparent"
height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim
icon: root.icon
last: true
text: root.label
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
onClicked: root.open = true
}
Loader {
id: dialogContent
active: opacity > 0
anchors.fill: parent
asynchronous: true
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
ColumnLayout {
anchors.bottomMargin: Appearance.padding.largeIncreased
anchors.fill: parent
anchors.margins: Appearance.padding.extraLarge
spacing: 0
CustomText {
font.pointSize: Appearance.font.size.large
text: root.header
}
Loader {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
active: root.separateContent
sourceComponent: CustomRect {
color: DynamicColors.palette.m3outline
implicitHeight: 1
}
}
Loader {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: root.horizontalContentMargin
Layout.rightMargin: root.horizontalContentMargin
sourceComponent: root.content
}
Loader {
Layout.bottomMargin: Appearance.spacing.normal
Layout.fillWidth: true
active: root.separateContent
sourceComponent: CustomRect {
color: DynamicColors.palette.m3outline
implicitHeight: 1
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: Appearance.spacing.extraSmall
TextButton {
horizontalPadding: Appearance.padding.largeIncreased
isRound: true
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Appearance.padding.normal
onClicked: {
root.cancelled();
root.open = false;
}
}
TextButton {
enabled: root.acceptAllowed
horizontalPadding: Appearance.padding.largeIncreased
isRound: true
text: root.acceptLabel
type: TextButton.Text
verticalPadding: Appearance.padding.normal
onClicked: {
root.accepted();
root.open = false;
}
}
}
}
}
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
}
}
}
@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Config
import qs.Components
DialogRowButton {
id: root
required property var model
property var selectedItem
function keyFor(item: var): string {
return item.id;
}
function labelFor(item: var): string {
return item.label;
}
acceptAllowed: !!selectedItem
horizontalContentMargin: -Appearance.padding.small
separateContent: true
content: Component {
VerticalFadeListView {
bottomMargin: Appearance.padding.large
model: root.model
spacing: 0
topMargin: Appearance.padding.large
delegate: CustomRect {
id: item
required property var modelData
readonly property bool selected: root.selectedItem === root.keyFor(modelData)
anchors.left: ListView.view.contentItem.left
anchors.margins: 1 // Gets cut off for some reason without this
anchors.right: ListView.view.contentItem.right
color: Qt.alpha(DynamicColors.palette.m3tertiaryContainer, selected ? 1 : 0)
implicitHeight: label.implicitHeight + Appearance.padding.normal * 2
radius: stateLayer.pressed ? Appearance.rounding.extraSmall : selected ? Appearance.rounding.large : Appearance.rounding.normal
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
StateLayer {
id: stateLayer
onClicked: root.selectedItem = root.keyFor(item.modelData)
}
CustomText {
id: label
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: item.selected ? checkIcon.left : parent.right
anchors.rightMargin: item.selected ? Appearance.spacing.normal : anchors.margins
anchors.verticalCenter: parent.verticalCenter
color: item.selected ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
font.pointSize: Appearance.font.size.smaller
text: root.labelFor(item.modelData)
}
MaterialIcon {
id: checkIcon
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onTertiaryContainer
font.pointSize: Appearance.font.size.medium
opacity: item.selected ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
}
}
onOpenChanged: {
if (open)
selectedItem = null;
}
}
+317
View File
@@ -0,0 +1,317 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Config
import qs.Components
ListView {
id: root
property bool first
property alias values: valuesModel.values
signal itemMoved(from: int, to: int)
signal itemRemoved(index: int)
signal itemToggled(index: int, checked: bool)
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
return overshoot / (1 + overshoot / maxOvershoot);
}
function labelFor(item: var): string {
return item.label;
}
function toggledFor(item: var): bool {
return false;
}
Layout.fillWidth: true
implicitHeight: contentHeight
interactive: false
spacing: Appearance.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "y"
}
}
Behavior on implicitHeight {
Anim {
}
}
model: DelegateModel {
id: visualModel
delegate: ListRow {
}
model: ScriptModel {
id: valuesModel
}
}
move: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "y"
}
}
remove: Transition {
Anim {
properties: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
component ListRow: Item {
id: item
property bool held
property real lastMoveY
required property var modelData
property int pressIndex
property point pressPos
property real radiusLerpProg
property real topRadius: root?.first && DelegateModel.itemsIndex === 0 ? Appearance.rounding.large : Appearance.rounding.extraSmall
function lerpRadius(a: real, b: real): real {
return a + (b - a) * radiusLerpProg;
}
anchors.left: root?.contentItem.left
anchors.right: root?.contentItem.right
implicitHeight: row.implicitHeight + row.anchors.margins * 2
state: held ? "held" : ""
z: held || returnAnim.running ? 1 : 0
states: State {
name: "held"
PropertyChanges {
dragIcon.color: DynamicColors.palette.m3onPrimaryContainer
elevation.opacity: 1
item.radiusLerpProg: 1
itemBg.color: DynamicColors.palette.m3primaryContainer
label.color: DynamicColors.palette.m3onPrimaryContainer
placeholder.opacity: 0.1
}
}
Behavior on topRadius {
Anim {
type: Anim.DefaultEffects
}
}
transitions: Transition {
Anim {
properties: "opacity,radiusLerpProg"
type: Anim.SlowEffects
}
PropertyAction {
properties: "color,inactiveOnColor"
}
}
CustomRect {
id: placeholder
anchors.fill: parent
color: DynamicColors.palette.m3primary
opacity: 0
radius: Appearance.rounding.extraSmall
topLeftRadius: item.topRadius
topRightRadius: item.topRadius
}
MouseArea {
id: mouse
anchors.fill: parent
preventStealing: true
onPositionChanged: e => {
item.lastMoveY = item.y;
const absY = item.mapToItem(root.contentItem, 0, e.y).y;
const swap = root.itemAt(0, absY);
if (!swap || swap === item)
return;
const idx = item.DelegateModel.itemsIndex;
const swapIdx = swap.DelegateModel.itemsIndex;
visualModel.items.move(idx, swapIdx);
}
onPressed: e => {
returnAnim.stop();
item.pressPos = Qt.point(e.x, e.y);
item.pressIndex = item.DelegateModel.itemsIndex;
item.lastMoveY = item.y;
item.held = true;
itemContent.x = Qt.binding(() => {
if (!root)
return 0;
const maxOvershoot = Appearance.padding.extraExtraLarge;
const x = mouse.mouseX - item.pressPos.x;
return root.dampOvershoot(Math.abs(x), maxOvershoot) * Math.sign(x);
});
itemContent.y = Qt.binding(() => {
if (!root)
return 0;
const maxOvershoot = Appearance.padding.extraLarge;
const yDiff = item.y - item.lastMoveY; // Extra offset if the y of the item changed between mouseY updates
const y = mouse.mouseY - item.pressPos.y - yDiff;
const absY = item.mapToItem(root.contentItem, 0, y).y;
const maxY = root.implicitHeight - item.implicitHeight;
if (absY < 0)
return y - absY - root.dampOvershoot(-absY, maxOvershoot);
if (absY > maxY)
return y - absY + maxY + root.dampOvershoot(absY - maxY, maxOvershoot);
return y;
});
}
onReleased: e => {
// Break bindings
itemContent.x = itemContent.x;
itemContent.y = itemContent.y;
returnAnim.start();
item.held = false;
const idx = item.DelegateModel.itemsIndex;
if (idx !== item.pressIndex)
root.itemMoved(item.pressIndex, idx);
}
}
Anim {
id: returnAnim
properties: "x,y"
target: itemContent
to: 0
}
Item {
id: itemContent
implicitHeight: item.implicitHeight
implicitWidth: item.width
Elevation {
id: elevation
anchors.fill: parent
level: 3
opacity: 0
radius: itemBg.radius
topLeftRadius: itemBg.topLeftRadius
topRightRadius: itemBg.topRightRadius
}
CustomRect {
id: itemBg
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: item.lerpRadius(Appearance.rounding.extraSmall, Appearance.rounding.large)
topLeftRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
topRightRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
}
MouseArea {
id: stateLayer
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: mouse.pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
hoverEnabled: true
CustomRect {
anchors.fill: parent
color: DynamicColors.palette.m3onSurface
opacity: parent.containsMouse && !item.held ? 0.08 : 0
radius: itemBg.radius
topLeftRadius: itemBg.topLeftRadius
topRightRadius: itemBg.topRightRadius
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
RowLayout {
id: row
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.large
spacing: Appearance.spacing.normal
MaterialIcon {
id: dragIcon
color: Qt.alpha(DynamicColors.palette.m3onSurfaceVariant, enabledSwitch.checked ? 1 : 0.5)
font.pointSize: Appearance.font.size.medium
text: "drag_indicator"
}
CustomText {
id: label
Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3onSurface, enabledSwitch.checked ? 1 : 0.5)
elide: Text.ElideRight
font.pointSize: Appearance.font.size.smaller
text: root.labelFor(item.modelData)
}
CustomSwitch {
id: enabledSwitch
checked: root.toggledFor(item.modelData)
font.pointSize: Appearance.font.size.medium
onToggled: root.itemToggled(item.DelegateModel.itemsIndex, checked)
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "delete"
inactiveOnColor: DynamicColors.palette.m3error
isRound: true
label.fill: 0
type: IconButton.Text
onClicked: root.itemRemoved(item.DelegateModel.itemsIndex)
}
}
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import QtQuick
import QtQuick.Shapes
import qs.Config
import qs.Components
Item {
id: root
readonly property real designHeight: 80.419373
readonly property real designWidth: 98.057144
implicitHeight: designHeight
implicitWidth: designWidth
Shape {
id: shape
anchors.centerIn: parent
height: root.designHeight
preferredRendererType: Shape.CurveRenderer
scale: Math.min(root.implicitHeight / height, root.implicitWidth / width)
width: root.designWidth
ShapePath {
strokeColor: "transparent"
fillGradient: LinearGradient {
x1: 0
x2: shape.width
y1: 0
y2: shape.height
GradientStop {
color: DynamicColors.palette.m3primary
position: 0.0
}
GradientStop {
color: DynamicColors.palette.m3tertiary
position: 1.0
}
}
PathSvg {
path: "m 51.981821,0.026059 c -1.39501,-0.04986 -2.77607,-0.02697 -4.1305,0.05839 -9.029544,0.569111 -16.873893,3.910515 -19.747638,6.917924 -4.597996,4.811856 -9.516732,6.095236 -9.516732,6.095236 -4.063345,1.817811 -10.372493,1.390096 -10.372493,1.390096 -1.283162,6.736594 -5.560032,10.051544 -7.377844,12.724804 -1.817812,2.67325 -0.106971,4.38371 -0.106971,4.38371 7.805897,9.40985 5.132504,15.93288 5.132504,15.93288 3.421763,5.13265 5.132503,8.87491 5.132503,8.87491 0.855441,17.21575 24.059513,19.14095 24.059513,19.14095 8.019764,4.38413 21.813738,6.09496 33.041398,3.95635 11.22766,-2.1386 22.13426,-8.66133 25.44909,-12.40389 3.31484,-3.74255 4.70506,-4.70528 4.4912,-7.80625 -0.21386,-3.10098 -8.34057,-6.73654 -8.34057,-6.73654 4.70492,-26.73253 -10.5857,-43.627569 -25.12819,-49.829514 -4.09007,-1.744297 -8.40025,-2.549484 -12.58527,-2.69906 z m 0.0522,6.437333 c 2.11073,-0.04742 4.33196,0.144541 6.65179,0.645956 18.55865,4.011316 31.22355,22.883281 24.70082,47.477201 0,0 0.42763,1.06939 6.62957,4.38423 0,0 -6.09519,11.86918 -29.40596,14.97015 0,0 -16.039134,2.13849 -27.908379,-8.55452 0,0 -4.491338,2.24573 -10.372493,0.53485 0,0 6.094988,-3.3149 4.063317,-8.44755 -2.031672,-5.13264 -10.051071,-6.84383 -11.441162,-7.69927 -1.390091,-0.85544 -2.673221,-2.24534 -2.673221,-2.24534 0,0 8.12664,-1.49702 9.516731,-8.23361 1.390092,-6.7366 -2.994298,-11.01376 -4.812109,-12.29692 -1.817812,-1.28317 -3.421899,-2.5665 -4.491199,-4.59816 0,0 6.415801,-1.069 9.623702,-3.31453 0,0 2.566413,-0.42817 2.994133,9.83712 0,0 3.40241,0.98386 2.780192,-10.05106 0,0 9.369134,-12.076644 24.144268,-12.408547 z m 1.44952,11.296987 c -7.539424,-0.42396 -14.807124,3.94504 -19.819474,11.69747 -8.019756,12.40389 -4.597866,33.04123 15.505004,39.45703 20.10286,6.41581 34.21755,-8.76794 34.21755,-8.76794 0,0 -6.20221,-5.34688 -7.5923,-16.14682 -1.39009,-10.79994 -8.87492,-19.99568 -8.87492,-19.99568 -4.29057,-4.00988 -8.91222,-5.98969 -13.43587,-6.24406 z m 0.43874,3.91759 c 5.90958,0.2395 10.57783,4.70548 11.60704,7.352 1.49702,3.84948 2.88726,10.26522 -3.74241,13.58005 1e-5,-1.81781 1.06895,-2.13837 -0.96273,-4.91856 1.81781,-1.17623 2.99392,-3.10096 1.06918,-5.45342 -1.71088,0.53465 -0.85529,1.38993 -3.7424,-0.42788 -2.88711,-1.81781 -0.85514,-3.10083 -3.31453,-5.1325 0,0 -1.06951,-0.42772 -1.92495,0 -0.85544,0.42772 0.96245,2.88717 -2.56625,4.27726 -3.528694,1.39009 -2.673344,-0.21375 -2.994134,-2.45928 0,0 -4.49095,4.27695 -1.06919,10.37197 3.421764,6.09501 9.730474,-0.10662 7.805734,-1.92443 -1.92474,-1.81782 -2.13858,-2.1386 -4.06332,-1.39009 0,0 0.21415,-3.7426 4.91908,-1.49707 4.70492,2.24553 3.84939,13.90048 -5.77432,12.61732 -9.623714,-1.28316 -13.366163,-15.07666 -3.849384,-22.13405 2.973994,-2.20543 5.916404,-2.97018 8.602584,-2.86132 z m -44.322368,1.76475 c 0.177759,-0.002 0.290475,0.19502 0.325562,0.66869 0.21386,2.88712 7.806121,5.98809 7.485331,12.40389 -0.32079,6.41581 -6.309176,5.77433 -6.309176,5.77433 0.962372,-5.34651 -7.591784,-13.36663 -7.591784,-13.36663 2.592219,-0.26816 5.184343,-5.46781 6.090067,-5.48028 z m 60.847968,11.36158 c 0,0 3.10099,17.10864 5.56039,18.81952 0,0 -4.7046,-3.20747 -7.37785,-1.60352 -2.67325,1.60395 -2.67322,5.23955 -2.67322,6.84351 0,0 -3.31509,-4.27761 -7.5923,-2.4598 -4.2772,1.81781 -11.013604,-2.13842 -11.869044,-5.02553 0,0 9.623444,2.45961 14.435294,-3.52847 0,0 8.98208,-0.85568 9.51673,-13.04571 z"
}
}
}
}
+93
View File
@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
ConnectedRect {
id: root
property alias icon: iconLabel.text
readonly property alias iconLabel: iconLabel
readonly property alias label: label
property alias stateEnabled: stateLayer.enabled
readonly property alias subLabel: subLabel
property alias subtext: subLabel.text
property alias text: label.text
property string trailingIcon
signal clicked(event: MouseEvent)
Layout.fillWidth: true
implicitHeight: row.implicitHeight + Appearance.padding.normal * 2
StateLayer {
id: stateLayer
onClicked: e => root.clicked(e)
}
RowLayout {
id: row
anchors.left: parent.left
anchors.margins: Appearance.padding.largeIncreased
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
opacity: root.enabled ? 1 : 0.5
spacing: Appearance.spacing.normal
Behavior on opacity {
Anim {
}
}
MaterialIcon {
id: iconLabel
color: DynamicColors.palette.m3onSurfaceVariant
fill: 1
font.pointSize: Appearance.font.size.medium
}
Column {
id: column
Layout.fillWidth: true
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
}
CustomText {
id: subLabel
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
visible: text
}
}
Loader {
active: root.trailingIcon
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: root.trailingIcon
}
}
}
}
+12
View File
@@ -62,7 +62,13 @@ CustomSwitch {
f.pointSize = Appearance.font.size.smaller; f.pointSize = Appearance.font.size.smaller;
return f; return f;
} }
opacity: root.enabled ? 1 : 0.5
text: root.text text: root.text
Behavior on opacity {
Anim {
}
}
} }
CustomText { CustomText {
@@ -77,8 +83,14 @@ CustomSwitch {
f.pointSize = Appearance.font.size.small; f.pointSize = Appearance.font.size.small;
return f; return f;
} }
opacity: root.enabled ? 1 : 0.5
text: root.subtext text: root.subtext
visible: root.subtext visible: root.subtext
Behavior on opacity {
Anim {
}
}
} }
} }
} }
+2 -2
View File
@@ -15,9 +15,9 @@ Singleton {
category: "appearance" category: "appearance"
}, },
{ {
name: qsTr("Screenshot"), name: qsTr("Screenshots"),
icon: "screenshot_region", icon: "screenshot_region",
description: qsTr("Set screenshot effects"), description: qsTr("Set screenshot options and effects"),
category: "appearance" category: "appearance"
}, },
+8
View File
@@ -44,6 +44,14 @@ PageBase {
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
width: parent.width - Appearance.padding.largeIncreased * 2 width: parent.width - Appearance.padding.largeIncreased * 2
Logo {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: implicitHeight
Layout.preferredWidth: implicitWidth
implicitHeight: designHeight * 2
implicitWidth: designWidth * 2
}
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Appearance.spacing.small Layout.topMargin: Appearance.spacing.small
@@ -7,6 +7,12 @@ import qs.Modules.Settings.Common
PageBase { PageBase {
id: root id: root
readonly property var builtinIcons: ({
audio: qsTr("Speakers"),
microphone: qsTr("Microphone"),
power: qsTr("Battery")
})
isSubPage: true isSubPage: true
title: qsTr("System icons") title: qsTr("System icons")
@@ -22,77 +28,89 @@ PageBase {
text: qsTr("Visible icons") text: qsTr("Visible icons")
} }
ToggleRow { ListEditor {
checked: Config.bar.tray.showAudio function labelFor(item: var): string {
const prettyName = root.builtinIcons[item.id];
if (prettyName)
return prettyName;
const label = item.id.replace(/([A-Z])/g, " $1");
return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase();
}
function toggledFor(item: var): bool {
return item.enabled;
}
first: true first: true
settingAnchor: "bar-status-speakers" values: Config.bar.tray.statusIcons
text: qsTr("Speakers") z: 1
onToggled: Config.bar.tray.showAudio = checked onItemMoved: (from, to) => {
const icons = Config.bar.tray.statusIcons.slice();
const [moved] = icons.splice(from, 1);
icons.splice(to, 0, moved);
Config.bar.tray.statusIcons = icons;
}
onItemRemoved: index => {
const icons = Config.bar.tray.statusIcons.slice();
icons.splice(index, 1);
Config.bar.tray.statusIcons = icons;
}
onItemToggled: (index, checked) => {
const icons = Config.bar.tray.statusIcons.slice();
icons[index] = Object.assign({}, icons[index], {
enabled: checked
});
Config.bar.tray.statusIcons = icons;
}
} }
ToggleRow { DialogSelectButton {
checked: Config.bar.tray.showMicrophone id: addItemContainer
settingAnchor: "bar-status-mic"
text: qsTr("Microphone")
onToggled: Config.bar.tray.showMicrophone = checked acceptLabel: qsTr("Add")
enabled: {
console.log(Object.keys(model).length);
return Object.keys(model).length > 0;
}
header: qsTr("Add new entry")
icon: "add"
label: qsTr("Add entry")
model: {
const present = new Set(Config.bar.tray.statusIcons.map(item => item.id));
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
id: id,
label: root.builtinIcons[id]
}));
}
rootParent: root.flickable
onAccepted: {
if (!selectedItem)
return;
const icons = Config.bar.tray.statusIcons.slice();
icons.push({
id: selectedItem,
enabled: true
});
Config.bar.tray.statusIcons = icons;
}
} }
//////// FOR LATER: // Behavior
// ToggleRow {
// checked: Config.bar.tray.showNetwork
// text: qsTr("Network")
//
// onToggled: Config.bar.tray.showNetwork = checked
// }
//
// ToggleRow {
// checked: Config.bar.tray.showWifi
// text: qsTr("Wi-Fi")
//
// onToggled: Config.bar.tray.showWifi = checked
// }
//
// ToggleRow {
// checked: Config.bar.tray.showBluetooth
// text: qsTr("Bluetooth")
//
// onToggled: Config.bar.tray.showBluetooth = checked
// }
ToggleRow {
checked: Config.bar.tray.showPower
last: true
settingAnchor: "bar-status-power"
text: qsTr("Battery")
onToggled: Config.bar.tray.showPower = checked
}
// Behaviour
SectionHeader { SectionHeader {
text: qsTr("Behavior") text: qsTr("Behavior")
} }
ToggleRow { ToggleRow {
checked: Config.bar.popouts.audio checked: Config.bar.popouts.statusIcons
first: true first: true
settingAnchor: "bar-status-audio-popout" settingAnchor: "bar-status-popout-on-hover"
subtext: qsTr("Show a details popout when hovering the audio icons") subtext: qsTr("Show a details popout when hovering the status icons")
text: qsTr("Audio popout on hover") text: qsTr("Popout on hover")
onToggled: Config.bar.popouts.audio = checked onToggled: Config.bar.popouts.statusIcons = checked
}
ToggleRow {
checked: Config.bar.popouts.upower
last: true
settingAnchor: "bar-status-power-popout"
subtext: qsTr("Show a details popout when hovering the power icon")
text: qsTr("Power popout on hover")
onToggled: Config.bar.popouts.upower = checked
} }
} }
} }
@@ -28,8 +28,18 @@ PageBase {
onMoved: value => Config.bar.tray.trayIconSize = value onMoved: value => Config.bar.tray.trayIconSize = value
} }
ToggleRow {
checked: Config.bar.popouts.tray
settingAnchor: "bar-tray-enable-tray-popouts"
subtext: Config.bar.popouts.tray ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Enable tray menu popouts")
onToggled: Config.bar.popouts.tray = checked
}
ToggleRow { ToggleRow {
checked: Config.bar.tray.showOnHover checked: Config.bar.tray.showOnHover
enabled: Config.bar.popouts.tray
settingAnchor: "bar-tray-show-popout-on-hover" settingAnchor: "bar-tray-show-popout-on-hover"
subtext: Config.bar.tray.showOnHover ? qsTr("Will show context menu on hover") : qsTr("Will show context menu on right-click") subtext: Config.bar.tray.showOnHover ? qsTr("Will show context menu on hover") : qsTr("Will show context menu on right-click")
text: qsTr("Show popout on hover") text: qsTr("Show popout on hover")
+16 -1
View File
@@ -12,7 +12,7 @@ import qs.Modules.Settings.Common
PageBase { PageBase {
id: root id: root
title: qsTr("Screenshot effects") title: qsTr("Screenshots")
ColumnLayout { ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -22,6 +22,21 @@ PageBase {
SectionHeader { SectionHeader {
first: true first: true
text: qsTr("Behavior")
}
ToggleRow {
checked: Config.screenshot.saveOnCopy
first: true
last: true
settingAnchor: "screenshot-save-on-copy"
subtext: qsTr("Also save screenshot to disk when copying to clipboard")
text: qsTr("Save on copy")
onToggled: Config.screenshot.saveOnCopy = checked
}
SectionHeader {
text: qsTr("Effects") text: qsTr("Effects")
} }
+128
View File
@@ -0,0 +1,128 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Helpers
import qs.Modules.SysTray.Widgets
import qs.Config
import qs.Components
CustomClippingRect {
id: root
readonly property int firstPresent: {
const values = model.values;
for (let i = 0; i < values.length; i++)
if (!collapsed(values[i]))
return i;
return -1;
}
// Index of the first/last entry that isn't collapsed, for edge margin gating
readonly property alias items: row
readonly property int lastPresent: {
const values = model.values;
for (let i = values.length - 1; i >= 0; i--)
if (!collapsed(values[i]))
return i;
return -1;
}
readonly property int spacing: Appearance.spacing.normal / 2
// Entries that can shrink to nothing, spacing included
function collapsed(entry: var): bool {
if (entry.id === "lockStatus")
return !Hypr.capsLock && !Hypr.numLock;
return false;
}
bottomLeftRadius: Appearance.rounding.smallest / 2
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Appearance.padding.smallest * 2
implicitWidth: row.implicitWidth + Appearance.padding.small * 2
radius: Appearance.rounding.full
topLeftRadius: Appearance.rounding.smallest / 2
RowLayout {
id: row
anchors.fill: parent
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
Repeater {
model: ScriptModel {
id: model
values: Config.bar.tray.statusIcons.filter(e => e.enabled)
}
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "audio"
delegate: EntryWrapper {
name: "audio"
AudioWidget {
}
}
}
DelegateChoice {
roleValue: "microphone"
delegate: EntryWrapper {
name: "audio"
MicWidget {
}
}
}
DelegateChoice {
roleValue: "power"
delegate: EntryWrapper {
name: "upower"
UPowerWidget {
}
}
}
}
}
}
component EntryWrapper: Item {
required property int index
default property Item item
property real leftGap: present && index !== root.lastPresent ? margin : 0
property int margin: root.spacing / 2
required property var modelData
property string name: modelData.id.toLowerCase()
readonly property bool present: !root.collapsed(modelData)
property real rightGap: present && index !== root.firstPresent ? margin : 0
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: Math.round(leftGap)
Layout.rightMargin: Math.round(rightGap)
children: item
implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0
Behavior on leftGap {
Anim {
type: Anim.SlowEffects
}
}
Behavior on rightGap {
Anim {
type: Anim.SlowEffects
}
}
}
}
+53
View File
@@ -0,0 +1,53 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
import qs.Components
import qs.Config
import qs.Modules.SysTray.Widgets
import qs.Modules
CustomClippingRect {
id: root
readonly property alias items: repeater
readonly property alias layout: sysRow
required property RowLayout loader
readonly property int padding: Appearance.padding.small
required property Wrapper popouts
bottomRightRadius: Appearance.rounding.smallest / 2
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Appearance.padding.smallest * 2
implicitWidth: sysRow.implicitWidth + Appearance.padding.small * 2
radius: Appearance.rounding.full
topRightRadius: Appearance.rounding.smallest / 2
RowLayout {
id: sysRow
anchors.centerIn: parent
spacing: 0
Repeater {
id: repeater
model: SystemTray.items
TrayItem {
id: trayItem
required property int index
required property SystemTrayItem modelData
implicitHeight: 34
implicitWidth: 34
ind: index
item: modelData
loader: root.loader
popouts: root.popouts
}
}
}
}
+12 -24
View File
@@ -44,15 +44,20 @@ RowLayout {
function getHoveredSubItem(localX, localY) { function getHoveredSubItem(localX, localY) {
let modPos = mapToItem(sysTrayMod, localX, localY); let modPos = mapToItem(sysTrayMod, localX, localY);
if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) { if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) {
let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y); let modRowPos = sysTrayMod.mapToItem(sysIcons, modPos.x, modPos.y);
let child = closestRowChild(sysModRow, modRowPos.x); let child = closestRowChild(sysIcons, modRowPos.x);
if (child) { if (child) {
if (child.objectName === "audioWidget" && Config.bar.popouts.audio) if (child.objectName === "audio" && Config.bar.popouts.audio)
return { return {
id: "audio", id: "audio",
item: child item: child
}; };
if (child.objectName === "upowerWidget" && Config.bar.popouts.upower) if (child.objectName === "microphone" && Config.bar.popouts.audio)
return {
id: "audio",
item: child
};
if (child.objectName === "upower" && Config.bar.popouts.upower)
return { return {
id: "upower", id: "upower",
item: child item: child
@@ -123,33 +128,16 @@ RowLayout {
Layout.fillHeight: true Layout.fillHeight: true
bottomLeftRadius: Appearance.rounding.smallest / 2 bottomLeftRadius: Appearance.rounding.smallest / 2
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: sysModRow.implicitWidth + Appearance.padding.smaller + Appearance.padding.normal implicitWidth: sysIcons.implicitWidth + Appearance.padding.smaller + Appearance.padding.normal
radius: Appearance.rounding.full radius: Appearance.rounding.full
topLeftRadius: Appearance.rounding.smallest / 2 topLeftRadius: Appearance.rounding.smallest / 2
RowLayout { StatusIcons {
id: sysModRow id: sysIcons
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
anchors.rightMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.normal
AudioWidget {
Layout.fillHeight: true
Layout.rightMargin: hasContent ? Appearance.spacing.extraSmall / 2 : 0
objectName: "audioWidget"
Behavior on Layout.rightMargin {
Anim {
type: Anim.FastEffects
}
}
}
UPowerWidget {
Layout.fillHeight: true
objectName: "upowerWidget"
}
} }
} }
} }
+14 -61
View File
@@ -7,72 +7,25 @@ import qs.Modules
import qs.Config import qs.Config
import qs.Components import qs.Components
RowLayout { MaterialIcon {
id: root id: speaker
property color barColor: DynamicColors.palette.m3primary animate: true
readonly property bool hasContent: mic.visible || speaker.visible color: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface
property color textColor: DynamicColors.palette.m3onSurface fill: 1
font.pointSize: Appearance.font.size.larger
text: Audio.muted ? "volume_off" : "volume_up"
width: hasContent ? implicitWidth : 0 Behavior on Layout.maximumWidth {
Anim {
MaterialIcon {
id: mic
readonly property bool shouldBeVisible: Config.bar.tray.showMicrophone
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: shouldBeVisible ? implicitWidth : 0
animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor
fill: 1
font.pointSize: Appearance.font.size.larger
opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0
text: Audio.sourceMuted ? "mic_off" : "mic"
visible: opacity > 0
Behavior on Layout.maximumWidth {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
} }
} }
Behavior on opacity {
MaterialIcon { Anim {
id: speaker
readonly property bool shouldBeVisible: Config.bar.tray.showAudio
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: shouldBeVisible ? implicitWidth : 0
animate: true
color: Audio.muted ? DynamicColors.palette.m3error : root.textColor
fill: 1
font.pointSize: Appearance.font.size.larger
opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0
text: Audio.muted ? "volume_off" : "volume_up"
visible: opacity > 0
Behavior on Layout.maximumWidth {
Anim {
}
} }
Behavior on opacity { }
Anim { Behavior on scale {
} Anim {
}
Behavior on scale {
Anim {
}
} }
} }
} }
+31
View File
@@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Daemons
import qs.Modules
import qs.Config
import qs.Components
MaterialIcon {
id: mic
animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface
fill: 1
font.pointSize: Appearance.font.size.larger
text: Audio.sourceMuted ? "mic_off" : "mic"
Behavior on Layout.maximumWidth {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
}
-7
View File
@@ -8,15 +8,8 @@ import qs.Helpers
Item { Item {
id: root id: root
readonly property bool shouldBeActive: Config.bar.tray.showPower
Layout.preferredHeight: shouldBeActive ? implicitHeight : 0
Layout.preferredWidth: shouldBeActive ? implicitWidth : 0
implicitHeight: Battery.isLaptop ? batteryIconLoader.item.implicitHeight : upowerIconLoader.item.implicitHeight implicitHeight: Battery.isLaptop ? batteryIconLoader.item.implicitHeight : upowerIconLoader.item.implicitHeight
implicitWidth: Battery.isLaptop ? batteryIconLoader.item.implicitWidth : upowerIconLoader.item.implicitWidth implicitWidth: Battery.isLaptop ? batteryIconLoader.item.implicitWidth : upowerIconLoader.item.implicitWidth
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0
visible: opacity > 0
Behavior on Layout.preferredHeight { Behavior on Layout.preferredHeight {
Anim { Anim {
+1
View File
@@ -17,6 +17,7 @@ Singleton {
readonly property string notifimagecache: `${imagecache}/notifs` readonly property string notifimagecache: `${imagecache}/notifs`
readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures` readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures`
readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings` readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings`
readonly property string screenshots: Quickshell.env("ZSHELL_SCREENSHOTS_DIR") || `${home}/Pictures/Screenshots`
readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell` readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell`
readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos`
readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.general.wallpaperPath) readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.general.wallpaperPath)
+1
View File
@@ -14,6 +14,7 @@ struct Stroke {
qreal width; qreal width;
int groupId = -1; int groupId = -1;
bool isSinglePoint = false; bool isSinglePoint = false;
bool isPolyline = false;
}; };
}; // namespace ZShell::internal }; // namespace ZShell::internal
+31 -4
View File
@@ -25,7 +25,8 @@ static bool shouldAddPoint(
return QPointF::dotProduct(delta, delta) >= minDistance * minDistance; return QPointF::dotProduct(delta, delta) >= minDistance * minDistance;
} }
static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width) { static QCanvasPath buildStrokePath(
const QVector<QPointF>& points, qreal width, bool polyline) {
QCanvasPath path; QCanvasPath path;
if (points.size() == 1) { if (points.size() == 1) {
@@ -33,6 +34,13 @@ static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width)
return path; return path;
} }
if (polyline) {
path.moveTo(points[0]);
for (int i = 1; i < points.size(); i++)
path.lineTo(points[i]);
return path;
}
auto catmullToBezier = [](const QPointF& p0, auto catmullToBezier = [](const QPointF& p0,
const QPointF& p1, const QPointF& p1,
const QPointF& p2, const QPointF& p2,
@@ -61,6 +69,16 @@ static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width)
return path; return path;
} }
void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId;
for (int i = m_strokes.size() - 1;
i >= 0 && m_strokes[i].groupId == lastGroup;
--i)
m_strokes.remove(i);
update();
}
void StrokeCanvasItem::setPenColor(const QColor& color) { void StrokeCanvasItem::setPenColor(const QColor& color) {
if (m_penColor == color) return; if (m_penColor == color) return;
@@ -121,13 +139,14 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
emit penWidthChanged(); emit penWidthChanged();
} }
void StrokeCanvasItem::beginStroke(qreal x, qreal y) { void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) {
m_isDrawing = true; m_isDrawing = true;
m_currentStroke.points.clear(); m_currentStroke.points.clear();
m_currentStroke.points.append({x, y}); m_currentStroke.points.append({x, y});
m_currentStroke.color = m_penColor; m_currentStroke.color = m_penColor;
m_currentStroke.width = m_penWidth; m_currentStroke.width = m_penWidth;
m_currentStroke.isPolyline = polyline;
update(); update();
} }
@@ -135,6 +154,12 @@ void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
void StrokeCanvasItem::appendPoint(qreal x, qreal y) { void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
const QPointF incoming{x, y}; const QPointF incoming{x, y};
if (m_currentStroke.isPolyline) {
m_currentStroke.points.append(incoming);
update();
return;
}
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return; if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return;
QPointF smoothed; QPointF smoothed;
@@ -166,8 +191,10 @@ void StrokeCanvasItem::endStroke() {
if (m_currentStroke.points.isEmpty()) return; if (m_currentStroke.points.isEmpty()) return;
m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1); m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1);
m_currentStroke.path = m_currentStroke.path = buildStrokePath(
buildStrokePath(m_currentStroke.points, m_currentStroke.width); m_currentStroke.points,
m_currentStroke.width,
m_currentStroke.isPolyline);
m_currentStroke.groupId = m_nextGroupId++; m_currentStroke.groupId = m_nextGroupId++;
m_currentStroke.points.clear(); m_currentStroke.points.clear();
m_strokes.append(m_currentStroke); m_strokes.append(m_currentStroke);
+2 -1
View File
@@ -39,6 +39,7 @@ class StrokeCanvasItem : public QCanvasPainterItem {
Q_INVOKABLE void showHover(qreal x, qreal y); Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void hideHover(); Q_INVOKABLE void hideHover();
Q_INVOKABLE void undoLastGroup();
[[nodiscard]] QColor penColor() const { return m_penColor; } [[nodiscard]] QColor penColor() const { return m_penColor; }
[[nodiscard]] qreal penWidth() const { return m_penWidth; } [[nodiscard]] qreal penWidth() const { return m_penWidth; }
@@ -48,7 +49,7 @@ class StrokeCanvasItem : public QCanvasPainterItem {
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE void beginStroke(qreal x, qreal y); Q_INVOKABLE void beginStroke(qreal x, qreal y, bool polyline = false);
Q_INVOKABLE void appendPoint(qreal x, qreal y); Q_INVOKABLE void appendPoint(qreal x, qreal y);
Q_INVOKABLE void endStroke(); Q_INVOKABLE void endStroke();
@@ -8,7 +8,8 @@ static void drawStroke(
QCanvasPainter* painter, QCanvasPainter* painter,
const QVector<QPointF>& points, const QVector<QPointF>& points,
const QColor& color, const QColor& color,
qreal width) { qreal width,
bool polyline) {
if (points.isEmpty()) return; if (points.isEmpty()) return;
painter->setStrokeStyle(color); painter->setStrokeStyle(color);
@@ -17,6 +18,15 @@ static void drawStroke(
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round);
if (polyline) {
painter->beginPath();
painter->moveTo(points[0]);
for (int i = 1; i < points.size(); i++)
painter->lineTo(points[i]);
painter->stroke();
return;
}
if (points.size() == 1) { if (points.size() == 1) {
painter->beginPath(); painter->beginPath();
painter->circle(points.front(), width * 0.5); painter->circle(points.front(), width * 0.5);
@@ -180,7 +190,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
painter, painter,
m_currentStroke.points, m_currentStroke.points,
m_currentStroke.color, m_currentStroke.color,
m_currentStroke.width); m_currentStroke.width,
m_currentStroke.isPolyline);
if (m_hoverVisible) if (m_hoverVisible)
drawHoverCursor( drawHoverCursor(
+13 -9
View File
@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="/images/shell.png" alt="ZShell screenshot"> <img src="./assets/logo.svg" alt="ZShell logo" width="480">
</div> </div>
# ZShell # ZShell
@@ -8,6 +8,10 @@ ZShell is a Hyprland-focused desktop shell built with Quickshell + Qt6/QML.
It includes a configurable top bar, launcher, notifications daemon + sidebar, wallpaper and dynamic color pipeline, lock screen, OSD widgets, dashboard, dock, desktop icons, and a separate greetd-compatible greeter. It includes a configurable top bar, launcher, notifications daemon + sidebar, wallpaper and dynamic color pipeline, lock screen, OSD widgets, dashboard, dock, desktop icons, and a separate greetd-compatible greeter.
<div align="center">
<img src="/images/shell.png" alt="ZShell screenshot">
</div>
## Highlights ## Highlights
- Multi-window shell layout driven by `Drawers/Windows.qml` and panel/interactions wrappers - Multi-window shell layout driven by `Drawers/Windows.qml` and panel/interactions wrappers
@@ -218,14 +222,14 @@ Action-driven flows (`>` prefix by default) include calculator, wallpaper picker
### `shell` — daemon management ### `shell` — daemon management
| Command | Description | | Command | Description |
|---------|-------------| | --------- | ---------------------------------------------------------------- |
| `start` | Start the shell daemon (pass `--no-daemon` to run in foreground) | | `start` | Start the shell daemon (pass `--no-daemon` to run in foreground) |
| `kill` | Kill the running shell daemon | | `kill` | Kill the running shell daemon |
| `restart` | Kill then restart the daemon | | `restart` | Kill then restart the daemon |
| `lock` | Lock the session via IPC | | `lock` | Lock the session via IPC |
| `show` | Show the shell window via IPC | | `show` | Show the shell window via IPC |
| `log` | Print daemon logs | | `log` | Print daemon logs |
### `scheme` — color scheme generation ### `scheme` — color scheme generation
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="370.60901"
height="370.60901"
viewBox="0 0 98.056968 98.056968"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:export-filename="logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><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:document-units="px"
showborder="false"><inkscape:page
x="0"
y="0"
width="98.056969"
height="98.056969"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><style
id="theme-style">
.grad-stop-1 { stop-color: #9898ff; stop-opacity: 1; }
.grad-stop-2 { stop-color: #ff9ac3; stop-opacity: 1; }
@media (prefers-color-scheme: light) {
.grad-stop-1 { stop-color: #000080; stop-opacity: 1; }
.grad-stop-2 { stop-color: #800037; stop-opacity: 1; }
}
</style><linearGradient
id="linearGradient7"
inkscape:collect="always"><stop
class="grad-stop-1"
offset="0.00635612"
id="stop7" /><stop
class="grad-stop-2"
offset="1"
id="stop8" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7"
id="linearGradient8"
x1="52.200813"
y1="75.440575"
x2="150.25795"
y2="155.85995"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-8.4478442e-5,8.8187943)" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-52.200813,-75.440577)"><path
id="path2"
style="fill:url(#linearGradient8);stroke-width:0.264583"
d="m 104.18255,84.28543 c -1.39501,-0.04986 -2.77607,-0.02697 -4.1305,0.05839 -9.029544,0.569111 -16.873893,3.910515 -19.747638,6.917924 -4.597996,4.811856 -9.516732,6.095236 -9.516732,6.095236 -4.063345,1.817811 -10.372493,1.390096 -10.372493,1.390096 -1.283162,6.736594 -5.560032,10.051544 -7.377844,12.724804 -1.817812,2.67325 -0.106971,4.38371 -0.106971,4.38371 7.805897,9.40985 5.132504,15.93288 5.132504,15.93288 3.421763,5.13265 5.132503,8.87491 5.132503,8.87491 0.855441,17.21575 24.059513,19.14095 24.059513,19.14095 8.019764,4.38413 21.813738,6.09496 33.041398,3.95635 11.22766,-2.1386 22.13426,-8.66133 25.44909,-12.40389 3.31484,-3.74255 4.70506,-4.70528 4.4912,-7.80625 -0.21386,-3.10098 -8.34057,-6.73654 -8.34057,-6.73654 4.70492,-26.73253 -10.5857,-43.627569 -25.12819,-49.829514 -4.09007,-1.744297 -8.40025,-2.549484 -12.58527,-2.69906 z m 0.0522,6.437333 c 2.11073,-0.04742 4.33196,0.144541 6.65179,0.645956 18.55865,4.011316 31.22355,22.883281 24.70082,47.477201 0,0 0.42763,1.06939 6.62957,4.38423 0,0 -6.09519,11.86918 -29.40596,14.97015 0,0 -16.039134,2.13849 -27.908379,-8.55452 0,0 -4.491338,2.24573 -10.372493,0.53485 0,0 6.094988,-3.3149 4.063317,-8.44755 -2.031672,-5.13264 -10.051071,-6.84383 -11.441162,-7.69927 -1.390091,-0.85544 -2.673221,-2.24534 -2.673221,-2.24534 0,0 8.12664,-1.49702 9.516731,-8.23361 1.390092,-6.7366 -2.994298,-11.01376 -4.812109,-12.29692 -1.817812,-1.28317 -3.421899,-2.5665 -4.491199,-4.59816 0,0 6.415801,-1.069 9.623702,-3.31453 0,0 2.566413,-0.42817 2.994133,9.83712 0,0 3.40241,0.98386 2.780192,-10.05106 0,0 9.369134,-12.076644 24.144268,-12.408547 z m 1.44952,11.296987 c -7.539424,-0.42396 -14.807124,3.94504 -19.819474,11.69747 -8.019756,12.40389 -4.597866,33.04123 15.505004,39.45703 20.10286,6.41581 34.21755,-8.76794 34.21755,-8.76794 0,0 -6.20221,-5.34688 -7.5923,-16.14682 -1.39009,-10.79994 -8.87492,-19.99568 -8.87492,-19.99568 -4.29057,-4.00988 -8.91222,-5.98969 -13.43587,-6.24406 z m 0.43874,3.91759 c 5.90958,0.2395 10.57783,4.70548 11.60704,7.352 1.49702,3.84948 2.88726,10.26522 -3.74241,13.58005 1e-5,-1.81781 1.06895,-2.13837 -0.96273,-4.91856 1.81781,-1.17623 2.99392,-3.10096 1.06918,-5.45342 -1.71088,0.53465 -0.85529,1.38993 -3.7424,-0.42788 -2.88711,-1.81781 -0.85514,-3.10083 -3.31453,-5.1325 0,0 -1.06951,-0.42772 -1.92495,0 -0.85544,0.42772 0.96245,2.88717 -2.56625,4.27726 -3.528694,1.39009 -2.673344,-0.21375 -2.994134,-2.45928 0,0 -4.49095,4.27695 -1.06919,10.37197 3.421764,6.09501 9.730474,-0.10662 7.805734,-1.92443 -1.92474,-1.81782 -2.13858,-2.1386 -4.06332,-1.39009 0,0 0.21415,-3.7426 4.91908,-1.49707 4.70492,2.24553 3.84939,13.90048 -5.77432,12.61732 -9.623714,-1.28316 -13.366163,-15.07666 -3.849384,-22.13405 2.973994,-2.20543 5.916404,-2.97018 8.602584,-2.86132 z m -44.322368,1.76475 c 0.177759,-0.002 0.290475,0.19502 0.325562,0.66869 0.21386,2.88712 7.806121,5.98809 7.485331,12.40389 -0.32079,6.41581 -6.309176,5.77433 -6.309176,5.77433 0.962372,-5.34651 -7.591784,-13.36663 -7.591784,-13.36663 2.592219,-0.26816 5.184343,-5.46781 6.090067,-5.48028 z m 60.847968,11.36158 c 0,0 3.10099,17.10864 5.56039,18.81952 0,0 -4.7046,-3.20747 -7.37785,-1.60352 -2.67325,1.60395 -2.67322,5.23955 -2.67322,6.84351 0,0 -3.31509,-4.27761 -7.5923,-2.4598 -4.2772,1.81781 -11.013604,-2.13842 -11.869044,-5.02553 0,0 9.623444,2.45961 14.435294,-3.52847 0,0 8.98208,-0.85568 9.51673,-13.04571 z" /></g></svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

-310
View File
@@ -1,310 +0,0 @@
#!/bin/bash
# =============================================================================
# wifi-sim.sh — Virtual WiFi environment for development & testing
#
# Creates 3 dummy networks using mac80211_hwsim + hostapd:
# - FreeWifi (open, strong signal, ch 1)
# - OfficeNetwork (open, medium signal, ch 6)
# - HomeNetwork (WPA2, weak signal, ch 11)
#
# Usage:
# sudo ./wifi-sim.sh start
# sudo ./wifi-sim.sh stop
# sudo ./wifi-sim.sh status
# =============================================================================
set -euo pipefail
# --- User configuration ------------------------------------------------------
SSID_STRONG="FreeWifi" # Open network, strong signal
SSID_MEDIUM="OfficeNetwork" # Open network, medium signal
SSID_SECURE="HomeNetwork" # WPA2 network, weak signal
WIFI_PASSWORD="testpassword123"
# TX power levels in mBm (millibel-milliwatts). Higher = stronger signal.
# These affect the RSSI reported to scanning clients.
TXPOWER_STRONG=3000 # 30 dBm
TXPOWER_MEDIUM=1500 # 15 dBm
TXPOWER_WEAK=100 # 1 dBm
RUN_DIR="/tmp/wifi-sim"
# --- Helpers -----------------------------------------------------------------
die() {
echo "ERROR: $*" >&2
exit 1
}
info() { echo "[*] $*"; }
ok() { echo "[+] $*"; }
require_cmds() {
local missing=()
for cmd in "$@"; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
die "Missing required tools: ${missing[*]}
Install with: sudo apt install hostapd wpasupplicant iw"
fi
}
get_phy_for_iface() {
iw dev "$1" info | awk '/wiphy/{print "phy"$2}'
}
# --- Start -------------------------------------------------------------------
cmd_start() {
[[ $EUID -ne 0 ]] && die "Please run as root: sudo $0 start"
require_cmds hostapd iw ip
if [[ -d "$RUN_DIR" && -f "$RUN_DIR/interfaces" ]]; then
die "wifi-sim appears to already be running. Run 'sudo $0 stop' first."
fi
mkdir -p "$RUN_DIR"
# Snapshot existing wlan interfaces so we can identify the new ones
info "Snapshotting existing interfaces..."
BEFORE=($(iw dev 2>/dev/null | awk '/Interface/{print $2}' | grep -v '^hwsim' || true))
# Load the kernel module with 4 radios (3 APs + 1 client)
info "Loading mac80211_hwsim (4 radios)..."
if lsmod | grep -q mac80211_hwsim; then
die "mac80211_hwsim is already loaded. Run 'sudo $0 stop' or 'sudo rmmod mac80211_hwsim' first."
fi
modprobe mac80211_hwsim radios=4
sleep 1 # Give the kernel a moment to register all interfaces
# Find the interfaces created by our modprobe
AFTER=($(iw dev 2>/dev/null | awk '/Interface/{print $2}' | grep -v '^hwsim'))
NEW_IFACES=()
for iface in "${AFTER[@]}"; do
[[ ! " ${BEFORE[*]:-} " =~ " $iface " ]] && NEW_IFACES+=("$iface")
done
if [[ ${#NEW_IFACES[@]} -lt 4 ]]; then
rmmod mac80211_hwsim 2>/dev/null || true
die "Expected 4 new wlan interfaces, found ${#NEW_IFACES[@]}. Check 'dmesg' for errors."
fi
AP1="${NEW_IFACES[0]}" # Will host SSID_STRONG
AP2="${NEW_IFACES[1]}" # Will host SSID_MEDIUM
AP3="${NEW_IFACES[2]}" # Will host SSID_SECURE
CLIENT="${NEW_IFACES[3]}" # Client-side interface for your code to use
info "Interfaces assigned: AP1=$AP1 AP2=$AP2 AP3=$AP3 Client=$CLIENT"
echo "$AP1 $AP2 $AP3 $CLIENT" >"$RUN_DIR/interfaces"
# Prevent NetworkManager from fighting us over these interfaces
if command -v nmcli &>/dev/null && nmcli general status &>/dev/null 2>&1; then
info "Marking interfaces as unmanaged in NetworkManager..."
for iface in "$AP1" "$AP2" "$AP3"; do
nmcli device set "$iface" managed no 2>/dev/null || true
done
nmcli device set "$CLIENT" managed yes
fi
# Set TX power on each physical radio to simulate signal strength differences.
# mac80211_hwsim factors TX power into the RSSI reported to scanning clients.
# Note: for precise RSSI control, consider 'wmediumd' (a wireless medium daemon).
info "Setting TX power levels for signal strength simulation..."
PHY1=$(get_phy_for_iface "$AP1")
PHY2=$(get_phy_for_iface "$AP2")
PHY3=$(get_phy_for_iface "$AP3")
iw phy "$PHY1" set txpower fixed $TXPOWER_STRONG
iw phy "$PHY2" set txpower fixed $TXPOWER_MEDIUM
iw phy "$PHY3" set txpower fixed $TXPOWER_WEAK
# --- hostapd config: AP1 (open, strong, channel 1) -----------------------
cat >"$RUN_DIR/ap1.conf" <<EOF
interface=$AP1
driver=nl80211
ssid=$SSID_STRONG
hw_mode=g
channel=1
auth_algs=1
wmm_enabled=0
ignore_broadcast_ssid=0
EOF
# --- hostapd config: AP2 (open, medium, channel 6) -----------------------
cat >"$RUN_DIR/ap2.conf" <<EOF
interface=$AP2
driver=nl80211
ssid=$SSID_MEDIUM
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
ignore_broadcast_ssid=0
EOF
# --- hostapd config: AP3 (WPA2, weak, channel 11) ------------------------
cat >"$RUN_DIR/ap3.conf" <<EOF
interface=$AP3
driver=nl80211
ssid=$SSID_SECURE
hw_mode=g
channel=11
auth_algs=1
wpa=2
wpa_passphrase=$WIFI_PASSWORD
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
wmm_enabled=0
ignore_broadcast_ssid=0
EOF
# Start a hostapd instance for each AP
info "Starting hostapd instances..."
hostapd -B -P "$RUN_DIR/ap1.pid" "$RUN_DIR/ap1.conf" \
>"$RUN_DIR/ap1.log" 2>&1 ||
{
cat "$RUN_DIR/ap1.log"
die "hostapd failed for AP1. See log above."
}
hostapd -B -P "$RUN_DIR/ap2.pid" "$RUN_DIR/ap2.conf" \
>"$RUN_DIR/ap2.log" 2>&1 ||
{
cat "$RUN_DIR/ap2.log"
die "hostapd failed for AP2. See log above."
}
hostapd -B -P "$RUN_DIR/ap3.pid" "$RUN_DIR/ap3.conf" \
>"$RUN_DIR/ap3.log" 2>&1 ||
{
cat "$RUN_DIR/ap3.log"
die "hostapd failed for AP3. See log above."
}
# Bring up the client interface so it's ready to scan/connect
ip link set "$CLIENT" up
# --- Summary -------------------------------------------------------------
ok "Virtual WiFi environment is up!"
echo ""
echo " Networks:"
echo " ┌──────────────────┬──────────┬────────┬─────────────┐"
echo " │ SSID │ Security │ Signal │ Interface │"
echo " ├──────────────────┼──────────┼────────┼─────────────┤"
printf " │ %-16s │ %-8s │ %-6s │ %-11s │\n" "$SSID_STRONG" "Open" "Strong" "$AP1"
printf " │ %-16s │ %-8s │ %-6s │ %-11s │\n" "$SSID_MEDIUM" "Open" "Medium" "$AP2"
printf " │ %-16s │ %-8s │ %-6s │ %-11s │\n" "$SSID_SECURE" "WPA2" "Weak" "$AP3"
echo " └──────────────────┴──────────┴────────┴─────────────┘"
echo ""
echo " WPA2 password : $WIFI_PASSWORD"
echo " Client iface : $CLIENT (use this in your code)"
echo ""
echo " Useful commands:"
echo " Scan for networks : sudo iw dev $CLIENT scan | grep -E 'SSID|signal'"
echo " Connect (open) : sudo iw dev $CLIENT connect \"$SSID_STRONG\""
echo " Connect (WPA2) : sudo wpa_supplicant -Dnl80211 -i$CLIENT \\"
echo " -c <(wpa_passphrase \"$SSID_SECURE\" \"$WIFI_PASSWORD\")"
echo " Stop simulation : sudo $0 stop"
echo ""
echo " Logs: $RUN_DIR/ap{1,2,3}.log"
}
# --- Stop --------------------------------------------------------------------
cmd_stop() {
[[ $EUID -ne 0 ]] && die "Please run as root: sudo $0 stop"
info "Stopping virtual WiFi environment..."
local had_something=false
# Kill hostapd processes via PID files
for ap in ap1 ap2 ap3; do
PID_FILE="$RUN_DIR/$ap.pid"
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if kill "$PID" 2>/dev/null; then
info "Stopped hostapd for $ap (PID $PID)"
fi
rm -f "$PID_FILE"
had_something=true
fi
done
# Re-enable NetworkManager management for these interfaces
if [[ -f "$RUN_DIR/interfaces" ]]; then
read -r AP1 AP2 AP3 CLIENT <"$RUN_DIR/interfaces"
if command -v nmcli &>/dev/null; then
for iface in "$AP1" "$AP2" "$AP3" "$CLIENT"; do
nmcli device set "$iface" managed yes 2>/dev/null || true
done
fi
fi
# Unload the kernel module (also destroys all virtual interfaces)
if lsmod | grep -q mac80211_hwsim; then
info "Unloading mac80211_hwsim..."
rmmod mac80211_hwsim && had_something=true
fi
# Clean up runtime directory
rm -rf "$RUN_DIR"
if $had_something; then
ok "Virtual WiFi environment stopped."
else
echo "Nothing was running."
fi
}
# --- Status ------------------------------------------------------------------
cmd_status() {
if [[ ! -f "$RUN_DIR/interfaces" ]]; then
echo "wifi-sim is not running."
return
fi
read -r AP1 AP2 AP3 CLIENT <"$RUN_DIR/interfaces"
echo "wifi-sim is running."
echo ""
echo " Interfaces:"
for iface in "$AP1" "$AP2" "$AP3" "$CLIENT"; do
STATE=$(ip link show "$iface" 2>/dev/null | awk '/state/{print $9}' || echo "unknown")
CHANNEL=$(iw dev "$iface" info 2>/dev/null | awk '/channel/{print $2}' || echo "?")
printf " %-10s state=%-4s channel=%s\n" "$iface" "$STATE" "$CHANNEL"
done
echo ""
echo " Processes:"
for ap in ap1 ap2 ap3; do
PID_FILE="$RUN_DIR/$ap.pid"
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo " hostapd ($ap) PID=$PID running"
else
echo " hostapd ($ap) PID=$PID DEAD (check $RUN_DIR/$ap.log)"
fi
fi
done
}
# --- Entry point -------------------------------------------------------------
case "${1:-help}" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
*)
echo "Usage: sudo $0 {start|stop|status}"
echo ""
echo " start Load mac80211_hwsim and bring up 3 virtual APs"
echo " stop Tear down all virtual interfaces and unload module"
echo " status Show current state of interfaces and processes"
echo ""
echo "Networks created on start:"
printf " %-16s open, strong signal\n" "$SSID_STRONG"
printf " %-16s open, medium signal\n" "$SSID_MEDIUM"
printf " %-16s WPA2, weak signal (password: $WIFI_PASSWORD)\n" "$SSID_SECURE"
;;
esac
+4
View File
@@ -54,6 +54,10 @@ select = [
"RUF", "RUF",
] ]
[tool.basedpyright]
typeCheckingMode = "basic"
include = ["cli/src"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["cli/tests"] testpaths = ["cli/tests"]
pythonpath = ["cli/src"] pythonpath = ["cli/src"]