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

This commit is contained in:
2026-08-16 22:57:54 +02:00
81 changed files with 2443 additions and 1220 deletions
+22 -87
View File
@@ -1,12 +1,15 @@
name: Python 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: on:
pull_request: pull_request:
jobs: jobs:
fmt: static:
runs-on: alpine runs-on: debian
container: node:26-alpine container: node:26-trixie-slim
steps: steps:
- name: Checkout - name: Checkout
@@ -14,10 +17,8 @@ jobs:
- name: Install tools - name: Install tools
run: | run: |
apk add --no-cache \ apt-get update
git \ apt-get install -y --no-install-recommends git python3 python3-pip python3-venv
python3 \
py3-pip
python3 -m venv .venv python3 -m venv .venv
. .venv/bin/activate . .venv/bin/activate
pip install --no-cache-dir ruff pip install --no-cache-dir ruff
@@ -27,32 +28,15 @@ jobs:
. .venv/bin/activate . .venv/bin/activate
ruff format --check . 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 - name: Lint
run: | run: |
. .venv/bin/activate . .venv/bin/activate
ruff check . ruff check .
test: verify:
runs-on: alpine if: always()
container: node:26-alpine runs-on: debian
container: node:26-trixie-slim
steps: steps:
- name: Checkout - name: Checkout
@@ -60,20 +44,16 @@ jobs:
- name: Install tools - name: Install tools
run: | run: |
apk add --no-cache \ apt-get update
git \ apt-get install -y --no-install-recommends $APT_DEPS build-essential python3-dev
python3 \ python3 -m venv --system-site-packages .venv
py3-pip \
py3-pillow \
build-base
python3 -m venv .venv
. .venv/bin/activate . .venv/bin/activate
pip install --no-cache-dir \ pip install --no-cache-dir basedpyright nuitka .
typer \
pillow \ - name: Type check
materialyoucolor \ run: |
jinja2 \ . .venv/bin/activate
pytest basedpyright
- name: Test - name: Test
run: | run: |
@@ -81,52 +61,7 @@ jobs:
cd cli cd cli
python -m pytest tests/ -v python -m pytest tests/ -v
typecheck:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir basedpyright 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 - name: Nuitka module check
run: | run: |
. .venv/bin/activate . .venv/bin/activate
nuitka --module --include-package=zshell cli/src/zshell/ nuitka --module --include-package=zshell cli/src/zshell/
+2
View File
@@ -16,3 +16,5 @@ dist/
**/test-plugins/ **/test-plugins/
**/Charts/ **/Charts/
network-dev/ network-dev/
**/zshell.build/
**/zshell.dist/
+2
View File
@@ -93,6 +93,8 @@ if("shell" IN_LIST ENABLE_MODULES)
${NUITKA_EXECUTABLE} ${NUITKA_EXECUTABLE}
--standalone --standalone
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets --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-dir=${ZSHELL_CLI_BUILD_DIR}
--output-filename=zshell-cli --output-filename=zshell-cli
${CMAKE_SOURCE_DIR}/cli/src/zshell/ ${CMAKE_SOURCE_DIR}/cli/src/zshell/
+148 -130
View File
@@ -18,13 +18,14 @@ Item {
property real leftFadeStrength: overflowing && leftFadeEnabled ? fadeStrengthMoving : fadeStrengthIdle property real leftFadeStrength: overflowing && leftFadeEnabled ? fadeStrengthMoving : fadeStrengthIdle
property int leftFadeWidth: 28 property int leftFadeWidth: 28
property bool marqueeEnabled: true 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 int pauseMs: 1200
property real pixelsPerSecond: 40 property real pixelsPerSecond: 40
property real rightFadeStrength: overflowing ? fadeStrengthMoving : fadeStrengthIdle property real rightFadeStrength: overflowing ? fadeStrengthMoving : fadeStrengthIdle
property int rightFadeWidth: 28 property int rightFadeWidth: 28
property bool sliding: false property bool sliding: false
property alias text: elideText.text property alias text: elideText.text
property bool vertical: false
function durationForDistance(px): int { function durationForDistance(px): int {
return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000)); return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000));
@@ -41,8 +42,9 @@ Item {
} }
} }
clip: true clip: false
implicitHeight: elideText.implicitHeight implicitHeight: vertical ? elideText.implicitWidth : elideText.implicitHeight
implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth
Behavior on leftFadeStrength { Behavior on leftFadeStrength {
Anim { Anim {
@@ -53,171 +55,187 @@ Item {
} }
} }
onHeightChanged: resetMarquee()
onTextChanged: resetMarquee() onTextChanged: resetMarquee()
onVerticalChanged: resetMarquee()
onVisibleChanged: if (!visible) onVisibleChanged: if (!visible)
resetMarquee() resetMarquee()
onWidthChanged: 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 { Item {
id: marqueeViewport id: content
anchors.fill: parent anchors.centerIn: parent
clip: true height: root.vertical ? root.width : root.height
layer.enabled: true width: root.vertical ? root.height : root.width
visible: root.overflowing
layer.effect: OpacityMask { transform: Rotation {
maskSource: rightFadeMask 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 { Item {
id: strip id: marqueeViewport
anchors.verticalCenter: parent.verticalCenter anchors.fill: parent
height: t1.implicitHeight clip: false
width: t1.width + root.gap + t2.width layer.enabled: true
x: 0 visible: root.overflowing
CustomText { layer.effect: OpacityMask {
id: t1 maskSource: rightFadeMask
animate: root.animate
animateProp: "opacity"
color: root.color
font.pointSize: elideText.font.pointSize
text: elideText.text
} }
CustomText { Item {
id: t2 id: strip
animate: root.animate anchors.verticalCenter: parent.verticalCenter
animateProp: "opacity" height: t1.implicitHeight
color: root.color width: t1.width + root.gap + t2.width
font.pointSize: elideText.font.pointSize x: 0
text: t1.text
x: t1.width + root.gap
}
}
SequentialAnimation { CustomText {
id: marqueeAnim 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 { animate: root.animate
script: { animateProp: "opacity"
root.sliding = true; color: root.color
root.leftFadeEnabled = true; font.pointSize: elideText.font.pointSize
text: t1.text
x: t1.width + root.gap
} }
} }
Anim { SequentialAnimation {
duration: root.durationForDistance(t1.width) id: marqueeAnim
easing.bezierCurve: Easing.Linear
easing.type: Easing.Linear
from: 0
property: "x"
target: strip
to: -t1.width
}
ScriptAction { running: false
script: {
root.leftFadeEnabled = 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 { Timer {
duration: root.durationForDistance(root.gap) id: pauseTimer
easing.bezierCurve: Easing.Linear
easing.type: Easing.Linear
from: -t1.width
property: "x"
target: strip
to: -(t1.width + root.gap)
}
ScriptAction { interval: root.pauseMs
script: { repeat: false
root.sliding = false; running: true
strip.x = 0;
onTriggered: {
if (root.marqueeEnabled)
marqueeAnim.start();
} }
} }
} }
Timer { Rectangle {
id: pauseTimer id: rightFadeMask
interval: root.pauseMs readonly property real fadeStartPos: {
repeat: false const w = Math.max(1, width);
running: true return Math.max(0, Math.min(1, (w - root.rightFadeWidth) / w));
onTriggered: {
if (root.marqueeEnabled)
marqueeAnim.start();
} }
} readonly property real leftFadeEndPos: {
} const w = Math.max(1, width);
return Math.max(0, Math.min(1, root.leftFadeWidth / 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
} }
GradientStop { anchors.fill: marqueeViewport
color: Qt.rgba(1, 1, 1, 1.0) layer.enabled: true
position: rightFadeMask.leftFadeEndPos visible: false
}
GradientStop { gradient: Gradient {
color: Qt.rgba(1, 1, 1, 1.0) orientation: Gradient.Horizontal
position: rightFadeMask.fadeStartPos
}
GradientStop { GradientStop {
color: Qt.rgba(1, 1, 1, 1.0 - root.rightFadeStrength) color: Qt.rgba(1, 1, 1, 1.0 - root.leftFadeStrength)
position: 1.0 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
View File
@@ -126,7 +126,7 @@ MouseArea {
CustomRect { CustomRect {
id: item id: item
readonly property bool active: modelData === root.active readonly property bool active: modelData === root?.active
required property int index required property int index
required property MenuItem modelData required property MenuItem modelData
@@ -163,19 +163,19 @@ MouseArea {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.icon text: item.modelData?.icon ?? ""
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData.text text: item.modelData?.text ?? ""
} }
Loader { Loader {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0 active: item.modelData?.trailingIcon.length > 0
asynchronous: true asynchronous: true
visible: active visible: active
+33 -2
View File
@@ -11,15 +11,46 @@ Variants {
required property ShellScreen modelData 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 { Exclusions {
bar: content.bar bar: content.bar
screen: scope.modelData geometry: content.geometry
screen: drawer.screen
} }
Windows { Windows {
id: content id: content
screen: scope.modelData screen: drawer.screen
} }
} }
} }
+55
View File
@@ -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`);
}
}
+7 -2
View File
@@ -9,19 +9,21 @@ Scope {
id: root id: root
required property Item bar required property Item bar
required property EdgeGeometry geometry
required property ShellScreen screen required property ShellScreen screen
ExclusionZone { ExclusionZone {
id: top id: top
anchors.top: true anchors.top: true
exclusiveZone: root.bar.exclusiveZone hasBar: root.geometry.barOnTop
} }
ExclusionZone { ExclusionZone {
id: left id: left
anchors.left: true anchors.left: true
hasBar: root.geometry.barOnLeft
} }
ExclusionZone { ExclusionZone {
@@ -34,6 +36,7 @@ Scope {
id: bottom id: bottom
anchors.bottom: true anchors.bottom: true
hasBar: root.geometry.barOnBottom
} }
Timer { Timer {
@@ -44,7 +47,9 @@ Scope {
} }
component ExclusionZone: CustomWindow { component ExclusionZone: CustomWindow {
exclusiveZone: Config.bar.border property bool hasBar
exclusiveZone: hasBar ? root.bar.exclusiveZone : Config.bar.border
implicitHeight: 1 implicitHeight: 1
implicitWidth: 1 implicitWidth: 1
name: "Bar-Exclusion" name: "Bar-Exclusion"
+24 -22
View File
@@ -3,7 +3,7 @@ import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Helpers
import qs.Modules as BarPopouts import qs.Modules.Bar.Popouts
Item { Item {
id: root id: root
@@ -12,43 +12,47 @@ Item {
required property real borderThickness required property real borderThickness
property bool dashboardShortcutActive property bool dashboardShortcutActive
required property Drawing drawing required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive property bool osdShortcutActive
required property Panels panels required property Panels panels
required property BarPopouts.Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property PersistentProperties visibilities required property PersistentProperties visibilities
function inBottomPanel(panel: Item, x: real, y: real): bool { 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 { 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 { 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 { function inTopPanel(panel: Item, x: real, y: real): bool {
return y < bar.implicitHeight + panel.height && withinPanelWidth(panel, x, y); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
} return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
function onWheel(event: WheelEvent): void {
if (event.x < bar.implicitWidth) {
bar.handleWheel(event.y, event.angleDelta);
}
} }
function withinPanelHeight(panel: Item, x: real, y: real): bool { 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; return y >= panelY && y <= panelY + panel.height;
} }
function withinPanelWidth(panel: Item, x: real, y: real): bool { 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; return x >= panelX && x <= panelX + panel.width;
} }
@@ -81,7 +85,7 @@ Item {
if (root.singleGestureTriggered) if (root.singleGestureTriggered)
return; return;
if (centroid.pressPosition.y < root.bar.implicitHeight) { if (root.geometry.insetTop(root.borderThickness) > centroid.pressPosition.y) {
if (dragY > 20) { if (dragY > 20) {
root.visibilities.settings = true; root.visibilities.settings = true;
root.singleGestureTriggered = 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; 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) { if (dragY < -10) {
root.visibilities.dock = true; root.visibilities.dock = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
@@ -193,7 +197,7 @@ Item {
return; 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; root.bar.isHovered = true;
if (root.panels.sidebar.offsetScale === 1) { 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)) if (Config.dock.enabled && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.visibilities.dock = true; root.visibilities.dock = true;
if (y < root.bar.implicitHeight) if (root.geometry.barContains(x, y))
root.bar.checkPopout(x); root.bar.checkPopout(root.geometry.axisPos(x, y));
} }
} }
@@ -298,8 +302,6 @@ Item {
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
root.panels.popouts.hasCurrent = false; root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false; root.visibilities.launcher = false;
} else {
Config.save();
} }
} }
+19 -16
View File
@@ -1,7 +1,7 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.Components 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 as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils import qs.Modules.Notifications.Sidebar.Utils as Utils
@@ -27,6 +27,7 @@ Item {
readonly property alias dock: dock readonly property alias dock: dock
readonly property alias drawing: drawing readonly property alias drawing: drawing
required property var drawingItem required property var drawingItem
required property EdgeGeometry geometry
readonly property alias launcher: launcher readonly property alias launcher: launcher
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias osd: osd readonly property alias osd: osd
@@ -43,24 +44,26 @@ Item {
readonly property alias utilities: utilities readonly property alias utilities: utilities
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: geometry.insetLeft(borderThickness)
anchors.margins: borderThickness anchors.margins: borderThickness
anchors.topMargin: bar.implicitHeight anchors.topMargin: geometry.insetTop(borderThickness)
Item { Item {
id: resourcesWrapper id: resourcesWrapper
anchors.bottom: root.geometry.position === "bottom" ? parent.bottom : undefined
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: root.geometry.position !== "bottom" ? parent.top : undefined
clip: true clip: true
implicitHeight: resources.implicitHeight * (1 - resources.offsetScale) implicitHeight: root.geometry.horizontal ? resources.implicitHeight * (1 - resources.offsetScale) : resources.implicitHeight
implicitWidth: resources.implicitWidth implicitWidth: root.geometry.horizontal ? resources.implicitWidth : resources.implicitWidth * (1 - resources.offsetScale)
Resources.Wrapper { Resources.Wrapper {
id: resources id: resources
anchors.left: parent.left position: root.geometry.position
anchors.top: parent.top
visibilities: root.visibilities visibilities: root.visibilities
} }
} }
@@ -96,11 +99,11 @@ Item {
visibilities: root.visibilities visibilities: root.visibilities
} }
Modules.ClipWrapper { Popouts.ClipWrapper {
id: popouts id: popouts
anchors.top: parent.top
borderThickness: root.borderThickness borderThickness: root.borderThickness
position: root.geometry.position
screen: root.screen screen: root.screen
} }
@@ -147,11 +150,13 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1 property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.right: parent.right anchors.bottom: root.geometry.barOnLeft || root.geometry.barOnBottom ? parent.bottom : undefined
anchors.top: parent.top 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 clip: true
implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitHeight: root.geometry.horizontal ? dashboard.implicitHeight * (1 - offsetScale) : dashboard.implicitHeight
implicitWidth: dashboard.implicitWidth implicitWidth: root.geometry.horizontal ? dashboard.implicitWidth : dashboard.implicitWidth * (1 - offsetScale)
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -163,10 +168,8 @@ Item {
Dashboard.Wrapper { Dashboard.Wrapper {
id: dashboard id: dashboard
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: (-implicitHeight - 5) * offsetScale
offsetScale: dashboardWrapper.offsetScale offsetScale: dashboardWrapper.offsetScale
position: root.geometry.position
visibilities: root.visibilities visibilities: root.visibilities
} }
} }
+13 -8
View File
@@ -7,17 +7,18 @@ import qs.Modules.Bar as Bar
Region { Region {
id: root id: root
required property Bar.BarLoader bar // required property Bar.BarLoader bar
readonly property real borderThickness: win.borderThickness readonly property real borderThickness: win.borderThickness
required property EdgeGeometry geometry
readonly property alias menuPopoutRegion: menuPopoutRegion readonly property alias menuPopoutRegion: menuPopoutRegion
required property Panels panels required property Panels panels
required property var win 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 intersection: Intersection.Xor
width: win.width - win.borderThickness * 2 - win.dragMaskPadding * 2 width: win.width - geometry.insetLeft(borderThickness, true) * 2 - win.dragMaskPadding * 2
x: win.borderThickness + win.dragMaskPadding x: geometry.insetLeft(borderThickness, true) + win.dragMaskPadding
y: bar.implicitHeight + win.dragMaskPadding y: geometry.insetTop(borderThickness, true) + win.dragMaskPadding
R { R {
panel: root.panels.dashboardWrapper panel: root.panels.dashboardWrapper
@@ -41,17 +42,21 @@ Region {
} }
R { R {
height: panel.height + root.geometry.insetTop(root.borderThickness)
panel: root.panels.notifications panel: root.panels.notifications
y: 0
} }
R { 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 panel: root.panels.utilities
y: root.win.height - height y: root.win.height - height
} }
R { R {
height: root.geometry.horizontal ? panel.height * (1 - root.panels.popoutsWrapper.offsetScale) : panel.height
panel: root.panels.popoutsWrapper panel: root.panels.popoutsWrapper
width: root.geometry.horizontal ? panel.width : panel.width * (1 - root.panels.popoutsWrapper.offsetScale)
} }
R { R {
@@ -82,7 +87,7 @@ Region {
height: panel.height height: panel.height
intersection: Intersection.Subtract intersection: Intersection.Subtract
width: panel.width width: panel.width
x: panel.x + root.borderThickness x: panel.x + root.geometry.insetLeft(root.borderThickness)
y: panel.y + root.bar.implicitHeight y: panel.y + root.geometry.insetTop(root.borderThickness)
} }
} }
+40 -26
View File
@@ -8,10 +8,10 @@ import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import ZShell.Blobs import ZShell.Blobs
import qs.Modules.SysTray.Popouts.Network as N
import qs.Daemons import qs.Daemons
import qs.Components import qs.Components
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.Bar.Popouts.Network as N
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Helpers
import qs.Drawers import qs.Drawers
@@ -34,6 +34,7 @@ CustomWindow {
return 100; return 100;
} }
property real fsTransitionProg: hasFullscreen ? 1 : 0 property real fsTransitionProg: hasFullscreen ? 1 : 0
readonly property alias geometry: geometry
readonly property bool hasFullscreen: { readonly property bool hasFullscreen: {
if (hasSpecialWorkspace) { if (hasSpecialWorkspace) {
const specialName = monitor?.lastIpcObject.specialWorkspace?.name; const specialName = monitor?.lastIpcObject.specialWorkspace?.name;
@@ -96,13 +97,21 @@ CustomWindow {
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
EdgeGeometry {
id: geometry
bar: bar
configPosition: Config.bar.position
win: root
}
Region { Region {
id: emptyRegion id: emptyRegion
height: panels.notifications.height height: panels.notifications.height
width: panels.notifications.width width: panels.notifications.width
x: panels.notifications.x + root.borderThickness x: panels.notifications.x + geometry.insetLeft(root.borderThickness)
y: panels.notifications.y + bar.implicitHeight y: panels.notifications.y + geometry.insetTop(root.borderThickness)
Region { Region {
height: panels.osd.height height: panels.osd.height
@@ -115,7 +124,7 @@ CustomWindow {
Regions { Regions {
id: regions id: regions
bar: bar geometry: geometry
panels: panels panels: panels
win: root win: root
} }
@@ -204,10 +213,10 @@ CustomWindow {
BlobInvertedRect { BlobInvertedRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: -50 anchors.margins: -50
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset borderLeft: geometry.insetLeft(root.borderThickness) - anchors.margins - root.sdfBorderOffset
borderRight: 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 group: blobGroup
radius: root.borderRounding radius: root.borderRounding
} }
@@ -215,15 +224,15 @@ CustomWindow {
PanelBg { PanelBg {
id: dashBg id: dashBg
property real extraHeight: 0.2 property real extraExtent: 0.2
deformAmount: 0.06 deformAmount: 0.06
implicitHeight: panels.dashboard.height * (1 + extraHeight) implicitHeight: panels.dashboard.height * (geometry.horizontal ? 1 + extraExtent : 1)
implicitWidth: panels.dashboard.width implicitWidth: panels.dashboard.width * (geometry.horizontal ? 1 : 1 + extraExtent)
panel: panels.dashboardWrapper panel: panels.dashboardWrapper
radius: Tokens.rounding.normal radius: Tokens.rounding.normal
x: panels.dashboardWrapper.x + panels.dashboard.x + root.borderThickness 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 + bar.implicitHeight - panels.dashboard.height * extraHeight y: panels.dashboardWrapper.y + panels.dashboard.y + geometry.insetTop(root.borderThickness) - (geometry.horizontal && geometry.position !== "bottom" ? panels.dashboard.height * extraExtent : 0)
} }
PanelBg { PanelBg {
@@ -235,7 +244,7 @@ CustomWindow {
implicitHeight: panels.launcher.height * (1 + extraHeight) implicitHeight: panels.launcher.height * (1 + extraHeight)
panel: panels.launcher panel: panels.launcher
radius: Tokens.rounding.smallest + 5 radius: Tokens.rounding.smallest + 5
y: panels.launcher.y + bar.implicitHeight y: panels.launcher.y + geometry.insetTop(root.borderThickness)
} }
PanelBg { PanelBg {
@@ -256,8 +265,7 @@ CustomWindow {
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
panel: panels.osdWrapper panel: panels.osdWrapper
radius: 20 radius: 20
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness x: panels.osdWrapper.x + panels.osd.x + geometry.insetLeft(root.borderThickness)
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
} }
PanelBg { PanelBg {
@@ -279,17 +287,17 @@ CustomWindow {
PanelBg { PanelBg {
id: popoutBg id: popoutBg
property real extraHeight: 0.2 property real extraExtent: 0.2
deformAmount: panels.popouts.currentName.startsWith("traymenu") ? 0.15 : 0.08 deformAmount: panels.popouts.currentName.startsWith("traymenu") ? 0.15 : 0.08
implicitHeight: panels.popouts.height * (1 + extraHeight) implicitHeight: panels.popouts.height * (geometry.horizontal ? 1 + extraExtent : 1)
implicitWidth: panels.popouts.width implicitWidth: panels.popouts.width * (geometry.horizontal ? 1 : 1 + extraExtent)
panel: panels.popoutsWrapper panel: panels.popoutsWrapper
radius: panels.popouts.current?.panelRadius ?? Tokens.rounding.normal radius: panels.popouts.current?.panelRadius ?? Tokens.rounding.normal
x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness 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 + bar.implicitHeight - panels.popouts.height * extraHeight 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 { Anim {
} }
} }
@@ -303,8 +311,8 @@ CustomWindow {
implicitWidth: panels.resources.width implicitWidth: panels.resources.width
panel: panels.resourcesWrapper panel: panels.resourcesWrapper
radius: Tokens.rounding.large radius: Tokens.rounding.large
x: panels.resourcesWrapper.x + panels.resources.x + root.borderThickness x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness)
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness)
} }
PanelBg { PanelBg {
@@ -382,6 +390,7 @@ CustomWindow {
borderThickness: root.borderLayoutThickness borderThickness: root.borderLayoutThickness
drawing: drawing drawing: drawing
enabled: true enabled: true
geometry: geometry
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: root.screen screen: root.screen
@@ -393,6 +402,7 @@ CustomWindow {
bar: bar bar: bar
borderThickness: root.borderThickness borderThickness: root.borderThickness
drawingItem: drawing drawingItem: drawing
geometry: geometry
screen: root.screen screen: root.screen
visibilities: visibilities visibilities: visibilities
@@ -434,14 +444,18 @@ CustomWindow {
BarLoader { BarLoader {
id: bar id: bar
anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.top: geometry.barOnBottom ? undefined : parent.top
enabled: !visibilities.isDrawing enabled: !visibilities.isDrawing
fullscreen: root.hasFullscreen fullscreen: root.hasFullscreen
height: geometry.horizontal ? implicitHeight : parent.height
popouts: panels.popouts popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper popoutsWrapper: panels.popoutsWrapper
position: geometry.position
screen: root.screen screen: root.screen
visibilities: visibilities visibilities: visibilities
width: geometry.horizontal ? parent.width : implicitWidth
} }
} }
@@ -454,7 +468,7 @@ CustomWindow {
implicitHeight: panel.height implicitHeight: panel.height
implicitWidth: panel.width implicitWidth: panel.width
radius: Tokens.rounding.smallest radius: Tokens.rounding.smallest
x: panel.x + root.borderThickness x: panel.x + geometry.insetLeft(root.borderThickness)
y: panel.y + bar.implicitHeight y: panel.y + geometry.insetTop(root.borderThickness)
} }
} }
+40 -12
View File
@@ -1,35 +1,63 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower import Quickshell.Services.UPower
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
import qs.Paths
Singleton { Singleton {
id: root id: root
readonly property var colors: { readonly property list<var> allPeripherals: [...logiDevices, ...upowerDevices.filter(d => !Battery.logiDevices.some(l => l.nativePath === d.nativePath))]
if (deviceState === UPowerDeviceState.Charging || deviceState === UPowerDeviceState.FullyCharged) 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 { return {
fg: Colors.swapRG(Colors.palette.m3error), fg: Colors.swapRG(Colors.palette.m3error),
bg: Colors.swapRG(Colors.palette.m3onError) bg: Colors.swapRG(Colors.palette.m3onError)
}; };
else if (currentPerc <= 0.2) else if (percentage <= 0.2)
return { return {
fg: Colors.palette.m3error, fg: Colors.palette.m3error,
bg: Colors.palette.m3onError bg: Colors.palette.m3onError
}; };
else else
return { return {
fg: Colors.palette.m3onSurface, fg: Colors.palette.m3tertiary,
bg: Colors.palette.m3surface bg: Colors.palette.m3onTertiary
}; };
} }
readonly property real currentPerc: UPower.displayDevice.percentage
readonly property var deviceState: UPower.displayDevice.state function startDaemon(): void {
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery Quickshell.execDetached(["zshell-cli", "battery", "daemon"]);
readonly property bool onBattery: UPower.onBattery }
readonly property bool ready: UPower.displayDevice.ready
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty FileView {
readonly property real timeToFull: UPower.displayDevice.timeToFull 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
View File
@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
import ZShell.Config import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
@@ -166,6 +166,32 @@ Singleton {
return "chat"; 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 { function getVolumeIcon(volume: real, isMuted: bool): string {
if (isMuted) if (isMuted)
return "no_sound"; return "no_sound";
@@ -181,4 +207,19 @@ Singleton {
return weatherIcons[code]; return weatherIcons[code];
return "air"; 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;
}
} }
-1
View File
@@ -73,7 +73,6 @@ Singleton {
Config.dock.pinnedApps = pinnedApps; Config.dock.pinnedApps = pinnedApps;
root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id))); root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id)));
Config.saveNoToast();
} }
function isPinned(appId) { function isPinned(appId) {
+8 -41
View File
@@ -11,8 +11,8 @@ Singleton {
id: root id: root
property int availableUpdates: 0 property int availableUpdates: 0
property string cmd: ""
property bool commandReady property bool commandReady
property bool hasHelper
property bool loaded property bool loaded
property double now: Date.now() property double now: Date.now()
property var updates: ({}) property var updates: ({})
@@ -38,18 +38,12 @@ Singleton {
} }
function performPackageUpdate(pkg: string): void { function performPackageUpdate(pkg: string): void {
if (root.cmd === "pacman") pkgUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Sy", pkg];
pkgUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Sy", pkg];
else
pkgUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Sy", pkg];
pkgUpdateProc.running = true; pkgUpdateProc.running = true;
} }
function performSystemUpdate(): void { function performSystemUpdate(): void {
if (root.cmd === "pacman") sysUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Syu"];
sysUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Syu"];
else
sysUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Syu"];
sysUpdateProc.running = true; sysUpdateProc.running = true;
} }
@@ -87,7 +81,7 @@ Singleton {
Process { Process {
id: cmdDetect id: cmdDetect
command: ["sh", "-c", "command -v checkupdates || command -v yay || command -v paru"] command: ["sh", "-c", "command -v checkupdates"]
running: true running: true
stdout: StdioCollector { stdout: StdioCollector {
@@ -96,47 +90,20 @@ Singleton {
let helper; let helper;
if (cmd.length > 0) { if (cmd.length > 0) {
helper = cmd.split("/").pop(); helper = true;
} else { } else {
helper = "pacman"; helper = false;
}
if (helper === "checkupdates") {
updatesProc.command = [helper];
} else {
updatesProc.command = [helper, "-Qu"];
} }
root.hasHelper = helper;
root.commandReady = true; 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 { Process {
id: updatesProc id: updatesProc
command: [] command: root.hasHelper ? ["checkupdates"] : []
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
+41 -21
View File
@@ -3,23 +3,28 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules
import ZShell.Config import ZShell.Config
import qs.Modules.SysTray import qs.Modules.Bar.Components
import qs.Modules.Updates import qs.Modules.Bar.Popouts
RowLayout { GridLayout {
id: root id: root
required property bool fullscreen required property bool fullscreen
required property bool horizontal
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
function checkPopout(x: real): void { function axisCenterOf(item: Item): real {
const ch = childAt(x, height / 2) as EntryWrapper; 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; const id = ch?.entryId;
if (!ch || id === undefined) { if (!ch || id === undefined) {
@@ -33,26 +38,21 @@ RowLayout {
if (id === "statusIcons" && Config.bar.popouts.statusIcons) { if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const items = (ch.item as StatusIcons).items; const items = (ch.item as StatusIcons).items;
const localX = mapToItem(items, x, 0).x; const icon = horizontal ? items.childAt(mapToItem(items, pos, 0).x, items.height / 2) : items.childAt(items.width / 2, mapToItem(items, 0, pos).y);
const icon = items.childAt(localX, items.height / 2);
if (icon) { if (icon) {
popouts.currentName = icon.name; 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; popouts.hasCurrent = true;
} }
} else if (id === "tray" && Config.bar.popouts.tray && Config.bar.tray.showOnHover) { } else if (id === "tray" && Config.bar.popouts.tray && Config.bar.tray.showOnHover) {
const tray = ch.item as TrayIcons; const tray = ch.item as TrayIcons;
const layout = tray.layout; const layout = tray.layout;
const localX = mapToItem(layout, x, 0).x; const trayItem = horizontal ? layout.childAt(mapToItem(layout, pos, 0).x, layout.height / 2) : layout.childAt(tray.width / 2, mapToItem(layout, 0, pos).y);
const trayItem = layout.childAt(localX, layout.height / 2);
if (trayItem) { if (trayItem) {
const idx = trayItem.index; const idx = trayItem.index;
popouts.currentName = `traymenu${idx}`; popouts.currentName = `traymenu${idx}`;
popouts.currentCenter = Qt.binding(() => { popouts.currentCenter = Qt.binding(() => axisCenterOf(trayItem));
const it = tray.items.itemAt(idx);
return it ? it.mapToItem(root, it.implicitWidth / 2, 0).x : popouts.currentCenter;
});
popouts.hasCurrent = true; popouts.hasCurrent = true;
} }
@@ -62,12 +62,19 @@ RowLayout {
if (id === "updates") { if (id === "updates") {
popouts.currentName = "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; 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 { Repeater {
id: repeater id: repeater
@@ -84,6 +91,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
HyprsunsetWidget { HyprsunsetWidget {
horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -93,7 +101,8 @@ RowLayout {
roleValue: "spacer" roleValue: "spacer"
delegate: EntryWrapper { delegate: EntryWrapper {
Layout.fillWidth: true Layout.fillHeight: !root.horizontal
Layout.fillWidth: root.horizontal
} }
} }
@@ -102,6 +111,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
Workspaces { Workspaces {
horizontal: root.horizontal
screen: root.screen screen: root.screen
visible: !root.fullscreen visible: !root.fullscreen
} }
@@ -113,6 +123,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
TrayIcons { TrayIcons {
horizontal: root.horizontal
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visible: !root.fullscreen visible: !root.fullscreen
@@ -125,6 +136,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
StatusIcons { StatusIcons {
horizontal: root.horizontal
} }
} }
} }
@@ -134,6 +146,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
Resources { Resources {
horizontal: root.horizontal
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen
} }
@@ -145,6 +158,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
UpdatesWidget { UpdatesWidget {
horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -155,6 +169,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
NotifBell { NotifBell {
horizontal: root.horizontal
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen
@@ -167,6 +182,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
Clock { Clock {
horizontal: root.horizontal
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
@@ -181,6 +197,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
WindowTitle { WindowTitle {
bar: root bar: root
horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -191,6 +208,7 @@ RowLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
MediaWidget { MediaWidget {
horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen
} }
} }
@@ -204,9 +222,11 @@ RowLayout {
default property Item item default property Item item
required property var modelData required property var modelData
Layout.alignment: Qt.AlignVCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
Layout.leftMargin: index === 0 ? root.vPadding : (entryId === "statusIcons") ? -root.spacing + Tokens.spacing.extraSmall : 0 Layout.bottomMargin: !root.horizontal && index === repeater.count - 1 ? root.vPadding : 0
Layout.rightMargin: 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 children: item
implicitHeight: item?.implicitHeight ?? 0 implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0 implicitWidth: item?.implicitWidth ?? 0
+26 -19
View File
@@ -1,44 +1,48 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Hyprland
import QtQuick import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Modules.Bar.Popouts
import qs.Modules.SysTray import qs.Components
import qs.Modules.SysTray.Widgets
Item { Item {
id: root id: root
readonly property int contentHeight: Config.bar.height + padding * 2 readonly property int clampedExtent: Math.max(Config.bar.border, extent)
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentHeight 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 required property bool fullscreen
readonly property bool horizontal: position !== "left"
property bool isHovered property bool isHovered
readonly property int padding: Math.max(Tokens.padding.smaller, Config.bar.border) readonly property int padding: Math.max(Tokens.padding.smaller, Config.bar.border)
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property string position
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: !fullscreen && (!Config.bar.autoHide || visibilities.bar || isHovered) readonly property bool shouldBeVisible: !fullscreen && (!Config.bar.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
function checkPopout(x: real): void { function checkPopout(pos: real): void {
content.item?.checkPopout(x); (content.item as Bar)?.checkPopout(pos);
} }
implicitHeight: fullscreen ? 0 : Config.bar.border function entryAt(pos: real): string {
visible: height > Config.bar.border return (content.item as Bar)?.entryAt(pos) ?? "";
}
implicitHeight: extent
implicitWidth: extent
visible: extent > Config.bar.border
states: State { states: State {
name: "visible" name: "visible"
when: root.shouldBeVisible when: root.shouldBeVisible
PropertyChanges { PropertyChanges {
root.implicitHeight: root.contentHeight root.extent: root.contentThickness
} }
} }
transitions: [ transitions: [
@@ -47,7 +51,7 @@ Item {
to: "visible" to: "visible"
Anim { Anim {
property: "implicitHeight" property: "extent"
target: root target: root
} }
}, },
@@ -56,7 +60,7 @@ Item {
to: "" to: ""
Anim { Anim {
property: "implicitHeight" property: "extent"
target: root target: root
} }
} }
@@ -66,13 +70,16 @@ Item {
id: content id: content
active: root.shouldBeVisible || root.visible active: root.shouldBeVisible || root.visible
anchors.bottom: parent.bottom anchors.bottom: root.position !== "bottom" ? parent.bottom : undefined
anchors.left: parent.left anchors.left: root.horizontal ? parent.left : undefined
anchors.right: parent.right anchors.right: parent.right
anchors.top: root.position !== "top" ? parent.top : undefined
height: root.contentThickness
width: root.contentThickness
sourceComponent: Bar { sourceComponent: Bar {
fullscreen: root.fullscreen fullscreen: root.fullscreen
height: root.contentHeight horizontal: root.horizontal
popouts: root.popouts popouts: root.popouts
popoutsWrapper: root.popoutsWrapper popoutsWrapper: root.popoutsWrapper
screen: root.screen screen: root.screen
+157
View File
@@ -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 QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Bar.Components.Common
import qs.Helpers
import qs.Services import qs.Services
CustomRect { WidgetBase {
id: root id: root
property bool tempEnabled: Hyprsunset.enabled property bool tempEnabled: Hyprsunset.enabled
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
implicitWidth: implicitHeight
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
@@ -26,6 +25,7 @@ CustomRect {
animate: true animate: true
color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.tempEnabled ? 1 : 0 fill: root.tempEnabled ? 1 : 0
font.pointSize: Tokens.font.size.larger
text: root.tempEnabled ? "lightbulb" : "light_off" text: root.tempEnabled ? "lightbulb" : "light_off"
Behavior on fill { Behavior on fill {
+92
View File
@@ -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();
}
}
}
}
+25
View File
@@ -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 Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import qs.Modules.Bar.Components.Common
import qs.Modules.Bar.Popouts
import qs.Daemons import qs.Daemons
import qs.Components import qs.Components
import qs.Services import qs.Services
CustomRect { WidgetBase {
id: root id: root
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities required property PersistentProperties visibilities
color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2
implicitWidth: implicitHeight
radius: Tokens.rounding.full radius: Tokens.rounding.full
MaterialIcon { MaterialIcon {
@@ -22,7 +22,6 @@ CustomRect {
anchors.centerIn: parent anchors.centerIn: parent
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0 fill: root.visibilities.sidebar ? 1 : 0
font.family: "Material Symbols Rounded"
font.pointSize: Tokens.font.size.larger font.pointSize: Tokens.font.size.larger
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4" text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
@@ -5,13 +5,14 @@ import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
RowLayout { GridLayout {
id: root id: root
property color accentColor: warning ? Colors.palette.m3error : mainColor property color accentColor: warning ? Colors.palette.m3error : mainColor
property real animatedPercentage: 0 property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI readonly property real arcSweep: 1.5 * Math.PI
required property bool horizontal
property string icon property string icon
required property color iconColor required property color iconColor
required property color mainColor required property color mainColor
@@ -21,8 +22,9 @@ RowLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
columnSpacing: Tokens.spacing.smaller
columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
spacing: Tokens.spacing.smaller
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {
@@ -47,8 +49,8 @@ RowLayout {
} }
CustomClippingRect { CustomClippingRect {
Layout.preferredHeight: root.height - Tokens.padding.small Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.small : 4
Layout.preferredWidth: 4 Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full radius: Tokens.rounding.full
@@ -57,9 +59,11 @@ RowLayout {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left 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 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 { // Behavior on implicitHeight {
// Anim { // Anim {
@@ -6,18 +6,18 @@ import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Services import ZShell.Services
import qs.Services import qs.Services
import qs.Modules import qs.Modules.Bar.Components.Common
import qs.Components import qs.Components
CustomRect { WidgetBase {
id: root id: root
required property PersistentProperties visibilities required property PersistentProperties visibilities
clip: true clip: true
color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2 implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.normal * 2
implicitWidth: rowLayout.implicitWidth + Tokens.padding.larger * 2 implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.larger * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
@@ -26,13 +26,14 @@ CustomRect {
onClicked: root.visibilities.resources = !root.visibilities.resources onClicked: root.visibilities.resources = !root.visibilities.resources
} }
RowLayout { GridLayout {
id: rowLayout id: gridLayout
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: -2 anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
implicitHeight: root.implicitHeight anchors.verticalCenterOffset: root.horizontal ? 0 : -3
spacing: Tokens.spacing.smaller columnSpacing: Tokens.spacing.smaller
columns: root.horizontal ? -1 : 1
ServiceRef { ServiceRef {
service: Gpu service: Gpu
@@ -48,7 +49,9 @@ CustomRect {
Resource { Resource {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory" icon: "memory"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
@@ -57,7 +60,9 @@ CustomRect {
} }
Resource { Resource {
Layout.fillHeight: true Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory_alt" icon: "memory_alt"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
@@ -66,7 +71,9 @@ CustomRect {
} }
Resource { Resource {
Layout.fillHeight: true Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "gamepad" icon: "gamepad"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
@@ -74,7 +81,9 @@ CustomRect {
} }
Resource { Resource {
Layout.fillHeight: true Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "developer_board" icon: "developer_board"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
@@ -3,13 +3,14 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Helpers
import qs.Modules.SysTray.Widgets
import ZShell.Config import ZShell.Config
import qs.Helpers
import qs.Modules.Bar.Components.StatusIcons
import qs.Modules.Bar.Components.Common
import qs.Components import qs.Components
import qs.Services import qs.Services
CustomClippingRect { WidgetBase {
id: root id: root
readonly property int firstPresent: { readonly property int firstPresent: {
@@ -21,7 +22,7 @@ CustomClippingRect {
} }
// Index of the first/last entry that isn't collapsed, for edge margin gating // 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: { readonly property int lastPresent: {
const values = model.values; const values = model.values;
for (let i = values.length - 1; i >= 0; i--) for (let i = values.length - 1; i >= 0; i--)
@@ -29,6 +30,7 @@ CustomClippingRect {
return i; return i;
return -1; 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 readonly property int spacing: Tokens.spacing.normal / 2
// Entries that can shrink to nothing, spacing included // Entries that can shrink to nothing, spacing included
@@ -38,19 +40,36 @@ CustomClippingRect {
return false; return false;
} }
bottomLeftRadius: Tokens.rounding.smallest / 2 bottomLeftRadius: horizontal ? Tokens.rounding.smallest / 2 : Tokens.rounding.full
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2 implicitHeight: horizontal ? baseSize : size
implicitWidth: row.implicitWidth + Tokens.padding.small * 2 implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
topLeftRadius: Tokens.rounding.smallest / 2 topLeftRadius: Tokens.rounding.smallest / 2
topRightRadius: horizontal ? Tokens.rounding.full : Tokens.rounding.smallest / 2
RowLayout { Behavior on implicitHeight {
id: row 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.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: root.horizontal ? Tokens.padding.small : 0
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: root.horizontal ? Tokens.padding.small : 0
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.small
columns: root.horizontal ? -1 : 1
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -69,6 +88,7 @@ CustomClippingRect {
name: "audio" name: "audio"
AudioWidget { AudioWidget {
horizontal: root.horizontal
} }
} }
} }
@@ -80,6 +100,7 @@ CustomClippingRect {
name: "audio" name: "audio"
MicWidget { MicWidget {
horizontal: root.horizontal
} }
} }
} }
@@ -131,8 +152,10 @@ CustomClippingRect {
property real rightGap: present && index !== root.firstPresent ? margin : 0 property real rightGap: present && index !== root.firstPresent ? margin : 0
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: Math.round(leftGap) Layout.bottomMargin: root.horizontal ? 0 : Math.round(rightGap)
Layout.rightMargin: 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 children: item
implicitHeight: item?.implicitHeight ?? 0 implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0 implicitWidth: item?.implicitWidth ?? 0
@@ -1,16 +1,15 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Daemons
import qs.Modules
import ZShell.Config import ZShell.Config
import qs.Daemons
import qs.Components import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: speaker id: speaker
required property bool horizontal
animate: true animate: true
color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1 fill: 1
@@ -1,16 +1,15 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Daemons
import qs.Modules
import ZShell.Config import ZShell.Config
import qs.Daemons
import qs.Components import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: mic id: mic
required property bool horizontal
animate: true animate: true
color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1 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.Layouts
import QtQuick import QtQuick
import QtQuick.VectorImage
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Helpers import qs.Helpers
import qs.Modules import qs.Modules.Bar.Popouts
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -13,10 +12,10 @@ Item {
id: root id: root
property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent 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 int ind
required property SystemTrayItem item required property SystemTrayItem item
required property RowLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
function resolveIcon(app: string, icon: string): string { function resolveIcon(app: string, icon: string): string {
@@ -3,33 +3,37 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Modules.SysTray.Widgets import qs.Components
import qs.Modules import qs.Modules.Bar.Components.Common
import qs.Modules.Bar.Components.Tray
import qs.Modules.Bar.Popouts
import qs.Services import qs.Services
CustomClippingRect { WidgetBase {
id: root id: root
readonly property alias items: repeater readonly property alias items: repeater
readonly property alias layout: sysRow readonly property alias layout: sysGrid
required property RowLayout loader required property GridLayout loader
readonly property int padding: Tokens.padding.small readonly property int padding: Tokens.padding.small
required property Wrapper popouts 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 bottomRightRadius: Tokens.rounding.smallest / 2
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: Config.bar.height + Tokens.padding.smallest * 2 implicitHeight: horizontal ? baseSize : size
implicitWidth: sysRow.implicitWidth + Tokens.padding.small * 2 implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
topRightRadius: Tokens.rounding.smallest / 2 topRightRadius: horizontal ? Tokens.rounding.smallest / 2 : Tokens.rounding.full
RowLayout { GridLayout {
id: sysRow id: sysGrid
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 columns: root.horizontal ? -1 : 1
rowSpacing: -2
Repeater { Repeater {
id: repeater id: repeater
+41
View File
@@ -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
}
}
}
+98
View File
@@ -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
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import ZShell.Config import ZShell.Config
import qs.Modules.Bar.Components.Workspaces
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -14,41 +14,57 @@ Item {
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.small / 2) property real activeWorkspaceMargin: Math.ceil(Tokens.padding.small / 2)
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen 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 property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
readonly property int workspacesShown: workspaces.length readonly property int workspacesShown: workspaces.length
height: implicitHeight implicitHeight: horizontal ? shortSize : size
implicitHeight: Config.bar.height + Tokens.padding.smaller * 2 implicitWidth: horizontal ? size : shortSize
implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2
Behavior on implicitHeight {
enabled: !root.horizontal
Anim {
}
}
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.horizontal
Anim { Anim {
} }
} }
CustomRect { CustomRect {
// qmllint disable Quick.anchor-combinations
id: bgRect id: bgRect
anchors.left: parent.left anchors.bottom: root.horizontal ? undefined : parent.bottom
anchors.right: parent.right anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter 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 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 radius: height / 2
// qmllint enable Quick.anchor-combinations
Grid { Grid {
id: grid id: grid
anchors.fill: parent anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin anchors.margins: root.activeWorkspaceMargin
columnSpacing: 0 columnSpacing: 0
columns: root.workspacesShown columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: 0 rowSpacing: 0
rows: 1 rows: root.horizontal ? 1 : root.workspacesShown
Repeater { Repeater {
model: root.workspaces model: root.workspaces
@@ -74,7 +90,7 @@ Item {
color: Colors.palette.m3onSecondaryContainer color: Colors.palette.m3onSecondaryContainer
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: button.modelData.name text: button.modelData?.name ?? ""
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
@@ -94,13 +110,15 @@ Item {
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorThickness: root.workspaceButtonWidth 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 clip: true
color: Colors.palette.m3primary color: Colors.palette.m3primary
implicitHeight: indicatorThickness implicitHeight: root.horizontal ? indicatorThickness : indicatorLength
implicitWidth: indicatorLength implicitWidth: root.horizontal ? indicatorLength : indicatorThickness
radius: Tokens.rounding.full radius: Tokens.rounding.full
x: indicatorPosition x: root.horizontal ? indicatorPosition : 0
y: root.horizontal ? 0 : indicatorPosition
AnimatedTabIndexPair { AnimatedTabIndexPair {
id: idxPair id: idxPair
@@ -114,7 +132,8 @@ Item {
implicitWidth: grid.width implicitWidth: grid.width
source: grid source: grid
sourceColor: Colors.palette.m3onSurface sourceColor: Colors.palette.m3onSurface
x: -indicator.x + 3 x: root.horizontal ? -indicator.x + 3 : 0
y: root.horizontal ? 0 : -indicator.y + 3
} }
} }
} }
+87
View File
@@ -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 QtQuick
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.WSOverview
import qs.Modules.SysTray.Popouts
import qs.Modules.Updates
Item { Item {
id: root id: root
@@ -5,7 +5,7 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
import qs.Modules.SysTray.Popouts.Network import qs.Modules.Bar.Popouts.Network
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
@@ -1,8 +1,10 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell.Services.UPower import Quickshell.Services.UPower
import ZShell.Config import ZShell.Config
import qs.Modules.Bar.Components.Common
import qs.Helpers import qs.Helpers
import qs.Components import qs.Components
import qs.Services 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 { CustomText {
function formatSeconds(s: int, fallback: string): string { function formatSeconds(s: int, fallback: string): string {
const day = Math.floor(s / 86400); const day = Math.floor(s / 86400);
@@ -75,7 +102,7 @@ Column {
CustomText { CustomText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onError color: Colors.palette.m3onError
font.family: Appearance.font.family.mono font.family: Config.appearance.font.family.mono
text: qsTr("Performance Degraded") text: qsTr("Performance Degraded")
} }
@@ -78,8 +78,6 @@ CustomClippingRect {
required property var modelData required property var modelData
readonly property list<string> sections: modelData.update.split(" ") readonly property list<string> sections: modelData.update.split(" ")
// anchors.left: parent.left
// anchors.right: parent.right
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.itemHeight implicitHeight: root.itemHeight
implicitWidth: 600 implicitWidth: 600
-56
View File
@@ -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
}
}
-44
View File
@@ -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;
}
}
}
+12 -2
View File
@@ -16,9 +16,16 @@ Item {
} }
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
required property real offsetScale required property real offsetScale
required property string position
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled
required property PersistentProperties visibilities 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 implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854 implicitWidth: content.implicitWidth || 854
opacity: 1 - offsetScale opacity: 1 - offsetScale
@@ -28,8 +35,11 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible active: root.shouldBeActive || root.visible
anchors.bottom: parent.bottom anchors.bottom: root.position === "top" ? parent.bottom : undefined
anchors.horizontalCenter: parent.horizontalCenter 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 { sourceComponent: Content {
dashState: root.dashState dashState: root.dashState
@@ -82,7 +82,6 @@ Searcher {
list.visibilities.launcher = false; list.visibilities.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant; Config.colors.schemeType = variant;
Config.save();
} }
} }
} }
-77
View File
@@ -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();
}
}
}
}
}
}
+12 -2
View File
@@ -10,10 +10,16 @@ Item {
readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0 readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property string position
readonly property bool shouldBeActive: root.visibilities.resources readonly property bool shouldBeActive: root.visibilities.resources
required property PersistentProperties visibilities 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 clip: true
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open
@@ -31,7 +37,11 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible 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 { sourceComponent: Content {
wrapper: root wrapper: root
+6 -2
View File
@@ -2,8 +2,8 @@ pragma Singleton
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Settings.Common import qs.Modules.Settings.Common
import qs.Modules.Settings.Pages import qs.Modules.Settings.Pages
import qs.Modules.Settings.Pages.Wallpaper import qs.Modules.Settings.Pages.Wallpaper
@@ -45,7 +45,11 @@ QtObject {
// Connectivity // Connectivity
Component { Component {
PlaceholderComp { StackPage {
Component {
NetworkPage {
}
}
} }
}, },
Component { Component {
+22 -21
View File
@@ -3,11 +3,12 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common import qs.Modules.Settings.Common
import qs.Modules.SysTray.Popouts.Network import qs.Modules.Bar.Popouts.Network
import qs.Components import qs.Components
import qs.Config
import qs.Helpers import qs.Helpers
import qs.Services
PageBase { PageBase {
id: root id: root
@@ -19,7 +20,7 @@ PageBase {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2 spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth width: root.cappedWidth
Toggle { Toggle {
@@ -56,7 +57,7 @@ PageBase {
id: networkItem id: networkItem
readonly property bool connecting: Network.connectingNetwork === modelData readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Appearance.padding.largeIncreased property int horizontalPadding: Tokens.padding.largeIncreased
required property int index required property int index
readonly property bool isSecure: Network.isSecure(modelData.security) readonly property bool isSecure: Network.isSecure(modelData.security)
required property bool known required property bool known
@@ -64,10 +65,10 @@ PageBase {
required property Repeater repeater required property Repeater repeater
Layout.fillWidth: true 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 first: index === 0
last: index === (repeater.count - 1) last: index === (repeater.count - 1)
radius: Appearance.rounding.small radius: Tokens.rounding.small
RowLayout { RowLayout {
id: rowLayout id: rowLayout
@@ -83,9 +84,9 @@ PageBase {
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.extraSmall Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Appearance.spacing.extraSmall Layout.rightMargin: Tokens.spacing.extraSmall
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
elide: Text.ElideRight elide: Text.ElideRight
text: networkItem.modelData.name text: networkItem.modelData.name
} }
@@ -95,9 +96,9 @@ PageBase {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
color: DynamicColors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight 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) + "%") text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
} }
} }
@@ -110,8 +111,8 @@ PageBase {
MaterialIcon { MaterialIcon {
id: icon id: icon
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large font.pointSize: Tokens.font.size.large
opacity: networkItem.connecting ? 0 : 1 opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure) text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
@@ -139,7 +140,7 @@ PageBase {
} }
StateLayer { StateLayer {
color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: { onClicked: {
if (networkItem.isSecure && !networkItem.known) { if (networkItem.isSecure && !networkItem.known) {
@@ -157,7 +158,7 @@ PageBase {
required property Repeater repeater 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 { component Toggle: CustomSwitch {
id: toggleItem id: toggleItem
@@ -169,13 +170,13 @@ PageBase {
Layout.fillWidth: true Layout.fillWidth: true
cLayer: 2 cLayer: 2
horizontalPadding: Appearance.padding.largeIncreased horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Appearance.padding.normal verticalPadding: Tokens.padding.normal
background: ConnectedRect { background: ConnectedRect {
id: bg id: bg
@@ -190,7 +191,7 @@ PageBase {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left anchors.right: toggleItem.indicator.left
anchors.rightMargin: Appearance.spacing.normal anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth implicitWidth: column.implicitWidth
@@ -210,7 +211,7 @@ PageBase {
elide: Text.ElideRight elide: Text.ElideRight
font: { font: {
const f = Qt.font(label.font); const f = Qt.font(label.font);
f.pointSize = Appearance.font.size.smaller; f.pointSize = Tokens.font.size.smaller;
return f; return f;
} }
text: toggleItem.text text: toggleItem.text
@@ -221,11 +222,11 @@ PageBase {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
color: DynamicColors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font: { font: {
const f = Qt.font(subtext.font); const f = Qt.font(subtext.font);
f.pointSize = Appearance.font.size.small; f.pointSize = Tokens.font.size.small;
return f; return f;
} }
text: toggleItem.subtext text: toggleItem.subtext
+29 -1
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Settings.Common import qs.Modules.Settings.Common
PageBase { PageBase {
@@ -25,7 +26,6 @@ PageBase {
ToggleRow { ToggleRow {
checked: Config.bar.autoHide checked: Config.bar.autoHide
first: true first: true
last: true
settingAnchor: "bar-autohide" settingAnchor: "bar-autohide"
subtext: qsTr("Hide the bar, reveal on hover") subtext: qsTr("Hide the bar, reveal on hover")
text: qsTr("Auto hide") text: qsTr("Auto hide")
@@ -33,6 +33,34 @@ PageBase {
onToggled: Config.bar.autoHide = checked 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 // Components
SectionHeader { SectionHeader {
text: qsTr("Components") text: qsTr("Components")
+1 -4
View File
@@ -70,10 +70,7 @@ PageBase {
} }
] ]
onSelected: item => { onSelected: item => Config.screenshot.mode = item.value
Config.screenshot.mode = item.value;
Config.save();
}
} }
SectionHeader { SectionHeader {
-2
View File
@@ -104,7 +104,6 @@ PageBase {
onApplySettings: (start, end) => { onApplySettings: (start, end) => {
Config.general.color.scheduleDarkStart = start; Config.general.color.scheduleDarkStart = start;
Config.general.color.scheduleDarkEnd = end; Config.general.color.scheduleDarkEnd = end;
Config.save();
ModeScheduler.checkStartup(); ModeScheduler.checkStartup();
PopupManager.requestClose(); PopupManager.requestClose();
} }
@@ -146,7 +145,6 @@ PageBase {
onApplySettings: (start, end) => { onApplySettings: (start, end) => {
Config.general.color.scheduleHyprsunsetStart = start; Config.general.color.scheduleHyprsunsetStart = start;
Config.general.color.scheduleHyprsunsetEnd = end; Config.general.color.scheduleHyprsunsetEnd = end;
Config.save();
Hyprsunset.checkStartup(); Hyprsunset.checkStartup();
PopupManager.requestClose(); PopupManager.requestClose();
} }
-154
View File
@@ -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
}
}
}
-135
View File
@@ -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";
}
}
}
}
-37
View File
@@ -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
}
}
}
-86
View File
@@ -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
}
}
}
}
}
}
}
}
-75
View File
@@ -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 {
}
}
}
}
+3
View File
@@ -2,6 +2,8 @@
#include "configlist.hpp" #include "configlist.hpp"
#include "configobject.hpp" #include "configobject.hpp"
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qqmlregistration.h> #include <qqmlregistration.h>
namespace ZShell::config { namespace ZShell::config {
@@ -61,6 +63,7 @@ class Bar : public ConfigObject {
CFG_PROPERTY(int, revealDelay, 100) CFG_PROPERTY(int, revealDelay, 100)
CFG_PROPERTY(int, rounding, 14) CFG_PROPERTY(int, rounding, 14)
CFG_PROPERTY(int, smoothing, 32) CFG_PROPERTY(int, smoothing, 32)
CFG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_SUBOBJECT(Tray, tray) CONFIG_SUBOBJECT(Tray, tray)
CONFIG_LIST( CONFIG_LIST(
EntryList, EntryList,
+40 -19
View File
@@ -61,6 +61,8 @@ Config::Config(QObject* parent)
m_reloadTimer.setInterval(50); m_reloadTimer.setInterval(50);
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync); connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
connectAutoSave(this);
connect( connect(
&m_watcher, &m_watcher,
&QFileSystemWatcher::directoryChanged, &QFileSystemWatcher::directoryChanged,
@@ -93,15 +95,31 @@ QString Config::filePath() const {
void Config::loadSync() { void Config::loadSync() {
m_loading = true; m_loading = true;
QFile f(filePath()); QFile f(filePath());
QJsonObject before;
bool existed = false;
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll()); 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 { } else {
qInfo() << "Config: no existing config at" << filePath() qInfo() << "Config: no existing config at" << filePath()
<< "- using defaults"; << "- using defaults";
} }
loadFromJson(QJsonValue(before));
m_loading = false; m_loading = false;
const auto after = toJson().toObject();
if (!existed || after != before) saveNow();
} }
void Config::updateWatch() { void Config::updateWatch() {
@@ -146,27 +164,21 @@ void Config::loadAsync() {
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll()); const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
const QJsonObject before = valid ? doc.object() : QJsonObject();
if (doc.isObject()) { QMetaObject::invokeMethod(
QMetaObject::invokeMethod( this,
this, [this, valid, before]() {
[this, doc]() { loadFromJson(QJsonValue(before));
loadFromJson(QJsonValue(doc.object())); m_loading = false;
m_loading = false;
if (m_reloadPending) m_reloadTimer.start(); const auto after = toJson().toObject();
}, if (!valid || after != before) saveNow();
Qt::QueuedConnection);
} else {
QMetaObject::invokeMethod(
this,
[this]() {
m_loading = false;
if (m_reloadPending) m_reloadTimer.start(); if (m_reloadPending) m_reloadTimer.start();
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
}
} else { } else {
qInfo() << "Config: failed to reload from" << filePath() qInfo() << "Config: failed to reload from" << filePath()
<< "- using in-memory values"; << "- using in-memory values";
@@ -232,6 +244,15 @@ void Config::saveNow() {
flushAsync(); 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) { bool Config::writeAtomically(const QByteArray& data) {
QSaveFile f(filePath()); QSaveFile f(filePath());
+1
View File
@@ -90,6 +90,7 @@ class Config : public ConfigObject {
void updateWatch(); void updateWatch();
void loadSync(); void loadSync();
void loadAsync(); void loadAsync();
void connectAutoSave(ConfigNode* node);
QTimer m_saveTimer; QTimer m_saveTimer;
QTimer m_reloadTimer; QTimer m_reloadTimer;
+14 -5
View File
@@ -79,13 +79,14 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
if (!json.isArray()) { if (!json.isArray()) {
qCWarning( qCWarning(
lcConfig, lcConfig,
"Option '%s' must be a list, ignoring", "Option '%s' must be a list, resetting to default",
qUtf8Printable(propertyPath())); qUtf8Printable(propertyPath()));
m_rejectedJson = json;
resetToDefaults();
materializeDefaults();
return; return;
} }
m_rejectedJson = QJsonValue::Undefined;
populate(json.toArray()); populate(json.toArray());
m_loaded = true; m_loaded = true;
} }
@@ -93,12 +94,11 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
QJsonValue ConfigList::toJson() const { QJsonValue ConfigList::toJson() const {
if (m_loaded) return elementsToJson(); if (m_loaded) return elementsToJson();
return m_rejectedJson; return QJsonValue::Undefined;
} }
void ConfigList::clearLoadedKeys() { void ConfigList::clearLoadedKeys() {
m_loaded = false; m_loaded = false;
m_rejectedJson = QJsonValue::Undefined;
} }
QStringList ConfigList::unknownKeys() const { QStringList ConfigList::unknownKeys() const {
@@ -113,6 +113,15 @@ QStringList ConfigList::unknownKeys() const {
return keys; return keys;
} }
void ConfigList::materializeDefaults() {
for (auto* const item : m_items)
item->materializeDefaults();
if (m_global) return;
m_loaded = true;
}
void ConfigList::resyncFromGlobal() { void ConfigList::resyncFromGlobal() {
syncValuesFromGlobal(); syncValuesFromGlobal();
} }
+3 -5
View File
@@ -33,6 +33,7 @@ class ConfigList : public ConfigNode {
[[nodiscard]] QJsonValue toJson() const override; [[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override; void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override; [[nodiscard]] QStringList unknownKeys() const override;
void materializeDefaults() override;
void resyncFromGlobal() override; void resyncFromGlobal() override;
signals: signals:
@@ -71,7 +72,6 @@ class ConfigList : public ConfigNode {
QJsonArray m_defaults; QJsonArray m_defaults;
QList<ConfigObject*> m_items; QList<ConfigObject*> m_items;
bool m_loaded = false; bool m_loaded = false;
QJsonValue m_rejectedJson = QJsonValue::Undefined;
}; };
} // namespace ZShell::config } // namespace ZShell::config
@@ -103,7 +103,6 @@ class ConfigList : public ConfigNode {
} \ } \
}; };
#define CONFIG_LIST(Type, name, ...) \ #define CONFIG_LIST(Type, name, ...) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \ Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\ \
@@ -115,6 +114,8 @@ class ConfigList : public ConfigNode {
private: \ private: \
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__)); 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 { namespace ZShell::config {
@@ -136,6 +137,3 @@ class ListEntry : public ConfigObject {
CONFIG_LIST_TYPE(ListEntry, EntryList) CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config } // namespace ZShell::config
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
+2
View File
@@ -26,6 +26,8 @@ class ConfigNode : public QObject {
[[nodiscard]] virtual QStringList unknownKeys() const = 0; [[nodiscard]] virtual QStringList unknownKeys() const = 0;
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const; [[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
virtual void materializeDefaults() = 0;
void syncFromGlobal(ConfigNode* global); void syncFromGlobal(ConfigNode* global);
virtual void resyncFromGlobal() = 0; virtual void resyncFromGlobal() = 0;
+78 -4
View File
@@ -7,6 +7,51 @@
namespace ZShell::config { 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) {} ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
void ConfigObject::loadFromJson(const QJsonValue& json) { void ConfigObject::loadFromJson(const QJsonValue& json) {
@@ -14,6 +59,7 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
const auto* meta = metaObject(); const auto* meta = metaObject();
QSet<QString> known; QSet<QString> known;
QSet<QString> invalid;
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) { for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i); auto prop = meta->property(i);
@@ -34,6 +80,15 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
if (!prop.isWritable()) continue; 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) { if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list; QStringList list;
const auto jsonArr = jsonVal.toArray(); const auto jsonArr = jsonVal.toArray();
@@ -56,10 +111,12 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
m_extras = {}; m_extras = {};
for (auto it = obj.begin(); it != obj.end(); ++it) { 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()); m_extras.insert(it.key(), it.value());
} }
} }
materializeDefaults();
} }
QJsonValue ConfigObject::toJson() const { QJsonValue ConfigObject::toJson() const {
@@ -104,9 +161,6 @@ QJsonValue ConfigObject::toJson() const {
obj.insert(key, QJsonValue::fromVariant(value)); 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; if (obj.isEmpty()) return QJsonValue::Undefined;
return obj; return obj;
@@ -159,6 +213,26 @@ QList<ConfigNode*> ConfigObject::childNodes() const {
return nodes; 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() { void ConfigObject::syncValuesFromGlobal() {
const auto* meta = metaObject(); const auto* meta = metaObject();
+1
View File
@@ -62,6 +62,7 @@ class ConfigObject : public ConfigNode {
void clearLoadedKeys() override; void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override; [[nodiscard]] QStringList unknownKeys() const override;
[[nodiscard]] QList<ConfigNode*> childNodes() const override; [[nodiscard]] QList<ConfigNode*> childNodes() const override;
void materializeDefaults() override;
void resyncFromGlobal() override; void resyncFromGlobal() override;
[[nodiscard]] virtual QStringList identityKeys() const; [[nodiscard]] virtual QStringList identityKeys() const;
+15 -1
View File
@@ -3,7 +3,9 @@
#include "configobject.hpp" #include "configobject.hpp"
#include <QVariantList> #include <QVariantList>
#include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmlregistration.h> #include <qqmlregistration.h>
namespace ZShell::config { namespace ZShell::config {
@@ -195,6 +197,16 @@ class AnimTokens : public ConfigObject {
, m_durations(new AnimDurations(this)) {} , 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 { class Tokens : public ConfigObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
@@ -205,6 +217,7 @@ class Tokens : public ConfigObject {
CONFIG_SUBOBJECT(AppearanceSpacing, spacing) CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(FontTokens, font) CONFIG_SUBOBJECT(FontTokens, font)
CONFIG_SUBOBJECT(AnimTokens, anim) CONFIG_SUBOBJECT(AnimTokens, anim)
CONFIG_SUBOBJECT(BarTokens, bar)
public: public:
explicit Tokens(QObject* parent = nullptr) explicit Tokens(QObject* parent = nullptr)
@@ -213,7 +226,8 @@ class Tokens : public ConfigObject {
, m_padding(new AppearancePadding(this)) , m_padding(new AppearancePadding(this))
, m_spacing(new AppearanceSpacing(this)) , m_spacing(new AppearanceSpacing(this))
, m_font(new FontTokens(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(); } static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
}; };
-1
View File
@@ -113,7 +113,6 @@ Singleton {
function setMode(mode: string): void { function setMode(mode: string): void {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
Config.general.color.mode = mode; Config.general.color.mode = mode;
Config.save();
} }
function swapRG(c: color): color { function swapRG(c: color): color {
+9 -1
View File
@@ -8,7 +8,14 @@ import typer
from typer._completion_classes import completion_init from typer._completion_classes import completion_init
from typer._completion_shared import _get_shell_name, install 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) 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(screenshot.app, name="screenshot")
app.add_typer(wallpaper.app, name="wallpaper") app.add_typer(wallpaper.app, name="wallpaper")
app.add_typer(record.app, name="record") app.add_typer(record.app, name="record")
app.add_typer(battery.app, name="battery")
def _completion_installed() -> bool: def _completion_installed() -> bool:
+819
View File
@@ -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
View File
@@ -10,12 +10,11 @@ dependencies = [
"typer", "typer",
"pillow", "pillow",
"jinja2", "jinja2",
"materialyoucolor" "materialyoucolor",
"solaar",
"pytest"
] ]
[project.optional-dependencies]
dev = ["pytest"]
[project.scripts] [project.scripts]
zshell-cli = "zshell:main" zshell-cli = "zshell:main"