Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 2m11s
C++ / build (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m30s
Python / verify (pull_request) Successful in 2m55s
C++ / clang-tidy (pull_request) Successful in 3m59s
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 2m11s
C++ / build (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m30s
Python / verify (pull_request) Successful in 2m55s
C++ / clang-tidy (pull_request) Successful in 3m59s
This commit is contained in:
+22
-87
@@ -1,12 +1,15 @@
|
||||
name: Python
|
||||
|
||||
env:
|
||||
APT_DEPS: git python3 python3-pip python3-venv python3-gi python3-cairo python3-dbus python3-pyudev python3-psutil python3-evdev python3-yaml python3-xlib python3-pillow gir1.2-gtk-3.0
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
static:
|
||||
runs-on: debian
|
||||
container: node:26-trixie-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -14,10 +17,8 @@ jobs:
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends git python3 python3-pip python3-venv
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir ruff
|
||||
@@ -27,32 +28,15 @@ jobs:
|
||||
. .venv/bin/activate
|
||||
ruff format --check .
|
||||
|
||||
lint:
|
||||
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 ruff
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff check .
|
||||
|
||||
test:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
verify:
|
||||
if: always()
|
||||
runs-on: debian
|
||||
container: node:26-trixie-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -60,20 +44,16 @@ jobs:
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-pillow \
|
||||
build-base
|
||||
python3 -m venv .venv
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends $APT_DEPS build-essential python3-dev
|
||||
python3 -m venv --system-site-packages .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir \
|
||||
typer \
|
||||
pillow \
|
||||
materialyoucolor \
|
||||
jinja2 \
|
||||
pytest
|
||||
pip install --no-cache-dir basedpyright nuitka .
|
||||
|
||||
- name: Type check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
basedpyright
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -81,52 +61,7 @@ jobs:
|
||||
cd cli
|
||||
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 typer pillow materialyoucolor jinja2
|
||||
|
||||
- name: Type check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
basedpyright
|
||||
|
||||
buildcheck:
|
||||
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 \
|
||||
build-base \
|
||||
python3-dev \
|
||||
gcc \
|
||||
g++
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir nuitka
|
||||
|
||||
- name: Nuitka module check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
@@ -16,3 +16,5 @@ dist/
|
||||
**/test-plugins/
|
||||
**/Charts/
|
||||
network-dev/
|
||||
**/zshell.build/
|
||||
**/zshell.dist/
|
||||
|
||||
@@ -93,6 +93,8 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
${NUITKA_EXECUTABLE}
|
||||
--standalone
|
||||
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||
--include-package=gi.overrides
|
||||
--include-package-data=solaar
|
||||
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||
--output-filename=zshell-cli
|
||||
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||
|
||||
+148
-130
@@ -18,13 +18,14 @@ Item {
|
||||
property real leftFadeStrength: overflowing && leftFadeEnabled ? fadeStrengthMoving : fadeStrengthIdle
|
||||
property int leftFadeWidth: 28
|
||||
property bool marqueeEnabled: true
|
||||
readonly property bool overflowing: metrics.width > root.width
|
||||
readonly property bool overflowing: metrics.width > content.width
|
||||
property int pauseMs: 1200
|
||||
property real pixelsPerSecond: 40
|
||||
property real rightFadeStrength: overflowing ? fadeStrengthMoving : fadeStrengthIdle
|
||||
property int rightFadeWidth: 28
|
||||
property bool sliding: false
|
||||
property alias text: elideText.text
|
||||
property bool vertical: false
|
||||
|
||||
function durationForDistance(px): int {
|
||||
return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000));
|
||||
@@ -41,8 +42,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: elideText.implicitHeight
|
||||
clip: false
|
||||
implicitHeight: vertical ? elideText.implicitWidth : elideText.implicitHeight
|
||||
implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth
|
||||
|
||||
Behavior on leftFadeStrength {
|
||||
Anim {
|
||||
@@ -53,171 +55,187 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onHeightChanged: resetMarquee()
|
||||
onTextChanged: resetMarquee()
|
||||
onVerticalChanged: resetMarquee()
|
||||
onVisibleChanged: if (!visible)
|
||||
resetMarquee()
|
||||
onWidthChanged: resetMarquee()
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
font: elideText.font
|
||||
text: elideText.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: elideText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
animate: root.animate
|
||||
animateProp: "scale,opacity"
|
||||
color: root.color
|
||||
elide: Text.ElideNone
|
||||
visible: !root.overflowing
|
||||
width: root.width
|
||||
}
|
||||
|
||||
Item {
|
||||
id: marqueeViewport
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
visible: root.overflowing
|
||||
anchors.centerIn: parent
|
||||
height: root.vertical ? root.width : root.height
|
||||
width: root.vertical ? root.height : root.width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: rightFadeMask
|
||||
transform: Rotation {
|
||||
angle: root.vertical ? -90 : 0
|
||||
origin.x: content.width / 2
|
||||
origin.y: content.height / 2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
font: elideText.font
|
||||
text: elideText.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: elideText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
animate: root.animate
|
||||
animateProp: "scale,opacity"
|
||||
color: root.color
|
||||
elide: Text.ElideNone
|
||||
visible: !root.overflowing
|
||||
width: content.width
|
||||
}
|
||||
|
||||
Item {
|
||||
id: strip
|
||||
id: marqueeViewport
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: t1.implicitHeight
|
||||
width: t1.width + root.gap + t2.width
|
||||
x: 0
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
layer.enabled: true
|
||||
visible: root.overflowing
|
||||
|
||||
CustomText {
|
||||
id: t1
|
||||
|
||||
animate: root.animate
|
||||
animateProp: "opacity"
|
||||
color: root.color
|
||||
font.pointSize: elideText.font.pointSize
|
||||
text: elideText.text
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: rightFadeMask
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: t2
|
||||
Item {
|
||||
id: strip
|
||||
|
||||
animate: root.animate
|
||||
animateProp: "opacity"
|
||||
color: root.color
|
||||
font.pointSize: elideText.font.pointSize
|
||||
text: t1.text
|
||||
x: t1.width + root.gap
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: t1.implicitHeight
|
||||
width: t1.width + root.gap + t2.width
|
||||
x: 0
|
||||
|
||||
SequentialAnimation {
|
||||
id: marqueeAnim
|
||||
CustomText {
|
||||
id: t1
|
||||
|
||||
running: false
|
||||
animate: root.animate
|
||||
animateProp: "opacity"
|
||||
color: root.color
|
||||
font.pointSize: elideText.font.pointSize
|
||||
text: elideText.text
|
||||
}
|
||||
|
||||
onFinished: pauseTimer.restart()
|
||||
CustomText {
|
||||
id: t2
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = true;
|
||||
root.leftFadeEnabled = true;
|
||||
animate: root.animate
|
||||
animateProp: "opacity"
|
||||
color: root.color
|
||||
font.pointSize: elideText.font.pointSize
|
||||
text: t1.text
|
||||
x: t1.width + root.gap
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(t1.width)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -t1.width
|
||||
}
|
||||
SequentialAnimation {
|
||||
id: marqueeAnim
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.leftFadeEnabled = false;
|
||||
running: false
|
||||
|
||||
onFinished: pauseTimer.restart()
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = true;
|
||||
root.leftFadeEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(t1.width)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -t1.width
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.leftFadeEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(root.gap)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: -t1.width
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -(t1.width + root.gap)
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = false;
|
||||
strip.x = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(root.gap)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: -t1.width
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -(t1.width + root.gap)
|
||||
}
|
||||
Timer {
|
||||
id: pauseTimer
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = false;
|
||||
strip.x = 0;
|
||||
interval: root.pauseMs
|
||||
repeat: false
|
||||
running: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.marqueeEnabled)
|
||||
marqueeAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: pauseTimer
|
||||
Rectangle {
|
||||
id: rightFadeMask
|
||||
|
||||
interval: root.pauseMs
|
||||
repeat: false
|
||||
running: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.marqueeEnabled)
|
||||
marqueeAnim.start();
|
||||
readonly property real fadeStartPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, (w - root.rightFadeWidth) / w));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightFadeMask
|
||||
|
||||
readonly property real fadeStartPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, (w - root.rightFadeWidth) / w));
|
||||
}
|
||||
readonly property real leftFadeEndPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, root.leftFadeWidth / w));
|
||||
}
|
||||
|
||||
anchors.fill: marqueeViewport
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.leftFadeStrength)
|
||||
position: 0.0
|
||||
readonly property real leftFadeEndPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, root.leftFadeWidth / w));
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.leftFadeEndPos
|
||||
}
|
||||
anchors.fill: marqueeViewport
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.fadeStartPos
|
||||
}
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.rightFadeStrength)
|
||||
position: 1.0
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.leftFadeStrength)
|
||||
position: 0.0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.leftFadeEndPos
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.fadeStartPos
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.rightFadeStrength)
|
||||
position: 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -126,7 +126,7 @@ MouseArea {
|
||||
CustomRect {
|
||||
id: item
|
||||
|
||||
readonly property bool active: modelData === root.active
|
||||
readonly property bool active: modelData === root?.active
|
||||
required property int index
|
||||
required property MenuItem modelData
|
||||
|
||||
@@ -163,19 +163,19 @@ MouseArea {
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
|
||||
text: item.modelData.icon
|
||||
text: item.modelData?.icon ?? ""
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
|
||||
text: item.modelData.text
|
||||
text: item.modelData?.text ?? ""
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
active: item.modelData.trailingIcon.length > 0
|
||||
active: item.modelData?.trailingIcon.length > 0
|
||||
asynchronous: true
|
||||
visible: active
|
||||
|
||||
|
||||
+33
-2
@@ -11,15 +11,46 @@ Variants {
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
function rebuild(): void {
|
||||
loader.active = false;
|
||||
loader.active = true;
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: loader
|
||||
|
||||
active: true
|
||||
|
||||
Drawer {
|
||||
screen: scope.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onConfigPositionChanged(): void {
|
||||
Qt.callLater(scope.rebuild);
|
||||
}
|
||||
|
||||
target: (loader.item as Drawer)?.geometry ?? null
|
||||
}
|
||||
}
|
||||
|
||||
component Drawer: Scope {
|
||||
id: drawer
|
||||
|
||||
readonly property alias geometry: content.geometry
|
||||
required property ShellScreen screen
|
||||
|
||||
Exclusions {
|
||||
bar: content.bar
|
||||
screen: scope.modelData
|
||||
geometry: content.geometry
|
||||
screen: drawer.screen
|
||||
}
|
||||
|
||||
Windows {
|
||||
id: content
|
||||
|
||||
screen: scope.modelData
|
||||
screen: drawer.screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import QtQuick
|
||||
import qs.Modules.Bar as Bar
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
required property Bar.BarLoader bar
|
||||
readonly property real barClamped: bar.clampedExtent
|
||||
readonly property real barExtent: bar.extent
|
||||
readonly property bool barOnBottom: position === "bottom"
|
||||
readonly property bool barOnLeft: position === "left"
|
||||
readonly property bool barOnTop: position === "top"
|
||||
required property string configPosition
|
||||
readonly property bool horizontal: position !== "left"
|
||||
readonly property string position: configPosition === "top" || configPosition === "bottom" ? configPosition : "left"
|
||||
required property var win
|
||||
|
||||
function axisPos(x: real, y: real): real {
|
||||
return horizontal ? x : y;
|
||||
}
|
||||
|
||||
function barContains(x: real, y: real, clamped = false): bool {
|
||||
const extent = clamped ? barClamped : barExtent;
|
||||
if (barOnTop)
|
||||
return y < extent;
|
||||
if (barOnBottom)
|
||||
return y > win.height - extent;
|
||||
return x < extent;
|
||||
}
|
||||
|
||||
function insetBottom(border: real, clamped = false): real {
|
||||
return barOnBottom ? (clamped ? barClamped : barExtent) : border;
|
||||
}
|
||||
|
||||
function insetLeft(border: real, clamped = false): real {
|
||||
return barOnLeft ? (clamped ? barClamped : barExtent) : border;
|
||||
}
|
||||
|
||||
function insetTop(border: real, clamped = false): real {
|
||||
return barOnTop ? (clamped ? barClamped : barExtent) : border;
|
||||
}
|
||||
|
||||
function inwardDrag(dragX: real, dragY: real): real {
|
||||
if (barOnTop)
|
||||
return dragY;
|
||||
if (barOnBottom)
|
||||
return -dragY;
|
||||
return dragX;
|
||||
}
|
||||
|
||||
onConfigPositionChanged: {
|
||||
if (!["left", "top", "bottom"].includes(configPosition))
|
||||
console.warn(`Invalid bar position '${configPosition}', falling back to left`);
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,21 @@ Scope {
|
||||
id: root
|
||||
|
||||
required property Item bar
|
||||
required property EdgeGeometry geometry
|
||||
required property ShellScreen screen
|
||||
|
||||
ExclusionZone {
|
||||
id: top
|
||||
|
||||
anchors.top: true
|
||||
exclusiveZone: root.bar.exclusiveZone
|
||||
hasBar: root.geometry.barOnTop
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
id: left
|
||||
|
||||
anchors.left: true
|
||||
hasBar: root.geometry.barOnLeft
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
@@ -34,6 +36,7 @@ Scope {
|
||||
id: bottom
|
||||
|
||||
anchors.bottom: true
|
||||
hasBar: root.geometry.barOnBottom
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -44,7 +47,9 @@ Scope {
|
||||
}
|
||||
|
||||
component ExclusionZone: CustomWindow {
|
||||
exclusiveZone: Config.bar.border
|
||||
property bool hasBar
|
||||
|
||||
exclusiveZone: hasBar ? root.bar.exclusiveZone : Config.bar.border
|
||||
implicitHeight: 1
|
||||
implicitWidth: 1
|
||||
name: "Bar-Exclusion"
|
||||
|
||||
+24
-22
@@ -3,7 +3,7 @@ import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules as BarPopouts
|
||||
import qs.Modules.Bar.Popouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -12,43 +12,47 @@ Item {
|
||||
required property real borderThickness
|
||||
property bool dashboardShortcutActive
|
||||
required property Drawing drawing
|
||||
required property EdgeGeometry geometry
|
||||
property bool osdShortcutActive
|
||||
required property Panels panels
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property Wrapper popouts
|
||||
required property ShellScreen screen
|
||||
property bool singleGestureTriggered: false
|
||||
property bool utilitiesShortcutActive
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
function inBottomPanel(panel: Item, x: real, y: real): bool {
|
||||
return y > root.height - panel.height - Config.bar.border && withinPanelWidth(panel, x, y);
|
||||
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
|
||||
return y > root.height - (panelHeight + geometry.insetBottom(borderThickness)) && withinPanelWidth(panel, x, y);
|
||||
}
|
||||
|
||||
function inLeftPanel(panel: Item, x: real, y: real): bool {
|
||||
return x < panel.x + panel.width + Config.bar.border && withinPanelHeight(panel, x, y);
|
||||
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inPopoutArea(x: real, y: real): bool {
|
||||
const panel = panels.popoutsWrapper;
|
||||
if (!geometry.horizontal)
|
||||
return inLeftPanel(panel, x, y);
|
||||
return withinPanelWidth(panel, x, y) && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inRightPanel(panel: Item, x: real, y: real): bool {
|
||||
return x > panel.x - Config.bar.border && withinPanelHeight(panel, x, y);
|
||||
return x > panel.x + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inTopPanel(panel: Item, x: real, y: real): bool {
|
||||
return y < bar.implicitHeight + panel.height && withinPanelWidth(panel, x, y);
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
if (event.x < bar.implicitWidth) {
|
||||
bar.handleWheel(event.y, event.angleDelta);
|
||||
}
|
||||
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
|
||||
return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
|
||||
}
|
||||
|
||||
function withinPanelHeight(panel: Item, x: real, y: real): bool {
|
||||
const panelY = panel.y + bar.implicitHeight;
|
||||
const panelY = panel.y + geometry.insetTop(borderThickness);
|
||||
return y >= panelY && y <= panelY + panel.height;
|
||||
}
|
||||
|
||||
function withinPanelWidth(panel: Item, x: real, y: real): bool {
|
||||
const panelX = panel.x + root.borderThickness;
|
||||
const panelX = panel.x + geometry.insetLeft(borderThickness);
|
||||
return x >= panelX && x <= panelX + panel.width;
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ Item {
|
||||
if (root.singleGestureTriggered)
|
||||
return;
|
||||
|
||||
if (centroid.pressPosition.y < root.bar.implicitHeight) {
|
||||
if (root.geometry.insetTop(root.borderThickness) > centroid.pressPosition.y) {
|
||||
if (dragY > 20) {
|
||||
root.visibilities.settings = true;
|
||||
root.singleGestureTriggered = true;
|
||||
@@ -91,10 +95,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
if (centroid.pressPosition.y > root.screen.height - Config.bar.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
|
||||
if (centroid.pressPosition.y > root.screen.height - root.geometry.insetBottom(root.borderThickness) && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
|
||||
root.visibilities.clipboard = true;
|
||||
|
||||
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher)
|
||||
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.geometry.insetTop(root.borderThickness) && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher)
|
||||
if (dragY < -10) {
|
||||
root.visibilities.dock = true;
|
||||
root.singleGestureTriggered = true;
|
||||
@@ -193,7 +197,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.visibilities.bar && Config.bar.autoHide && y < root.bar.implicitHeight)
|
||||
if (!root.visibilities.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
|
||||
root.bar.isHovered = true;
|
||||
|
||||
if (root.panels.sidebar.offsetScale === 1) {
|
||||
@@ -219,8 +223,8 @@ Item {
|
||||
if (Config.dock.enabled && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y))
|
||||
root.visibilities.dock = true;
|
||||
|
||||
if (y < root.bar.implicitHeight)
|
||||
root.bar.checkPopout(x);
|
||||
if (root.geometry.barContains(x, y))
|
||||
root.bar.checkPopout(root.geometry.axisPos(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,8 +302,6 @@ Item {
|
||||
root.visibilities.sidebar = false;
|
||||
root.panels.popouts.hasCurrent = false;
|
||||
root.visibilities.launcher = false;
|
||||
} else {
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-16
@@ -1,7 +1,7 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import qs.Modules as Modules
|
||||
import qs.Modules.Bar.Popouts as Popouts
|
||||
import qs.Modules.Notifications as Notifications
|
||||
import qs.Modules.Notifications.Sidebar as Sidebar
|
||||
import qs.Modules.Notifications.Sidebar.Utils as Utils
|
||||
@@ -27,6 +27,7 @@ Item {
|
||||
readonly property alias dock: dock
|
||||
readonly property alias drawing: drawing
|
||||
required property var drawingItem
|
||||
required property EdgeGeometry geometry
|
||||
readonly property alias launcher: launcher
|
||||
readonly property alias notifications: notifications
|
||||
readonly property alias osd: osd
|
||||
@@ -43,24 +44,26 @@ Item {
|
||||
readonly property alias utilities: utilities
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
anchors.bottomMargin: geometry.insetBottom(borderThickness)
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: geometry.insetLeft(borderThickness)
|
||||
anchors.margins: borderThickness
|
||||
anchors.topMargin: bar.implicitHeight
|
||||
anchors.topMargin: geometry.insetTop(borderThickness)
|
||||
|
||||
Item {
|
||||
id: resourcesWrapper
|
||||
|
||||
anchors.bottom: root.geometry.position === "bottom" ? parent.bottom : undefined
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.top: root.geometry.position !== "bottom" ? parent.top : undefined
|
||||
clip: true
|
||||
implicitHeight: resources.implicitHeight * (1 - resources.offsetScale)
|
||||
implicitWidth: resources.implicitWidth
|
||||
implicitHeight: root.geometry.horizontal ? resources.implicitHeight * (1 - resources.offsetScale) : resources.implicitHeight
|
||||
implicitWidth: root.geometry.horizontal ? resources.implicitWidth : resources.implicitWidth * (1 - resources.offsetScale)
|
||||
|
||||
Resources.Wrapper {
|
||||
id: resources
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
position: root.geometry.position
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
@@ -96,11 +99,11 @@ Item {
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Modules.ClipWrapper {
|
||||
Popouts.ClipWrapper {
|
||||
id: popouts
|
||||
|
||||
anchors.top: parent.top
|
||||
borderThickness: root.borderThickness
|
||||
position: root.geometry.position
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
@@ -147,11 +150,13 @@ Item {
|
||||
|
||||
property real offsetScale: dashboard.shouldBeActive ? 0 : 1
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: root.geometry.barOnLeft || root.geometry.barOnBottom ? parent.bottom : undefined
|
||||
anchors.left: root.geometry.barOnLeft ? parent.left : undefined
|
||||
anchors.right: root.geometry.barOnLeft ? undefined : parent.right
|
||||
anchors.top: root.geometry.barOnBottom || root.geometry.barOnLeft ? undefined : parent.top
|
||||
clip: true
|
||||
implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
|
||||
implicitWidth: dashboard.implicitWidth
|
||||
implicitHeight: root.geometry.horizontal ? dashboard.implicitHeight * (1 - offsetScale) : dashboard.implicitHeight
|
||||
implicitWidth: root.geometry.horizontal ? dashboard.implicitWidth : dashboard.implicitWidth * (1 - offsetScale)
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
@@ -163,10 +168,8 @@ Item {
|
||||
Dashboard.Wrapper {
|
||||
id: dashboard
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (-implicitHeight - 5) * offsetScale
|
||||
offsetScale: dashboardWrapper.offsetScale
|
||||
position: root.geometry.position
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -7,17 +7,18 @@ import qs.Modules.Bar as Bar
|
||||
Region {
|
||||
id: root
|
||||
|
||||
required property Bar.BarLoader bar
|
||||
// required property Bar.BarLoader bar
|
||||
readonly property real borderThickness: win.borderThickness
|
||||
required property EdgeGeometry geometry
|
||||
readonly property alias menuPopoutRegion: menuPopoutRegion
|
||||
required property Panels panels
|
||||
required property var win
|
||||
|
||||
height: win.height - bar.implicitHeight - win.borderThickness - win.dragMaskPadding * 2
|
||||
height: win.height - geometry.insetTop(borderThickness, true) - geometry.insetBottom(borderThickness, true) - win.dragMaskPadding * 2
|
||||
intersection: Intersection.Xor
|
||||
width: win.width - win.borderThickness * 2 - win.dragMaskPadding * 2
|
||||
x: win.borderThickness + win.dragMaskPadding
|
||||
y: bar.implicitHeight + win.dragMaskPadding
|
||||
width: win.width - geometry.insetLeft(borderThickness, true) * 2 - win.dragMaskPadding * 2
|
||||
x: geometry.insetLeft(borderThickness, true) + win.dragMaskPadding
|
||||
y: geometry.insetTop(borderThickness, true) + win.dragMaskPadding
|
||||
|
||||
R {
|
||||
panel: root.panels.dashboardWrapper
|
||||
@@ -41,17 +42,21 @@ Region {
|
||||
}
|
||||
|
||||
R {
|
||||
height: panel.height + root.geometry.insetTop(root.borderThickness)
|
||||
panel: root.panels.notifications
|
||||
y: 0
|
||||
}
|
||||
|
||||
R {
|
||||
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness
|
||||
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.geometry.insetBottom(root.borderThickness)
|
||||
panel: root.panels.utilities
|
||||
y: root.win.height - height
|
||||
}
|
||||
|
||||
R {
|
||||
height: root.geometry.horizontal ? panel.height * (1 - root.panels.popoutsWrapper.offsetScale) : panel.height
|
||||
panel: root.panels.popoutsWrapper
|
||||
width: root.geometry.horizontal ? panel.width : panel.width * (1 - root.panels.popoutsWrapper.offsetScale)
|
||||
}
|
||||
|
||||
R {
|
||||
@@ -82,7 +87,7 @@ Region {
|
||||
height: panel.height
|
||||
intersection: Intersection.Subtract
|
||||
width: panel.width
|
||||
x: panel.x + root.borderThickness
|
||||
y: panel.y + root.bar.implicitHeight
|
||||
x: panel.x + root.geometry.insetLeft(root.borderThickness)
|
||||
y: panel.y + root.geometry.insetTop(root.borderThickness)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-26
@@ -8,10 +8,10 @@ import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import ZShell.Blobs
|
||||
import qs.Modules.SysTray.Popouts.Network as N
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Modules.Bar
|
||||
import qs.Modules.Bar.Popouts.Network as N
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Drawers
|
||||
@@ -34,6 +34,7 @@ CustomWindow {
|
||||
return 100;
|
||||
}
|
||||
property real fsTransitionProg: hasFullscreen ? 1 : 0
|
||||
readonly property alias geometry: geometry
|
||||
readonly property bool hasFullscreen: {
|
||||
if (hasSpecialWorkspace) {
|
||||
const specialName = monitor?.lastIpcObject.specialWorkspace?.name;
|
||||
@@ -96,13 +97,21 @@ CustomWindow {
|
||||
panels.popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
EdgeGeometry {
|
||||
id: geometry
|
||||
|
||||
bar: bar
|
||||
configPosition: Config.bar.position
|
||||
win: root
|
||||
}
|
||||
|
||||
Region {
|
||||
id: emptyRegion
|
||||
|
||||
height: panels.notifications.height
|
||||
width: panels.notifications.width
|
||||
x: panels.notifications.x + root.borderThickness
|
||||
y: panels.notifications.y + bar.implicitHeight
|
||||
x: panels.notifications.x + geometry.insetLeft(root.borderThickness)
|
||||
y: panels.notifications.y + geometry.insetTop(root.borderThickness)
|
||||
|
||||
Region {
|
||||
height: panels.osd.height
|
||||
@@ -115,7 +124,7 @@ CustomWindow {
|
||||
Regions {
|
||||
id: regions
|
||||
|
||||
bar: bar
|
||||
geometry: geometry
|
||||
panels: panels
|
||||
win: root
|
||||
}
|
||||
@@ -204,10 +213,10 @@ CustomWindow {
|
||||
BlobInvertedRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -50
|
||||
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset
|
||||
borderLeft: geometry.insetLeft(root.borderThickness) - anchors.margins - root.sdfBorderOffset
|
||||
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset
|
||||
borderTop: geometry.insetTop(root.borderThickness) - anchors.margins - root.sdfBorderOffset
|
||||
group: blobGroup
|
||||
radius: root.borderRounding
|
||||
}
|
||||
@@ -215,15 +224,15 @@ CustomWindow {
|
||||
PanelBg {
|
||||
id: dashBg
|
||||
|
||||
property real extraHeight: 0.2
|
||||
property real extraExtent: 0.2
|
||||
|
||||
deformAmount: 0.06
|
||||
implicitHeight: panels.dashboard.height * (1 + extraHeight)
|
||||
implicitWidth: panels.dashboard.width
|
||||
implicitHeight: panels.dashboard.height * (geometry.horizontal ? 1 + extraExtent : 1)
|
||||
implicitWidth: panels.dashboard.width * (geometry.horizontal ? 1 : 1 + extraExtent)
|
||||
panel: panels.dashboardWrapper
|
||||
radius: Tokens.rounding.normal
|
||||
x: panels.dashboardWrapper.x + panels.dashboard.x + root.borderThickness
|
||||
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
|
||||
x: panels.dashboardWrapper.x + panels.dashboard.x + geometry.insetLeft(root.borderThickness) - (geometry.horizontal ? 0 : panels.dashboard.width * extraExtent)
|
||||
y: panels.dashboardWrapper.y + panels.dashboard.y + geometry.insetTop(root.borderThickness) - (geometry.horizontal && geometry.position !== "bottom" ? panels.dashboard.height * extraExtent : 0)
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -235,7 +244,7 @@ CustomWindow {
|
||||
implicitHeight: panels.launcher.height * (1 + extraHeight)
|
||||
panel: panels.launcher
|
||||
radius: Tokens.rounding.smallest + 5
|
||||
y: panels.launcher.y + bar.implicitHeight
|
||||
y: panels.launcher.y + geometry.insetTop(root.borderThickness)
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -256,8 +265,7 @@ CustomWindow {
|
||||
implicitWidth: panels.osd.width
|
||||
panel: panels.osdWrapper
|
||||
radius: 20
|
||||
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness
|
||||
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
|
||||
x: panels.osdWrapper.x + panels.osd.x + geometry.insetLeft(root.borderThickness)
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -279,17 +287,17 @@ CustomWindow {
|
||||
PanelBg {
|
||||
id: popoutBg
|
||||
|
||||
property real extraHeight: 0.2
|
||||
property real extraExtent: 0.2
|
||||
|
||||
deformAmount: panels.popouts.currentName.startsWith("traymenu") ? 0.15 : 0.08
|
||||
implicitHeight: panels.popouts.height * (1 + extraHeight)
|
||||
implicitWidth: panels.popouts.width
|
||||
implicitHeight: panels.popouts.height * (geometry.horizontal ? 1 + extraExtent : 1)
|
||||
implicitWidth: panels.popouts.width * (geometry.horizontal ? 1 : 1 + extraExtent)
|
||||
panel: panels.popoutsWrapper
|
||||
radius: panels.popouts.current?.panelRadius ?? Tokens.rounding.normal
|
||||
x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness
|
||||
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
|
||||
x: panels.popoutsWrapper.x + panels.popouts.x + geometry.insetLeft(root.borderThickness) - (geometry.horizontal ? 0 : panels.popouts.width * extraExtent)
|
||||
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
|
||||
|
||||
Behavior on extraHeight {
|
||||
Behavior on extraExtent {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
@@ -303,8 +311,8 @@ CustomWindow {
|
||||
implicitWidth: panels.resources.width
|
||||
panel: panels.resourcesWrapper
|
||||
radius: Tokens.rounding.large
|
||||
x: panels.resourcesWrapper.x + panels.resources.x + root.borderThickness
|
||||
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
|
||||
x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness)
|
||||
y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness)
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -382,6 +390,7 @@ CustomWindow {
|
||||
borderThickness: root.borderLayoutThickness
|
||||
drawing: drawing
|
||||
enabled: true
|
||||
geometry: geometry
|
||||
panels: panels
|
||||
popouts: panels.popouts
|
||||
screen: root.screen
|
||||
@@ -393,6 +402,7 @@ CustomWindow {
|
||||
bar: bar
|
||||
borderThickness: root.borderThickness
|
||||
drawingItem: drawing
|
||||
geometry: geometry
|
||||
screen: root.screen
|
||||
visibilities: visibilities
|
||||
|
||||
@@ -434,14 +444,18 @@ CustomWindow {
|
||||
BarLoader {
|
||||
id: bar
|
||||
|
||||
anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: geometry.barOnBottom ? undefined : parent.top
|
||||
enabled: !visibilities.isDrawing
|
||||
fullscreen: root.hasFullscreen
|
||||
height: geometry.horizontal ? implicitHeight : parent.height
|
||||
popouts: panels.popouts
|
||||
popoutsWrapper: panels.popoutsWrapper
|
||||
position: geometry.position
|
||||
screen: root.screen
|
||||
visibilities: visibilities
|
||||
width: geometry.horizontal ? parent.width : implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +468,7 @@ CustomWindow {
|
||||
implicitHeight: panel.height
|
||||
implicitWidth: panel.width
|
||||
radius: Tokens.rounding.smallest
|
||||
x: panel.x + root.borderThickness
|
||||
y: panel.y + bar.implicitHeight
|
||||
x: panel.x + geometry.insetLeft(root.borderThickness)
|
||||
y: panel.y + geometry.insetTop(root.borderThickness)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-12
@@ -1,35 +1,63 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Paths
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var colors: {
|
||||
if (deviceState === UPowerDeviceState.Charging || deviceState === UPowerDeviceState.FullyCharged)
|
||||
readonly property list<var> allPeripherals: [...logiDevices, ...upowerDevices.filter(d => !Battery.logiDevices.some(l => l.nativePath === d.nativePath))]
|
||||
readonly property real currentPerc: UPower.displayDevice.percentage
|
||||
readonly property var deviceState: UPower.displayDevice.state
|
||||
readonly property string deviceStateString: UPowerDeviceState.toString(deviceState)
|
||||
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
|
||||
readonly property alias logiDevices: adapter.devices
|
||||
readonly property var lowestPeripheral: allPeripherals.reduce((lowest, current) => current.percentage < lowest.percentage ? current : lowest)
|
||||
readonly property bool onBattery: UPower.onBattery
|
||||
readonly property bool ready: UPower.displayDevice.ready
|
||||
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
|
||||
readonly property real timeToFull: UPower.displayDevice.timeToFull
|
||||
readonly property list<UPowerDevice> upowerDevices: UPower.devices.values.filter(d => d.state !== UPowerDeviceState.Unknown)
|
||||
|
||||
function getColors(percentage: real, state: string): var {
|
||||
if (state === "charging" || state === "full" || state === "recharging")
|
||||
return {
|
||||
fg: Colors.swapRG(Colors.palette.m3error),
|
||||
bg: Colors.swapRG(Colors.palette.m3onError)
|
||||
};
|
||||
else if (currentPerc <= 0.2)
|
||||
else if (percentage <= 0.2)
|
||||
return {
|
||||
fg: Colors.palette.m3error,
|
||||
bg: Colors.palette.m3onError
|
||||
};
|
||||
else
|
||||
return {
|
||||
fg: Colors.palette.m3onSurface,
|
||||
bg: Colors.palette.m3surface
|
||||
fg: Colors.palette.m3tertiary,
|
||||
bg: Colors.palette.m3onTertiary
|
||||
};
|
||||
}
|
||||
readonly property real currentPerc: UPower.displayDevice.percentage
|
||||
readonly property var deviceState: UPower.displayDevice.state
|
||||
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
|
||||
readonly property bool onBattery: UPower.onBattery
|
||||
readonly property bool ready: UPower.displayDevice.ready
|
||||
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
|
||||
readonly property real timeToFull: UPower.displayDevice.timeToFull
|
||||
|
||||
function startDaemon(): void {
|
||||
Quickshell.execDetached(["zshell-cli", "battery", "daemon"]);
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileView
|
||||
|
||||
path: `${Paths.cache}/battery.json`
|
||||
watchChanges: true
|
||||
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property list<var> devices: []
|
||||
property real updated: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
|
||||
import ZShell.Config
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
@@ -166,6 +166,32 @@ Singleton {
|
||||
return "chat";
|
||||
}
|
||||
|
||||
function getSpecialWsIcon(name: string): string {
|
||||
name = name.toLowerCase().slice("special:".length);
|
||||
|
||||
if (name === "special")
|
||||
return "star";
|
||||
if (name === "communication")
|
||||
return "forum";
|
||||
if (name === "music")
|
||||
return "music_cast";
|
||||
if (name === "todo")
|
||||
return "checklist";
|
||||
if (name === "sysmon")
|
||||
return "monitor_heart";
|
||||
return name[0].toUpperCase();
|
||||
}
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
const file = name.slice(name.lastIndexOf("/") + 1);
|
||||
const themed = Quickshell.iconPath(file, true);
|
||||
icon = themed ? themed : Qt.resolvedUrl(`${path}/${file}`);
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
function getVolumeIcon(volume: real, isMuted: bool): string {
|
||||
if (isMuted)
|
||||
return "no_sound";
|
||||
@@ -181,4 +207,19 @@ Singleton {
|
||||
return weatherIcons[code];
|
||||
return "air";
|
||||
}
|
||||
|
||||
function matchIconConfig(name: string, iconConfig: var): bool {
|
||||
if (!iconConfig.icon)
|
||||
return false;
|
||||
|
||||
if (iconConfig.regex) {
|
||||
const re = new RegExp(iconConfig.regex, iconConfig.flags ?? "");
|
||||
if (re.test(name))
|
||||
return true;
|
||||
} else if (iconConfig.name === name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ Singleton {
|
||||
|
||||
Config.dock.pinnedApps = pinnedApps;
|
||||
root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id)));
|
||||
Config.saveNoToast();
|
||||
}
|
||||
|
||||
function isPinned(appId) {
|
||||
|
||||
+8
-41
@@ -11,8 +11,8 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property int availableUpdates: 0
|
||||
property string cmd: ""
|
||||
property bool commandReady
|
||||
property bool hasHelper
|
||||
property bool loaded
|
||||
property double now: Date.now()
|
||||
property var updates: ({})
|
||||
@@ -38,18 +38,12 @@ Singleton {
|
||||
}
|
||||
|
||||
function performPackageUpdate(pkg: string): void {
|
||||
if (root.cmd === "pacman")
|
||||
pkgUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Sy", pkg];
|
||||
else
|
||||
pkgUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Sy", pkg];
|
||||
pkgUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Sy", pkg];
|
||||
pkgUpdateProc.running = true;
|
||||
}
|
||||
|
||||
function performSystemUpdate(): void {
|
||||
if (root.cmd === "pacman")
|
||||
sysUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Syu"];
|
||||
else
|
||||
sysUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Syu"];
|
||||
sysUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Syu"];
|
||||
sysUpdateProc.running = true;
|
||||
}
|
||||
|
||||
@@ -87,7 +81,7 @@ Singleton {
|
||||
Process {
|
||||
id: cmdDetect
|
||||
|
||||
command: ["sh", "-c", "command -v checkupdates || command -v yay || command -v paru"]
|
||||
command: ["sh", "-c", "command -v checkupdates"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
@@ -96,47 +90,20 @@ Singleton {
|
||||
let helper;
|
||||
|
||||
if (cmd.length > 0) {
|
||||
helper = cmd.split("/").pop();
|
||||
helper = true;
|
||||
} else {
|
||||
helper = "pacman";
|
||||
}
|
||||
|
||||
if (helper === "checkupdates") {
|
||||
updatesProc.command = [helper];
|
||||
} else {
|
||||
updatesProc.command = [helper, "-Qu"];
|
||||
helper = false;
|
||||
}
|
||||
root.hasHelper = helper;
|
||||
root.commandReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateCmdDetect
|
||||
|
||||
command: ["sh", "-c", "command -v yay || command -v paru"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const cmd = this.text.trim();
|
||||
let helper;
|
||||
|
||||
if (cmd.length > 0) {
|
||||
helper = cmd.split("/").pop();
|
||||
} else {
|
||||
helper = "pacman";
|
||||
}
|
||||
|
||||
root.cmd = helper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updatesProc
|
||||
|
||||
command: []
|
||||
command: root.hasHelper ? ["checkupdates"] : []
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
|
||||
+41
-21
@@ -3,23 +3,28 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules
|
||||
import ZShell.Config
|
||||
import qs.Modules.SysTray
|
||||
import qs.Modules.Updates
|
||||
import qs.Modules.Bar.Components
|
||||
import qs.Modules.Bar.Popouts
|
||||
|
||||
RowLayout {
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
required property bool fullscreen
|
||||
required property bool horizontal
|
||||
required property Wrapper popouts
|
||||
required property ClipWrapper popoutsWrapper
|
||||
required property ShellScreen screen
|
||||
readonly property int vPadding: 6
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
function checkPopout(x: real): void {
|
||||
const ch = childAt(x, height / 2) as EntryWrapper;
|
||||
function axisCenterOf(item: Item): real {
|
||||
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
|
||||
return horizontal ? c.x : c.y;
|
||||
}
|
||||
|
||||
function checkPopout(pos: real): void {
|
||||
const ch = (horizontal ? childAt(pos, height / 2) : childAt(width / 2, pos)) as EntryWrapper;
|
||||
const id = ch?.entryId;
|
||||
|
||||
if (!ch || id === undefined) {
|
||||
@@ -33,26 +38,21 @@ RowLayout {
|
||||
|
||||
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
|
||||
const items = (ch.item as StatusIcons).items;
|
||||
const localX = mapToItem(items, x, 0).x;
|
||||
const icon = items.childAt(localX, items.height / 2);
|
||||
const icon = horizontal ? items.childAt(mapToItem(items, pos, 0).x, items.height / 2) : items.childAt(items.width / 2, mapToItem(items, 0, pos).y);
|
||||
if (icon) {
|
||||
popouts.currentName = icon.name;
|
||||
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
|
||||
popouts.currentCenter = Qt.binding(() => axisCenterOf(icon));
|
||||
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);
|
||||
const trayItem = horizontal ? layout.childAt(mapToItem(layout, pos, 0).x, layout.height / 2) : layout.childAt(tray.width / 2, mapToItem(layout, 0, pos).y);
|
||||
|
||||
if (trayItem) {
|
||||
const idx = trayItem.index;
|
||||
popouts.currentName = `traymenu${idx}`;
|
||||
popouts.currentCenter = Qt.binding(() => {
|
||||
const it = tray.items.itemAt(idx);
|
||||
return it ? it.mapToItem(root, it.implicitWidth / 2, 0).x : popouts.currentCenter;
|
||||
});
|
||||
popouts.currentCenter = Qt.binding(() => axisCenterOf(trayItem));
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
|
||||
@@ -62,12 +62,19 @@ RowLayout {
|
||||
|
||||
if (id === "updates") {
|
||||
popouts.currentName = "updates";
|
||||
popouts.currentCenter = Qt.binding(() => ch.item.mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x);
|
||||
popouts.currentCenter = Qt.binding(() => axisCenterOf(ch.item as Item));
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
function entryAt(pos: real): string {
|
||||
const ch = (horizontal ? childAt(pos, height / 2) : childAt(width / 2, pos)) as EntryWrapper;
|
||||
return ch?.entryId ?? "";
|
||||
}
|
||||
|
||||
columnSpacing: Tokens.spacing.small
|
||||
columns: horizontal ? -1 : 1
|
||||
rowSpacing: Tokens.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
@@ -84,6 +91,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
HyprsunsetWidget {
|
||||
horizontal: root.horizontal
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -93,7 +101,8 @@ RowLayout {
|
||||
roleValue: "spacer"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: !root.horizontal
|
||||
Layout.fillWidth: root.horizontal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +111,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
Workspaces {
|
||||
horizontal: root.horizontal
|
||||
screen: root.screen
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
@@ -113,6 +123,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
TrayIcons {
|
||||
horizontal: root.horizontal
|
||||
loader: root
|
||||
popouts: root.popouts
|
||||
visible: !root.fullscreen
|
||||
@@ -125,6 +136,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
StatusIcons {
|
||||
horizontal: root.horizontal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +146,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
Resources {
|
||||
horizontal: root.horizontal
|
||||
visibilities: root.visibilities
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
@@ -145,6 +158,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
UpdatesWidget {
|
||||
horizontal: root.horizontal
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -155,6 +169,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
NotifBell {
|
||||
horizontal: root.horizontal
|
||||
popouts: root.popouts
|
||||
visibilities: root.visibilities
|
||||
visible: !root.fullscreen
|
||||
@@ -167,6 +182,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
Clock {
|
||||
horizontal: root.horizontal
|
||||
loader: root
|
||||
popouts: root.popouts
|
||||
visibilities: root.visibilities
|
||||
@@ -181,6 +197,7 @@ RowLayout {
|
||||
delegate: EntryWrapper {
|
||||
WindowTitle {
|
||||
bar: root
|
||||
horizontal: root.horizontal
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -191,6 +208,7 @@ RowLayout {
|
||||
|
||||
delegate: EntryWrapper {
|
||||
MediaWidget {
|
||||
horizontal: root.horizontal
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -204,9 +222,11 @@ RowLayout {
|
||||
default property Item item
|
||||
required property var modelData
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: index === 0 ? root.vPadding : (entryId === "statusIcons") ? -root.spacing + Tokens.spacing.extraSmall : 0
|
||||
Layout.rightMargin: index === repeater.count - 1 ? root.vPadding : 0
|
||||
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
|
||||
Layout.bottomMargin: !root.horizontal && index === repeater.count - 1 ? root.vPadding : 0
|
||||
Layout.leftMargin: root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && root.horizontal) ? -root.rowSpacing + Tokens.spacing.extraSmall : 0
|
||||
Layout.rightMargin: root.horizontal && index === repeater.count - 1 ? root.vPadding : 0
|
||||
Layout.topMargin: !root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && !root.horizontal) ? -root.columnSpacing + Tokens.spacing.extraSmall : 0
|
||||
children: item
|
||||
implicitHeight: item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.implicitWidth ?? 0
|
||||
|
||||
+26
-19
@@ -1,44 +1,48 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules.SysTray
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import qs.Modules.Bar.Popouts
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int contentHeight: Config.bar.height + padding * 2
|
||||
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentHeight
|
||||
readonly property int clampedExtent: Math.max(Config.bar.border, extent)
|
||||
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
|
||||
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness
|
||||
property real extent: fullscreen ? 0 : Config.bar.border
|
||||
required property bool fullscreen
|
||||
readonly property bool horizontal: position !== "left"
|
||||
property bool isHovered
|
||||
readonly property int padding: Math.max(Tokens.padding.smaller, Config.bar.border)
|
||||
required property Wrapper popouts
|
||||
required property ClipWrapper popoutsWrapper
|
||||
required property string position
|
||||
required property ShellScreen screen
|
||||
readonly property bool shouldBeVisible: !fullscreen && (!Config.bar.autoHide || visibilities.bar || isHovered)
|
||||
readonly property int vPadding: 6
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
function checkPopout(x: real): void {
|
||||
content.item?.checkPopout(x);
|
||||
function checkPopout(pos: real): void {
|
||||
(content.item as Bar)?.checkPopout(pos);
|
||||
}
|
||||
|
||||
implicitHeight: fullscreen ? 0 : Config.bar.border
|
||||
visible: height > Config.bar.border
|
||||
function entryAt(pos: real): string {
|
||||
return (content.item as Bar)?.entryAt(pos) ?? "";
|
||||
}
|
||||
|
||||
implicitHeight: extent
|
||||
implicitWidth: extent
|
||||
visible: extent > Config.bar.border
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.shouldBeVisible
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitHeight: root.contentHeight
|
||||
root.extent: root.contentThickness
|
||||
}
|
||||
}
|
||||
transitions: [
|
||||
@@ -47,7 +51,7 @@ Item {
|
||||
to: "visible"
|
||||
|
||||
Anim {
|
||||
property: "implicitHeight"
|
||||
property: "extent"
|
||||
target: root
|
||||
}
|
||||
},
|
||||
@@ -56,7 +60,7 @@ Item {
|
||||
to: ""
|
||||
|
||||
Anim {
|
||||
property: "implicitHeight"
|
||||
property: "extent"
|
||||
target: root
|
||||
}
|
||||
}
|
||||
@@ -66,13 +70,16 @@ Item {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeVisible || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: root.position !== "bottom" ? parent.bottom : undefined
|
||||
anchors.left: root.horizontal ? parent.left : undefined
|
||||
anchors.right: parent.right
|
||||
anchors.top: root.position !== "top" ? parent.top : undefined
|
||||
height: root.contentThickness
|
||||
width: root.contentThickness
|
||||
|
||||
sourceComponent: Bar {
|
||||
fullscreen: root.fullscreen
|
||||
height: root.contentHeight
|
||||
horizontal: root.horizontal
|
||||
popouts: root.popouts
|
||||
popoutsWrapper: root.popoutsWrapper
|
||||
screen: root.screen
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Popouts
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
readonly property color contentColor: visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
readonly property string effectiveFormat: {
|
||||
let fmt = Config.general.dateFormat;
|
||||
if (!root.horizontal)
|
||||
fmt = fmt.replace("dddd", "ddd");
|
||||
return fmt;
|
||||
}
|
||||
readonly property var formatParts: {
|
||||
const segments = root.effectiveFormat.split(" - ");
|
||||
let datePart = "";
|
||||
let timePart = "";
|
||||
|
||||
if (segments.length > 1) {
|
||||
datePart = segments[0];
|
||||
timePart = segments[1];
|
||||
} else if (/[hms]/.test(segments[0])) {
|
||||
timePart = segments[0];
|
||||
} else {
|
||||
datePart = segments[0];
|
||||
}
|
||||
|
||||
const dateTokens = datePart.trim().length ? datePart.trim().split(/\s+/) : [];
|
||||
const timeTokens = timePart.trim().length ? timePart.trim().split(":") : [];
|
||||
|
||||
return {
|
||||
dateTokens: dateTokens,
|
||||
timeTokens: timeTokens
|
||||
};
|
||||
}
|
||||
required property GridLayout loader
|
||||
required property Wrapper popouts
|
||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.normal * 2 : verticalColumn.implicitHeight + Tokens.padding.larger * 2
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: horizontal ? baseSize : size
|
||||
implicitWidth: horizontal ? size : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
CustomText {
|
||||
id: timeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.contentColor
|
||||
font: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
||||
height: implicitHeight
|
||||
text: Time.dateStr
|
||||
visible: root.horizontal
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: verticalColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.padding.small ?? 2
|
||||
visible: !root.horizontal
|
||||
width: root.baseSize
|
||||
|
||||
Repeater {
|
||||
model: root.formatParts.dateTokens
|
||||
|
||||
CustomText {
|
||||
required property string modelData
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: root.contentColor
|
||||
font: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
||||
text: Qt.formatDateTime(Time.date, modelData)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Tokens.padding.small ?? 2
|
||||
Layout.topMargin: Tokens.padding.small ?? 2
|
||||
color: root.contentColor
|
||||
height: 1
|
||||
implicitWidth: root.baseSize * 0.5
|
||||
opacity: 0.4
|
||||
radius: 1
|
||||
visible: root.formatParts.dateTokens.length > 0 && root.formatParts.timeTokens.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.formatParts.timeTokens
|
||||
|
||||
CustomText {
|
||||
required property string modelData
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: root.contentColor
|
||||
font: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
||||
text: {
|
||||
if (modelData.includes("h"))
|
||||
return Time.hourStr;
|
||||
if (modelData.includes("m"))
|
||||
return Time.minuteStr;
|
||||
if (modelData.includes("s"))
|
||||
return Time.secondStr;
|
||||
return "";
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: root.contentColor
|
||||
font: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
||||
text: Qt.formatDateTime(Time.date, "AP")
|
||||
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
color: root.contentColor
|
||||
|
||||
onClicked: {
|
||||
root.visibilities.dashboard = !root.visibilities.dashboard;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property real batHeight: Tokens.padding.smaller * 2
|
||||
property real batWidth: Tokens.padding.larger * 2
|
||||
required property string devState
|
||||
property real nubHeight: Tokens.padding.small
|
||||
property real nubWidth: 2
|
||||
required property real percentage
|
||||
property real radius: Tokens.rounding.smallest / 2
|
||||
|
||||
spacing: 1
|
||||
|
||||
CustomRect {
|
||||
id: track
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Battery.getColors(root.percentage, root.devState).bg
|
||||
height: root.batHeight
|
||||
radius: root.radius
|
||||
width: root.batWidth
|
||||
|
||||
CustomText {
|
||||
color: Battery.getColors(root.percentage, root.devState).fg
|
||||
font.pointSize: Tokens.font.size.larger / 1.5
|
||||
font.weight: 800
|
||||
height: track.height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Math.round(root.percentage * 100)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: track.width
|
||||
}
|
||||
|
||||
Item {
|
||||
clip: true
|
||||
width: parent.width * root.percentage
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: fill
|
||||
|
||||
color: Battery.getColors(root.percentage, root.devState).fg
|
||||
height: track.height
|
||||
radius: track.radius
|
||||
width: track.width
|
||||
|
||||
CustomText {
|
||||
id: batteryLabel
|
||||
|
||||
clip: true
|
||||
color: Battery.getColors(root.percentage, root.devState).bg
|
||||
font.pointSize: Tokens.font.size.larger / 1.5
|
||||
font.weight: 800
|
||||
height: track.height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Math.round(root.percentage * 100)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: track.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: nub
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
bottomRightRadius: 20
|
||||
color: root.percentage < 0.99 ? track.color : fill.color
|
||||
height: root.nubHeight
|
||||
topRightRadius: 20
|
||||
width: root.nubWidth
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.bar.innerSize * 1.5))
|
||||
required property bool horizontal
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: horizontal ? baseSize : width
|
||||
implicitWidth: horizontal ? height : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
property bool tempEnabled: Hyprsunset.enabled
|
||||
|
||||
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
@@ -26,6 +25,7 @@ CustomRect {
|
||||
animate: true
|
||||
color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
fill: root.tempEnabled ? 1 : 0
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: root.tempEnabled ? "lightbulb" : "light_off"
|
||||
|
||||
Behavior on fill {
|
||||
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||
|
||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.normal * 2
|
||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.normal * 2 : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
font: mediatext.font
|
||||
text: mediatext.text
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: layout
|
||||
|
||||
anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.normal
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.horizontal ? Tokens.padding.normal : 0
|
||||
anchors.rightMargin: root.horizontal ? Tokens.padding.normal : 0
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.normal
|
||||
columns: root.horizontal ? -1 : 1
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: Players.active?.isPlaying ? "music_note" : "music_off"
|
||||
}
|
||||
|
||||
MarqueeText {
|
||||
id: mediatext
|
||||
|
||||
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
implicitHeight: root.horizontal ? root.implicitHeight : root.textWidth
|
||||
implicitWidth: root.horizontal ? root.textWidth : root.implicitWidth
|
||||
marqueeEnabled: false
|
||||
pauseMs: 4000
|
||||
text: root.currentMedia
|
||||
vertical: !root.horizontal
|
||||
}
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (!containsMouse) {
|
||||
mediatext.marqueeEnabled = false;
|
||||
} else {
|
||||
mediatext.marqueeEnabled = true;
|
||||
mediatext.anim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
implicitWidth: layout.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: "android_wifi_4_bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Modules.Bar.Popouts
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
required property Wrapper popouts
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
@@ -22,7 +22,6 @@ CustomRect {
|
||||
anchors.centerIn: parent
|
||||
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
fill: root.visibilities.sidebar ? 1 : 0
|
||||
font.family: "Material Symbols Rounded"
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
|
||||
|
||||
@@ -5,13 +5,14 @@ import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
RowLayout {
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
property color accentColor: warning ? Colors.palette.m3error : mainColor
|
||||
property real animatedPercentage: 0
|
||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||
readonly property real arcSweep: 1.5 * Math.PI
|
||||
required property bool horizontal
|
||||
property string icon
|
||||
required property color iconColor
|
||||
required property color mainColor
|
||||
@@ -21,8 +22,9 @@ RowLayout {
|
||||
property bool warning: percentage * 100 >= warningThreshold
|
||||
property int warningThreshold: 80
|
||||
|
||||
columnSpacing: Tokens.spacing.smaller
|
||||
columns: horizontal ? -1 : 1
|
||||
percentage: 0
|
||||
spacing: Tokens.spacing.smaller
|
||||
|
||||
Behavior on animatedPercentage {
|
||||
Anim {
|
||||
@@ -47,8 +49,8 @@ RowLayout {
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.preferredHeight: root.height - Tokens.padding.small
|
||||
Layout.preferredWidth: 4
|
||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.small : 4
|
||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
@@ -57,9 +59,11 @@ RowLayout {
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.right: root.horizontal ? parent.right : undefined
|
||||
anchors.top: root.horizontal ? undefined : parent.top
|
||||
color: root.mainColor
|
||||
implicitHeight: Math.ceil(root.percentage * parent.height)
|
||||
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0
|
||||
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width)
|
||||
|
||||
// Behavior on implicitHeight {
|
||||
// Anim {
|
||||
@@ -6,18 +6,18 @@ import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Services
|
||||
import qs.Modules
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Components
|
||||
|
||||
CustomRect {
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
clip: true
|
||||
color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: rowLayout.implicitWidth + Tokens.padding.larger * 2
|
||||
implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.normal * 2
|
||||
implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.larger * 2 : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
@@ -26,13 +26,14 @@ CustomRect {
|
||||
onClicked: root.visibilities.resources = !root.visibilities.resources
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
GridLayout {
|
||||
id: gridLayout
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -2
|
||||
implicitHeight: root.implicitHeight
|
||||
spacing: Tokens.spacing.smaller
|
||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||
columnSpacing: Tokens.spacing.smaller
|
||||
columns: root.horizontal ? -1 : 1
|
||||
|
||||
ServiceRef {
|
||||
service: Gpu
|
||||
@@ -48,7 +49,9 @@ CustomRect {
|
||||
|
||||
Resource {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
horizontal: root.horizontal
|
||||
icon: "memory"
|
||||
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
|
||||
@@ -57,7 +60,9 @@ CustomRect {
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
horizontal: root.horizontal
|
||||
icon: "memory_alt"
|
||||
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
|
||||
@@ -66,7 +71,9 @@ CustomRect {
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
horizontal: root.horizontal
|
||||
icon: "gamepad"
|
||||
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
|
||||
@@ -74,7 +81,9 @@ CustomRect {
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
horizontal: root.horizontal
|
||||
icon: "developer_board"
|
||||
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
|
||||
@@ -3,13 +3,14 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Helpers
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules.Bar.Components.StatusIcons
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
readonly property int firstPresent: {
|
||||
@@ -21,7 +22,7 @@ CustomClippingRect {
|
||||
}
|
||||
|
||||
// Index of the first/last entry that isn't collapsed, for edge margin gating
|
||||
readonly property alias items: row
|
||||
readonly property alias items: grid
|
||||
readonly property int lastPresent: {
|
||||
const values = model.values;
|
||||
for (let i = values.length - 1; i >= 0; i--)
|
||||
@@ -29,6 +30,7 @@ CustomClippingRect {
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
readonly property real size: horizontal ? grid.implicitWidth + Tokens.padding.small * 2 : grid.implicitHeight + Tokens.padding.small * 2
|
||||
readonly property int spacing: Tokens.spacing.normal / 2
|
||||
|
||||
// Entries that can shrink to nothing, spacing included
|
||||
@@ -38,19 +40,36 @@ CustomClippingRect {
|
||||
return false;
|
||||
}
|
||||
|
||||
bottomLeftRadius: Tokens.rounding.smallest / 2
|
||||
bottomLeftRadius: horizontal ? Tokens.rounding.smallest / 2 : Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: row.implicitWidth + Tokens.padding.small * 2
|
||||
implicitHeight: horizontal ? baseSize : size
|
||||
implicitWidth: horizontal ? size : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
topLeftRadius: Tokens.rounding.smallest / 2
|
||||
topRightRadius: horizontal ? Tokens.rounding.full : Tokens.rounding.smallest / 2
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: grid
|
||||
|
||||
anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.small
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.small
|
||||
anchors.rightMargin: Tokens.padding.small
|
||||
anchors.leftMargin: root.horizontal ? Tokens.padding.small : 0
|
||||
anchors.rightMargin: root.horizontal ? Tokens.padding.small : 0
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.small
|
||||
columns: root.horizontal ? -1 : 1
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
@@ -69,6 +88,7 @@ CustomClippingRect {
|
||||
name: "audio"
|
||||
|
||||
AudioWidget {
|
||||
horizontal: root.horizontal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +100,7 @@ CustomClippingRect {
|
||||
name: "audio"
|
||||
|
||||
MicWidget {
|
||||
horizontal: root.horizontal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,8 +152,10 @@ CustomClippingRect {
|
||||
property real rightGap: present && index !== root.firstPresent ? margin : 0
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: Math.round(leftGap)
|
||||
Layout.rightMargin: Math.round(rightGap)
|
||||
Layout.bottomMargin: root.horizontal ? 0 : Math.round(rightGap)
|
||||
Layout.leftMargin: root.horizontal ? Math.round(leftGap) : 0
|
||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||
children: item
|
||||
implicitHeight: item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.implicitWidth ?? 0
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Daemons
|
||||
import qs.Modules
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: speaker
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
animate: true
|
||||
color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface
|
||||
fill: 1
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Daemons
|
||||
import qs.Modules
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: mic
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
animate: true
|
||||
color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface
|
||||
fill: 1
|
||||
@@ -0,0 +1,80 @@
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property Item currentItem: batteryIconLoader?.item ?? peripheralIconLoader?.item ?? upowerIconLoader.item
|
||||
|
||||
implicitHeight: currentItem?.implicitHeight ?? 0
|
||||
implicitWidth: currentItem?.implicitWidth ?? 0
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Battery.startDaemon()
|
||||
|
||||
Loader {
|
||||
id: batteryIconLoader
|
||||
|
||||
active: Battery.isLaptop
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: BatteryIcon {
|
||||
devState: Battery.deviceStateString
|
||||
percentage: Battery.currentPerc
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: peripheralIconLoader
|
||||
|
||||
active: (Battery.lowestPeripheral?.isPresent ?? false) && !batteryIconLoader.active
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: BatteryIcon {
|
||||
devState: Battery.lowestPeripheral?.state ?? ""
|
||||
percentage: Battery.lowestPeripheral?.percentage ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: upowerIconLoader
|
||||
|
||||
active: !batteryIconLoader.active && !peripheralIconLoader.active
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
animate: true
|
||||
fill: 1
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: {
|
||||
if (PowerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "energy_savings_leaf";
|
||||
if (PowerProfiles.profile === PowerProfile.Performance)
|
||||
return "bolt";
|
||||
return "balance";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import QtQuick.Layouts
|
||||
import QtQuick
|
||||
import QtQuick.VectorImage
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.Helpers
|
||||
import qs.Modules
|
||||
import qs.Modules.Bar.Popouts
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
@@ -13,10 +12,10 @@ Item {
|
||||
id: root
|
||||
|
||||
property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent
|
||||
readonly property real dpr: Hypr.monitorFor(loader.screen).scale
|
||||
readonly property real dpr: Hypr.monitorFor(loader?.screen).scale
|
||||
required property int ind
|
||||
required property SystemTrayItem item
|
||||
required property RowLayout loader
|
||||
required property GridLayout loader
|
||||
required property Wrapper popouts
|
||||
|
||||
function resolveIcon(app: string, icon: string): string {
|
||||
@@ -3,33 +3,37 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import qs.Modules
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Modules.Bar.Components.Tray
|
||||
import qs.Modules.Bar.Popouts
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
readonly property alias items: repeater
|
||||
readonly property alias layout: sysRow
|
||||
required property RowLayout loader
|
||||
readonly property alias layout: sysGrid
|
||||
required property GridLayout loader
|
||||
readonly property int padding: Tokens.padding.small
|
||||
required property Wrapper popouts
|
||||
readonly property real size: horizontal ? sysGrid.implicitWidth + Tokens.padding.small : sysGrid.implicitHeight + Tokens.padding.small
|
||||
|
||||
bottomLeftRadius: horizontal ? Tokens.rounding.full : Tokens.rounding.smallest / 2
|
||||
bottomRightRadius: Tokens.rounding.smallest / 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: sysRow.implicitWidth + Tokens.padding.small * 2
|
||||
implicitHeight: horizontal ? baseSize : size
|
||||
implicitWidth: horizontal ? size : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
topRightRadius: Tokens.rounding.smallest / 2
|
||||
topRightRadius: horizontal ? Tokens.rounding.smallest / 2 : Tokens.rounding.full
|
||||
|
||||
RowLayout {
|
||||
id: sysRow
|
||||
GridLayout {
|
||||
id: sysGrid
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
columns: root.horizontal ? -1 : 1
|
||||
rowSpacing: -2
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
WidgetBase {
|
||||
id: root
|
||||
|
||||
property int countUpdates: Updates.availableUpdates
|
||||
property color textColor: Colors.palette.m3onSurface
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: horizontal ? baseSize : content.implicitHeight + Tokens.spacing.small
|
||||
implicitWidth: horizontal ? content.implicitWidth + Tokens.spacing.small * 2 : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
GridLayout {
|
||||
id: content
|
||||
|
||||
anchors.centerIn: parent
|
||||
columnSpacing: Tokens.spacing.small
|
||||
columns: root.horizontal ? -1 : 1
|
||||
rowSpacing: -Tokens.spacing.extraSmall / 2
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
|
||||
font.pointSize: root.horizontal ? Tokens.font.size.larger : Tokens.font.size.large
|
||||
text: "package_2"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
|
||||
color: root.textColor
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: root.countUpdates
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var bar
|
||||
property color color: Colors.palette.m3primary
|
||||
property Title current: text1
|
||||
required property bool horizontal
|
||||
// readonly property int maxWidth: 300
|
||||
readonly property int maxSize: {
|
||||
const otherModules = bar.children.filter(c => c.enabled && c.entryId && c.item !== this && c.entryId !== "spacer");
|
||||
const otherSize = otherModules.reduce((acc, curr) => acc + (horizontal ? (curr.item?.nonAnimWidth ?? curr.width ?? 0) : (curr.item.nonAnimHeight ?? curr.height ?? 0)), 0);
|
||||
return horizontal ? (bar.width - otherSize - bar.columnSpacing * (bar.children.length - 1) - bar.vPadding * 2) : (bar.height - otherSize - bar.rowSpacing * (bar.children.length - 1) - bar.vPadding * 4);
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: horizontal ? current.implicitHeight : current.implicitWidth + current.anchors.topMargin
|
||||
implicitWidth: horizontal ? current.implicitWidth + current.anchors.leftMargin : current.implicitHeight + current.anchors.topMargin
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text1
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
elide: Qt.ElideRight
|
||||
elideWidth: root.maxSize - (root.horizontal ? root.current.anchors.leftMargin : 0)
|
||||
font.family: "Rubik"
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
|
||||
|
||||
onElideWidthChanged: root.current.text = elidedText
|
||||
onTextChanged: {
|
||||
const next = root.current === text1 ? text2 : text1;
|
||||
next.text = elidedText;
|
||||
root.current = next;
|
||||
}
|
||||
}
|
||||
|
||||
component Title: CustomText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||
anchors.left: root.horizontal ? parent.left : undefined
|
||||
anchors.leftMargin: root.horizontal ? Tokens.spacing.small : 0
|
||||
anchors.top: root.horizontal ? undefined : parent.top
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.spacing.small
|
||||
anchors.verticalCenter: root.horizontal ? parent.verticalCenter : undefined
|
||||
color: root.color
|
||||
font.family: metrics.font.family
|
||||
font.pointSize: metrics.font.pointSize
|
||||
height: root.horizontal ? implicitHeight : implicitWidth
|
||||
opacity: root.current === this ? 1 : 0
|
||||
width: root.horizontal ? implicitWidth : implicitHeight
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
transform: [
|
||||
Translate {
|
||||
x: !root.horizontal ? -text.implicitWidth + text.implicitHeight : 0
|
||||
},
|
||||
Rotation {
|
||||
angle: root.horizontal ? 0 : 270
|
||||
origin.x: text.implicitHeight / 2
|
||||
origin.y: text.implicitHeight / 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Components.Workspaces
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -14,41 +14,57 @@ Item {
|
||||
|
||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.small / 2)
|
||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||
required property bool horizontal
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||
required property ShellScreen screen
|
||||
property int workspaceButtonWidth: bgRect.implicitHeight - root.activeWorkspaceMargin * 2
|
||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.bar.innerSize)
|
||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||
readonly property int workspacesShown: workspaces.length
|
||||
|
||||
height: implicitHeight
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smaller * 2
|
||||
implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2
|
||||
implicitHeight: horizontal ? shortSize : size
|
||||
implicitWidth: horizontal ? size : shortSize
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
// qmllint disable Quick.anchor-combinations
|
||||
id: bgRect
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.bottom: root.horizontal ? undefined : parent.bottom
|
||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||
anchors.left: root.horizontal ? parent.left : undefined
|
||||
anchors.right: root.horizontal ? parent.right : undefined
|
||||
anchors.top: root.horizontal ? undefined : parent.top
|
||||
anchors.verticalCenter: root.horizontal ? parent.verticalCenter : undefined
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: root.implicitHeight - ((Tokens.padding.small - 1) * 2)
|
||||
implicitHeight: root.horizontal ? root.implicitHeight : 0
|
||||
implicitWidth: root.horizontal ? 0 : root.implicitWidth
|
||||
radius: height / 2
|
||||
|
||||
// qmllint enable Quick.anchor-combinations
|
||||
Grid {
|
||||
id: grid
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.activeWorkspaceMargin
|
||||
columnSpacing: 0
|
||||
columns: root.workspacesShown
|
||||
columns: root.horizontal ? root.workspacesShown : 1
|
||||
rowSpacing: 0
|
||||
rows: 1
|
||||
rows: root.horizontal ? 1 : root.workspacesShown
|
||||
|
||||
Repeater {
|
||||
model: root.workspaces
|
||||
@@ -74,7 +90,7 @@ Item {
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: button.modelData.name
|
||||
text: button.modelData?.name ?? ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
@@ -94,13 +110,15 @@ Item {
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
|
||||
property real indicatorThickness: root.workspaceButtonWidth
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: root.horizontal ? parent.verticalCenter : undefined
|
||||
clip: true
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: indicatorThickness
|
||||
implicitWidth: indicatorLength
|
||||
implicitHeight: root.horizontal ? indicatorThickness : indicatorLength
|
||||
implicitWidth: root.horizontal ? indicatorLength : indicatorThickness
|
||||
radius: Tokens.rounding.full
|
||||
x: indicatorPosition
|
||||
x: root.horizontal ? indicatorPosition : 0
|
||||
y: root.horizontal ? 0 : indicatorPosition
|
||||
|
||||
AnimatedTabIndexPair {
|
||||
id: idxPair
|
||||
@@ -114,7 +132,8 @@ Item {
|
||||
implicitWidth: grid.width
|
||||
source: grid
|
||||
sourceColor: Colors.palette.m3onSurface
|
||||
x: -indicator.x + 3
|
||||
x: root.horizontal ? -indicator.x + 3 : 0
|
||||
y: root.horizontal ? 0 : -indicator.y + 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property real borderThickness
|
||||
readonly property alias content: content
|
||||
readonly property bool horizontal: position !== "left"
|
||||
// property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
|
||||
property real offsetScale: {
|
||||
if (content.hasCurrent)
|
||||
return 0;
|
||||
if (position === "top")
|
||||
return y > 0 ? 0 : 1;
|
||||
if (position === "bottom")
|
||||
return 1;
|
||||
return x > 0 ? 0 : 1;
|
||||
}
|
||||
required property string position
|
||||
required property ShellScreen screen
|
||||
|
||||
anchors.bottom: position === "bottom" ? parent.bottom : undefined
|
||||
clip: true
|
||||
implicitHeight: horizontal ? content.implicitHeight * (1 - offsetScale) : content.implicitHeight
|
||||
implicitWidth: horizontal ? content.implicitWidth : content.implicitWidth * (1 - offsetScale)
|
||||
visible: width > 0 && height > 0
|
||||
x: {
|
||||
if (!horizontal)
|
||||
return 0;
|
||||
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
|
||||
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return Math.floor(Math.max(off, 0));
|
||||
}
|
||||
y: {
|
||||
if (horizontal)
|
||||
return 0;
|
||||
|
||||
const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2;
|
||||
const diff = parent.height - Math.floor(off + content.nonAnimHeight);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return Math.max(off, 0);
|
||||
}
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
enabled: !root.horizontal || root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: root.horizontal || root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Wrapper {
|
||||
id: content
|
||||
|
||||
anchors.bottom: root.position === "bottom" ? parent.bottom : undefined
|
||||
anchors.bottomMargin: (-implicitHeight - 5) * root.offsetScale
|
||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale
|
||||
anchors.top: root.position === "top" ? parent.top : undefined
|
||||
anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
offsetScale: root.offsetScale
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,6 @@ import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.WSOverview
|
||||
import qs.Modules.SysTray.Popouts
|
||||
import qs.Modules.Updates
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -5,7 +5,7 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Modules.SysTray.Popouts.Network
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
@@ -22,6 +24,31 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Battery.allPeripherals
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
required property var modelData
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.normal
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: layout.modelData.model
|
||||
}
|
||||
|
||||
BatteryIcon {
|
||||
devState: layout.modelData.state
|
||||
percentage: layout.modelData.percentage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
function formatSeconds(s: int, fallback: string): string {
|
||||
const day = Math.floor(s / 86400);
|
||||
@@ -75,7 +102,7 @@ Column {
|
||||
CustomText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onError
|
||||
font.family: Appearance.font.family.mono
|
||||
font.family: Config.appearance.font.family.mono
|
||||
text: qsTr("Performance Degraded")
|
||||
}
|
||||
|
||||
@@ -78,8 +78,6 @@ CustomClippingRect {
|
||||
required property var modelData
|
||||
readonly property list<string> sections: modelData.update.split(" ")
|
||||
|
||||
// anchors.left: parent.left
|
||||
// anchors.right: parent.right
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: root.itemHeight
|
||||
implicitWidth: 600
|
||||
@@ -1,56 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property real borderThickness
|
||||
readonly property alias content: content
|
||||
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
|
||||
required property ShellScreen screen
|
||||
|
||||
clip: true
|
||||
implicitHeight: content.implicitHeight * (1 - offsetScale)
|
||||
implicitWidth: content.implicitWidth
|
||||
visible: width > 0 && height > 0
|
||||
x: {
|
||||
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
|
||||
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return Math.floor(Math.max(off, 0));
|
||||
}
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
enabled: root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Wrapper {
|
||||
id: content
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
|
||||
offsetScale: root.offsetScale
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property RowLayout loader
|
||||
required property Wrapper popouts
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: timeText.contentWidth + Tokens.padding.normal * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
CustomText {
|
||||
id: timeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
font: Config.appearance.font.family.mono
|
||||
text: Time.dateStr
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
color: root.visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
root.visibilities.dashboard = !root.visibilities.dashboard;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,16 @@ Item {
|
||||
}
|
||||
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
|
||||
required property real offsetScale
|
||||
required property string position
|
||||
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
anchors.bottom: position === "bottom" ? parent.bottom : undefined
|
||||
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
anchors.left: position === "left" ? parent.left : undefined
|
||||
anchors.leftMargin: position === "left" ? (-implicitWidth - 5) * offsetScale : 0
|
||||
anchors.top: position === "top" ? parent.top : undefined
|
||||
anchors.topMargin: position === "top" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
implicitHeight: content.implicitHeight
|
||||
implicitWidth: content.implicitWidth || 854
|
||||
opacity: 1 - offsetScale
|
||||
@@ -28,8 +35,11 @@ Item {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: root.position === "top" ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined
|
||||
anchors.right: root.position !== "left" ? undefined : parent.right
|
||||
anchors.top: root.position === "bottom" ? parent.top : undefined
|
||||
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
dashState: root.dashState
|
||||
|
||||
@@ -82,7 +82,6 @@ Searcher {
|
||||
list.visibilities.launcher = false;
|
||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
|
||||
Config.colors.schemeType = variant;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Daemons
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
font: mediatext.font
|
||||
text: mediatext.text
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: Players.active?.isPlaying ? "music_note" : "music_off"
|
||||
}
|
||||
|
||||
MarqueeText {
|
||||
id: mediatext
|
||||
|
||||
Layout.preferredWidth: root.textWidth
|
||||
animate: true
|
||||
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
marqueeEnabled: false
|
||||
pauseMs: 4000
|
||||
text: root.currentMedia
|
||||
width: root.textWidth
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (!containsMouse) {
|
||||
mediatext.marqueeEnabled = false;
|
||||
} else {
|
||||
mediatext.marqueeEnabled = true;
|
||||
mediatext.anim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,16 @@ Item {
|
||||
|
||||
readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property string position
|
||||
readonly property bool shouldBeActive: root.visibilities.resources
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
anchors.topMargin: (-implicitHeight - 5) * offsetScale
|
||||
anchors.bottom: position === "bottom" ? parent.bottom : undefined
|
||||
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: position === "left" ? (-implicitWidth - 5) * offsetScale : 0
|
||||
anchors.top: position !== "bottom" ? parent.top : undefined
|
||||
anchors.topMargin: position === "top" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
clip: true
|
||||
implicitHeight: content.implicitHeight
|
||||
implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open
|
||||
@@ -31,7 +37,11 @@ Item {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.centerIn: parent
|
||||
anchors.bottom: root.position === "top" ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined
|
||||
anchors.right: root.position !== "left" ? undefined : parent.right
|
||||
anchors.top: root.position === "bottom" ? parent.top : undefined
|
||||
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
wrapper: root
|
||||
|
||||
@@ -2,8 +2,8 @@ pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Settings.Pages
|
||||
import qs.Modules.Settings.Pages.Wallpaper
|
||||
@@ -45,7 +45,11 @@ QtObject {
|
||||
|
||||
// Connectivity
|
||||
Component {
|
||||
PlaceholderComp {
|
||||
StackPage {
|
||||
Component {
|
||||
NetworkPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
|
||||
@@ -3,11 +3,12 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.SysTray.Popouts.Network
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
@@ -19,7 +20,7 @@ PageBase {
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.extraSmall / 2
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
Toggle {
|
||||
@@ -56,7 +57,7 @@ PageBase {
|
||||
id: networkItem
|
||||
|
||||
readonly property bool connecting: Network.connectingNetwork === modelData
|
||||
property int horizontalPadding: Appearance.padding.largeIncreased
|
||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||
required property int index
|
||||
readonly property bool isSecure: Network.isSecure(modelData.security)
|
||||
required property bool known
|
||||
@@ -64,10 +65,10 @@ PageBase {
|
||||
required property Repeater repeater
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0
|
||||
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
|
||||
first: index === 0
|
||||
last: index === (repeater.count - 1)
|
||||
radius: Appearance.rounding.small
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
@@ -83,9 +84,9 @@ PageBase {
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Appearance.spacing.extraSmall
|
||||
Layout.rightMargin: Appearance.spacing.extraSmall
|
||||
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
|
||||
Layout.leftMargin: Tokens.spacing.extraSmall
|
||||
Layout.rightMargin: Tokens.spacing.extraSmall
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: networkItem.modelData.name
|
||||
}
|
||||
@@ -95,9 +96,9 @@ PageBase {
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.palette.m3outline
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
|
||||
}
|
||||
}
|
||||
@@ -110,8 +111,8 @@ PageBase {
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: networkItem.connecting ? 0 : 1
|
||||
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
|
||||
|
||||
@@ -139,7 +140,7 @@ PageBase {
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
if (networkItem.isSecure && !networkItem.known) {
|
||||
@@ -157,7 +158,7 @@ PageBase {
|
||||
|
||||
required property Repeater repeater
|
||||
|
||||
Layout.preferredHeight: repeater.count > 0 ? Appearance.spacing.extraSmall : 0
|
||||
Layout.preferredHeight: repeater.count > 0 ? Tokens.spacing.extraSmall : 0
|
||||
}
|
||||
component Toggle: CustomSwitch {
|
||||
id: toggleItem
|
||||
@@ -169,13 +170,13 @@ PageBase {
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Appearance.padding.largeIncreased
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
|
||||
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
|
||||
indicator.anchors.right: right
|
||||
indicator.anchors.rightMargin: toggleItem.horizontalPadding
|
||||
indicator.anchors.verticalCenter: verticalCenter
|
||||
verticalPadding: Appearance.padding.normal
|
||||
verticalPadding: Tokens.padding.normal
|
||||
|
||||
background: ConnectedRect {
|
||||
id: bg
|
||||
@@ -190,7 +191,7 @@ PageBase {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: toggleItem.horizontalPadding
|
||||
anchors.right: toggleItem.indicator.left
|
||||
anchors.rightMargin: Appearance.spacing.normal
|
||||
anchors.rightMargin: Tokens.spacing.normal
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: column.implicitWidth
|
||||
|
||||
@@ -210,7 +211,7 @@ PageBase {
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(label.font);
|
||||
f.pointSize = Appearance.font.size.smaller;
|
||||
f.pointSize = Tokens.font.size.smaller;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.text
|
||||
@@ -221,11 +222,11 @@ PageBase {
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.palette.m3outline
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(subtext.font);
|
||||
f.pointSize = Appearance.font.size.small;
|
||||
f.pointSize = Tokens.font.size.small;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.subtext
|
||||
|
||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
|
||||
PageBase {
|
||||
@@ -25,7 +26,6 @@ PageBase {
|
||||
ToggleRow {
|
||||
checked: Config.bar.autoHide
|
||||
first: true
|
||||
last: true
|
||||
settingAnchor: "bar-autohide"
|
||||
subtext: qsTr("Hide the bar, reveal on hover")
|
||||
text: qsTr("Auto hide")
|
||||
@@ -33,6 +33,34 @@ PageBase {
|
||||
onToggled: Config.bar.autoHide = checked
|
||||
}
|
||||
|
||||
SelectRow {
|
||||
active: Config.bar.position === "top" ? menuItems[0] : Config.bar.position === "left" ? menuItems[1] : menuItems[2]
|
||||
last: true
|
||||
settingAnchor: "bar-position"
|
||||
subtext: qsTr("Change which edge the bar appears on")
|
||||
text: qsTr("Position")
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
icon: "build"
|
||||
text: qsTr("Top")
|
||||
value: "top"
|
||||
},
|
||||
MenuItem {
|
||||
icon: "rotate_auto"
|
||||
text: qsTr("Left")
|
||||
value: "left"
|
||||
},
|
||||
MenuItem {
|
||||
icon: "rotate_auto"
|
||||
text: qsTr("Bottom")
|
||||
value: "bottom"
|
||||
}
|
||||
]
|
||||
|
||||
onSelected: item => Config.bar.position = item.value
|
||||
}
|
||||
|
||||
// Components
|
||||
SectionHeader {
|
||||
text: qsTr("Components")
|
||||
|
||||
@@ -70,10 +70,7 @@ PageBase {
|
||||
}
|
||||
]
|
||||
|
||||
onSelected: item => {
|
||||
Config.screenshot.mode = item.value;
|
||||
Config.save();
|
||||
}
|
||||
onSelected: item => Config.screenshot.mode = item.value
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
|
||||
@@ -104,7 +104,6 @@ PageBase {
|
||||
onApplySettings: (start, end) => {
|
||||
Config.general.color.scheduleDarkStart = start;
|
||||
Config.general.color.scheduleDarkEnd = end;
|
||||
Config.save();
|
||||
ModeScheduler.checkStartup();
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
@@ -146,7 +145,6 @@ PageBase {
|
||||
onApplySettings: (start, end) => {
|
||||
Config.general.color.scheduleHyprsunsetStart = start;
|
||||
Config.general.color.scheduleHyprsunsetEnd = end;
|
||||
Config.save();
|
||||
Hyprsunset.checkStartup();
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import qs.Modules
|
||||
import qs.Services
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
readonly property alias items: repeater
|
||||
required property RowLayout loader
|
||||
required property Wrapper popouts
|
||||
|
||||
function closestRowChild(row, x) {
|
||||
let child = row.childAt(x, row.height / 2);
|
||||
if (child)
|
||||
return child;
|
||||
|
||||
let closest = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < row.children.length; ++i) {
|
||||
let c = row.children[i];
|
||||
|
||||
if (!c.visible || c.width <= 0)
|
||||
continue;
|
||||
|
||||
let centerX = c.x + c.width / 2;
|
||||
let dist = Math.abs(x - centerX);
|
||||
|
||||
if (dist < closestDistance) {
|
||||
closestDistance = dist;
|
||||
closest = c;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
function getHoveredSubItem(localX, localY) {
|
||||
let modPos = mapToItem(sysTrayMod, localX, localY);
|
||||
if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) {
|
||||
let modRowPos = sysTrayMod.mapToItem(sysIcons, modPos.x, modPos.y);
|
||||
let child = closestRowChild(sysIcons, modRowPos.x);
|
||||
if (child) {
|
||||
if (child.objectName === "audio" && Config.bar.popouts.audio)
|
||||
return {
|
||||
id: "audio",
|
||||
item: child
|
||||
};
|
||||
if (child.objectName === "networkWidget" && Config.bar.popouts.network)
|
||||
return {
|
||||
id: "network",
|
||||
item: child
|
||||
};
|
||||
if (child.objectName === "microphone" && Config.bar.popouts.audio)
|
||||
return {
|
||||
id: "audio",
|
||||
item: child
|
||||
};
|
||||
if (child.objectName === "upower" && Config.bar.popouts.upower)
|
||||
return {
|
||||
id: "upower",
|
||||
item: child
|
||||
};
|
||||
if (child.objectName === "bluetoothWidget")
|
||||
return {
|
||||
id: "bluetooth",
|
||||
item: child
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let trayPos = mapToItem(sysTray, localX, localY);
|
||||
if (Config.bar.tray.showOnHover && sysTray.contains(Qt.point(trayPos.x, trayPos.y))) {
|
||||
let trayRowPos = sysTray.mapToItem(sysRow, trayPos.x, trayPos.y);
|
||||
let child = sysRow.childAt(trayRowPos.x, trayRowPos.y);
|
||||
if (child && child.hasOwnProperty("ind")) {
|
||||
return {
|
||||
id: `traymenu${child.ind}`,
|
||||
item: child
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
height: Config.bar.height + Tokens.padding.smallest * 2
|
||||
spacing: Tokens.padding.small
|
||||
width: sysTray.implicitWidth + sysTrayMod.implicitWidth + Tokens.padding.small
|
||||
|
||||
CustomClippingRect {
|
||||
id: sysTray
|
||||
|
||||
Layout.fillHeight: true
|
||||
bottomRightRadius: Tokens.rounding.smallest / 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitWidth: sysRow.width + Tokens.padding.small * 2
|
||||
radius: Tokens.rounding.full
|
||||
topRightRadius: Tokens.rounding.smallest / 2
|
||||
|
||||
Row {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: sysTrayMod
|
||||
|
||||
Layout.fillHeight: true
|
||||
bottomLeftRadius: Tokens.rounding.smallest / 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitWidth: sysIcons.implicitWidth + Tokens.padding.smaller + Tokens.padding.normal
|
||||
radius: Tokens.rounding.full
|
||||
topLeftRadius: Tokens.rounding.smallest / 2
|
||||
|
||||
StatusIcons {
|
||||
id: sysIcons
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.smaller
|
||||
anchors.rightMargin: Tokens.padding.normal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: Battery.isLaptop ? batteryIconLoader.item.implicitHeight : upowerIconLoader.item.implicitHeight
|
||||
implicitWidth: Battery.isLaptop ? batteryIconLoader.item.implicitWidth : upowerIconLoader.item.implicitWidth
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: batteryIconLoader
|
||||
|
||||
active: Battery.isLaptop
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: Row {
|
||||
id: batteryIcon
|
||||
|
||||
property real batHeight: 16
|
||||
property real batWidth: 30
|
||||
property real nubHeight: 6
|
||||
property real nubWidth: 2
|
||||
property real radius: Tokens.rounding.smallest / 2
|
||||
|
||||
spacing: 1
|
||||
|
||||
CustomRect {
|
||||
id: track
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Battery.colors.bg
|
||||
height: batteryIcon.batHeight
|
||||
radius: batteryIcon.radius
|
||||
width: batteryIcon.batWidth
|
||||
|
||||
CustomText {
|
||||
color: Battery.colors.fg
|
||||
font.pointSize: Tokens.font.size.larger / 1.5
|
||||
font.weight: 800
|
||||
height: track.height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Math.round(Battery.currentPerc * 100)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: track.width
|
||||
}
|
||||
|
||||
Item {
|
||||
clip: true
|
||||
width: parent.width * Battery.currentPerc
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: fill
|
||||
|
||||
color: Battery.colors.fg
|
||||
height: track.height
|
||||
radius: track.radius
|
||||
width: track.width
|
||||
|
||||
CustomText {
|
||||
id: batteryLabel
|
||||
|
||||
clip: true
|
||||
color: Battery.colors.bg
|
||||
font.pointSize: 7.5
|
||||
font.weight: 800
|
||||
height: track.height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Math.round(Battery.currentPerc * 100)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: track.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: nub
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
bottomRightRadius: 20
|
||||
color: Battery.currentPerc < 0.99 ? track.color : fill.color
|
||||
height: batteryIcon.nubHeight
|
||||
topRightRadius: 20
|
||||
width: batteryIcon.nubWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: upowerIconLoader
|
||||
|
||||
active: !Battery.isLaptop
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
animate: true
|
||||
fill: 1
|
||||
text: {
|
||||
if (PowerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "energy_savings_leaf";
|
||||
if (PowerProfiles.profile === PowerProfile.Performance)
|
||||
return "bolt";
|
||||
return "balance";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property int countUpdates: Updates.availableUpdates
|
||||
property color textColor: Colors.palette.m3onSurface
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
|
||||
implicitWidth: contentRow.implicitWidth + Tokens.spacing.small * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: "package_2"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: root.textColor
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: root.countUpdates
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Item wrapper
|
||||
|
||||
implicitHeight: layout.implicitHeight + 16
|
||||
implicitWidth: layout.implicitWidth + 16
|
||||
|
||||
GridLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
columnSpacing: 8
|
||||
rowSpacing: 8
|
||||
|
||||
Repeater {
|
||||
model: Hypr.workspaces
|
||||
|
||||
CustomRect {
|
||||
id: workspacePreview
|
||||
|
||||
required property HyprlandWorkspace modelData
|
||||
|
||||
Layout.preferredHeight: 180 + 10
|
||||
Layout.preferredWidth: 320 + 10
|
||||
border.color: "white"
|
||||
border.width: 1
|
||||
radius: Tokens.rounding.smallest
|
||||
|
||||
Repeater {
|
||||
model: workspacePreview.modelData.toplevels
|
||||
|
||||
Item {
|
||||
id: preview
|
||||
|
||||
property rect appPosition: {
|
||||
let {
|
||||
at: [cx, cy],
|
||||
size: [cw, ch]
|
||||
} = modelData.lastIpcObject;
|
||||
|
||||
cx -= modelData.monitor.x;
|
||||
cy -= modelData.monitor.y;
|
||||
|
||||
return Qt.rect((cx / 8), (cy / 8), (cw / 8), (ch / 8));
|
||||
}
|
||||
required property HyprlandToplevel modelData
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
CustomRect {
|
||||
border.color: Colors.tPalette.m3outline
|
||||
border.width: 1
|
||||
implicitHeight: preview.appPosition.height
|
||||
implicitWidth: preview.appPosition.width
|
||||
radius: Tokens.rounding.smallest / 2
|
||||
x: preview.appPosition.x
|
||||
y: preview.appPosition.y - 3.4
|
||||
|
||||
ScreencopyView {
|
||||
id: previewCopy
|
||||
|
||||
anchors.fill: parent
|
||||
captureSource: preview.modelData.wayland
|
||||
live: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var bar
|
||||
property color colour: Colors.palette.m3primary
|
||||
property Title current: text1
|
||||
// readonly property int maxWidth: 300
|
||||
readonly property int maxWidth: {
|
||||
const otherModules = bar.children.filter(c => c.enabled && c.id && c.item !== this && c.id !== "spacer");
|
||||
const otherWidth = otherModules.reduce((acc, curr) => {
|
||||
return acc + (curr.item?.nonAnimWidth ?? curr.width ?? 0);
|
||||
}, 0);
|
||||
return bar.width - otherWidth - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: current.implicitHeight
|
||||
implicitWidth: Math.min(current.implicitWidth, root.maxWidth)
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text1
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
elide: Qt.ElideRight
|
||||
elideWidth: root.maxWidth
|
||||
font.family: "Rubik"
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
|
||||
|
||||
onElideWidthChanged: root.current.text = elidedText
|
||||
onTextChanged: {
|
||||
const next = root.current === text1 ? text2 : text1;
|
||||
next.text = elidedText;
|
||||
root.current = next;
|
||||
}
|
||||
}
|
||||
|
||||
component Title: CustomText {
|
||||
id: text
|
||||
|
||||
anchors.leftMargin: 7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.colour
|
||||
font.family: metrics.font.family
|
||||
font.pointSize: metrics.font.pointSize
|
||||
height: implicitHeight
|
||||
opacity: root.current === this ? 1 : 0
|
||||
width: implicitWidth
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "configlist.hpp"
|
||||
#include "configobject.hpp"
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhashfunctions.h>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
@@ -61,6 +63,7 @@ class Bar : public ConfigObject {
|
||||
CFG_PROPERTY(int, revealDelay, 100)
|
||||
CFG_PROPERTY(int, rounding, 14)
|
||||
CFG_PROPERTY(int, smoothing, 32)
|
||||
CFG_PROPERTY(QString, position, QStringLiteral("top"))
|
||||
CONFIG_SUBOBJECT(Tray, tray)
|
||||
CONFIG_LIST(
|
||||
EntryList,
|
||||
|
||||
@@ -61,6 +61,8 @@ Config::Config(QObject* parent)
|
||||
m_reloadTimer.setInterval(50);
|
||||
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
|
||||
|
||||
connectAutoSave(this);
|
||||
|
||||
connect(
|
||||
&m_watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
@@ -93,15 +95,31 @@ QString Config::filePath() const {
|
||||
|
||||
void Config::loadSync() {
|
||||
m_loading = true;
|
||||
|
||||
QFile f(filePath());
|
||||
QJsonObject before;
|
||||
|
||||
bool existed = false;
|
||||
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||
if (doc.isObject()) loadFromJson(QJsonValue(doc.object()));
|
||||
if (doc.isObject()) {
|
||||
before = doc.object();
|
||||
existed = true;
|
||||
} else {
|
||||
qInfo() << "Config: existing config at" << filePath()
|
||||
<< "is empty or not a valid JSON object - using defaults";
|
||||
}
|
||||
} else {
|
||||
qInfo() << "Config: no existing config at" << filePath()
|
||||
<< "- using defaults";
|
||||
}
|
||||
|
||||
loadFromJson(QJsonValue(before));
|
||||
m_loading = false;
|
||||
|
||||
const auto after = toJson().toObject();
|
||||
if (!existed || after != before) saveNow();
|
||||
}
|
||||
|
||||
void Config::updateWatch() {
|
||||
@@ -146,27 +164,21 @@ void Config::loadAsync() {
|
||||
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||
const bool valid = doc.isObject();
|
||||
const QJsonObject before = valid ? doc.object() : QJsonObject();
|
||||
|
||||
if (doc.isObject()) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, doc]() {
|
||||
loadFromJson(QJsonValue(doc.object()));
|
||||
m_loading = false;
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, valid, before]() {
|
||||
loadFromJson(QJsonValue(before));
|
||||
m_loading = false;
|
||||
|
||||
if (m_reloadPending) m_reloadTimer.start();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
} else {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() {
|
||||
m_loading = false;
|
||||
const auto after = toJson().toObject();
|
||||
if (!valid || after != before) saveNow();
|
||||
|
||||
if (m_reloadPending) m_reloadTimer.start();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
if (m_reloadPending) m_reloadTimer.start();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
} else {
|
||||
qInfo() << "Config: failed to reload from" << filePath()
|
||||
<< "- using in-memory values";
|
||||
@@ -232,6 +244,15 @@ void Config::saveNow() {
|
||||
flushAsync();
|
||||
}
|
||||
|
||||
void Config::connectAutoSave(ConfigNode* node) {
|
||||
connect(node, &ConfigNode::propertiesChanged, this, [this] {
|
||||
scheduleSave();
|
||||
});
|
||||
|
||||
for (auto* child : node->childNodes())
|
||||
connectAutoSave(child);
|
||||
}
|
||||
|
||||
bool Config::writeAtomically(const QByteArray& data) {
|
||||
QSaveFile f(filePath());
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class Config : public ConfigObject {
|
||||
void updateWatch();
|
||||
void loadSync();
|
||||
void loadAsync();
|
||||
void connectAutoSave(ConfigNode* node);
|
||||
|
||||
QTimer m_saveTimer;
|
||||
QTimer m_reloadTimer;
|
||||
|
||||
@@ -79,13 +79,14 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
|
||||
if (!json.isArray()) {
|
||||
qCWarning(
|
||||
lcConfig,
|
||||
"Option '%s' must be a list, ignoring",
|
||||
"Option '%s' must be a list, resetting to default",
|
||||
qUtf8Printable(propertyPath()));
|
||||
m_rejectedJson = json;
|
||||
|
||||
resetToDefaults();
|
||||
materializeDefaults();
|
||||
return;
|
||||
}
|
||||
|
||||
m_rejectedJson = QJsonValue::Undefined;
|
||||
populate(json.toArray());
|
||||
m_loaded = true;
|
||||
}
|
||||
@@ -93,12 +94,11 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
|
||||
QJsonValue ConfigList::toJson() const {
|
||||
if (m_loaded) return elementsToJson();
|
||||
|
||||
return m_rejectedJson;
|
||||
return QJsonValue::Undefined;
|
||||
}
|
||||
|
||||
void ConfigList::clearLoadedKeys() {
|
||||
m_loaded = false;
|
||||
m_rejectedJson = QJsonValue::Undefined;
|
||||
}
|
||||
|
||||
QStringList ConfigList::unknownKeys() const {
|
||||
@@ -113,6 +113,15 @@ QStringList ConfigList::unknownKeys() const {
|
||||
return keys;
|
||||
}
|
||||
|
||||
void ConfigList::materializeDefaults() {
|
||||
for (auto* const item : m_items)
|
||||
item->materializeDefaults();
|
||||
|
||||
if (m_global) return;
|
||||
|
||||
m_loaded = true;
|
||||
}
|
||||
|
||||
void ConfigList::resyncFromGlobal() {
|
||||
syncValuesFromGlobal();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ConfigList : public ConfigNode {
|
||||
[[nodiscard]] QJsonValue toJson() const override;
|
||||
void clearLoadedKeys() override;
|
||||
[[nodiscard]] QStringList unknownKeys() const override;
|
||||
void materializeDefaults() override;
|
||||
void resyncFromGlobal() override;
|
||||
|
||||
signals:
|
||||
@@ -71,7 +72,6 @@ class ConfigList : public ConfigNode {
|
||||
QJsonArray m_defaults;
|
||||
QList<ConfigObject*> m_items;
|
||||
bool m_loaded = false;
|
||||
QJsonValue m_rejectedJson = QJsonValue::Undefined;
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -103,7 +103,6 @@ class ConfigList : public ConfigNode {
|
||||
} \
|
||||
};
|
||||
|
||||
|
||||
#define CONFIG_LIST(Type, name, ...) \
|
||||
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
|
||||
\
|
||||
@@ -115,6 +114,8 @@ class ConfigList : public ConfigNode {
|
||||
private: \
|
||||
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__));
|
||||
|
||||
#define LIST_ENTRY(id, enabled) \
|
||||
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
@@ -136,6 +137,3 @@ class ListEntry : public ConfigObject {
|
||||
CONFIG_LIST_TYPE(ListEntry, EntryList)
|
||||
|
||||
} // namespace ZShell::config
|
||||
|
||||
#define LIST_ENTRY(id, enabled) \
|
||||
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
|
||||
|
||||
@@ -26,6 +26,8 @@ class ConfigNode : public QObject {
|
||||
[[nodiscard]] virtual QStringList unknownKeys() const = 0;
|
||||
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
|
||||
|
||||
virtual void materializeDefaults() = 0;
|
||||
|
||||
void syncFromGlobal(ConfigNode* global);
|
||||
virtual void resyncFromGlobal() = 0;
|
||||
|
||||
|
||||
@@ -7,6 +7,51 @@
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
namespace {
|
||||
|
||||
bool isStringArray(const QJsonArray& arr) {
|
||||
for (const auto& v : arr) {
|
||||
if (!v.isString()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
|
||||
switch (val.type()) {
|
||||
case QJsonValue::Bool:
|
||||
return type.id() == QMetaType::Bool;
|
||||
|
||||
case QJsonValue::Double:
|
||||
switch (type.id()) {
|
||||
case QMetaType::Int:
|
||||
case QMetaType::UInt:
|
||||
case QMetaType::LongLong:
|
||||
case QMetaType::ULongLong:
|
||||
case QMetaType::Double:
|
||||
case QMetaType::Float:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
case QJsonValue::String:
|
||||
return type.id() == QMetaType::QString;
|
||||
|
||||
case QJsonValue::Array:
|
||||
if (type.id() == QMetaType::QStringList)
|
||||
return isStringArray(val.toArray());
|
||||
return type.id() == QMetaType::QVariantList;
|
||||
|
||||
case QJsonValue::Object:
|
||||
case QJsonValue::Null:
|
||||
case QJsonValue::Undefined:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
|
||||
|
||||
void ConfigObject::loadFromJson(const QJsonValue& json) {
|
||||
@@ -14,6 +59,7 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
|
||||
const auto* meta = metaObject();
|
||||
|
||||
QSet<QString> known;
|
||||
QSet<QString> invalid;
|
||||
|
||||
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
||||
auto prop = meta->property(i);
|
||||
@@ -34,6 +80,15 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
|
||||
|
||||
if (!prop.isWritable()) continue;
|
||||
|
||||
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
|
||||
qWarning() << "Config: type mismatch for" << key << "in"
|
||||
<< meta->className() << "- expected"
|
||||
<< prop.metaType().name() << "got value" << jsonVal
|
||||
<< "- resetting to default";
|
||||
invalid.insert(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop.metaType().id() == QMetaType::QStringList) {
|
||||
QStringList list;
|
||||
const auto jsonArr = jsonVal.toArray();
|
||||
@@ -56,10 +111,12 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
|
||||
|
||||
m_extras = {};
|
||||
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
||||
if (!known.contains(it.key())) {
|
||||
if (!known.contains(it.key()) || invalid.contains(it.key())) {
|
||||
m_extras.insert(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
materializeDefaults();
|
||||
}
|
||||
|
||||
QJsonValue ConfigObject::toJson() const {
|
||||
@@ -104,9 +161,6 @@ QJsonValue ConfigObject::toJson() const {
|
||||
obj.insert(key, QJsonValue::fromVariant(value));
|
||||
}
|
||||
|
||||
for (auto it = m_extras.begin(); it != m_extras.end(); ++it)
|
||||
obj.insert(it.key(), it.value());
|
||||
|
||||
if (obj.isEmpty()) return QJsonValue::Undefined;
|
||||
|
||||
return obj;
|
||||
@@ -159,6 +213,26 @@ QList<ConfigNode*> ConfigObject::childNodes() const {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
void ConfigObject::materializeDefaults() {
|
||||
const auto* meta = metaObject();
|
||||
|
||||
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
||||
const auto prop = meta->property(i);
|
||||
const auto key = QString::fromUtf8(prop.name());
|
||||
|
||||
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
||||
node->materializeDefaults();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!prop.isWritable()) continue;
|
||||
|
||||
if (m_global) continue;
|
||||
|
||||
m_loadedKeys.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigObject::syncValuesFromGlobal() {
|
||||
const auto* meta = metaObject();
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class ConfigObject : public ConfigNode {
|
||||
void clearLoadedKeys() override;
|
||||
[[nodiscard]] QStringList unknownKeys() const override;
|
||||
[[nodiscard]] QList<ConfigNode*> childNodes() const override;
|
||||
void materializeDefaults() override;
|
||||
void resyncFromGlobal() override;
|
||||
|
||||
[[nodiscard]] virtual QStringList identityKeys() const;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "configobject.hpp"
|
||||
|
||||
#include <QVariantList>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
@@ -195,6 +197,16 @@ class AnimTokens : public ConfigObject {
|
||||
, m_durations(new AnimDurations(this)) {}
|
||||
};
|
||||
|
||||
class BarTokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
CFG_PROPERTY(int, innerSize, 28)
|
||||
|
||||
public:
|
||||
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
};
|
||||
|
||||
class Tokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
@@ -205,6 +217,7 @@ class Tokens : public ConfigObject {
|
||||
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
|
||||
CONFIG_SUBOBJECT(FontTokens, font)
|
||||
CONFIG_SUBOBJECT(AnimTokens, anim)
|
||||
CONFIG_SUBOBJECT(BarTokens, bar)
|
||||
|
||||
public:
|
||||
explicit Tokens(QObject* parent = nullptr)
|
||||
@@ -213,7 +226,8 @@ class Tokens : public ConfigObject {
|
||||
, m_padding(new AppearancePadding(this))
|
||||
, m_spacing(new AppearanceSpacing(this))
|
||||
, m_font(new FontTokens(this))
|
||||
, m_anim(new AnimTokens(this)) {}
|
||||
, m_anim(new AnimTokens(this))
|
||||
, m_bar(new BarTokens(this)) {}
|
||||
|
||||
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
|
||||
};
|
||||
|
||||
@@ -113,7 +113,6 @@ Singleton {
|
||||
function setMode(mode: string): void {
|
||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
|
||||
Config.general.color.mode = mode;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
function swapRG(c: color): color {
|
||||
|
||||
@@ -8,7 +8,14 @@ import typer
|
||||
from typer._completion_classes import completion_init
|
||||
from typer._completion_shared import _get_shell_name, install
|
||||
|
||||
from zshell.subcommands import record, scheme, screenshot, shell, wallpaper
|
||||
from zshell.subcommands import (
|
||||
battery,
|
||||
record,
|
||||
scheme,
|
||||
screenshot,
|
||||
shell,
|
||||
wallpaper,
|
||||
)
|
||||
|
||||
app = typer.Typer(name="zshell-cli", add_completion=False)
|
||||
|
||||
@@ -17,6 +24,7 @@ app.add_typer(scheme.app, name="scheme")
|
||||
app.add_typer(screenshot.app, name="screenshot")
|
||||
app.add_typer(wallpaper.app, name="wallpaper")
|
||||
app.add_typer(record.app, name="record")
|
||||
app.add_typer(battery.app, name="battery")
|
||||
|
||||
|
||||
def _completion_installed() -> bool:
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import struct
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from logitech_receiver import hidpp10, hidpp20
|
||||
from logitech_receiver.common import Battery, BatteryStatus, Notification
|
||||
from logitech_receiver.hidpp10_constants import Registers
|
||||
from logitech_receiver.hidpp20_constants import SupportedFeature
|
||||
|
||||
app = typer.Typer(
|
||||
help="Read live battery status from Logitech HID++ peripherals."
|
||||
)
|
||||
|
||||
logger = logging.getLogger("zshell.battery")
|
||||
|
||||
DEFAULT_OUTPUT = Path.home() / ".cache" / "zshell" / "battery.json"
|
||||
|
||||
_RUNTIME_DIR = Path(
|
||||
os.environ.get("XDG_RUNTIME_DIR") or (Path.home() / ".cache" / "zshell")
|
||||
)
|
||||
DEFAULT_LOCK = _RUNTIME_DIR / "zshell-battery-daemon.lock"
|
||||
|
||||
|
||||
def _acquire_singleton_lock(path: Path) -> int | None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o644)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
os.close(fd)
|
||||
return None
|
||||
os.ftruncate(fd, 0)
|
||||
os.write(fd, str(os.getpid()).encode())
|
||||
return fd
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceBattery:
|
||||
model: str
|
||||
nativePath: str | None
|
||||
serial: str | None
|
||||
type: str | None
|
||||
wired: bool
|
||||
percentage: float | None
|
||||
state: str | None
|
||||
charging: bool
|
||||
isPresent: bool
|
||||
|
||||
|
||||
_SYSFS_POWER_SUPPLY = Path("/sys/class/power_supply")
|
||||
|
||||
|
||||
def _normalize_serial(s: str) -> str:
|
||||
return re.sub(r"[^0-9A-Za-z]", "", s).upper()
|
||||
|
||||
|
||||
def _find_upower_native_path(serial: str | None) -> str | None:
|
||||
if not serial or not _SYSFS_POWER_SUPPLY.is_dir():
|
||||
return None
|
||||
target = _normalize_serial(serial)
|
||||
for entry in _SYSFS_POWER_SUPPLY.glob("hidpp_battery_*"):
|
||||
try:
|
||||
raw = (entry / "serial_number").read_text().strip()
|
||||
except OSError:
|
||||
logger.debug(
|
||||
"couldn't read serial_number under %s", entry, exc_info=True
|
||||
)
|
||||
continue
|
||||
if _normalize_serial(raw) == target:
|
||||
return entry.name
|
||||
return None
|
||||
|
||||
|
||||
def _process_battery_notification(dev, n) -> bool:
|
||||
|
||||
if not getattr(dev, "isDevice", False):
|
||||
return False
|
||||
|
||||
if int(n.sub_id) & 0x80:
|
||||
return False
|
||||
if n.sub_id == Notification.NO_OPERATION:
|
||||
return False
|
||||
|
||||
if getattr(dev, "protocol", None) is not None and dev.protocol < 2.0:
|
||||
if n.sub_id in (Registers.BATTERY_STATUS, Registers.BATTERY_CHARGE):
|
||||
if n.data[-1:] != b"\x00":
|
||||
return False
|
||||
data = bytes([n.address]) + n.data
|
||||
dev.set_battery_info(hidpp10.parse_battery_status(n.sub_id, data))
|
||||
return True
|
||||
return False
|
||||
|
||||
features = getattr(dev, "features", None)
|
||||
if not features:
|
||||
return False
|
||||
|
||||
try:
|
||||
feature = features.get_feature(n.sub_id)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if feature == SupportedFeature.BATTERY_STATUS:
|
||||
if n.address == 0x00:
|
||||
dev.set_battery_info(hidpp20.decipher_battery_status(n.data)[1])
|
||||
return True
|
||||
return False
|
||||
|
||||
if feature == SupportedFeature.BATTERY_VOLTAGE:
|
||||
if n.address == 0x00:
|
||||
dev.set_battery_info(hidpp20.decipher_battery_voltage(n.data)[1])
|
||||
return True
|
||||
return False
|
||||
|
||||
if feature == SupportedFeature.UNIFIED_BATTERY:
|
||||
if n.address == 0x00:
|
||||
dev.set_battery_info(hidpp20.decipher_battery_unified(n.data)[1])
|
||||
return True
|
||||
return False
|
||||
|
||||
if feature == SupportedFeature.ADC_MEASUREMENT:
|
||||
if n.address == 0x00:
|
||||
result = hidpp20.decipher_adc_measurement(n.data)
|
||||
if result:
|
||||
dev.set_battery_info(result[1])
|
||||
return True
|
||||
return False
|
||||
|
||||
if feature == SupportedFeature.CENTURION_BATTERY_SOC:
|
||||
dev.set_battery_info(hidpp20.decipher_battery_centurion(n.data)[1])
|
||||
return True
|
||||
|
||||
if feature == SupportedFeature.SOLAR_DASHBOARD:
|
||||
if n.data[5:9] == b"GOOD":
|
||||
charge, lux, _ = struct.unpack("!BHH", n.data[:5])
|
||||
status = BatteryStatus.DISCHARGING
|
||||
if n.address == 0x10 and lux > 200:
|
||||
status = BatteryStatus.RECHARGING
|
||||
dev.set_battery_info(Battery(charge, None, status, None, lux))
|
||||
return True
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _import_listener_deps():
|
||||
try:
|
||||
from logitech_receiver import base, device, receiver
|
||||
from logitech_receiver.listener import EventsListener
|
||||
except ImportError as e:
|
||||
raise RuntimeError(
|
||||
"logitech_receiver isn't importable -- install it with `pip install solaar`."
|
||||
) from e
|
||||
except ValueError as e:
|
||||
raise RuntimeError(
|
||||
"logitech_receiver failed to import because a GTK3 typelib is missing "
|
||||
f"({e}). Install the same GTK3 + PyGObject packages Solaar's GUI needs "
|
||||
"(e.g. python3-gi + gir1.2-gtk-3.0 on Debian/Ubuntu, or the gtk3/"
|
||||
"python3-gobject equivalents on your distro)."
|
||||
) from e
|
||||
|
||||
class _BatteryEventsListener(EventsListener):
|
||||
def __init__(self, receiver_or_device, on_change):
|
||||
super().__init__(receiver_or_device, self._handle_notification)
|
||||
self._on_change = on_change
|
||||
|
||||
def has_started(self):
|
||||
|
||||
if not self.receiver.isDevice:
|
||||
try:
|
||||
self.receiver.notification_flags = (
|
||||
self.receiver.enable_connection_notifications()
|
||||
)
|
||||
self.receiver.notify_devices()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"failed enabling notifications for %s", self.receiver
|
||||
)
|
||||
|
||||
def _handle_notification(self, n):
|
||||
if self.receiver.isDevice:
|
||||
try:
|
||||
_process_battery_notification(self.receiver, n)
|
||||
except Exception:
|
||||
logger.exception("processing %s for %s", n, self.receiver)
|
||||
self._on_change(self.receiver)
|
||||
return
|
||||
|
||||
if n.devnumber == 0xFF:
|
||||
try:
|
||||
_process_battery_notification(self.receiver, n)
|
||||
except Exception:
|
||||
logger.exception("processing receiver notification %s", n)
|
||||
self._on_change(self.receiver)
|
||||
return
|
||||
|
||||
if not (0 < n.devnumber <= 16):
|
||||
logger.warning(
|
||||
"unexpected device number %s in %s", n.devnumber, n
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
dev = self.receiver[n.devnumber]
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"resolving device number %s on %s",
|
||||
n.devnumber,
|
||||
self.receiver,
|
||||
)
|
||||
return
|
||||
|
||||
if not dev:
|
||||
logger.warning(
|
||||
"%s: received %s for invalid device %d",
|
||||
self.receiver,
|
||||
n,
|
||||
n.devnumber,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
_process_battery_notification(dev, n)
|
||||
except Exception:
|
||||
logger.exception("processing %s for %s", n, dev)
|
||||
|
||||
self._on_change(dev)
|
||||
|
||||
return base, device, receiver, _BatteryEventsListener
|
||||
|
||||
|
||||
def _start_hotplug_watcher(on_hotplug: Callable[[], None]):
|
||||
try:
|
||||
import pyudev
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"pyudev isn't importable -- hotplugged devices will only be "
|
||||
"noticed on the next --rescan-interval tick, not instantly. "
|
||||
"Install it with `pip install pyudev` (it's a core Solaar "
|
||||
"dependency, so it's normally already present)."
|
||||
)
|
||||
return None
|
||||
|
||||
debounce_lock = threading.Lock()
|
||||
debounce_state: dict[str, threading.Timer | None] = {"timer": None}
|
||||
DEBOUNCE_SECONDS = 0.5
|
||||
|
||||
def _fire():
|
||||
with debounce_lock:
|
||||
debounce_state["timer"] = None
|
||||
on_hotplug()
|
||||
|
||||
def _handle_event(_device):
|
||||
if _device.action not in ("add", "remove"):
|
||||
return
|
||||
with debounce_lock:
|
||||
existing = debounce_state["timer"]
|
||||
if existing is not None:
|
||||
existing.cancel()
|
||||
timer = threading.Timer(DEBOUNCE_SECONDS, _fire)
|
||||
timer.daemon = True
|
||||
debounce_state["timer"] = timer
|
||||
timer.start()
|
||||
|
||||
context = pyudev.Context()
|
||||
monitor = pyudev.Monitor.from_netlink(context)
|
||||
monitor.filter_by(subsystem="hidraw")
|
||||
observer = pyudev.MonitorObserver(monitor, callback=_handle_event)
|
||||
observer.start()
|
||||
return observer
|
||||
|
||||
|
||||
class _ListenerRegistry:
|
||||
def __init__(self, on_change):
|
||||
self._on_change = on_change
|
||||
self._entries = {}
|
||||
self._last_change: dict[int, float] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._rescan_lock = threading.Lock()
|
||||
self._min_rescan_interval = 1.0
|
||||
self._last_rescan_time = 0.0
|
||||
|
||||
self._hidpp_io_lock = threading.RLock()
|
||||
|
||||
def _touch_and_notify(self, dev_or_receiver, *args, **kwargs):
|
||||
with self._lock:
|
||||
self._last_change[id(dev_or_receiver)] = time.time()
|
||||
self._on_change(dev_or_receiver, *args, **kwargs)
|
||||
|
||||
def last_change(self, dev) -> float:
|
||||
with self._lock:
|
||||
return self._last_change.get(id(dev), 0.0)
|
||||
|
||||
def rescan(self):
|
||||
now = time.monotonic()
|
||||
if now - self._last_rescan_time < self._min_rescan_interval:
|
||||
return
|
||||
if not self._rescan_lock.acquire(blocking=False):
|
||||
return
|
||||
|
||||
try:
|
||||
self._last_rescan_time = now
|
||||
with self._hidpp_io_lock:
|
||||
base, device, receiver, ListenerClass = _import_listener_deps()
|
||||
|
||||
with self._lock:
|
||||
dead = [
|
||||
p
|
||||
for p, (_obj, listener) in self._entries.items()
|
||||
if not listener._active
|
||||
]
|
||||
for p in dead:
|
||||
dying_obj, _dying_listener = self._entries[p]
|
||||
|
||||
if dying_obj.isDevice:
|
||||
ident = getattr(
|
||||
dying_obj, "unitId", None
|
||||
) or getattr(dying_obj, "serial", None)
|
||||
info = getattr(dying_obj, "battery_info", None)
|
||||
if ident and info is not None:
|
||||
for (
|
||||
other_path,
|
||||
(other_obj, _other_listener),
|
||||
) in self._entries.items():
|
||||
if other_path == p or other_obj.isDevice:
|
||||
continue
|
||||
for child in other_obj:
|
||||
child_ident = getattr(
|
||||
child, "unitId", None
|
||||
) or getattr(child, "serial", None)
|
||||
if child_ident == ident:
|
||||
child.set_battery_info(info)
|
||||
break
|
||||
del self._entries[p]
|
||||
known_paths = set(self._entries.keys())
|
||||
|
||||
with self._lock:
|
||||
id_to_entry = {}
|
||||
for path, (obj, listener) in self._entries.items():
|
||||
if obj.isDevice:
|
||||
ident = obj.unitId or obj.serial
|
||||
if ident:
|
||||
id_to_entry[ident] = (path, obj, listener)
|
||||
|
||||
for dev_info in base.receivers_and_devices():
|
||||
if dev_info.path in known_paths:
|
||||
continue
|
||||
try:
|
||||
if dev_info.isDevice:
|
||||
obj = device.create_device(base, dev_info)
|
||||
else:
|
||||
obj = receiver.create_receiver(base, dev_info)
|
||||
except OSError as e:
|
||||
if e.errno == 13:
|
||||
logger.error(
|
||||
"permission denied opening %s -- check the Solaar udev rule "
|
||||
"(rules.d/42-logitech-unify-permissions.rules) is installed",
|
||||
dev_info.path,
|
||||
)
|
||||
else:
|
||||
logger.exception("failed opening %s", dev_info)
|
||||
continue
|
||||
except Exception:
|
||||
logger.exception("failed opening %s", dev_info)
|
||||
continue
|
||||
|
||||
if obj is None:
|
||||
continue
|
||||
|
||||
listener = ListenerClass(obj, self._touch_and_notify)
|
||||
to_join = None
|
||||
|
||||
if obj.isDevice:
|
||||
ident = None
|
||||
try:
|
||||
if obj.protocol >= 2.0:
|
||||
obj.get_ids()
|
||||
ident = obj.unitId or obj.serial
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ident:
|
||||
with self._lock:
|
||||
existing = id_to_entry.get(ident)
|
||||
if existing:
|
||||
old_path, old_obj, old_listener = existing
|
||||
|
||||
if old_path != dev_info.path:
|
||||
logger.info(
|
||||
"replacing device %s (old path %s) with new path %s",
|
||||
ident,
|
||||
old_path,
|
||||
dev_info.path,
|
||||
)
|
||||
|
||||
if (
|
||||
getattr(obj, "battery_info", None)
|
||||
is None
|
||||
and getattr(
|
||||
old_obj, "battery_info", None
|
||||
)
|
||||
is not None
|
||||
):
|
||||
obj.set_battery_info(
|
||||
old_obj.battery_info
|
||||
)
|
||||
|
||||
old_listener.stop()
|
||||
|
||||
del self._entries[old_path]
|
||||
|
||||
self._entries[dev_info.path] = (
|
||||
obj,
|
||||
listener,
|
||||
)
|
||||
self._last_change[id(obj)] = time.time()
|
||||
|
||||
to_join = old_listener
|
||||
break
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
if (
|
||||
getattr(obj, "battery_info", None)
|
||||
is None
|
||||
):
|
||||
for (
|
||||
_,
|
||||
(other_obj, _other_listener),
|
||||
) in self._entries.items():
|
||||
if other_obj.isDevice:
|
||||
continue
|
||||
for child in other_obj:
|
||||
child_ident = getattr(
|
||||
child, "unitId", None
|
||||
) or getattr(
|
||||
child, "serial", None
|
||||
)
|
||||
if child_ident == ident:
|
||||
obj.set_battery_info(
|
||||
child.battery_info
|
||||
)
|
||||
break
|
||||
if (
|
||||
getattr(
|
||||
obj,
|
||||
"battery_info",
|
||||
None,
|
||||
)
|
||||
is not None
|
||||
):
|
||||
break
|
||||
|
||||
with self._lock:
|
||||
if dev_info.path not in self._entries:
|
||||
self._entries[dev_info.path] = (obj, listener)
|
||||
self._last_change[id(obj)] = time.time()
|
||||
|
||||
listener.start()
|
||||
|
||||
logger.info(
|
||||
"listening on %s (%s)",
|
||||
dev_info.path,
|
||||
"device" if dev_info.isDevice else "receiver",
|
||||
)
|
||||
|
||||
if to_join is not None:
|
||||
try:
|
||||
to_join.join(timeout=1.0)
|
||||
except Exception:
|
||||
logger.exception("error joining replaced listener")
|
||||
del to_join
|
||||
|
||||
finally:
|
||||
self._rescan_lock.release()
|
||||
|
||||
def known_devices(self):
|
||||
with self._lock:
|
||||
entries = list(self._entries.values())
|
||||
|
||||
result = []
|
||||
seen_idents = set()
|
||||
for obj, _listener in entries:
|
||||
if obj.isDevice:
|
||||
ident = obj.unitId or obj.serial
|
||||
if ident and ident in seen_idents:
|
||||
continue
|
||||
if ident:
|
||||
seen_idents.add(ident)
|
||||
result.append(obj)
|
||||
else:
|
||||
for child in obj:
|
||||
ident = child.unitId or child.serial
|
||||
if ident and ident in seen_idents:
|
||||
continue
|
||||
if ident:
|
||||
seen_idents.add(ident)
|
||||
result.append(child)
|
||||
return result
|
||||
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
entries = list(self._entries.values())
|
||||
for _obj, listener in entries:
|
||||
try:
|
||||
listener.stop()
|
||||
except Exception:
|
||||
logger.exception("error stopping listener")
|
||||
for _obj, listener in entries:
|
||||
listener.join(timeout=2.0)
|
||||
|
||||
|
||||
def _snapshot(registry: _ListenerRegistry) -> list[DeviceBattery]:
|
||||
results: list[DeviceBattery] = []
|
||||
|
||||
with registry._hidpp_io_lock:
|
||||
for dev in registry.known_devices():
|
||||
info = dev.battery_info
|
||||
if info is None:
|
||||
try:
|
||||
if dev.ping():
|
||||
info = dev.battery()
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"initial battery() failed for %s", dev, exc_info=True
|
||||
)
|
||||
info = None
|
||||
entry = _to_device_battery(dev, info)
|
||||
if entry is not None:
|
||||
results.append(entry)
|
||||
return results
|
||||
|
||||
|
||||
def _clean_json(path: Path) -> None:
|
||||
_write_json_atomic(path, [])
|
||||
|
||||
|
||||
def _iter_open_devices():
|
||||
try:
|
||||
from logitech_receiver import base, device, receiver
|
||||
except ImportError as e:
|
||||
raise RuntimeError(
|
||||
"logitech_receiver isn't importable -- install it with `pip install solaar`."
|
||||
) from e
|
||||
except ValueError as e:
|
||||
raise RuntimeError(
|
||||
"logitech_receiver failed to import because a GTK3 typelib is missing "
|
||||
f"({e}). Install the same GTK3 + PyGObject packages Solaar's GUI needs "
|
||||
"(e.g. python3-gi + gir1.2-gtk-3.0 on Debian/Ubuntu, or the gtk3/"
|
||||
"python3-gobject equivalents on your distro)."
|
||||
) from e
|
||||
|
||||
for dev_info in base.receivers_and_devices():
|
||||
try:
|
||||
if dev_info.isDevice:
|
||||
d = device.create_device(base, dev_info) # pyright: ignore[reportArgumentType]
|
||||
if d is not None:
|
||||
yield d
|
||||
else:
|
||||
d = receiver.create_receiver(base, dev_info) # pyright: ignore[reportArgumentType]
|
||||
if d is not None:
|
||||
yield from d
|
||||
except OSError as e:
|
||||
if e.errno == 13:
|
||||
logger.error(
|
||||
"permission denied opening %s -- check the Solaar udev rule "
|
||||
"(rules.d/42-logitech-unify-permissions.rules) is installed "
|
||||
"and you're in the right group",
|
||||
dev_info.path,
|
||||
)
|
||||
else:
|
||||
logger.exception("failed opening %s", dev_info)
|
||||
continue
|
||||
except Exception:
|
||||
logger.exception("failed opening %s", dev_info)
|
||||
continue
|
||||
|
||||
|
||||
def _to_device_battery(dev, battery) -> DeviceBattery | None:
|
||||
if battery is None:
|
||||
return None
|
||||
|
||||
percentage = battery.level / 100 if isinstance(battery.level, int) else None
|
||||
state = battery.status.name.lower() if battery.status is not None else None
|
||||
|
||||
serial = (
|
||||
getattr(dev, "serial", None) or getattr(dev, "unitId", None) or None
|
||||
)
|
||||
|
||||
return DeviceBattery(
|
||||
model=dev.name or dev.codename or "Unknown device",
|
||||
nativePath=_find_upower_native_path(serial),
|
||||
serial=serial,
|
||||
type=str(dev.kind) if dev.kind is not None else None,
|
||||
wired=dev.receiver is None,
|
||||
percentage=percentage,
|
||||
state=state,
|
||||
charging=battery.charging(),
|
||||
isPresent=battery.ok(),
|
||||
)
|
||||
|
||||
|
||||
def _read_battery(dev) -> DeviceBattery | None:
|
||||
try:
|
||||
if not dev.ping():
|
||||
return None
|
||||
except Exception:
|
||||
logger.debug("ping failed for %s", dev, exc_info=True)
|
||||
return None
|
||||
|
||||
try:
|
||||
battery = dev.battery()
|
||||
except Exception:
|
||||
logger.debug("battery() failed for %s", dev, exc_info=True)
|
||||
return None
|
||||
|
||||
return _to_device_battery(dev, battery)
|
||||
|
||||
|
||||
def poll_once() -> list[DeviceBattery]:
|
||||
results: list[DeviceBattery] = []
|
||||
seen_idents = set()
|
||||
for dev in _iter_open_devices():
|
||||
try:
|
||||
info = _read_battery(dev)
|
||||
if info is not None:
|
||||
entry = _to_device_battery(dev, info)
|
||||
if entry:
|
||||
ident = entry.serial or (getattr(dev, "unitId", None))
|
||||
if ident and ident in seen_idents:
|
||||
continue
|
||||
if ident:
|
||||
seen_idents.add(ident)
|
||||
results.append(entry)
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
dev.close()
|
||||
return results
|
||||
|
||||
|
||||
def _write_json_atomic(path: Path, results: list[DeviceBattery]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"updated": time.time(),
|
||||
"devices": [asdict(r) for r in results],
|
||||
}
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
dir=path.parent, prefix=".battery-", suffix=".tmp"
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(payload, f, indent=2)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
@app.command()
|
||||
def daemon(
|
||||
rescan_interval: float = typer.Option(
|
||||
60.0,
|
||||
"--rescan-interval",
|
||||
"-i",
|
||||
help=(
|
||||
"Seconds between fallback rescans. This is NOT a battery poll "
|
||||
"interval -- battery updates are event-driven and written as "
|
||||
"soon as a device reports a change. New/removed devices "
|
||||
"(e.g. plugging a keyboard in to charge) are normally noticed "
|
||||
"within milliseconds via a udev hotplug watcher; this interval "
|
||||
"is only a safety net in case a udev event is ever missed."
|
||||
),
|
||||
),
|
||||
out: Path = typer.Option(
|
||||
DEFAULT_OUTPUT,
|
||||
"--out",
|
||||
"-o",
|
||||
help="Where to write the JSON status file.",
|
||||
),
|
||||
lock_file: Path = typer.Option(
|
||||
DEFAULT_LOCK,
|
||||
"--lock-file",
|
||||
help="Path used to ensure only one daemon runs at a time.",
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose",
|
||||
"-v",
|
||||
help="Log every listener start and battery-change write to stderr.",
|
||||
),
|
||||
):
|
||||
lock_fd = _acquire_singleton_lock(lock_file)
|
||||
if lock_fd is None:
|
||||
try:
|
||||
holder_pid = lock_file.read_text().strip()
|
||||
except OSError:
|
||||
holder_pid = "unknown"
|
||||
typer.echo(
|
||||
f"error: a battery daemon is already running (pid {holder_pid}, lock: {lock_file})",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
if verbose:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
write_lock = threading.Lock()
|
||||
last_payload = None
|
||||
|
||||
pending_write_timer: list[threading.Timer | None] = [None]
|
||||
pending_write_lock = threading.Lock()
|
||||
WRITE_SETTLE_SECONDS = 0.3
|
||||
|
||||
write_seq_lock = threading.Lock()
|
||||
write_seq = 0
|
||||
last_written_seq = 0
|
||||
|
||||
def write_if_changed():
|
||||
nonlocal last_payload, write_seq, last_written_seq
|
||||
with write_seq_lock:
|
||||
write_seq += 1
|
||||
my_seq = write_seq
|
||||
|
||||
results = _snapshot(registry)
|
||||
with write_lock:
|
||||
with write_seq_lock:
|
||||
if my_seq <= last_written_seq:
|
||||
return
|
||||
last_written_seq = my_seq
|
||||
payload = [asdict(r) for r in results]
|
||||
if payload != last_payload:
|
||||
_write_json_atomic(out, results)
|
||||
last_payload = payload
|
||||
logger.info(
|
||||
"battery status changed, wrote %d device(s)", len(results)
|
||||
)
|
||||
|
||||
def write_if_changed_coalesced():
|
||||
with pending_write_lock:
|
||||
existing = pending_write_timer[0]
|
||||
if existing is not None:
|
||||
existing.cancel()
|
||||
timer = threading.Timer(WRITE_SETTLE_SECONDS, write_if_changed)
|
||||
timer.daemon = True
|
||||
pending_write_timer[0] = timer
|
||||
timer.start()
|
||||
|
||||
def on_change(_device_or_receiver, alert=None, reason=None):
|
||||
write_if_changed_coalesced()
|
||||
|
||||
registry = _ListenerRegistry(on_change)
|
||||
|
||||
def rescan_and_write():
|
||||
try:
|
||||
registry.rescan()
|
||||
except Exception:
|
||||
logger.exception("rescan failed")
|
||||
write_if_changed_coalesced()
|
||||
|
||||
try:
|
||||
registry.rescan()
|
||||
except RuntimeError as e:
|
||||
typer.echo(f"error: {e}", err=True)
|
||||
raise typer.Exit(code=1) from None
|
||||
|
||||
write_if_changed()
|
||||
|
||||
hotplug_observer = _start_hotplug_watcher(rescan_and_write)
|
||||
|
||||
running = True
|
||||
|
||||
def _stop(signum, frame):
|
||||
nonlocal running
|
||||
running = False
|
||||
|
||||
signal.signal(signal.SIGINT, _stop)
|
||||
signal.signal(signal.SIGTERM, _stop)
|
||||
|
||||
typer.echo(
|
||||
f"Listening for battery events, writing to {out} (lock: {lock_file})"
|
||||
)
|
||||
|
||||
try:
|
||||
while running:
|
||||
remaining = rescan_interval
|
||||
while running and remaining > 0:
|
||||
step = min(0.2, remaining)
|
||||
time.sleep(step)
|
||||
remaining -= step
|
||||
if running:
|
||||
rescan_and_write()
|
||||
finally:
|
||||
if hotplug_observer is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
hotplug_observer.stop()
|
||||
with contextlib.suppress(Exception):
|
||||
_clean_json(out)
|
||||
|
||||
registry.stop()
|
||||
typer.echo("Stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
+3
-4
@@ -10,12 +10,11 @@ dependencies = [
|
||||
"typer",
|
||||
"pillow",
|
||||
"jinja2",
|
||||
"materialyoucolor"
|
||||
"materialyoucolor",
|
||||
"solaar",
|
||||
"pytest"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest"]
|
||||
|
||||
[project.scripts]
|
||||
zshell-cli = "zshell:main"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user