6 Commits
Author SHA1 Message Date
AramJonghu 5ee4f370d9 ci: rework Dockerfile image + buildx dep added
C++ / fmt (pull_request) Failing after 5s
C++ / build & lint (pull_request) Failing after 10s
JS/TS / fmt & lint (pull_request) Successful in 12s
Python / fmt & lint (pull_request) Successful in 28s
Python / test & nuitka build (pull_request) Successful in 1m38s
Rust / build, clippy & fmt (pull_request) Failing after 4m58s
2026-10-23 03:29:28 +02:00
AramJonghu 068319a4cb refactor(workflows): combined steps where it makes sense 2026-10-23 03:05:40 +02:00
AramJonghu bdb95af7a1 fix(ci): missing dependency wayland-scanner 2026-10-23 03:05:04 +02:00
zach cbd4e6caa7 chore: move sidebar directory to modules 2026-10-23 00:35:20 +02:00
zach 36cddc96c3 fix: reduce shader usage 2026-10-23 00:05:08 +02:00
zach 6781078467 fix: short nightlight fade duration if missed start or end time or if manually activated 2026-10-19 19:21:57 +02:00
77 changed files with 1041 additions and 290 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Docker CLI - name: Install Docker CLI
run: apk add --no-cache docker-cli run: apk add --no-cache docker-cli docker-cli-buildx
- name: Login to registry - name: Login to registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
+1 -19
View File
@@ -20,28 +20,10 @@ jobs:
-exec clang-format -i --style=file {} + -exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed" git diff --exit-code && echo "clang-format: passed"
build: build & lint:
runs-on: alpine runs-on: alpine
container: container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build
run: ninja -C build
clang-tidy:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
+1 -15
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
jobs: jobs:
fmt: fmt & lint:
runs-on: alpine runs-on: alpine
container: node:26-alpine container: node:26-alpine
@@ -24,20 +24,6 @@ jobs:
else else
echo "No JS/TS files found" echo "No JS/TS files found"
fi fi
lint:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git
- name: ESLint - name: ESLint
run: | run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
+2 -2
View File
@@ -7,7 +7,7 @@ on:
pull_request: pull_request:
jobs: jobs:
static: fmt & lint:
runs-on: debian runs-on: debian
container: node:26-trixie-slim container: node:26-trixie-slim
@@ -33,7 +33,7 @@ jobs:
. .venv/bin/activate . .venv/bin/activate
ruff check . ruff check .
verify: test & nuitka build:
if: always() if: always()
runs-on: debian runs-on: debian
container: node:26-trixie-slim container: node:26-trixie-slim
+21 -86
View File
@@ -3,8 +3,9 @@ name: Rust
on: on:
pull_request: pull_request:
# jobs can be separated if we integrate tests
jobs: jobs:
build: build, clippy & fmt:
runs-on: alpine runs-on: alpine
container: node:26-alpine container: node:26-alpine
env: env:
@@ -34,7 +35,9 @@ jobs:
apk add --no-cache \ apk add --no-cache \
git \ git \
cargo \ cargo \
rust rust \
rust-clippy \
rustfmt
- name: Cargo check - name: Cargo check
run: | run: |
@@ -50,90 +53,6 @@ jobs:
else else
echo "No Rust project found" echo "No Rust project found"
fi fi
fmt:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rustfmt
- name: Format check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo fmt --manifest-path "$manifest" --check && \
echo "$manifest: formatting OK" || \
{ echo "$manifest: needs formatting"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
clippy:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rust-clippy
- name: Clippy - name: Clippy
run: | run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
@@ -150,3 +69,19 @@ jobs:
else else
echo "No Rust project found" echo "No Rust project found"
fi fi
- name: Format check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo fmt --manifest-path "$manifest" --check && \
echo "$manifest: formatting OK" || \
{ echo "$manifest: needs formatting"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
+41
View File
@@ -0,0 +1,41 @@
import QtQuick
import ZShell.Components
Item {
id: root
property real radius
property real topLeftRadius: radius
property real topRightRadius: radius
property real bottomRightRadius: radius
property real bottomLeftRadius: radius
property color color: "transparent"
default property alias contentData: contentItem.data
Item {
id: contentItem
anchors.fill: parent
}
ShaderEffectSource {
id: source
hideSource: true
sourceItem: contentItem
}
RoundedMask {
anchors.fill: parent
source: source
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
color: root.color
}
Behavior on color {
CAnim {}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ TextField {
implicitHeight: contentHeight + topPadding + bottomPadding implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering
selectedTextColor: color selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4) selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
+3
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell
import qs.Effects import qs.Effects
CustomFlickable { CustomFlickable {
@@ -9,6 +10,7 @@ CustomFlickable {
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property real fadeAmount: 0.1 property real fadeAmount: 0.1
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
function fadeShouldBeActive(isStart: bool): bool { function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts // When content is smaller than flickable size, hide fade when rebound starts
@@ -28,6 +30,7 @@ CustomFlickable {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
// layer.textureSize: Qt.size(width * dpr, height * dpr)
layer.effect: Mask { layer.effect: Mask {
maskSource: mask maskSource: mask
+2 -2
View File
@@ -3,8 +3,8 @@ import QtQuick
import qs.Components import qs.Components
import qs.Modules.Bar.Popouts as Popouts 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.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils import qs.Modules.Sidebar.Utils as Utils
import qs.Modules.Dashboard as Dashboard import qs.Modules.Dashboard as Dashboard
import qs.Modules.Osd as Osd import qs.Modules.Osd as Osd
import qs.Components.Toast as Toasts import qs.Components.Toast as Toasts
+1 -1
View File
@@ -23,7 +23,7 @@ Singleton {
function toggleNightLight(): void { function toggleNightLight(): void {
service.manualToggle = true; service.manualToggle = true;
service.toggle(); service.toggle(true);
} }
NightlightManager { NightlightManager {
+3 -1
View File
@@ -19,7 +19,7 @@ Item {
required property Wrapper popouts required property Wrapper popouts
function resolveIcon(app: string, icon: string): string { function resolveIcon(app: string, icon: string): string {
if (app === "chrome_status_icon_1") { if (app === "discord_status_icon_1" && Config.bar.tray.replaceIcons) {
return Quickshell.iconPath("discord-tray"); return Quickshell.iconPath("discord-tray");
} else if (app === "AyuGramDesktop") { } else if (app === "AyuGramDesktop") {
if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic")) if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic"))
@@ -50,6 +50,8 @@ Item {
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
id: layer
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent anchors.fill: parent
color: icon.layer.enabled && root.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: icon.layer.enabled && root.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+32 -28
View File
@@ -33,8 +33,7 @@ Item {
y: parent.y y: parent.y
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
} }
@@ -101,8 +100,7 @@ Item {
value: Audio.volume value: Audio.volume
Behavior on value { Behavior on value {
Anim { Anim {}
}
} }
onInteraction: value => Audio.setVolume(value) onInteraction: value => Audio.setVolume(value)
@@ -123,7 +121,7 @@ Item {
} }
} }
CustomClippingRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 65 + Tokens.spacing.small * 2 Layout.preferredHeight: 65 + Tokens.spacing.small * 2
Layout.topMargin: root.topMargin Layout.topMargin: root.topMargin
@@ -136,17 +134,21 @@ Item {
node: Audio.source node: Audio.source
} }
CustomRect { CustomClippingRect {
id: sourcePeakFill anchors.fill: parent
radius: parent.radius
anchors.bottom: parent.bottom CustomRect {
anchors.left: parent.left id: sourcePeakFill
anchors.top: parent.top
color: Qt.alpha(Colors.palette.m3primary, 0.15)
implicitWidth: parent.width * sourcePeak.peak
Behavior on implicitWidth { anchors.bottom: parent.bottom
Anim { anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(Colors.palette.m3primary, 0.15)
implicitWidth: parent.width * sourcePeak.peak
Behavior on implicitWidth {
Anim {}
} }
} }
} }
@@ -195,8 +197,7 @@ Item {
value: Audio.sourceVolume value: Audio.sourceVolume
Behavior on value { Behavior on value {
Anim { Anim {}
}
} }
onInteraction: value => Audio.setSourceVolume(value) onInteraction: value => Audio.setSourceVolume(value)
@@ -230,7 +231,7 @@ Item {
model: Audio.streams.filter(s => s.isSink) model: Audio.streams.filter(s => s.isSink)
CustomClippingRect { CustomRect {
id: appBox id: appBox
required property int index required property int index
@@ -248,17 +249,21 @@ Item {
node: appBox.modelData node: appBox.modelData
} }
CustomRect { CustomClippingRect {
id: peakFill anchors.fill: parent
radius: parent.radius
anchors.bottom: parent.bottom CustomRect {
anchors.left: parent.left id: peakFill
anchors.top: parent.top
color: Qt.alpha(Colors.palette.m3primary, 0.15)
implicitWidth: parent.width * peak.peak
Behavior on implicitWidth { anchors.bottom: parent.bottom
Anim { anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(Colors.palette.m3primary, 0.15)
implicitWidth: parent.width * peak.peak
Behavior on implicitWidth {
Anim {}
} }
} }
} }
@@ -316,8 +321,7 @@ Item {
value: appBox.modelData.audio.volume value: appBox.modelData.audio.volume
Behavior on value { Behavior on value {
Anim { Anim {}
}
} }
onInteraction: value => Audio.setStreamVolume(appBox.modelData, value) onInteraction: value => Audio.setStreamVolume(appBox.modelData, value)
+2 -1
View File
@@ -9,7 +9,7 @@ import qs.Modules
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
CustomClippingRect { CustomRect {
id: root id: root
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0 readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
@@ -73,6 +73,7 @@ CustomClippingRect {
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing) implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
implicitWidth: contentWidth implicitWidth: contentWidth
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
clip: true
delegate: CustomRect { delegate: CustomRect {
id: update id: update
-2
View File
@@ -1,6 +1,4 @@
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
+7 -11
View File
@@ -3,8 +3,9 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import qs.Components
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components
import qs.Daemons import qs.Daemons
import qs.Helpers import qs.Helpers
@@ -27,7 +28,7 @@ Item {
for (let i = 0; i < count; i++) for (let i = 0; i < count; i++)
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x) if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
return 0; return 0;
if (screenState.osd) { if (screenState.osd) {
@@ -36,25 +37,20 @@ Item {
height = h; height = h;
} }
if (screenState.session) {
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
}
return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2); return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
} }
implicitWidth: Config.notifs.sizes.width + padding * 2 implicitWidth: Config.notifs.sizes.width + padding * 2
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {}
} }
ClippingWrapperRectangle { Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.padding anchors.margins: root.padding
color: "transparent" clip: true
radius: Tokens.rounding.large - root.padding // radius: Tokens.rounding.large - root.padding
CustomListView { CustomListView {
id: list id: list
+14 -9
View File
@@ -4,7 +4,7 @@ import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
CustomClippingRect { CustomRect {
id: root id: root
required property color accent required property color accent
@@ -19,15 +19,20 @@ CustomClippingRect {
implicitWidth: 450 implicitWidth: 450
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
CustomRect { CustomClippingRect {
anchors.bottom: parent.bottom anchors.fill: parent
anchors.left: parent.left radius: parent.radius
anchors.top: parent.top
color: Qt.alpha(root.accent, 0.05)
implicitWidth: parent.width * root.usage
Behavior on implicitWidth { CustomRect {
Anim {} anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(root.accent, 0.05)
implicitWidth: parent.width * root.usage
Behavior on implicitWidth {
Anim {}
}
} }
} }
+3 -2
View File
@@ -149,7 +149,7 @@ ColumnLayout {
} }
} }
VerticalFadeFlickable { Flickable {
id: flickable id: flickable
Layout.fillHeight: true Layout.fillHeight: true
@@ -158,7 +158,8 @@ ColumnLayout {
bottomMargin: Tokens.padding.extraLarge bottomMargin: Tokens.padding.extraLarge
topMargin: Tokens.padding.large topMargin: Tokens.padding.large
fadeAmount: 0.1 clip: true
// fadeAmount: 0.1
contentHeight: root.contentChild?.implicitHeight ?? 0 contentHeight: root.contentChild?.implicitHeight ?? 0
contentItem.children: [root.contentChild] contentItem.children: [root.contentChild]
+3 -2
View File
@@ -6,7 +6,7 @@ import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomClippingRect { Item {
id: root id: root
property color blobColor: Colors.tPalette.m3surfaceContainerLow property color blobColor: Colors.tPalette.m3surfaceContainerLow
@@ -21,7 +21,6 @@ CustomClippingRect {
implicitHeight: sState.screen.height * 0.7 implicitHeight: sState.screen.height * 0.7
implicitWidth: implicitHeight * ratio implicitWidth: implicitHeight * ratio
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
Behavior on blobColor { Behavior on blobColor {
CAnim {} CAnim {}
@@ -46,6 +45,8 @@ CustomClippingRect {
borderTop: Tokens.padding.small borderTop: Tokens.padding.small
group: blobGroup group: blobGroup
opacity: root.blobColor.a opacity: root.blobColor.a
// outerRadius: 10
outerRadius: Tokens.rounding.largeIncreased + Tokens.padding.small
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
} }
+1 -1
View File
@@ -199,7 +199,7 @@ PageBase {
stepSize: 1 stepSize: 1
subtext: qsTr("How long it takes for the gradual nightlight to finish (s)") subtext: qsTr("How long it takes for the gradual nightlight to finish (s)")
text: qsTr("Fade duration") text: qsTr("Fade duration")
to: 60 * 5 to: 60 * 20
value: Config.display.nightlight.fadeDuration / 1000 value: Config.display.nightlight.fadeDuration / 1000
onMoved: v => Config.display.nightlight.fadeDuration = v * 1000 onMoved: v => Config.display.nightlight.fadeDuration = v * 1000
+11 -14
View File
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.Components
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components
Item { Item {
id: root id: root
@@ -27,21 +28,17 @@ Item {
} }
} }
CustomClippingRect { Loader {
anchors.fill: parent id: content
Loader { active: root.shouldBeActive || root.visible
id: content anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
active: root.shouldBeActive || root.visible sourceComponent: Content {
anchors.bottom: parent.bottom sState.animatingContainer: content.opacity < 1
anchors.horizontalCenter: parent.horizontalCenter sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
sState.screen: root.screen
sourceComponent: Content {
sState.animatingContainer: content.opacity < 1
sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
sState.screen: root.screen
}
} }
} }
} }
@@ -6,7 +6,7 @@ import QtQuick.Layouts
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat.Content import qs.Modules.Sidebar.Chat.Content
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -6,7 +6,7 @@ import qs.Effects
import qs.Components import qs.Components
import qs.Services import qs.Services
CustomClippingRect { CustomRect {
id: root id: root
property bool expanded: false property bool expanded: false
@@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Llm import ZShell.Llm
import ZShell.Config import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat.Content import qs.Modules.Sidebar.Chat.Content
import qs.Services import qs.Services
import qs.Components import qs.Components
@@ -185,7 +185,7 @@ Item {
} }
} }
CustomClippingRect { CustomRect {
id: fabMenuItem id: fabMenuItem
required property var modelData required property var modelData
@@ -197,6 +197,7 @@ Item {
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2 radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer color: Colors.palette.m3primaryContainer
visible: !(modelData.name === "pop out in window" && ChatState.isWindow) visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
clip: true
Behavior on radius { Behavior on radius {
Anim { Anim {
@@ -6,7 +6,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Helpers import qs.Helpers
import qs.Modules.Notifications.Sidebar import qs.Modules.Sidebar
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -3,7 +3,7 @@ import ZShell.Llm
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Sidebar.Chat
Item { Item {
id: root id: root
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Llm import ZShell.Llm
import ZShell.Config import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Sidebar.Chat
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -24,7 +24,7 @@ Item {
implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0) implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0)
CustomClippingRect { CustomRect {
id: bubble id: bubble
property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
@@ -37,8 +37,7 @@ Item {
implicitWidth: layoutFrozen ? frozenWidth : layoutWidth implicitWidth: layoutFrozen ? frozenWidth : layoutWidth
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2 implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2
anchors.right: root.isUser ? parent.right : undefined anchors.right: root.isUser ? parent.right : undefined
layer.enabled: false clip: true
layer.smooth: true
transform: Scale { transform: Scale {
id: stretchScale id: stretchScale
@@ -6,7 +6,7 @@ import Quickshell
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Sidebar.Chat
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -223,12 +223,13 @@ MouseArea {
height: width height: width
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
CustomClippingRect { CustomRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall anchors.margins: Tokens.spacing.extraSmall
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
clip: true
FadeImage { FadeImage {
id: attImage id: attImage
@@ -6,7 +6,7 @@ import ZShell.Config
import ZShell.Components import ZShell.Components
import ZShell.Llm import ZShell.Llm
import ZShell.Blobs import ZShell.Blobs
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Sidebar.Chat
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -8,7 +8,7 @@ import ZShell.Blobs
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Chat.Content import qs.Modules.Sidebar.Chat.Content
import qs.Services import qs.Services
Item { Item {
@@ -5,7 +5,7 @@ import ZShell.Llm
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Sidebar.Chat
import qs.Services import qs.Services
Item { Item {
@@ -63,7 +63,7 @@ Item {
} }
} }
ClippingRectangle { Item {
id: clipRect id: clipRect
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -71,8 +71,6 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: title.bottom anchors.top: title.bottom
anchors.topMargin: 10 anchors.topMargin: 10
color: "transparent"
radius: 6
Loader { Loader {
active: opacity > 0 active: opacity > 0
@@ -3,7 +3,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Utils.Cards import qs.Modules.Sidebar.Utils.Cards
Item { Item {
id: root id: root
+63
View File
@@ -5,6 +5,7 @@
#include <qsggeometry.h> #include <qsggeometry.h>
#include <qsgnode.h> #include <qsgnode.h>
#include <algorithm>
#include <cstring> #include <cstring>
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {}
@@ -129,6 +130,10 @@ QSGNode* BlobInvertedRect::updatePaintNode(
material->m_invertedInner, material->m_invertedInner,
m_cachedInvertedInner, m_cachedInvertedInner,
sizeof(m_cachedInvertedInner)); sizeof(m_cachedInvertedInner));
memcpy(
material->m_invertedOuterRadii,
m_cachedInvertedOuterRadii,
sizeof(m_cachedInvertedOuterRadii));
const int count = const int count =
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16))); static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
@@ -173,6 +178,64 @@ void BlobInvertedRect::setBorderBottom(qreal v) {
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
void BlobInvertedRect::setOuterRadius(qreal v) {
if (qFuzzyCompare(m_outerRadius, v)) return;
m_outerRadius = v;
emit outerRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect::setOuterTopLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerTopLeftRadius, v)) return;
m_outerTopLeftRadius = v;
emit outerTopLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect::setOuterTopRightRadius(qreal v) {
if (qFuzzyCompare(m_outerTopRightRadius, v)) return;
m_outerTopRightRadius = v;
emit outerTopRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect::setOuterBottomLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return;
m_outerBottomLeftRadius = v;
emit outerBottomLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect::setOuterBottomRightRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomRightRadius, v)) return;
m_outerBottomRightRadius = v;
emit outerBottomRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect::outerCornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
out[0] = std::min(
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
: base,
maxR);
out[1] = std::min(
m_outerBottomRightRadius >= 0
? static_cast<float>(m_outerBottomRightRadius)
: base,
maxR);
out[2] = std::min(
m_outerBottomLeftRadius >= 0
? static_cast<float>(m_outerBottomLeftRadius)
: base,
maxR);
out[3] = std::min(
m_outerTopLeftRadius >= 0 ? static_cast<float>(m_outerTopLeftRadius)
: base,
maxR);
}
void BlobInvertedRect::registerWithGroup() { void BlobInvertedRect::registerWithGroup() {
if (m_group) m_group->setInvertedRect(this); if (m_group) m_group->setInvertedRect(this);
} }
+53
View File
@@ -19,6 +19,21 @@ class BlobInvertedRect : public BlobShape {
Q_PROPERTY( Q_PROPERTY(
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged) borderBottomChanged)
Q_PROPERTY(
qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY
outerRadiusChanged)
Q_PROPERTY(
qreal outerTopLeftRadius READ outerTopLeftRadius WRITE
setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged)
Q_PROPERTY(
qreal outerTopRightRadius READ outerTopRightRadius WRITE
setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged)
Q_PROPERTY(
qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE
setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged)
Q_PROPERTY(
qreal outerBottomRightRadius READ outerBottomRightRadius WRITE
setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged)
public: public:
explicit BlobInvertedRect(QQuickItem* parent = nullptr); explicit BlobInvertedRect(QQuickItem* parent = nullptr);
@@ -40,15 +55,47 @@ class BlobInvertedRect : public BlobShape {
void setBorderBottom(qreal v); void setBorderBottom(qreal v);
[[nodiscard]] qreal outerRadius() const { return m_outerRadius; }
void setOuterRadius(qreal v);
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
void setOuterTopRightRadius(qreal v);
[[nodiscard]] qreal outerBottomLeftRadius() const {
return m_outerBottomLeftRadius;
}
void setOuterBottomLeftRadius(qreal v);
[[nodiscard]] qreal outerBottomRightRadius() const {
return m_outerBottomRightRadius;
}
void setOuterBottomRightRadius(qreal v);
signals: signals:
void borderLeftChanged(); void borderLeftChanged();
void borderRightChanged(); void borderRightChanged();
void borderTopChanged(); void borderTopChanged();
void borderBottomChanged(); void borderBottomChanged();
void outerRadiusChanged();
void outerTopLeftRadiusChanged();
void outerTopRightRadiusChanged();
void outerBottomLeftRadiusChanged();
void outerBottomRightRadiusChanged();
protected: protected:
[[nodiscard]] bool isInvertedRect() const override { return true; } [[nodiscard]] bool isInvertedRect() const override { return true; }
void outerCornerRadii(float out[4]) const override;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
void registerWithGroup() override; void registerWithGroup() override;
@@ -59,4 +106,10 @@ class BlobInvertedRect : public BlobShape {
qreal m_borderRight = 0; qreal m_borderRight = 0;
qreal m_borderTop = 0; qreal m_borderTop = 0;
qreal m_borderBottom = 0; qreal m_borderBottom = 0;
qreal m_outerRadius = 0;
qreal m_outerTopLeftRadius = -1;
qreal m_outerTopRightRadius = -1;
qreal m_outerBottomLeftRadius = -1;
qreal m_outerBottomRightRadius = -1;
}; };
+4 -1
View File
@@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData(
Q_UNUSED(oldMaterial); Q_UNUSED(oldMaterial);
auto* mat = static_cast<BlobMaterial*>(newMaterial); auto* mat = static_cast<BlobMaterial*>(newMaterial);
QByteArray* buf = state.uniformData(); QByteArray* buf = state.uniformData();
Q_ASSERT(buf->size() >= 1472); Q_ASSERT(buf->size() >= 1488);
if (state.isMatrixDirty()) { if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix(); const QMatrix4x4 m = state.combinedMatrix();
@@ -105,5 +105,8 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
memcpy(buf->data() + 1456, mat->m_shadowColor, 16); memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
// Outer edge corner radii, after shadowColor, at 1472.
memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16);
return true; return true;
} }
+3
View File
@@ -43,6 +43,9 @@ class BlobMaterial : public QSGMaterial {
// puts these after rectData. // puts these after rectData.
float m_shadowRadius = 0.0f; float m_shadowRadius = 0.0f;
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
// Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which
// puts this after shadowColor.
float m_invertedOuterRadii[4] = {};
BlobRectData m_rects[16] = {}; BlobRectData m_rects[16] = {};
}; };
+9
View File
@@ -240,6 +240,8 @@ void BlobShape::updatePolish() {
m_cachedInvertedRadius = 0; m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
memset(
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
auto* inv = m_group->invertedRect(); auto* inv = m_group->invertedRect();
if (inv) { if (inv) {
@@ -290,6 +292,9 @@ void BlobShape::updatePolish() {
m_cachedInvertedInner[1] = innerCY; m_cachedInvertedInner[1] = innerCY;
m_cachedInvertedInner[2] = innerHW; m_cachedInvertedInner[2] = innerHW;
m_cachedInvertedInner[3] = innerHH; m_cachedInvertedInner[3] = innerHH;
static_cast<BlobShape*>(inv)->outerCornerRadii(
m_cachedInvertedOuterRadii);
} }
} }
@@ -417,6 +422,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
material->m_invertedInner, material->m_invertedInner,
m_cachedInvertedInner, m_cachedInvertedInner,
sizeof(m_cachedInvertedInner)); sizeof(m_cachedInvertedInner));
memcpy(
material->m_invertedOuterRadii,
m_cachedInvertedOuterRadii,
sizeof(m_cachedInvertedOuterRadii));
const int count = const int count =
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16))); static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
+11
View File
@@ -61,6 +61,16 @@ class BlobShape : public QQuickItem {
virtual void cornerRadii(float out[4]) const; virtual void cornerRadii(float out[4]) const;
// Outer-edge corner radii. Only the inverted rect draws an outer frame edge,
// so the base has no outer rounding and returns zeros. Subclasses that expose
// outer corner properties override this (order: tr, br, bl, tl).
virtual void outerCornerRadii(float out[4]) const {
out[0] = 0;
out[1] = 0;
out[2] = 0;
out[3] = 0;
}
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual void registerWithGroup(); virtual void registerWithGroup();
@@ -88,4 +98,5 @@ class BlobShape : public QQuickItem {
float m_cachedInvertedRadius = 0; float m_cachedInvertedRadius = 0;
float m_cachedInvertedOuter[4] = {}; float m_cachedInvertedOuter[4] = {};
float m_cachedInvertedInner[4] = {}; float m_cachedInvertedInner[4] = {};
float m_cachedInvertedOuterRadii[4] = {};
}; };
+16 -3
View File
@@ -21,6 +21,7 @@ layout(std140, binding = 0) uniform buf {
vec4 rectData[80]; vec4 rectData[80];
float shadowRadius; float shadowRadius;
vec4 shadowColor; vec4 shadowColor;
vec4 invertedOuterRadii;
}; };
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
@@ -55,7 +56,8 @@ float smax(float a, float b, float k) {
float smaxSharpA(float a, float b, float k) { float smaxSharpA(float a, float b, float k) {
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0). // smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
// Used for the frame outer edge so it always fills to the edges. // Used for the frame inner side, so the outer edge keeps its own shape and
// always fills to the edges.
float h = max(k - abs(a - b), 0.0) / k; float h = max(k - abs(a - b), 0.0) / k;
float blend = h * h * h * k * (1.0 / 6.0); float blend = h * h * h * k * (1.0 / 6.0);
blend *= smoothstep(0.0, k * 0.5, -a); blend *= smoothstep(0.0, k * 0.5, -a);
@@ -182,7 +184,12 @@ void main() {
} }
if (hasInverted != 0) { if (hasInverted != 0) {
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; // Outer edge rounds with its own per-corner radii. With all-zero radii
// sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is
// unchanged. smaxSharpA keeps this boundary authoritative for the fill.
float dOuter =
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
invertedOuterRadii) - 1.0;
float dInner = float dInner =
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
@@ -248,7 +255,13 @@ void main() {
dInner -= sinkValue; dInner -= sinkValue;
float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); // dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the
// outer arc is the active boundary; a smax term added there would pull the
// arc's sides inward (curvature/tangent mismatch), so the corner would not be
// a circle. Only blend on the inner side, where the inner boundary is active.
float negInner = -dInner;
float dFrame = dOuter >= negInner ? dOuter
: smaxSharpA(dOuter, negInner, smoothFactor);
mergedSdf = smin(mergedSdf, dFrame, smoothFactor); mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
if (dFrame < minDist) { if (dFrame < minDist) {
+11
View File
@@ -10,6 +10,8 @@ qml_module(ZShell-components
sparklinecontent.hpp sparklinecontent.cpp sparklinecontent.hpp sparklinecontent.cpp
sparklineitem.hpp sparklineitem.cpp sparklineitem.hpp sparklineitem.cpp
sparklinesceneitem.hpp sparklinesceneitem.cpp sparklinesceneitem.hpp sparklinesceneitem.cpp
roundedmask.hpp roundedmask.cpp
roundedclip.hpp roundedclip.cpp
wheelinverter.hpp wheelinverter.hpp
LIBRARIES LIBRARIES
Qt::Quick Qt::Quick
@@ -17,3 +19,12 @@ qml_module(ZShell-components
Qt::CanvasPainter Qt::CanvasPainter
ZShell-internal ZShell-internal
) )
qt_add_shaders(ZShell-components "components_shaders"
BATCHABLE OPTIMIZED NOHLSL NOMSL
GLSL "300es,330"
PREFIX "/"
FILES
shaders/roundedmask.frag
shaders/roundedmask.vert
)
+131
View File
@@ -0,0 +1,131 @@
#include "roundedclip.hpp"
#include <private/qquickclipnode_p.h>
#include <private/qquickitem_p.h>
#include <qsggeometry.h>
#include <algorithm>
#include <cmath>
namespace ZShell::components {
RoundedClip::RoundedClip(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
setClip(true);
}
bool RoundedClip::updateRadius(qreal& member, qreal radius) {
if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false;
member = radius;
update();
return true;
}
void RoundedClip::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (newGeometry.size() != oldGeometry.size()) update();
}
QSGNode* RoundedClip::updatePaintNode(
QSGNode* oldNode, UpdatePaintNodeData* data) {
Q_UNUSED(oldNode)
Q_UNUSED(data)
QQuickItemPrivate* priv = QQuickItemPrivate::get(this);
QQuickDefaultClipNode* clip = priv->clipNode();
if (!clip) return nullptr;
// Clip node geometry is in item-local coordinates. The clip node is a
// child of the item's transform node, so the clip rect is
// QRectF(0, 0, width, height), i.e. boundingRect()/clipRect().
const QRectF r(0, 0, width(), height());
// Per-corner radii fall back to the shared `radius` shorthand.
const qreal tl = m_topLeftRadius > 0 ? m_topLeftRadius : m_radius;
const qreal tr = m_topRightRadius > 0 ? m_topRightRadius : m_radius;
const qreal br = m_bottomRightRadius > 0 ? m_bottomRightRadius : m_radius;
const qreal bl = m_bottomLeftRadius > 0 ? m_bottomLeftRadius : m_radius;
// Nothing rounded: plain rectangular clip.
if (tl <= 0 && tr <= 0 && br <= 0 && bl <= 0) {
clip->setRect(r);
clip->update();
return nullptr;
}
// All corners equal: let QQuickDefaultClipNode build its own known-correct
// scanline geometry. This is the working path.
if (qFuzzyCompare(tl, tr) && qFuzzyCompare(tr, br) &&
qFuzzyCompare(br, bl)) {
clip->setRect(r);
clip->setRadius(tl);
clip->update();
return nullptr;
}
// Per-corner radii. setRadius() supports only a single value, so we
// must supply the geometry ourselves. Build it as a triangle fan from an
// interior point so the polygon interior fills for the stencil clip.
const qreal half = std::min(r.width() / 2, r.height() / 2);
const qreal rtl = std::min(tl, half);
const qreal rtr = std::min(tr, half);
const qreal rbr = std::min(br, half);
const qreal rbl = std::min(bl, half);
auto segments = [](qreal radius) {
return qMin(30, int(std::ceil(radius)));
};
const int s_tl = segments(rtl);
const int s_tr = segments(rtr);
const int s_br = segments(rbr);
const int s_bl = segments(rbl);
clip->setRect(r);
clip->setIsRectangular(false);
QSGGeometry* g = clip->geometry();
g->setDrawingMode(QSGGeometry::DrawTriangleFan);
// One apex + one point per arc step + one point per zero-radius corner,
// plus a closing apex to complete the fan.
g->allocate(1 + s_tl + s_tr + s_br + s_bl + 5);
QSGGeometry::Point2D* vertices = g->vertexDataAsPoint2D();
int vi = 0;
// Apex at an interior point.
vertices[vi++] =
QSGGeometry::Point2D(float(r.center().x()), float(r.center().y()));
// Each corner contributes its arc points (i = 0..segments) from its
// edge-entry tangent to its edge-exit tangent. Consecutive corners are
// joined by the implicit straight edges, forming the rounded rectangle.
auto addArc =
[&](int segs, qreal cx, qreal cy, qreal rad, qreal a0, qreal a1) {
for (int i = 0; i <= segs; ++i) {
const qreal angle = a0 + (a1 - a0) * qreal(i) / qreal(segs);
vertices[vi++] = QSGGeometry::Point2D(
float(cx + rad * std::cos(angle)),
float(cy + rad * std::sin(angle)));
}
};
addArc(s_tl, rtl, rtl, rtl, M_PI, 3.0 * M_PI / 2.0);
addArc(s_tr, r.width() - rtr, rtr, rtr, -M_PI / 2.0, 0.0);
addArc(s_br, r.width() - rbr, r.height() - rbr, rbr, 0.0, M_PI / 2.0);
addArc(s_bl, rbl, r.height() - rbl, rbl, M_PI / 2.0, M_PI);
// Close the fan back to the first perimeter vertex.
vertices[vi] = vertices[1];
clip->markDirty(QSGNode::DirtyGeometry);
return nullptr;
}
} // namespace ZShell::components
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qsgnode.h>
namespace ZShell::components {
class RoundedClip : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged)
Q_PROPERTY(
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
topRightRadiusChanged)
Q_PROPERTY(
qreal bottomRightRadius READ bottomRightRadius WRITE
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
Q_PROPERTY(
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
NOTIFY bottomLeftRadiusChanged)
public:
explicit RoundedClip(QQuickItem* parent = nullptr);
qreal radius() const { return m_radius; }
void setRadius(qreal radius) {
if (updateRadius(m_radius, radius)) emit radiusChanged();
}
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
void setTopLeftRadius(qreal radius) {
if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged();
}
[[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; }
void setTopRightRadius(qreal radius) {
if (updateRadius(m_topRightRadius, radius))
emit topRightRadiusChanged();
}
[[nodiscard]] qreal bottomRightRadius() const {
return m_bottomRightRadius;
}
void setBottomRightRadius(qreal radius) {
if (updateRadius(m_bottomRightRadius, radius))
emit bottomRightRadiusChanged();
}
[[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
void setBottomLeftRadius(qreal radius) {
if (updateRadius(m_bottomLeftRadius, radius))
emit bottomLeftRadiusChanged();
}
signals:
void radiusChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomRightRadiusChanged();
void bottomLeftRadiusChanged();
protected:
QSGNode* updatePaintNode(
QSGNode* oldNode, UpdatePaintNodeData* data) override;
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
private:
bool updateRadius(qreal& member, qreal radius);
qreal m_radius = 0;
qreal m_topLeftRadius = 0;
qreal m_topRightRadius = 0;
qreal m_bottomRightRadius = 0;
qreal m_bottomLeftRadius = 0;
};
} // namespace ZShell::components
+268
View File
@@ -0,0 +1,268 @@
#include "roundedmask.hpp"
#include <qimage.h>
#include <qquickwindow.h>
#include <qsggeometry.h>
#include <algorithm>
#include <cstring>
namespace ZShell::components {
using Qt::StringLiterals::operator""_s;
RoundedMask::RoundedMask(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
}
QQuickItem* RoundedMask::source() const {
return m_source;
}
void RoundedMask::setSource(QQuickItem* source) {
if (m_source == source) return;
m_source = source;
emit sourceChanged();
update();
}
qreal RoundedMask::topLeftRadius() const {
return m_topLeftRadius;
}
void RoundedMask::setTopLeftRadius(qreal radius) {
if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged();
}
qreal RoundedMask::topRightRadius() const {
return m_topRightRadius;
}
void RoundedMask::setTopRightRadius(qreal radius) {
if (updateRadius(m_topRightRadius, radius)) emit topRightRadiusChanged();
}
qreal RoundedMask::bottomRightRadius() const {
return m_bottomRightRadius;
}
void RoundedMask::setBottomRightRadius(qreal radius) {
if (updateRadius(m_bottomRightRadius, radius))
emit bottomRightRadiusChanged();
}
qreal RoundedMask::bottomLeftRadius() const {
return m_bottomLeftRadius;
}
void RoundedMask::setBottomLeftRadius(qreal radius) {
if (updateRadius(m_bottomLeftRadius, radius))
emit bottomLeftRadiusChanged();
}
bool RoundedMask::updateRadius(qreal& member, qreal radius) {
if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false;
member = radius;
update();
return true;
}
QColor RoundedMask::color() const {
return m_color;
}
void RoundedMask::setColor(const QColor& color) {
if (m_color == color) return;
m_color = color;
emit colorChanged();
update();
}
void RoundedMask::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (newGeometry.size() != oldGeometry.size()) update();
}
void RoundedMask::releaseResources() {
m_node = nullptr;
}
void RoundedMask::invalidateSceneGraph() {
m_node = nullptr;
}
void RoundedMask::watchProvider(QSGTextureProvider* provider) {
if (m_provider == provider) return;
if (m_provider) QObject::disconnect(m_provider, nullptr, this, nullptr);
m_provider = provider;
if (!m_provider) return;
QObject::connect(
m_provider,
&QSGTextureProvider::textureChanged,
this,
&QQuickItem::update);
// Emitted on the render thread by the provider's cleanup job; drop the texture before it is rendered again
QObject::connect(
m_provider,
&QObject::destroyed,
this,
[this] {
m_provider = nullptr;
if (m_node) {
static_cast<RoundedMaskMaterial*>(m_node->material())
->m_texture = nullptr;
m_node->markDirty(QSGNode::DirtyMaterial);
}
update();
},
Qt::DirectConnection);
}
QSGNode* RoundedMask::updatePaintNode(
QSGNode* oldNode, UpdatePaintNodeData* data) {
Q_UNUSED(data);
auto* const provider = m_source && m_source->isTextureProvider()
? m_source->textureProvider()
: nullptr;
watchProvider(provider);
auto* const texture = provider ? provider->texture() : nullptr;
if (!texture || width() <= 0 || height() <= 0) {
delete oldNode;
m_node = nullptr;
return nullptr;
}
auto* node = static_cast<RoundedMaskNode*>(oldNode);
if (!node) node = new RoundedMaskNode;
m_node = node;
QSGGeometry::updateTexturedRectGeometry(
node->geometry(), boundingRect(), QRectF(0, 0, 1, 1));
node->markDirty(QSGNode::DirtyGeometry);
const auto dpr = static_cast<float>(window()->effectiveDevicePixelRatio());
auto* const material = static_cast<RoundedMaskMaterial*>(node->material());
if (!material->m_fallback) {
QImage blank(1, 1, QImage::Format_RGBA8888_Premultiplied);
blank.fill(Qt::transparent);
material->m_fallback.reset(window()->createTextureFromImage(blank));
}
material->m_texture = texture;
material->m_size[0] = static_cast<float>(width()) * dpr;
material->m_size[1] = static_cast<float>(height()) * dpr;
const auto maxRadius = static_cast<float>(std::min(width(), height()) / 2);
const auto clampRadius = [&](qreal radius) {
return std::clamp(static_cast<float>(radius), 0.0f, maxRadius) * dpr;
};
material->m_radii[0] = clampRadius(m_topLeftRadius);
material->m_radii[1] = clampRadius(m_topRightRadius);
material->m_radii[2] = clampRadius(m_bottomRightRadius);
material->m_radii[3] = clampRadius(m_bottomLeftRadius);
const auto alpha = m_color.alphaF();
material->m_color[0] = m_color.redF() * alpha;
material->m_color[1] = m_color.greenF() * alpha;
material->m_color[2] = m_color.blueF() * alpha;
material->m_color[3] = alpha;
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
QSGMaterialType* RoundedMaskMaterial::type() const {
static QSGMaterialType s_type;
return &s_type;
}
QSGMaterialShader* RoundedMaskMaterial::createShader(
QSGRendererInterface::RenderMode mode) const {
Q_UNUSED(mode);
return new RoundedMaskMaterialShader;
}
int RoundedMaskMaterial::compare(const QSGMaterial* other) const {
const auto* const o = static_cast<const RoundedMaskMaterial*>(other);
if (m_texture != o->m_texture) return m_texture < o->m_texture ? -1 : 1;
const auto fuzzyEqual = [](float a, float b) {
return qFuzzyCompare(a + 1.0f, b + 1.0f);
};
if (!std::ranges::equal(m_size, o->m_size, fuzzyEqual) ||
!std::ranges::equal(m_radii, o->m_radii, fuzzyEqual) ||
!std::ranges::equal(m_color, o->m_color, fuzzyEqual))
return this < other ? -1 : 1;
return 0;
}
RoundedMaskMaterialShader::RoundedMaskMaterialShader() {
setShaderFileName(VertexStage, u":/shaders/roundedmask.vert.qsb"_s);
setShaderFileName(FragmentStage, u":/shaders/roundedmask.frag.qsb"_s);
}
bool RoundedMaskMaterialShader::updateUniformData(
RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
Q_UNUSED(oldMaterial);
auto* const mat = static_cast<RoundedMaskMaterial*>(newMaterial);
QByteArray* const buf = state.uniformData();
Q_ASSERT(buf->size() >= 112);
if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix();
std::memcpy(buf->data(), m.constData(), 64);
}
if (state.isOpacityDirty()) {
const float opacity = state.opacity();
std::memcpy(buf->data() + 64, &opacity, 4);
}
std::memcpy(buf->data() + 72, mat->m_size, 8);
std::memcpy(buf->data() + 80, mat->m_radii, 16);
std::memcpy(buf->data() + 96, mat->m_color, 16);
return true;
}
void RoundedMaskMaterialShader::updateSampledImage(
RenderState& state,
int binding,
QSGTexture** texture,
QSGMaterial* newMaterial,
QSGMaterial* oldMaterial) {
Q_UNUSED(oldMaterial);
if (binding != 1) return;
const auto* const mat =
static_cast<const RoundedMaskMaterial*>(newMaterial);
auto* const tex = mat->m_texture ? mat->m_texture : mat->m_fallback.data();
tex->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
*texture = tex;
}
RoundedMaskNode::RoundedMaskNode() {
auto* const geometry =
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
setGeometry(geometry);
setFlag(OwnsGeometry);
auto* const material = new RoundedMaskMaterial;
material->setFlag(QSGMaterial::Blending);
setMaterial(material);
setFlag(OwnsMaterial);
setFlag(UsePreprocess);
}
void RoundedMaskNode::preprocess() {
auto* const texture = qobject_cast<QSGDynamicTexture*>(
static_cast<RoundedMaskMaterial*>(material())->m_texture);
if (texture && texture->updateTexture()) markDirty(DirtyMaterial);
}
} // namespace ZShell::components
+128
View File
@@ -0,0 +1,128 @@
#pragma once
#include <qcolor.h>
#include <qpointer.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qscopedpointer.h>
#include <qsgmaterial.h>
#include <qsgmaterialshader.h>
#include <qsgnode.h>
#include <qsgtexture.h>
#include <qsgtextureprovider.h>
namespace ZShell::components {
class RoundedMaskNode;
class RoundedMask : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QQuickItem* source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged)
Q_PROPERTY(
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
topRightRadiusChanged)
Q_PROPERTY(
qreal bottomRightRadius READ bottomRightRadius WRITE
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
Q_PROPERTY(
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
NOTIFY bottomLeftRadiusChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
explicit RoundedMask(QQuickItem* parent = nullptr);
[[nodiscard]] QQuickItem* source() const;
void setSource(QQuickItem* source);
[[nodiscard]] qreal topLeftRadius() const;
void setTopLeftRadius(qreal radius);
[[nodiscard]] qreal topRightRadius() const;
void setTopRightRadius(qreal radius);
[[nodiscard]] qreal bottomRightRadius() const;
void setBottomRightRadius(qreal radius);
[[nodiscard]] qreal bottomLeftRadius() const;
void setBottomLeftRadius(qreal radius);
[[nodiscard]] QColor color() const;
void setColor(const QColor& color);
signals:
void sourceChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomRightRadiusChanged();
void bottomLeftRadiusChanged();
void colorChanged();
protected:
QSGNode* updatePaintNode(
QSGNode* oldNode, UpdatePaintNodeData* data) override;
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
void releaseResources() override;
private slots:
void invalidateSceneGraph();
private:
void watchProvider(QSGTextureProvider* provider);
bool updateRadius(qreal& member, qreal radius);
QPointer<QQuickItem> m_source;
qreal m_topLeftRadius = 0;
qreal m_topRightRadius = 0;
qreal m_bottomRightRadius = 0;
qreal m_bottomLeftRadius = 0;
QColor m_color = Qt::transparent;
QSGTextureProvider* m_provider = nullptr;
RoundedMaskNode* m_node = nullptr;
};
class RoundedMaskMaterial : public QSGMaterial {
public:
[[nodiscard]] QSGMaterialType* type() const override;
[[nodiscard]] QSGMaterialShader* createShader(
QSGRendererInterface::RenderMode mode) const override;
int compare(const QSGMaterial* other) const override;
QSGTexture* m_texture = nullptr;
QScopedPointer<QSGTexture> m_fallback;
float m_size[2] = {};
float m_radii[4] = {};
float m_color[4] = {};
};
class RoundedMaskMaterialShader : public QSGMaterialShader {
public:
RoundedMaskMaterialShader();
bool updateUniformData(
RenderState& state,
QSGMaterial* newMaterial,
QSGMaterial* oldMaterial) override;
void updateSampledImage(
RenderState& state,
int binding,
QSGTexture** texture,
QSGMaterial* newMaterial,
QSGMaterial* oldMaterial) override;
};
class RoundedMaskNode : public QSGGeometryNode {
public:
RoundedMaskNode();
void preprocess() override;
};
} // namespace ZShell::components
@@ -0,0 +1,26 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec2 size;
vec4 radii;
vec4 color;
};
layout(binding = 1) uniform sampler2D source;
void main() {
vec2 p = qt_TexCoord0 * size - size * 0.5;
float radius = p.x < 0.0 ? (p.y < 0.0 ? radii.x : radii.w) : (p.y < 0.0 ? radii.y : radii.z);
vec2 q = abs(p) - (size * 0.5 - radius);
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
float mask = 1.0 - smoothstep(-0.5, 0.5, d);
vec4 content = texture(source, qt_TexCoord0);
vec4 blended = content + color * (1.0 - content.a);
fragColor = blended * mask * qt_Opacity;
}
@@ -0,0 +1,19 @@
#version 440
layout(location = 0) in vec4 qt_VertexPosition;
layout(location = 1) in vec2 qt_VertexTexCoord;
layout(location = 0) out vec2 qt_TexCoord0;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec2 size;
vec4 radii;
vec4 color;
};
void main() {
gl_Position = qt_Matrix * qt_VertexPosition;
qt_TexCoord0 = qt_VertexTexCoord;
}
+1
View File
@@ -21,6 +21,7 @@ class Popouts : public settings::ObjectNode {
class Tray : public settings::ObjectNode { class Tray : public settings::ObjectNode {
CONFIG_NODE(Tray, settings::ObjectNode) CONFIG_NODE(Tray, settings::ObjectNode)
CONFIG_PROPERTY(bool, replaceIcons, false)
CONFIG_PROPERTY(bool, recolorIcons, false) CONFIG_PROPERTY(bool, recolorIcons, false)
CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, trayIconSize, 24) CONFIG_PROPERTY(int, trayIconSize, 24)
+13 -12
View File
@@ -129,15 +129,15 @@ void NightlightManager::setFadeDuration(double duration) {
emit fadeDurationChanged(); emit fadeDurationChanged();
} }
void NightlightManager::toggle() { void NightlightManager::toggle(bool fast) {
if (m_enabled) { if (m_enabled) {
end(); end(fast ? 2 : m_fadeDuration);
} else { } else {
start(); start(fast ? 2 : m_fadeDuration);
} }
} }
void NightlightManager::start() { void NightlightManager::start(double fade) {
if (m_enabled && m_initialized) return; if (m_enabled && m_initialized) return;
m_initialized = true; m_initialized = true;
@@ -149,9 +149,7 @@ void NightlightManager::start() {
m_process.setProgram("hyprctl"); m_process.setProgram("hyprctl");
m_process.setArguments( m_process.setArguments(
{"dispatch", {"dispatch",
QString("hl.plugin.zshell.nlSet(%1, %2)") QString("hl.plugin.zshell.nlSet(%1, %2)").arg(m_temp).arg(fade)});
.arg(m_temp)
.arg(m_fadeDuration)});
m_process.startDetached(); m_process.startDetached();
} else { } else {
m_process.setProgram("hyprctl"); m_process.setProgram("hyprctl");
@@ -161,7 +159,7 @@ void NightlightManager::start() {
} }
} }
void NightlightManager::end() { void NightlightManager::end(double fade) {
if (!m_enabled && m_initialized) return; if (!m_enabled && m_initialized) return;
m_initialized = true; m_initialized = true;
@@ -172,8 +170,7 @@ void NightlightManager::end() {
if (m_useNativeNightlight) { if (m_useNativeNightlight) {
m_process.setProgram("hyprctl"); m_process.setProgram("hyprctl");
m_process.setArguments( m_process.setArguments(
{"dispatch", {"dispatch", QString("hl.plugin.zshell.nlDisable(%1)").arg(fade)});
QString("hl.plugin.zshell.nlDisable(%1)").arg(m_fadeDuration)});
m_process.startDetached(); m_process.startDetached();
} else { } else {
m_process.setProgram("hyprctl"); m_process.setProgram("hyprctl");
@@ -195,10 +192,14 @@ void NightlightManager::apply() {
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime); isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
} }
const auto activationTime = isDarkTime ? m_startTime : m_endTime;
const auto elapsed = (currentMin - activationTime + 1440) % 1440;
const bool pastActivation = elapsed > 20;
if (isDarkTime) { if (isDarkTime) {
start(); start(pastActivation ? 2 : m_fadeDuration);
} else { } else {
end(); end(pastActivation ? 2 : m_fadeDuration);
} }
} }
@@ -54,7 +54,7 @@ class NightlightManager : public QObject {
[[nodiscard]] int lastTemp() const; [[nodiscard]] int lastTemp() const;
[[nodiscard]] int currentTemp() const; [[nodiscard]] int currentTemp() const;
Q_INVOKABLE void toggle(); Q_INVOKABLE void toggle(bool fast);
Q_INVOKABLE void apply(); Q_INVOKABLE void apply();
void setStartTime(const int& time); void setStartTime(const int& time);
@@ -105,8 +105,8 @@ class NightlightManager : public QObject {
QByteArray m_socketBuffer; QByteArray m_socketBuffer;
QTimer m_reconnectTimer; QTimer m_reconnectTimer;
void start(); void start(double fade);
void end(); void end(double fade);
[[nodiscard]] QString hyprlandDir() const; [[nodiscard]] QString hyprlandDir() const;
[[nodiscard]] QString eventSocketPath() const; [[nodiscard]] QString eventSocketPath() const;
[[nodiscard]] QString resolvePluginPath() const; [[nodiscard]] QString resolvePluginPath() const;
+25 -53
View File
@@ -1,56 +1,28 @@
FROM archlinux:latest FROM archlinux:latest
RUN pacman -Syyu --noconfirm RUN pacman -Syu --noconfirm \
&& pacman -S --needed --noconfirm \
base-devel cmake ninja clang lld nodejs tree-sitter \
jkqtplotter cmark-gfm git \
qt6-base qt6-declarative qt6-shadertools qt6-5compat \
qt6-canvaspainter hyprwayland-scanner \
libqalculate pipewire aubio cava lm_sensors glib2 \
&& pacman -Scc --noconfirm
# Build toolchain RUN pacman -Syu --noconfirm \
RUN pacman -S --noconfirm \ && pacman -S --needed --noconfirm \
base-devel \ sudo meson autoconf-archive python-tqdm \
cmake \ && useradd -m builder \
ninja \ && printf '%s\n' 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder \
clang \ && chmod 0440 /etc/sudoers.d/builder \
lld \ && for i in 1 2 3 4 5; do \
nodejs \ rm -rf /tmp/libcava; \
tree-sitter \ if git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava; then break; fi; \
jkqtplotter \ sleep 5; \
cmark-gfm \ done \
git && test -d /tmp/libcava/.git \
&& chown -R builder:builder /tmp/libcava \
# Qt6 && su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" \
RUN pacman -S --noconfirm \ && rm -rf /tmp/libcava /etc/sudoers.d/builder \
qt6-base \ && userdel -r builder \
qt6-declarative \ && pacman -Scc --noconfirm
qt6-shadertools \
qt6-5compat \
qt6-canvaspainter
# Project-specific dependencies
RUN pacman -S --noconfirm \
libqalculate \
pipewire \
aubio \
cava \
lm_sensors \
glib2
# AUR: libcava (provides development headers for cava)
RUN pacman -Syu --noconfirm && \
pacman -S --needed --noconfirm \
sudo \
meson \
autoconf-archive \
python-tqdm && \
useradd -m builder && \
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN for i in 1 2 3 4 5; do \
rm -rf /tmp/libcava && \
git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava && \
break || sleep 5; \
done && \
test -d /tmp/libcava/.git && \
chown -R builder /tmp/libcava
RUN su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" && \
rm -rf /tmp/libcava && \
userdel -r builder && \
sed -i '/^builder/d' /etc/sudoers