Compare commits
31 Commits
ab5587fd90
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
352eb99568
|
|||
|
f2268f65e7
|
|||
|
6357b949b3
|
|||
|
77c56f9945
|
|||
|
570eea59ae
|
|||
|
c9310e802b
|
|||
|
4453c280bf
|
|||
|
51906373aa
|
|||
|
8f8239727e
|
|||
|
5299945cfb
|
|||
|
abd48a9c66
|
|||
|
28aa79dd13
|
|||
|
dfab12482a
|
|||
|
33c19e35a5
|
|||
|
198ce4ff67
|
|||
|
e1906e5528
|
|||
|
5d84d68e26
|
|||
|
d53f375fa7
|
|||
|
adfd353e85
|
|||
|
ca7ac0696b
|
|||
|
de3c9274b5
|
|||
|
e95f62579b
|
|||
|
ab15dcf0ac
|
|||
|
3b01362a5a
|
|||
|
1ded80d1c0
|
|||
|
2b93af1a08
|
|||
|
3779974cb2
|
|||
|
2f122a9487
|
|||
|
494420d241
|
|||
|
ae6628f549
|
|||
|
a1533e73c7
|
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
result
|
||||
result-bin
|
||||
plugin/build/
|
||||
plugin/dist/
|
||||
71
AGENTS.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI agents working in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**tshell** is a personal Wayland desktop shell (top bar + notification OSD) built with [Quickshell](https://quickshell.outfoxxed.me) and QML (Qt 6). It targets wlroots-based compositors via wlr-layer-shell.
|
||||
|
||||
## Commands
|
||||
|
||||
- Enter dev environment: `nix develop`
|
||||
- Run the shell: `quickshell` (from repo root; entry point is `shell.qml`)
|
||||
- Format Nix files: `nix fmt` (treefmt + nixfmt)
|
||||
|
||||
There are no tests or linters beyond the formatter.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Quickshell** (git, from flake input) patched `withModules` for qtbase/qtdeclarative/qtmultimedia/qttranslations
|
||||
- **QML / QtQuick 6**
|
||||
- **cava** — external audio visualizer process (config in `assets/cava.conf`)
|
||||
- **wallust** — available in dev shell for palette generation
|
||||
|
||||
## Architecture
|
||||
|
||||
### Layered structure
|
||||
|
||||
```
|
||||
shell.qml Entry point: Scope { Bar {}; Notification {} }
|
||||
├── windows/ Top-level surfaces (PanelWindow roots)
|
||||
│ ├── Bar.qml Screen-edge bars + exclusive zones + corners
|
||||
│ └── Notification.qml Popup notification stack window
|
||||
├── modules/ Feature widgets, grouped by surface
|
||||
│ ├── bar/ Widgets placed in the bar (Clock, Workspaces,
|
||||
│ │ BatteryIndicator, AudioWidget, CavaVisualizer,
|
||||
│ │ SystemTray, LauncherButton, PowerProfileWidget,
|
||||
│ │ GhostButton)
|
||||
│ └── osd/ NotificationCard.qml
|
||||
├── services/ QML Singletons exposing system state
|
||||
│ ├── Time.qml SystemClock → formatted time string
|
||||
│ ├── Battery.qml UPower display device → percentage/icon
|
||||
│ ├── Audio.qml PipeWire default sink/source → device names/icons
|
||||
│ ├── PowerProfile.qml UPower power profiles → name/icon
|
||||
│ ├── Cava.qml Spawns `cava`, parses stdout into `values[]`
|
||||
│ └── Notifications.qml NotificationServer: popups, DND flag, history
|
||||
├── config/ QML Singletons for configuration/theming
|
||||
│ ├── Theme.qml base16 palettes (Theme.colors.base00-0F + semantic aliases), font, margin/radius/spacing/padding/duration, per-widget sizes
|
||||
│ └── BarLayout.qml Which widgets go in left/center/right bar sections
|
||||
├── ui/ Shared reusable primitives
|
||||
│ ├── BarButton.qml Themed button used by bar widgets
|
||||
│ ├── RoundedCorner.qml Decorative corner piece
|
||||
│ └── ExclusionZone.qml Zero-size PanelWindow reserving screen space
|
||||
└── assets/ icons/, cava.conf, gallery screenshots
|
||||
```
|
||||
|
||||
### Key patterns
|
||||
|
||||
- **Singletons for state and config**: everything in `services/` and `config/` is `pragma Singleton` + Quickshell `Singleton`. They are imported by directory alias (`import qs.services`, `import qs.config`, `import qs.ui`, `import qs.windows`) — never instantiate them directly.
|
||||
- **One-way data flow**: `services` (reactive properties) → `modules` (presentational widgets) ← `config` (theme/layout). Windows compose modules.
|
||||
- **Config-driven bar layout**: `config/BarLayout.qml` lists widget names per section; it resolves names to `.qml` file URLs under `modules/bar/`. `windows/Bar.qml` loads them dynamically through a `Repeater` of `Loader`s inside a `FlexboxLayout` (left / center / right sections). To add a bar widget: create `modules/bar/<Name>.qml`, then add `widget("<Name>")` to the desired section list.
|
||||
- **Bar rendering model**: `Bar.qml` is one fullscreen transparent `PanelWindow` anchored to all edges. It draws thin colored rectangles on each edge (left/right/bottom = 10px, top = 50px), reserves compositor space via four `ExclusionZone` layer-shell windows, and punches out the screen center using a subtractive input `mask` so only the bars/corners are interactive. Rounded corners come from `ui/RoundedCorner.qml`.
|
||||
- **External process service**: `services/Cava.qml` runs `cava -p assets/cava.conf` via Quickshell `Process` + `SplitParser` and republishes each line as a numeric array on `Cava.values`.
|
||||
- **Notifications**: `services/Notifications.qml` implements the freedesktop `NotificationServer`; popups are tracked notifications, rendered newest-first by `windows/Notification.qml` using `modules/osd/NotificationCard.qml`. Critical notifications don't auto-expire; hover pauses the countdown timer.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Start every QML file with `pragma ComponentBehavior: Bound` where bindings reference outer ids.
|
||||
- Indentation is inconsistent across the repo (2-space in newer UI/window files, 4-space in some services); match the surrounding file when editing.
|
||||
- Prefer `readonly property` for derived values; keep widgets presentational and pull all theme values from `Theme.*` rather than hardcoding colors/sizes/fonts.
|
||||
- Access asset paths via `Quickshell.shellPath(...)` (see `Battery.qml`, `NotificationCard.qml`) so paths work regardless of install location.
|
||||
- Commit messages follow Conventional Commits style (`feat(notifications): ...`, `refactor(bar): ...`).
|
||||
306
Bar.qml
@@ -1,306 +0,0 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.config
|
||||
|
||||
Scope {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
}
|
||||
|
||||
implicitHeight: 50
|
||||
implicitWidth: 1150
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 10
|
||||
radius: Appearance.radius
|
||||
color: Appearance.colors.background
|
||||
|
||||
FlexboxLayout {
|
||||
id: flexLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.margin
|
||||
|
||||
wrap: FlexboxLayout.Wrap
|
||||
direction: FlexboxLayout.Row
|
||||
justifyContent: FlexboxLayout.JustifySpaceBetween
|
||||
|
||||
Rectangle {
|
||||
color: 'transparent'
|
||||
implicitHeight: parent.height
|
||||
Layout.fillWidth: true
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Appearance.spacing
|
||||
|
||||
Button {
|
||||
padding: Appearance.padding
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
icon.color: Appearance.colors.foreground
|
||||
icon.source: Quickshell.shellPath("assets") + "/icons/nix.svg"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["vicinae", "toggle"]);
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
property real percentage: Math.round(UPower.displayDevice.percentage * 100)
|
||||
property real energyRate: UPower.displayDevice.changeRate
|
||||
property string iconBase: Quickshell.shellPath("assets") + "/icons/"
|
||||
property string iconName: parent.percentage < 30 ? "battery-low.svg" : parent.percentage < 70 ? "battery-medium.svg" : "battery-full.svg"
|
||||
|
||||
padding: Appearance.padding
|
||||
|
||||
text: `${percentage}% ${energyRate}W`
|
||||
palette.buttonText: Appearance.colors.foreground
|
||||
font.family: Appearance.font.family
|
||||
font.pointSize: Appearance.font.pointSize
|
||||
|
||||
icon.color: Appearance.colors.foreground
|
||||
icon.source: iconBase + iconName
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: Appearance.colors.inActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: 'transparent'
|
||||
implicitHeight: parent.height
|
||||
Layout.fillWidth: true
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Appearance.spacing
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Rectangle {
|
||||
property var ws: Hyprland.workspaces.values.find(w => w.id === index + 1)
|
||||
property bool isActive: Hyprland.focusedWorkspace?.id === (index + 1)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 1000
|
||||
implicitHeight: 15
|
||||
|
||||
implicitWidth: isActive ? this.implicitHeight * 2.3 : this.implicitHeight
|
||||
|
||||
color: {
|
||||
if (handler.hovered) {
|
||||
return Appearance.colors.accent;
|
||||
} else if (isActive || ws) {
|
||||
return Appearance.colors.accent;
|
||||
} else {
|
||||
return Appearance.colors.inActive;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Hyprland.dispatch("workspace " + (index + 1))
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.duration
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
id: handler
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.duration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
color: 'transparent'
|
||||
implicitHeight: parent.height
|
||||
Layout.fillWidth: true
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing
|
||||
|
||||
Button {
|
||||
padding: Appearance.padding
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
icon.color: Appearance.colors.foreground
|
||||
icon.source: Quickshell.shellPath("assets") + "/icons/wifi.svg"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["vicinae", "vicinae://extensions/dagimg-dot/wifi-commander/scan-wifi"]);
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
padding: Appearance.padding
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
icon.color: Appearance.colors.foreground
|
||||
icon.source: Quickshell.shellPath("assets") + "/icons/bluetooth.svg"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["vicinae", "vicinae://extensions/Gelei/bluetooth/devices"]);
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: sysTrayRow.implicitWidth + 10
|
||||
implicitHeight: parent.height
|
||||
radius: Appearance.radius
|
||||
color: Appearance.colors.inActive
|
||||
|
||||
Row {
|
||||
id: sysTrayRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items.values.length
|
||||
Image {
|
||||
source: SystemTray.items.values[index].icon
|
||||
height: parent.parent.height - 15
|
||||
width: parent.parent.height - 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
padding: Appearance.padding
|
||||
text: Time.time
|
||||
|
||||
palette.buttonText: Appearance.colors.foreground
|
||||
font.family: Appearance.font.family
|
||||
font.pointSize: Appearance.font.pointSize
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: Appearance.colors.inActive
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
padding: Appearance.padding
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
icon.color: Appearance.colors.foreground
|
||||
icon.source: Quickshell.shellPath("assets") + "/icons/ghost.svg"
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
assets/cava.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
[general]
|
||||
bars = 14
|
||||
framerate = 60
|
||||
sensitivity=140
|
||||
|
||||
[input]
|
||||
method = pulse
|
||||
|
||||
[output]
|
||||
method = raw
|
||||
raw_target = /dev/stdout
|
||||
data_format = ascii
|
||||
ascii_max_range = 100
|
||||
bar_delimiter = 59
|
||||
frame_delimiter = 10
|
||||
1
assets/icons/home.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M12.0838 3.5C10.1049 3.5 8.40696 4.71031 7.69312 6.43399C7.53464 6.81668 7.09592 6.99844 6.71323 6.83995C6.33053 6.68146 6.14878 6.24275 6.30727 5.86005C7.24515 3.5954 9.47729 2 12.0838 2C14.6904 2 16.9225 3.5954 17.8604 5.86005C18.0189 6.24275 17.8371 6.68146 17.4544 6.83995C17.0717 6.99844 16.633 6.81668 16.4745 6.43399C15.7607 4.71032 14.0627 3.5 12.0838 3.5Z" fill="#000"></path> <path d="M12.0846 6C11.0622 6 10.1973 6.68244 9.92427 7.6182C9.80824 8.01583 9.39183 8.24411 8.9942 8.12808C8.59657 8.01205 8.36829 7.59564 8.48432 7.19801C8.93906 5.63969 10.3777 4.5 12.0846 4.5C13.7914 4.5 15.2301 5.63969 15.6848 7.19801C15.8008 7.59564 15.5725 8.01205 15.1749 8.12808C14.7773 8.24411 14.3609 8.01583 14.2448 7.6182C13.9718 6.68244 13.1069 6 12.0846 6Z" fill="#000"></path> <path d="M13.084 7.75C13.084 8.30228 12.6363 8.75 12.084 8.75C11.5317 8.75 11.084 8.30228 11.084 7.75C11.084 7.19772 11.5317 6.75 12.084 6.75C12.6363 6.75 13.084 7.19772 13.084 7.75Z" fill="#000"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.65131 4.37802C3.4458 4.01839 2.98766 3.89344 2.62802 4.09895C2.26839 4.30445 2.14344 4.76259 2.34895 5.12223L6.13624 11.75H6C4.11438 11.75 3.17157 11.75 2.58579 12.3358C2 12.9216 2 13.8644 2 15.75C2 17.6356 2 18.5784 2.58579 19.1642C3.17157 19.75 4.11438 19.75 6 19.75H18C19.8856 19.75 20.8284 19.75 21.4142 19.1642C22 18.5784 22 17.6356 22 15.75C22 13.8644 22 12.9216 21.4142 12.3358C20.8284 11.75 19.8856 11.75 18 11.75H17.8638L21.6511 5.12223C21.8566 4.76259 21.7316 4.30445 21.372 4.09895C21.0123 3.89344 20.5542 4.01839 20.3487 4.37802L16.3487 11.378L16.1287 11.75H7.88128L7.65131 11.378L3.65131 4.37802ZM6 16.75C6.55228 16.75 7 16.3023 7 15.75C7 15.1977 6.55228 14.75 6 14.75C5.44772 14.75 5 15.1977 5 15.75C5 16.3023 5.44772 16.75 6 16.75ZM10 15.75C10 16.3023 9.55228 16.75 9 16.75C8.44772 16.75 8 16.3023 8 15.75C8 15.1977 8.44772 14.75 9 14.75C9.55228 14.75 10 15.1977 10 15.75ZM14 15C13.5858 15 13.25 15.3358 13.25 15.75C13.25 16.1642 13.5858 16.5 14 16.5H18C18.4142 16.5 18.75 16.1642 18.75 15.75C18.75 15.3358 18.4142 15 18 15H14Z" fill="#000"></path> </g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
5
assets/icons/microphone.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 9C4.41421 9 4.75 9.33579 4.75 9.75V10.75C4.75 14.7541 7.99594 18 12 18C16.0041 18 19.25 14.7541 19.25 10.75V9.75C19.25 9.33579 19.5858 9 20 9C20.4142 9 20.75 9.33579 20.75 9.75V10.75C20.75 15.3298 17.2314 19.0879 12.75 19.4683V21.75C12.75 22.1642 12.4142 22.5 12 22.5C11.5858 22.5 11.25 22.1642 11.25 21.75V19.4683C6.7686 19.0879 3.25 15.3298 3.25 10.75V9.75C3.25 9.33579 3.58579 9 4 9Z" fill="#1C274C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C8.82436 2 6.25 4.57436 6.25 7.75V10.75C6.25 13.9256 8.82436 16.5 12 16.5C15.1756 16.5 17.75 13.9256 17.75 10.75V7.75C17.75 4.57436 15.1756 2 12 2ZM14 11.5C14.4142 11.5 14.75 11.1642 14.75 10.75C14.75 10.3358 14.4142 10 14 10H10C9.58579 10 9.25 10.3358 9.25 10.75C9.25 11.1642 9.58579 11.5 10 11.5H14ZM13.75 7.75C13.75 8.16421 13.4142 8.5 13 8.5H11C10.5858 8.5 10.25 8.16421 10.25 7.75C10.25 7.33579 10.5858 7 11 7H13C13.4142 7 13.75 7.33579 13.75 7.75Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
assets/icons/network.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M14 22H10C6.22876 22 4.34315 22 3.17157 20.7881C2 19.5763 2 17.6258 2 13.725V12.2039C2 9.91549 2 8.77128 2.5192 7.82274C3.0384 6.87421 3.98695 6.28551 5.88403 5.10813L7.88403 3.86687C9.88939 2.62229 10.8921 2 12 2C13.1079 2 14.1106 2.62229 16.116 3.86687L18.116 5.10812C20.0131 6.28551 20.9616 6.87421 21.4808 7.82274C22 8.77128 22 9.91549 22 12.2039V13.725C22 17.6258 22 19.5763 20.8284 20.7881C19.6569 22 17.7712 22 14 22ZM17.4498 12.192C14.4329 8.93571 9.56706 8.93571 6.55017 12.192C6.26866 12.4959 5.79413 12.514 5.49028 12.2325C5.18643 11.951 5.16832 11.4764 5.44983 11.1726C9.06036 7.27552 14.9396 7.27552 18.5502 11.1726C18.8317 11.4764 18.8136 11.951 18.5097 12.2325C18.2059 12.514 17.7313 12.4959 17.4498 12.192ZM15.4498 14.3505C13.5375 12.2864 10.4625 12.2864 8.55019 14.3505C8.26868 14.6544 7.79415 14.6725 7.4903 14.391C7.18645 14.1095 7.16834 13.6349 7.44985 13.3311C9.95581 10.6262 14.0442 10.6262 16.5502 13.3311C16.8317 13.6349 16.8136 14.1095 16.5097 14.391C16.2059 14.6725 15.7314 14.6544 15.4498 14.3505ZM13.4499 16.5095C12.6421 15.6377 11.358 15.6377 10.5502 16.5095C10.2687 16.8134 9.79417 16.8315 9.49031 16.55C9.18646 16.2684 9.16835 15.7939 9.44986 15.4901C10.8513 13.9775 13.1488 13.9775 14.5502 15.4901C14.8317 15.7939 14.8136 16.2684 14.5098 16.55C14.2059 16.8315 13.7314 16.8134 13.4499 16.5095Z" fill="#000"></path> </g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
11
assets/icons/notification.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="style=bulk">
|
||||
<g id="notification-box-line">
|
||||
<path id="Subtract" fill-rule="evenodd" clip-rule="evenodd" d="M8.75 1.99994C5.02208 1.99994 2 5.02201 2 8.74994L2 15.2577C2 18.9857 5.02208 22.0077 8.75 22.0077L15.2578 22.0077C18.9857 22.0077 22.0078 18.9857 22.0078 15.2577L22.0078 8.74994C22.0078 8.09759 21.9153 7.46685 21.7426 6.87014C21.2141 8.15089 19.9531 9.05228 18.4816 9.05228C16.5342 9.05228 14.9554 7.47356 14.9554 5.52611C14.9554 4.05459 15.8568 2.79359 17.1376 2.26514C16.5409 2.09248 15.9101 1.99994 15.2578 1.99994L8.75 1.99994Z" fill="#BFBFBF"/>
|
||||
<path id="vector (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M8.75 15.5001C8.75 15.0858 9.08579 14.7501 9.5 14.7501H14.5C14.9142 14.7501 15.25 15.0858 15.25 15.5001C15.25 15.9143 14.9142 16.2501 14.5 16.2501H9.5C9.08579 16.2501 8.75 15.9143 8.75 15.5001Z" fill="#000000"/>
|
||||
<circle id="vector" cx="18.4816" cy="5.52623" r="2.77617" fill="#000000"/>
|
||||
</g>
|
||||
</g>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
35
assets/icons/nvidia.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="128" height="128" version="1.1">
|
||||
<title>Nvidia Logo</title>
|
||||
<desc>This is shape (source) for Clarity vector icon theme for gtk</desc>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:title>Nvidia Logo</dc:title>
|
||||
<dc:description>This is shape (source) for Clarity vector icon theme for gtk</dc:description>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:date>2010</dc:date>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m 19.777894,58.887563 c 0,0 10.05027,-14.828437 30.117316,-16.362058 l 0,-5.380182 c -22.2268,1.785057 -41.4742019,20.61011 -41.4742019,20.61011 0,0 10.9013539,31.514726 41.4742019,34.400204 l 0,-5.718258 C 27.458955,83.614426 19.777894,58.887563 19.777894,58.887563 z m 30.117316,16.177165 0,5.236824 C 32.938729,77.278522 28.232018,59.651693 28.232018,59.651693 c 0,0 8.141505,-9.019072 21.663192,-10.481238 l 0,5.746393 c -0.01032,0 -0.01741,-0.0031 -0.0259,-0.0031 -7.09691,-0.851665 -12.640994,5.777655 -12.640994,5.777655 0,0 3.106991,11.16141 12.666896,14.373351 m 0,-47.841948 0,9.922543 c 0.652482,-0.05047 1.304964,-0.09245 1.961018,-0.114779 25.269481,-0.851665 41.734256,20.724887 41.734256,20.724887 0,0 -18.910353,22.994504 -38.611635,22.994504 -1.805601,0 -3.495979,-0.167475 -5.083639,-0.448385 l 0,6.135827 c 1.357662,0.172387 2.765343,0.273767 4.234208,0.273767 18.333347,0 31.59154,-9.362955 44.429483,-20.444422 2.128489,1.704667 10.842539,5.851342 12.633849,7.667216 -12.206453,10.219531 -40.654381,18.457501 -56.782421,18.457501 -1.554612,0 -3.047594,-0.09424 -4.515119,-0.235803 l 0,8.6216 69.68378,0 0,-73.554458 z m 0,21.947675 0,-6.64495 c 0.645782,-0.04511 1.296477,-0.07995 1.961018,-0.100929 18.171232,-0.570753 30.0932,15.615345 30.0932,15.615345 0,0 -12.876351,17.883175 -26.682522,17.883175 -1.98692,0 -3.768404,-0.319318 -5.371696,-0.858364 l 0,-20.147881 c 7.074133,0.854792 8.497892,3.979647 12.750856,11.06941 l 9.458973,-7.976263 c 0,0 -6.904871,-9.055693 -18.544589,-9.055693 -1.266109,0 -2.476839,0.08888 -3.66524,0.216154"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
assets/icons/power.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M5.66953 9.91436L8.73167 5.77133C10.711 3.09327 11.7007 1.75425 12.6241 2.03721C13.5474 2.32018 13.5474 3.96249 13.5474 7.24712V7.55682C13.5474 8.74151 13.5474 9.33386 13.926 9.70541L13.946 9.72466C14.3327 10.0884 14.9492 10.0884 16.1822 10.0884C18.4011 10.0884 19.5106 10.0884 19.8855 10.7613C19.8917 10.7724 19.8977 10.7837 19.9036 10.795C20.2576 11.4784 19.6152 12.3475 18.3304 14.0857L15.2683 18.2287C13.2889 20.9067 12.2992 22.2458 11.3758 21.9628C10.4525 21.6798 10.4525 20.0375 10.4525 16.7528L10.4526 16.4433C10.4526 15.2585 10.4526 14.6662 10.074 14.2946L10.054 14.2754C9.6673 13.9117 9.05079 13.9117 7.81775 13.9117C5.59888 13.9117 4.48945 13.9117 4.1145 13.2387C4.10829 13.2276 4.10225 13.2164 4.09639 13.205C3.74244 12.5217 4.3848 11.6526 5.66953 9.91436Z" fill="#000"></path> </g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
assets/icons/speaker.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.75C10.2051 10.75 8.75 12.2051 8.75 14C8.75 15.7949 10.2051 17.25 12 17.25C13.7949 17.25 15.25 15.7949 15.25 14C15.25 12.2051 13.7949 10.75 12 10.75Z" fill="#1C274C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 10C4 6.22876 4 4.34315 5.17157 3.17157C6.34315 2 8.22876 2 12 2C15.7712 2 17.6569 2 18.8284 3.17157C20 4.34315 20 6.22876 20 10V14C20 17.7712 20 19.6569 18.8284 20.8284C17.6569 22 15.7712 22 12 22C8.22876 22 6.34315 22 5.17157 20.8284C4 19.6569 4 17.7712 4 14V10ZM7.25 14C7.25 11.3766 9.37665 9.25 12 9.25C14.6234 9.25 16.75 11.3766 16.75 14C16.75 16.6234 14.6234 18.75 12 18.75C9.37665 18.75 7.25 16.6234 7.25 14ZM10 5.25C9.58579 5.25 9.25 5.58579 9.25 6C9.25 6.41421 9.58579 6.75 10 6.75H14C14.4142 6.75 14.75 6.41421 14.75 6C14.75 5.58579 14.4142 5.25 14 5.25H10Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,35 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property QtObject colors
|
||||
property QtObject font
|
||||
|
||||
property int margin
|
||||
property int radius
|
||||
property int spacing
|
||||
property int padding
|
||||
property int duration
|
||||
|
||||
margin: 8
|
||||
radius: 8
|
||||
spacing: 8
|
||||
padding: 8
|
||||
duration: 150
|
||||
|
||||
font: QtObject {
|
||||
property string family: "FiraCode Nerd Font Mono SemBd"
|
||||
property int pointSize: 9
|
||||
}
|
||||
|
||||
colors: QtObject {
|
||||
property color accent: "#6791c9"
|
||||
property color foreground: "#ffffff"
|
||||
property color background: "#101213"
|
||||
property color inActive: "#1b1d1e"
|
||||
}
|
||||
}
|
||||
19
config/BarLayout.qml
Normal file
@@ -0,0 +1,19 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function widget(name: string): url {
|
||||
return `${Quickshell.shellPath("modules/bar")}/${name}.qml`;
|
||||
}
|
||||
|
||||
property var left: [root.widget("LauncherButton"), root.widget("NetworkWidget"), root.widget("PowerProfileWidget"), root.widget("GPUWidget"), root.widget("HomeWidget"), root.widget("MusicWidget")]
|
||||
|
||||
property var center: [root.widget("CavaVisualizer"), root.widget("Workspaces"), root.widget("CavaVisualizer"),]
|
||||
|
||||
property var right: [root.widget("AudioWidget"), root.widget("BatteryIndicator"), root.widget("SystemTrayWidget"), root.widget("ClockWidget"), root.widget("GhostButton")]
|
||||
}
|
||||
150
config/Theme.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string name: "poimandres"
|
||||
|
||||
readonly property Base16 scheme: {
|
||||
switch (root.name) {
|
||||
case "poimandres":
|
||||
return poimandres;
|
||||
default:
|
||||
return gruvbox;
|
||||
}
|
||||
}
|
||||
|
||||
component Base16: QtObject {
|
||||
property color base00
|
||||
property color base01
|
||||
property color base02
|
||||
property color base03
|
||||
property color base04
|
||||
property color base05
|
||||
property color base06
|
||||
property color base07
|
||||
property color base08
|
||||
property color base09
|
||||
property color base0A
|
||||
property color base0B
|
||||
property color base0C
|
||||
property color base0D
|
||||
property color base0E
|
||||
property color base0F
|
||||
}
|
||||
|
||||
Base16 {
|
||||
id: poimandres
|
||||
|
||||
base00: "#0f0f0f"
|
||||
base01: "#181818"
|
||||
base02: "#202020"
|
||||
base03: "#292929"
|
||||
base04: "#3f3f3f"
|
||||
base05: "#E4F0FB"
|
||||
base06: "#b6d7f4"
|
||||
base07: "#ffffff"
|
||||
base08: "#D0679D"
|
||||
base09: "#91B4D5"
|
||||
base0A: "#FFFAC2"
|
||||
base0B: "#5FB3A1"
|
||||
base0C: "#5DE4C7"
|
||||
base0D: "#89DDFF"
|
||||
base0E: "#A6ACCD"
|
||||
base0F: "#FCC5E9"
|
||||
}
|
||||
|
||||
readonly property QtObject colors: QtObject {
|
||||
readonly property color base00: root.scheme.base00
|
||||
readonly property color base01: root.scheme.base01
|
||||
readonly property color base02: root.scheme.base02
|
||||
readonly property color base03: root.scheme.base03
|
||||
readonly property color base04: root.scheme.base04
|
||||
readonly property color base05: root.scheme.base05
|
||||
readonly property color base06: root.scheme.base06
|
||||
readonly property color base07: root.scheme.base07
|
||||
readonly property color base08: root.scheme.base08
|
||||
readonly property color base09: root.scheme.base09
|
||||
readonly property color base0A: root.scheme.base0A
|
||||
readonly property color base0B: root.scheme.base0B
|
||||
readonly property color base0C: root.scheme.base0C
|
||||
readonly property color base0D: root.scheme.base0D
|
||||
readonly property color base0E: root.scheme.base0E
|
||||
readonly property color base0F: root.scheme.base0F
|
||||
|
||||
readonly property color accent: base0D
|
||||
readonly property color foreground: base05
|
||||
readonly property color background: base00
|
||||
readonly property color inActive: base01
|
||||
readonly property color hover: base02
|
||||
readonly property color border: base03
|
||||
readonly property color secondaryForeground: base04
|
||||
readonly property color error: base08
|
||||
readonly property color warning: base0A
|
||||
readonly property color success: base0B
|
||||
readonly property color info: base0C
|
||||
}
|
||||
|
||||
property int margin: 8
|
||||
property int radius: 8
|
||||
property int spacing: 8
|
||||
property int padding: 8
|
||||
property int duration: 150
|
||||
property int borderWidth: 1
|
||||
|
||||
property QtObject font: QtObject {
|
||||
property string family: "FiraCode Nerd Font Mono SemBd"
|
||||
property int pointSize: 9
|
||||
readonly property int body: pointSize - 1
|
||||
readonly property int caption: pointSize - 2
|
||||
}
|
||||
|
||||
property QtObject bar: QtObject {
|
||||
property int thickness: 10
|
||||
property int height: 50
|
||||
}
|
||||
|
||||
property QtObject workspaces: QtObject {
|
||||
property int height: 15
|
||||
property int radius: 1000
|
||||
property real activeWidthRatio: 2.3
|
||||
}
|
||||
|
||||
property QtObject cava: QtObject {
|
||||
property int width: 96
|
||||
property int height: 32
|
||||
property int innerMargin: 8
|
||||
property int barSpacing: 3
|
||||
property int barMinHeight: 2
|
||||
}
|
||||
|
||||
property QtObject button: QtObject {
|
||||
property int iconSize: 14
|
||||
property int verticalPadding: 4
|
||||
property int borderWidth: 2
|
||||
}
|
||||
|
||||
property QtObject tray: QtObject {
|
||||
property int iconSize: 16
|
||||
}
|
||||
|
||||
property QtObject notification: QtObject {
|
||||
property int width: 420
|
||||
property int iconSize: 40
|
||||
property int textSpacing: 2
|
||||
property real bodyOpacity: 0.8
|
||||
}
|
||||
|
||||
property QtObject volume: QtObject {
|
||||
property int width: 300
|
||||
property int height: 48
|
||||
property int iconSize: 22
|
||||
property int barHeight: 10
|
||||
property int timeout: 1500
|
||||
property real bottomMarginRatio: 0.2
|
||||
}
|
||||
}
|
||||
41
flake.lock
generated
@@ -2,15 +2,15 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771848320,
|
||||
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1787360063,
|
||||
"narHash": "sha256-dt4WdcvsA8/RCe+VZZwqU0X+XMM3wBbGCWA0/sFWzGo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||
"rev": "2c423e03bbafcff28bfadc6781a4a8257f205cb5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
@@ -23,11 +23,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771926182,
|
||||
"narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=",
|
||||
"lastModified": 1787279335,
|
||||
"narHash": "sha256-CLX2Zp5i5BuLbOxNOkwRd9YY84IOrACNxBV79o9/F9Y=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7",
|
||||
"revCount": 744,
|
||||
"rev": "1a4716cde794a59928d9d9fc15f2afc7a95de360",
|
||||
"revCount": 846,
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||
},
|
||||
@@ -39,7 +39,28 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"quickshell": "quickshell"
|
||||
"quickshell": "quickshell",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786901030,
|
||||
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
62
flake.nix
@@ -2,30 +2,76 @@
|
||||
description = "tux's widgets for wayland";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
treefmt-nix = {
|
||||
url = "github:numtide/treefmt-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
quickshell,
|
||||
treefmt-nix,
|
||||
}: let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShells.${system} = {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
quickshell.packages.${system}.default
|
||||
pkgs.kdePackages.qtdeclarative
|
||||
pkgs.kdePackages.qt5compat
|
||||
|
||||
quickshellWithModules = quickshell.packages.${system}.default.withModules [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtdeclarative
|
||||
pkgs.qt6.qtmultimedia
|
||||
pkgs.qt6.qttranslations
|
||||
];
|
||||
|
||||
treefmtEval = treefmt-nix.lib.evalModule pkgs {
|
||||
projectRootFile = "flake.nix";
|
||||
|
||||
programs = {
|
||||
alejandra.enable = true;
|
||||
qmlformat.enable = true;
|
||||
clang-format.enable = true;
|
||||
};
|
||||
};
|
||||
in rec {
|
||||
formatter.${system} = treefmtEval.config.build.wrapper;
|
||||
|
||||
packages.${system} = {
|
||||
tshell = pkgs.callPackage ./package.nix {
|
||||
inherit quickshellWithModules;
|
||||
};
|
||||
default = packages.${system}.tshell;
|
||||
};
|
||||
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = [
|
||||
quickshellWithModules
|
||||
pkgs.cava
|
||||
pkgs.wallust
|
||||
pkgs.dbus
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtdeclarative
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
if [ -d "$PWD/plugin/dist/lib/qt-6/qml" ]; then
|
||||
export NIXPKGS_QT6_QML_IMPORT_PATH="$PWD/plugin/dist/lib/qt-6/qml''${NIXPKGS_QT6_QML_IMPORT_PATH:+:$NIXPKGS_QT6_QML_IMPORT_PATH}"
|
||||
export QML2_IMPORT_PATH="$PWD/plugin/dist/lib/qt-6/qml''${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}"
|
||||
else
|
||||
echo "gpu plugin is not built; run from the repo root to enable 'import Tshell.Cardwire':"
|
||||
echo " cmake -S plugin -B plugin/dist -DCMAKE_INSTALL_PREFIX=\$PWD/plugin/dist"
|
||||
echo " cmake --build plugin/dist --target install"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
21
modules/bar/AudioWidget.qml
Normal file
@@ -0,0 +1,21 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick.Layouts
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
RowLayout {
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: Audio.inputName
|
||||
iconSource: Audio.inputIcon
|
||||
}
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: Audio.outputName
|
||||
iconSource: Audio.outputIcon
|
||||
}
|
||||
}
|
||||
11
modules/bar/BatteryIndicator.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: `${Battery.percentage}% ${Battery.energyRate}W`
|
||||
iconSource: Battery.icon
|
||||
}
|
||||
59
modules/bar/CavaVisualizer.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int barCount: 14
|
||||
property real maxValue: 100
|
||||
readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001)
|
||||
|
||||
implicitWidth: Theme.cava.width
|
||||
implicitHeight: Theme.cava.height
|
||||
radius: Theme.radius
|
||||
color: Theme.colors.inActive
|
||||
visible: shouldVisualize
|
||||
|
||||
Row {
|
||||
id: barRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.cava.innerMargin
|
||||
spacing: Theme.cava.barSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.barCount
|
||||
|
||||
Rectangle {
|
||||
id: bar
|
||||
|
||||
required property int index
|
||||
readonly property real level: Math.min(1, (Cava.values[index] ?? 0) / root.maxValue)
|
||||
|
||||
width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount
|
||||
height: Math.max(Theme.cava.barMinHeight, Math.pow(bar.level, 0.6) * barRow.height)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: bar.width / 2
|
||||
color: Theme.colors.accent
|
||||
antialiasing: true
|
||||
opacity: 0.45 + (bar.level * 0.55)
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 90
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
modules/bar/ClockWidget.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: Time.time
|
||||
}
|
||||
23
modules/bar/GPUWidget.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
iconSource: GPU.icon
|
||||
|
||||
visible: GPU.connected && GPU.gpu !== null
|
||||
opacity: GPU.gpu !== null && GPU.gpu.known ? 1 : 0
|
||||
|
||||
label: {
|
||||
if (!GPU.gpu)
|
||||
return "NA";
|
||||
return GPU.gpu.blocked ? "Blocked" : "Active";
|
||||
}
|
||||
}
|
||||
10
modules/bar/GhostButton.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import qs.ui
|
||||
|
||||
BarButton {
|
||||
iconOnly: true
|
||||
hoverHighlight: false
|
||||
iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg`
|
||||
}
|
||||
11
modules/bar/HomeWidget.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: Home.latency >= 0 ? `${Home.latency} ms` : "NA"
|
||||
iconSource: Home.icon
|
||||
}
|
||||
11
modules/bar/LauncherButton.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import qs.ui
|
||||
|
||||
BarButton {
|
||||
iconOnly: true
|
||||
hoverHighlight: false
|
||||
iconSource: `${Quickshell.shellPath("assets")}/icons/nix.svg`
|
||||
onClicked: Quickshell.execDetached(["vicinae", "toggle"])
|
||||
}
|
||||
17
modules/bar/MusicWidget.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.ui
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
|
||||
readonly property string track: Media.trackArtist !== "" ? `${Media.trackTitle} — ${Media.trackArtist}` : Media.trackTitle
|
||||
readonly property bool active: track !== ""
|
||||
|
||||
visible: active
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
iconSource: Media.icon
|
||||
label: `${Media.truncateText(root.track, 28)} ${Media.position} / ${Media.length}`
|
||||
}
|
||||
11
modules/bar/NetworkWidget.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: Network.statusText
|
||||
iconSource: Network.icon
|
||||
}
|
||||
11
modules/bar/PowerProfileWidget.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.ui
|
||||
import qs.services
|
||||
|
||||
BarButton {
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
label: PowerProfile.name
|
||||
iconSource: PowerProfile.icon
|
||||
}
|
||||
30
modules/bar/SystemTrayWidget.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.config
|
||||
import qs.ui
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
visible: SystemTray.items.values.length > 0
|
||||
|
||||
contentItem: Row {
|
||||
spacing: Theme.spacing
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items.values
|
||||
|
||||
Image {
|
||||
required property var modelData
|
||||
|
||||
source: modelData.icon
|
||||
width: Theme.tray.iconSize
|
||||
height: Theme.tray.iconSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
modules/bar/Workspaces.qml
Normal file
@@ -0,0 +1,144 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int count: 7
|
||||
|
||||
implicitWidth: pillsRow.implicitWidth
|
||||
implicitHeight: Theme.workspaces.height
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Theme.duration
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pillsRow
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing
|
||||
|
||||
Repeater {
|
||||
model: root.count
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
required property int index
|
||||
property var ws: Hyprland.workspaces.values.find(w => w.id === pill.index + 1)
|
||||
property bool isActive: Hyprland.focusedWorkspace?.id === (pill.index + 1)
|
||||
|
||||
z: 1
|
||||
radius: Theme.workspaces.radius
|
||||
implicitHeight: Theme.workspaces.height
|
||||
implicitWidth: pill.isActive ? pill.implicitHeight * Theme.workspaces.activeWidthRatio : pill.implicitHeight
|
||||
color: {
|
||||
if (pill.isActive)
|
||||
return "transparent";
|
||||
if (handler.hovered)
|
||||
return Theme.colors.accent;
|
||||
if (pill.ws)
|
||||
return Theme.colors.secondaryForeground;
|
||||
return Theme.colors.inActive;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + (pill.index + 1) + " })")
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: handler
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.duration + 50
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Theme.duration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: highlight
|
||||
|
||||
z: 2
|
||||
radius: Theme.workspaces.radius
|
||||
color: Theme.colors.accent
|
||||
|
||||
readonly property real activeW: Theme.workspaces.height * Theme.workspaces.activeWidthRatio
|
||||
readonly property real inactiveW: Theme.workspaces.height
|
||||
property int curIdx: Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id - 1 : -1
|
||||
property int prevIdx: 0
|
||||
|
||||
function getX(index, activeIndex) {
|
||||
if (index < 0)
|
||||
return 0;
|
||||
let x = 0;
|
||||
for (let i = 0; i < index; i++)
|
||||
x += (i === activeIndex ? activeW : inactiveW) + pillsRow.spacing;
|
||||
return x;
|
||||
}
|
||||
|
||||
property real targetLeft: curIdx >= 0 ? getX(curIdx, curIdx) : 0
|
||||
property real targetRight: curIdx >= 0 ? targetLeft + activeW : 0
|
||||
property real actualLeft: targetLeft
|
||||
property real actualRight: targetRight
|
||||
|
||||
Behavior on actualLeft {
|
||||
NumberAnimation {
|
||||
id: leftAnim
|
||||
duration: Theme.duration
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on actualRight {
|
||||
NumberAnimation {
|
||||
id: rightAnim
|
||||
duration: Theme.duration
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
|
||||
onCurIdxChanged: {
|
||||
if (curIdx >= 0 && prevIdx >= 0) {
|
||||
if (curIdx > prevIdx) {
|
||||
leftAnim.duration = 400;
|
||||
rightAnim.duration = 300;
|
||||
} else if (curIdx < prevIdx) {
|
||||
leftAnim.duration = 300;
|
||||
rightAnim.duration = 400;
|
||||
}
|
||||
}
|
||||
if (curIdx >= 0)
|
||||
prevIdx = curIdx;
|
||||
}
|
||||
|
||||
x: actualLeft
|
||||
width: actualRight - actualLeft
|
||||
height: parent.height
|
||||
opacity: curIdx >= 0 ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 180
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
modules/osd/NotificationCard.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import qs.config
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property Notification notif
|
||||
readonly property bool critical: notif?.urgency === NotificationUrgency.Critical
|
||||
readonly property int timeoutDuration: {
|
||||
const seconds = Math.round((notif?.expireTimeout ?? 0) / 1000);
|
||||
return seconds > 0 ? seconds : 5;
|
||||
}
|
||||
readonly property string iconName: {
|
||||
if (!notif)
|
||||
return "";
|
||||
|
||||
const hints = notif.hints ?? {};
|
||||
const hinted = String(hints["image-path"] ?? hints["image_path"] ?? hints["icon_path"] ?? "");
|
||||
return notif.appIcon || notif.desktopEntry || hinted;
|
||||
}
|
||||
readonly property url themedIconUrl: iconName !== "" ? Quickshell.iconPath(iconName, false) : ""
|
||||
readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/notification.svg`
|
||||
|
||||
implicitWidth: Theme.notification.width
|
||||
implicitHeight: content.implicitHeight + Theme.padding * 2
|
||||
radius: Theme.radius
|
||||
color: Theme.colors.background
|
||||
border.width: critical ? Theme.notification.borderWidth : 0
|
||||
border.color: Theme.colors.accent
|
||||
|
||||
Timer {
|
||||
id: countdown
|
||||
|
||||
interval: root.timeoutDuration * 1000
|
||||
repeat: false
|
||||
running: !root.critical && !hovered.hovered && visible
|
||||
onTriggered: root.notif.expire()
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.padding
|
||||
spacing: Theme.spacing
|
||||
|
||||
Item {
|
||||
width: Theme.notification.iconSize
|
||||
height: Theme.notification.iconSize
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.iconUrl
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.themedIconUrl
|
||||
visible: root.themedIconUrl !== "" && status === Image.Ready
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Theme.notification.textSpacing
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
text: root.notif?.appName ?? ""
|
||||
color: Theme.colors.accent
|
||||
font.family: Theme.font.family
|
||||
font.pointSize: Theme.font.caption
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: root.notif?.summary ?? ""
|
||||
color: Theme.colors.foreground
|
||||
font.family: Theme.font.family
|
||||
font.pointSize: Theme.font.pointSize
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: root.notif?.body ?? ""
|
||||
visible: text !== ""
|
||||
color: Theme.colors.foreground
|
||||
opacity: Theme.notification.bodyOpacity
|
||||
font.family: Theme.font.family
|
||||
font.pointSize: Theme.font.body
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
modules/osd/VolumeCard.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.ui
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
|
||||
readonly property real level: Math.max(0, Math.min(Audio.muted ? 0 : Audio.volume, 1))
|
||||
readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/speaker.svg`
|
||||
|
||||
hoverHighlight: false
|
||||
pointerCursor: false
|
||||
iconOnly: true
|
||||
iconSource: root.iconUrl
|
||||
icon.width: Theme.volume.iconSize
|
||||
icon.height: Theme.volume.iconSize
|
||||
|
||||
implicitWidth: Theme.volume.width
|
||||
implicitHeight: Theme.volume.height
|
||||
|
||||
Rectangle {
|
||||
id: fill
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: parent.width * root.level
|
||||
radius: Theme.radius
|
||||
color: Audio.muted ? Theme.colors.error : Theme.colors.accent
|
||||
opacity: 0.35
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
package.nix
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
makeWrapper,
|
||||
makeFontsConf,
|
||||
cmake,
|
||||
ninja,
|
||||
quickshellWithModules,
|
||||
qt6,
|
||||
cava,
|
||||
wallust,
|
||||
nerd-fonts,
|
||||
extraRuntimeDeps ? [],
|
||||
}: let
|
||||
version = "0.1.0";
|
||||
|
||||
runtimeDeps =
|
||||
[
|
||||
cava
|
||||
wallust
|
||||
]
|
||||
++ extraRuntimeDeps;
|
||||
|
||||
fontconfig = makeFontsConf {
|
||||
fontDirectories = [nerd-fonts.fira-code];
|
||||
};
|
||||
|
||||
# Unlike lib.fileset, this keeps untracked files (the flake only sees
|
||||
# git-tracked paths) while dropping build artifacts.
|
||||
src = lib.cleanSourceWith {
|
||||
src = lib.cleanSource ./.;
|
||||
filter = path: _: let
|
||||
name = baseNameOf path;
|
||||
in
|
||||
!(name == "dist" || name == "result" || name == "result-bin" || lib.hasPrefix "build" name);
|
||||
};
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
inherit version src;
|
||||
pname = "tshell";
|
||||
|
||||
dontWrapQtApps = true;
|
||||
|
||||
preConfigure = ''
|
||||
cd plugin
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
ninja
|
||||
makeWrapper
|
||||
];
|
||||
buildInputs = [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
];
|
||||
propagatedBuildInputs = runtimeDeps;
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/bin $out/share/tshell
|
||||
|
||||
for entry in shell.qml assets config modules services ui windows; do
|
||||
cp -r "$src/$entry" $out/share/tshell/
|
||||
done
|
||||
|
||||
makeWrapper ${quickshellWithModules}/bin/qs $out/bin/tshell \
|
||||
--prefix PATH : "${lib.makeBinPath runtimeDeps}" \
|
||||
--set FONTCONFIG_FILE "${fontconfig}" \
|
||||
--suffix NIXPKGS_QT6_QML_IMPORT_PATH : "$out/lib/qt-6/qml" \
|
||||
--add-flags "-p $out/share/tshell"
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "tux's widgets for wayland";
|
||||
homepage = "https://github.com/tuxdotrs/tshell";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "tshell";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
46
plugin/CMakeLists.txt
Normal file
@@ -0,0 +1,46 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(tshell-cardwire LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
|
||||
|
||||
qt_add_library(tshellcardwire SHARED src/cardwire.cpp src/cardwire.hpp)
|
||||
target_include_directories(tshellcardwire PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
|
||||
qt_add_qml_module(tshellcardwire
|
||||
URI Tshell.Cardwire
|
||||
VERSION 0.1
|
||||
DEPENDENCIES QtQml
|
||||
OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Tshell/Cardwire"
|
||||
)
|
||||
|
||||
target_link_libraries(tshellcardwire PRIVATE Qt6::Core Qt6::Qml Qt6::DBus)
|
||||
|
||||
# Keep the backing library next to the plugin so "$ORIGIN" rpaths resolve both
|
||||
# in the build tree and after install.
|
||||
set(module_build_dir "${CMAKE_CURRENT_BINARY_DIR}/Tshell/Cardwire")
|
||||
set_target_properties(tshellcardwire tshellcardwireplugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${module_build_dir}"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${module_build_dir}"
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${module_build_dir}"
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
|
||||
qt_query_qml_module(tshellcardwire
|
||||
PLUGIN_TARGET module_plugin_target
|
||||
TARGET_PATH module_target_path
|
||||
QMLDIR module_qmldir
|
||||
TYPEINFO module_typeinfo
|
||||
)
|
||||
|
||||
set(module_dir "${CMAKE_INSTALL_PREFIX}/lib/qt-6/qml/${module_target_path}")
|
||||
|
||||
install(TARGETS tshellcardwire "${module_plugin_target}"
|
||||
LIBRARY DESTINATION "${module_dir}"
|
||||
RUNTIME DESTINATION "${module_dir}"
|
||||
)
|
||||
install(FILES "${module_qmldir}" "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||
405
plugin/src/cardwire.cpp
Normal file
@@ -0,0 +1,405 @@
|
||||
#include "cardwire.hpp"
|
||||
|
||||
#include <qdbusargument.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qdbusmessage.h>
|
||||
#include <qdbusmetatype.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbusservicewatcher.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
// a{oa{sa{sv}}} as returned by GetManagedObjects
|
||||
using InterfacesMap = QMap<QString, QVariantMap>;
|
||||
using ManagedObjects = QMap<QDBusObjectPath, InterfacesMap>;
|
||||
|
||||
Q_DECLARE_METATYPE(InterfacesMap)
|
||||
Q_DECLARE_METATYPE(ManagedObjects)
|
||||
|
||||
namespace {
|
||||
|
||||
Q_LOGGING_CATEGORY(logCardwire, "tshell.cardwire", QtWarningMsg);
|
||||
|
||||
const QString SERVICE = "org.opengamingcollective.cardwire";
|
||||
const QString ROOT_PATH = "/org/opengamingcollective/cardwire";
|
||||
const QString GPU_PREFIX = "/org/opengamingcollective/cardwire/Gpu/";
|
||||
const QString GPU_IFACE = "org.opengamingcollective.cardwire.Gpu";
|
||||
const QString PROPS_IFACE = "org.freedesktop.DBus.Properties";
|
||||
const QString OBJMGR_IFACE = "org.freedesktop.DBus.ObjectManager";
|
||||
|
||||
void registerMetaTypes() {
|
||||
qDBusRegisterMetaType<InterfacesMap>();
|
||||
qDBusRegisterMetaType<ManagedObjects>();
|
||||
}
|
||||
|
||||
QDBusMessage methodCall(const QString &path, const QString &iface,
|
||||
const QString &member) {
|
||||
return QDBusMessage::createMethodCall(SERVICE, path, iface, member);
|
||||
}
|
||||
|
||||
QDBusPendingCallWatcher *asyncCall(QObject *parent,
|
||||
const QDBusMessage &message) {
|
||||
return new QDBusPendingCallWatcher(
|
||||
QDBusConnection::systemBus().asyncCall(message), parent);
|
||||
}
|
||||
|
||||
void logError(const QString &what, const QDBusMessage &reply) {
|
||||
if (reply.type() == QDBusMessage::ErrorMessage) {
|
||||
qCWarning(logCardwire).nospace()
|
||||
<< what << ": " << reply.errorName() << " " << reply.errorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CardwireGpu::CardwireGpu(QString objectPath, QObject *parent)
|
||||
: QObject(parent), mObjectPath(std::move(objectPath)) {
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
bus.connect(SERVICE, this->mObjectPath, PROPS_IFACE, "PropertiesChanged",
|
||||
this, SLOT(onPropertiesChanged(QDBusMessage)));
|
||||
bus.connect(SERVICE, this->mObjectPath, GPU_IFACE, "PowerStateChanged", this,
|
||||
SLOT(onPowerStateChanged(QString)));
|
||||
|
||||
this->refreshDevice();
|
||||
this->refreshProperties();
|
||||
}
|
||||
|
||||
void CardwireGpu::refreshDevice() {
|
||||
QObject::connect(
|
||||
asyncCall(this, methodCall(this->mObjectPath, GPU_IFACE, "GetDevice")),
|
||||
&QDBusPendingCallWatcher::finished, this,
|
||||
&CardwireGpu::onGetDeviceFinished);
|
||||
}
|
||||
|
||||
void CardwireGpu::onGetDeviceFinished() {
|
||||
auto *watcher = qobject_cast<QDBusPendingCallWatcher *>(this->sender());
|
||||
if (watcher == nullptr)
|
||||
return;
|
||||
watcher->deleteLater();
|
||||
|
||||
const QDBusMessage reply = watcher->reply();
|
||||
logError("GetDevice failed for " + this->mObjectPath, reply);
|
||||
if (reply.type() == QDBusMessage::ErrorMessage)
|
||||
return;
|
||||
|
||||
// (ssuubbbbssbs): name pci render card default discrete virtual available
|
||||
// vendor driver nvidia nvidia_minor
|
||||
QString pci;
|
||||
QString vendor;
|
||||
QString driver;
|
||||
QString nvidiaMinor;
|
||||
uint render = 0;
|
||||
uint card = 0;
|
||||
bool virtualGpu = false;
|
||||
|
||||
const auto arguments = reply.arguments();
|
||||
if (arguments.first().userType() != qMetaTypeId<QDBusArgument>()) {
|
||||
// The daemon actually sends the fields as separate out arguments.
|
||||
if (arguments.size() < 12) {
|
||||
qCWarning(logCardwire)
|
||||
<< "GetDevice returned" << arguments.size() << "arguments";
|
||||
return;
|
||||
}
|
||||
this->mName = arguments.at(0).toString();
|
||||
pci = arguments.at(1).toString();
|
||||
render = arguments.at(2).toUInt();
|
||||
card = arguments.at(3).toUInt();
|
||||
this->mDefault = arguments.at(4).toBool();
|
||||
this->mDiscrete = arguments.at(5).toBool();
|
||||
virtualGpu = arguments.at(6).toBool();
|
||||
this->mAvailable = arguments.at(7).toBool();
|
||||
vendor = arguments.at(8).toString();
|
||||
driver = arguments.at(9).toString();
|
||||
this->mNvidia = arguments.at(10).toBool();
|
||||
nvidiaMinor = arguments.at(11).toString();
|
||||
} else {
|
||||
// Documented shape: all fields packed into a single struct.
|
||||
// Must be const: demarshalling uses the const (read) overloads.
|
||||
const QDBusArgument arg = arguments.first().value<QDBusArgument>();
|
||||
arg.beginStructure();
|
||||
arg >> this->mName >> pci >> render >> card >> this->mDefault >>
|
||||
this->mDiscrete >> virtualGpu >> this->mAvailable >> vendor >> driver >>
|
||||
this->mNvidia >> nvidiaMinor;
|
||||
arg.endStructure();
|
||||
}
|
||||
|
||||
if (!this->mKnown) {
|
||||
this->mKnown = true;
|
||||
qCInfo(logCardwire).nospace() << "found gpu \"" << this->mName << "\" at "
|
||||
<< this->mObjectPath << " (" << pci << ")";
|
||||
}
|
||||
|
||||
emit this->deviceChanged();
|
||||
}
|
||||
|
||||
void CardwireGpu::refreshProperties() {
|
||||
QDBusMessage call = methodCall(this->mObjectPath, PROPS_IFACE, "GetAll");
|
||||
call << GPU_IFACE;
|
||||
|
||||
QObject::connect(asyncCall(this, call), &QDBusPendingCallWatcher::finished,
|
||||
this, &CardwireGpu::onGetAllFinished);
|
||||
}
|
||||
|
||||
void CardwireGpu::onGetAllFinished() {
|
||||
auto *watcher = qobject_cast<QDBusPendingCallWatcher *>(this->sender());
|
||||
if (watcher == nullptr)
|
||||
return;
|
||||
watcher->deleteLater();
|
||||
|
||||
const QDBusMessage reply = watcher->reply();
|
||||
logError("GetAll failed for " + this->mObjectPath, reply);
|
||||
if (reply.type() == QDBusMessage::ErrorMessage)
|
||||
return;
|
||||
|
||||
const auto properties = qdbus_cast<QVariantMap>(reply.arguments().at(0));
|
||||
for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
|
||||
if (it.key() == "Block")
|
||||
this->setBlockedValue(it.value().toBool());
|
||||
else if (it.key() == "Launchable")
|
||||
this->setLaunchableValue(it.value().toBool());
|
||||
}
|
||||
}
|
||||
|
||||
void CardwireGpu::setBlocked(bool blocked) {
|
||||
QDBusMessage call = methodCall(this->mObjectPath, PROPS_IFACE, "Set");
|
||||
call << GPU_IFACE << QStringLiteral("Block")
|
||||
<< QVariant::fromValue(QDBusVariant(blocked));
|
||||
|
||||
// The resulting state is confirmed asynchronously via PropertiesChanged.
|
||||
asyncCall(this, call);
|
||||
}
|
||||
|
||||
void CardwireGpu::onPropertiesChanged(const QDBusMessage &message) {
|
||||
const auto arguments = message.arguments();
|
||||
if (arguments.isEmpty() || arguments.first().toString() != GPU_IFACE)
|
||||
return;
|
||||
|
||||
if (arguments.size() > 1) {
|
||||
const auto properties = qdbus_cast<QVariantMap>(arguments.at(1));
|
||||
for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
|
||||
if (it.key() == "Block")
|
||||
this->setBlockedValue(it.value().toBool());
|
||||
else if (it.key() == "Launchable")
|
||||
this->setLaunchableValue(it.value().toBool());
|
||||
}
|
||||
}
|
||||
|
||||
if (arguments.size() > 2) { // invalidated properties: refetch everything
|
||||
const auto invalidated = qdbus_cast<QStringList>(arguments.at(2));
|
||||
if (invalidated.contains("Block") || invalidated.contains("Launchable")) {
|
||||
this->refreshProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardwireGpu::onPowerStateChanged(const QString &state) {
|
||||
emit this->powerStateChanged(state);
|
||||
}
|
||||
|
||||
void CardwireGpu::setBlockedValue(bool blocked) {
|
||||
if (blocked == this->mBlocked)
|
||||
return;
|
||||
this->mBlocked = blocked;
|
||||
qCDebug(logCardwire).nospace()
|
||||
<< this->mObjectPath << ": block = " << blocked;
|
||||
emit this->blockedChanged();
|
||||
}
|
||||
|
||||
void CardwireGpu::setLaunchableValue(bool launchable) {
|
||||
if (launchable == this->mLaunchable)
|
||||
return;
|
||||
this->mLaunchable = launchable;
|
||||
emit this->launchableChanged();
|
||||
}
|
||||
|
||||
Cardwire::Cardwire(QObject *parent) : QObject(parent), mWatcher(this) {}
|
||||
|
||||
Cardwire *Cardwire::instance() {
|
||||
static Cardwire *instance =
|
||||
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
if (instance == nullptr) {
|
||||
registerMetaTypes();
|
||||
instance = new Cardwire();
|
||||
instance->connectSignals();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
Cardwire *Cardwire::create(QQmlEngine * /*qmlEngine*/,
|
||||
QJSEngine * /*jsEngine*/) {
|
||||
auto *instance = Cardwire::instance();
|
||||
QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
|
||||
return instance;
|
||||
}
|
||||
|
||||
CardwireGpu *Cardwire::nvidiaGpu() const {
|
||||
for (auto *gpu : this->mGpus) {
|
||||
if (gpu->isNvidia())
|
||||
return gpu;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Cardwire::connectSignals() {
|
||||
// cardwired runs as a system service, so its API is on the system bus.
|
||||
this->mWatcher.setConnection(QDBusConnection::systemBus());
|
||||
QObject::connect(&this->mWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
|
||||
this, &Cardwire::onServiceOwnerChanged);
|
||||
this->mWatcher.setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
|
||||
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
bus.connect(SERVICE, ROOT_PATH, OBJMGR_IFACE, "InterfacesAdded", this,
|
||||
SLOT(onInterfacesAdded(QDBusMessage)));
|
||||
bus.connect(SERVICE, ROOT_PATH, OBJMGR_IFACE, "InterfacesRemoved", this,
|
||||
SLOT(onInterfacesRemoved(QDBusMessage)));
|
||||
|
||||
// Kick off the initial sync; if the daemon is not running yet the service
|
||||
// watcher triggers another sync once it appears.
|
||||
this->sync();
|
||||
}
|
||||
|
||||
void Cardwire::onServiceOwnerChanged(const QString &serviceName,
|
||||
const QString &oldOwner,
|
||||
const QString &newOwner) {
|
||||
if (serviceName != SERVICE)
|
||||
return;
|
||||
qCInfo(logCardwire).nospace()
|
||||
<< SERVICE << " owner: " << oldOwner << " -> " << newOwner;
|
||||
|
||||
this->sync();
|
||||
}
|
||||
|
||||
void Cardwire::sync() {
|
||||
const auto generation = ++this->mGeneration;
|
||||
auto *watcher =
|
||||
asyncCall(this, methodCall(ROOT_PATH, OBJMGR_IFACE, "GetManagedObjects"));
|
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this,
|
||||
[this, generation]() { this->handleSyncReply(generation); });
|
||||
}
|
||||
|
||||
void Cardwire::handleSyncReply(quint32 generation) {
|
||||
auto *watcher = qobject_cast<QDBusPendingCallWatcher *>(this->sender());
|
||||
if (watcher == nullptr)
|
||||
return;
|
||||
watcher->deleteLater();
|
||||
|
||||
if (generation != this->mGeneration)
|
||||
return; // a newer sync superseded this one
|
||||
|
||||
const QDBusMessage reply = watcher->reply();
|
||||
logError("GetManagedObjects failed", reply);
|
||||
if (reply.type() == QDBusMessage::ErrorMessage) {
|
||||
if (this->mConnected) {
|
||||
this->mConnected = false;
|
||||
emit this->connectedChanged();
|
||||
}
|
||||
|
||||
for (auto *gpu : this->mGpus)
|
||||
gpu->deleteLater();
|
||||
if (!this->mGpus.isEmpty()) {
|
||||
this->mGpus.clear();
|
||||
emit this->gpusChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QList<CardwireGpu *> seen;
|
||||
const auto objects = qdbus_cast<ManagedObjects>(reply.arguments().at(0));
|
||||
for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) {
|
||||
if (!it.value().contains(GPU_IFACE))
|
||||
continue;
|
||||
|
||||
const QString &path = it.key().path();
|
||||
if (!path.startsWith(GPU_PREFIX))
|
||||
continue;
|
||||
|
||||
this->ensureGpu(path);
|
||||
seen.append(this->findGpu(path));
|
||||
}
|
||||
|
||||
// Remove GPUs that disappeared (hotplug).
|
||||
for (auto *gpu : this->mGpus) {
|
||||
if (!seen.contains(gpu))
|
||||
gpu->deleteLater();
|
||||
}
|
||||
|
||||
if (seen != this->mGpus) {
|
||||
this->mGpus = seen;
|
||||
emit this->gpusChanged();
|
||||
}
|
||||
|
||||
if (!this->mConnected) {
|
||||
this->mConnected = true;
|
||||
qCInfo(logCardwire) << "connected to" << SERVICE;
|
||||
emit this->connectedChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Cardwire::ensureGpu(const QString &path) {
|
||||
if (this->findGpu(path) != nullptr)
|
||||
return;
|
||||
|
||||
auto *gpu = new CardwireGpu(path, this);
|
||||
// The nvidia flag only becomes known once GetDevice replies.
|
||||
QObject::connect(gpu, &CardwireGpu::deviceChanged, this,
|
||||
&Cardwire::updateNvidiaGpu);
|
||||
this->mGpus.append(gpu);
|
||||
qCInfo(logCardwire) << "tracking" << path;
|
||||
emit this->gpusChanged();
|
||||
}
|
||||
|
||||
CardwireGpu *Cardwire::findGpu(const QString &path) const {
|
||||
for (auto *gpu : this->mGpus) {
|
||||
if (gpu->objectPath() == path)
|
||||
return gpu;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Cardwire::removeGpu(const QString &path) {
|
||||
auto *gpu = this->findGpu(path);
|
||||
if (gpu == nullptr)
|
||||
return;
|
||||
|
||||
gpu->deleteLater();
|
||||
this->mGpus.removeOne(gpu);
|
||||
qCInfo(logCardwire) << "stopped tracking" << path;
|
||||
emit this->gpusChanged();
|
||||
}
|
||||
|
||||
void Cardwire::onInterfacesAdded(const QDBusMessage &message) {
|
||||
const auto arguments = message.arguments();
|
||||
if (arguments.size() < 2)
|
||||
return;
|
||||
|
||||
const auto path = arguments.first().toString();
|
||||
if (!path.startsWith(GPU_PREFIX))
|
||||
return;
|
||||
|
||||
if (qdbus_cast<InterfacesMap>(arguments.at(1)).contains(GPU_IFACE))
|
||||
this->ensureGpu(path);
|
||||
}
|
||||
|
||||
void Cardwire::onInterfacesRemoved(const QDBusMessage &message) {
|
||||
const auto arguments = message.arguments();
|
||||
if (arguments.size() < 2)
|
||||
return;
|
||||
|
||||
const auto path = arguments.first().toString();
|
||||
if (!path.startsWith(GPU_PREFIX))
|
||||
return;
|
||||
|
||||
if (qdbus_cast<QStringList>(arguments.at(1)).contains(GPU_IFACE))
|
||||
this->removeGpu(path);
|
||||
}
|
||||
|
||||
void Cardwire::updateNvidiaGpu() {
|
||||
auto *current = this->nvidiaGpu();
|
||||
if (current == this->mCurrentNvidiaGpu)
|
||||
return;
|
||||
|
||||
this->mCurrentNvidiaGpu = current;
|
||||
emit this->nvidiaGpuChanged();
|
||||
}
|
||||
129
plugin/src/cardwire.hpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusmessage.h>
|
||||
#include <qdbusservicewatcher.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qstring.h>
|
||||
|
||||
class QDBusPendingCallWatcher;
|
||||
class QJSEngine;
|
||||
class QQmlEngine;
|
||||
|
||||
/// A single GPU exposed by cardwire at
|
||||
/// /org/opengamingcollective/cardwire/Gpu/{id}.
|
||||
///
|
||||
/// Mirrors org.opengamingcollective.cardwire.Gpu: device details are fetched
|
||||
/// once via GetDevice, while Block/Launchable are kept in sync through
|
||||
/// org.freedesktop.DBus.Properties.PropertiesChanged.
|
||||
class CardwireGpu : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Gpu objects are managed by the Cardwire singleton")
|
||||
|
||||
Q_PROPERTY(QString objectPath READ objectPath CONSTANT)
|
||||
Q_PROPERTY(bool known READ known NOTIFY deviceChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY deviceChanged)
|
||||
Q_PROPERTY(bool nvidia READ isNvidia NOTIFY deviceChanged)
|
||||
Q_PROPERTY(bool defaultGpu READ isDefault NOTIFY deviceChanged)
|
||||
Q_PROPERTY(bool discrete READ isDiscrete NOTIFY deviceChanged)
|
||||
Q_PROPERTY(bool available READ isAvailable NOTIFY deviceChanged)
|
||||
Q_PROPERTY(bool blocked READ isBlocked NOTIFY blockedChanged)
|
||||
Q_PROPERTY(bool launchable READ isLaunchable NOTIFY launchableChanged)
|
||||
|
||||
public:
|
||||
explicit CardwireGpu(QString objectPath, QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] const QString &objectPath() const { return this->mObjectPath; }
|
||||
[[nodiscard]] bool known() const { return this->mKnown; }
|
||||
[[nodiscard]] const QString &name() const { return this->mName; }
|
||||
[[nodiscard]] bool isNvidia() const { return this->mNvidia; }
|
||||
[[nodiscard]] bool isDefault() const { return this->mDefault; }
|
||||
[[nodiscard]] bool isDiscrete() const { return this->mDiscrete; }
|
||||
[[nodiscard]] bool isAvailable() const { return this->mAvailable; }
|
||||
[[nodiscard]] bool isBlocked() const { return this->mBlocked; }
|
||||
[[nodiscard]] bool isLaunchable() const { return this->mLaunchable; }
|
||||
|
||||
// Writes the Block property. Only succeeds when the daemon mode allows it
|
||||
// (the daemon rejects the write otherwise and the state stays unchanged).
|
||||
Q_INVOKABLE void setBlocked(bool blocked);
|
||||
|
||||
signals:
|
||||
void deviceChanged();
|
||||
void blockedChanged();
|
||||
void launchableChanged();
|
||||
void powerStateChanged(const QString &state);
|
||||
|
||||
private slots:
|
||||
void onPropertiesChanged(const QDBusMessage &message);
|
||||
void onPowerStateChanged(const QString &state);
|
||||
void onGetDeviceFinished();
|
||||
void onGetAllFinished();
|
||||
|
||||
private:
|
||||
void refreshDevice();
|
||||
void refreshProperties();
|
||||
void setBlockedValue(bool blocked);
|
||||
void setLaunchableValue(bool launchable);
|
||||
|
||||
QString mObjectPath;
|
||||
bool mKnown = false;
|
||||
QString mName;
|
||||
bool mNvidia = false;
|
||||
bool mDefault = false;
|
||||
bool mDiscrete = false;
|
||||
bool mAvailable = false;
|
||||
bool mBlocked = false;
|
||||
bool mLaunchable = false;
|
||||
};
|
||||
|
||||
/// Exposes the GPUs tracked by the cardwire daemon over D-Bus.
|
||||
class Cardwire : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged)
|
||||
Q_PROPERTY(QList<CardwireGpu *> gpus READ gpus NOTIFY gpusChanged)
|
||||
Q_PROPERTY(CardwireGpu *nvidiaGpu READ nvidiaGpu NOTIFY nvidiaGpuChanged)
|
||||
|
||||
public:
|
||||
static Cardwire *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
|
||||
static Cardwire *instance();
|
||||
|
||||
[[nodiscard]] bool isConnected() const { return this->mConnected; }
|
||||
[[nodiscard]] QList<CardwireGpu *> gpus() const { return this->mGpus; }
|
||||
|
||||
/// The first NVIDIA GPU found by the daemon, or nullptr if there is none.
|
||||
[[nodiscard]] CardwireGpu *nvidiaGpu() const;
|
||||
|
||||
signals:
|
||||
void connectedChanged();
|
||||
void gpusChanged();
|
||||
void nvidiaGpuChanged();
|
||||
|
||||
private slots:
|
||||
void onServiceOwnerChanged(const QString &serviceName,
|
||||
const QString &oldOwner, const QString &newOwner);
|
||||
void onInterfacesAdded(const QDBusMessage &message);
|
||||
void onInterfacesRemoved(const QDBusMessage &message);
|
||||
|
||||
private:
|
||||
explicit Cardwire(QObject *parent = nullptr);
|
||||
|
||||
void connectSignals();
|
||||
void sync();
|
||||
void handleSyncReply(quint32 generation);
|
||||
void ensureGpu(const QString &path);
|
||||
[[nodiscard]] CardwireGpu *findGpu(const QString &path) const;
|
||||
void removeGpu(const QString &path);
|
||||
void updateNvidiaGpu();
|
||||
|
||||
QDBusServiceWatcher mWatcher;
|
||||
QList<CardwireGpu *> mGpus;
|
||||
CardwireGpu *mCurrentNvidiaGpu = nullptr;
|
||||
bool mConnected = false;
|
||||
quint32 mGeneration = 0;
|
||||
};
|
||||
30
services/Audio.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string outputName: truncateText(root.displayName(Pipewire.defaultAudioSink), 14)
|
||||
readonly property string inputName: truncateText(root.displayName(Pipewire.defaultAudioSource), 14)
|
||||
readonly property url outputIcon: `${Quickshell.shellPath("assets")}/icons/speaker.svg`
|
||||
readonly property url inputIcon: `${Quickshell.shellPath("assets")}/icons/microphone.svg`
|
||||
readonly property real volume: Pipewire.defaultAudioSink?.audio.volume ?? 0
|
||||
readonly property bool muted: Pipewire.defaultAudioSink?.audio.muted ?? false
|
||||
|
||||
function truncateText(text, maxLength) {
|
||||
return text.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text;
|
||||
}
|
||||
|
||||
function displayName(node: PwNode): string {
|
||||
if (!node)
|
||||
return "none";
|
||||
return node.description || node.name;
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
}
|
||||
}
|
||||
13
services/Battery.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property int percentage: Math.round(UPower.displayDevice.percentage * 100)
|
||||
readonly property real energyRate: UPower.displayDevice.changeRate.toFixed(1)
|
||||
readonly property string iconName: root.percentage < 30 ? "battery-low.svg" : root.percentage < 70 ? "battery-medium.svg" : "battery-full.svg"
|
||||
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/${root.iconName}`
|
||||
}
|
||||
26
services/Cava.qml
Normal file
@@ -0,0 +1,26 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string configPath: {
|
||||
const url = Quickshell.shellPath("assets/cava.conf").toString();
|
||||
return url.startsWith("file://") ? decodeURIComponent(url.substring(7)) : url;
|
||||
}
|
||||
property var values: []
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["cava", "-p", root.configPath]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: line => {
|
||||
root.values = line.split(";").map(Number).filter(v => !Number.isNaN(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
services/GPU.qml
Normal file
@@ -0,0 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Tshell.Cardwire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool connected: Cardwire.connected
|
||||
readonly property CardwireGpu gpu: Cardwire.nvidiaGpu
|
||||
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/nvidia.svg`
|
||||
}
|
||||
50
services/Home.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string host: "100.64.0.3"
|
||||
property int interval: 5000
|
||||
|
||||
// Round-trip time in ms; -1 means unknown/unreachable.
|
||||
property int latency: -1
|
||||
property bool replied: false
|
||||
|
||||
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/home.svg`
|
||||
|
||||
function ping(): void {
|
||||
proc.command = ["ping", "-c", "1", "-W", "2", root.host];
|
||||
root.replied = false;
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: root.interval
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.ping()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: line => {
|
||||
const match = line.match(/time[=<]([\d.]+)\s*ms/);
|
||||
if (match) {
|
||||
root.latency = Math.round(parseFloat(match[1]));
|
||||
root.replied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (!root.replied)
|
||||
root.latency = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
services/Media.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property MprisPlayer player: {
|
||||
const players = [...Mpris.players.values];
|
||||
return players.find(p => p.playbackState === MprisPlaybackState.Playing) ?? players[0] ?? null;
|
||||
}
|
||||
readonly property bool playing: root.player?.isPlaying ?? false
|
||||
readonly property string trackTitle: root.player?.trackTitle ?? ""
|
||||
readonly property string trackArtist: root.player?.trackArtist ?? ""
|
||||
readonly property url icon: {
|
||||
const entry = root.player?.desktopEntry ?? "";
|
||||
return entry !== "" ? Quickshell.iconPath(entry, false) : "";
|
||||
}
|
||||
readonly property string position: root.formatTime(root.player?.position ?? 0)
|
||||
readonly property string length: root.formatTime(root.player?.length ?? 0)
|
||||
|
||||
function truncateText(text, maxLength) {
|
||||
return text.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text;
|
||||
}
|
||||
|
||||
function formatTime(seconds: real): string {
|
||||
const total = Math.max(0, Math.floor(seconds));
|
||||
const minutes = Math.floor(total / 60);
|
||||
return `${minutes}:${(total % 60).toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// `position` does not update reactively on its own; emit the changed
|
||||
// signal every second while playing so bindings stay in sync.
|
||||
Timer {
|
||||
running: root.playing && (root.player?.positionSupported ?? false)
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.player?.positionChanged()
|
||||
}
|
||||
}
|
||||
81
services/Network.qml
Normal file
@@ -0,0 +1,81 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/network.svg`
|
||||
|
||||
property var wifiDevice: {
|
||||
for (const device of Networking.devices.values) {
|
||||
if (device.type === DeviceType.Wifi)
|
||||
return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
property var wiredDevice: {
|
||||
for (const device of Networking.devices.values) {
|
||||
if (device.type === DeviceType.Wired)
|
||||
return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly property bool ethernetConnected: !!(root.wiredDevice && root.wiredDevice.connected)
|
||||
|
||||
readonly property bool wifiConnected: !!(root.wifiDevice && root.wifiDevice.connected)
|
||||
|
||||
readonly property bool wifiConnecting: !!(root.wifiDevice && root.wifiDevice.state === ConnectionState.Connecting)
|
||||
|
||||
property var activeNetwork: null
|
||||
|
||||
function refreshActiveNetwork() {
|
||||
root.activeNetwork = null;
|
||||
|
||||
if (!root.wifiDevice || !root.wifiDevice.networks)
|
||||
return;
|
||||
|
||||
for (const network of root.wifiDevice.networks.values) {
|
||||
if (network.connected) {
|
||||
root.activeNetwork = network;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string statusText: {
|
||||
if (root.ethernetConnected)
|
||||
return "Wired";
|
||||
|
||||
if (root.wifiConnecting)
|
||||
return "Connecting";
|
||||
|
||||
if (root.wifiConnected && root.activeNetwork)
|
||||
return root.activeNetwork.name;
|
||||
|
||||
return "NA";
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wifiDevice ? root.wifiDevice.networks : null
|
||||
|
||||
function onValuesChanged() {
|
||||
root.refreshActiveNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
root.refreshActiveNetwork();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
services/Notifications.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Notifications currently shown in the OSD, tracked by the server.
|
||||
readonly property alias popups: server.trackedNotifications
|
||||
readonly property int popupCount: popups.values.length
|
||||
|
||||
// Do-not-disturb: notifications are recorded but not displayed.
|
||||
property bool dnd: false
|
||||
|
||||
// Maximum amount of simultaneously displayed popups.
|
||||
property int maxPopups: 5
|
||||
|
||||
// History of received notifications (newest first), for future dashboards.
|
||||
property var history: []
|
||||
property int historyLimit: 100
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
|
||||
bodySupported: true
|
||||
bodyMarkupSupported: true
|
||||
actionsSupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
root.addHistory(notification);
|
||||
|
||||
if (root.dnd || notification.transient)
|
||||
return;
|
||||
|
||||
notification.tracked = true;
|
||||
|
||||
const tracked = server.trackedNotifications.values;
|
||||
if (tracked.length > root.maxPopups)
|
||||
tracked[0].expire();
|
||||
}
|
||||
}
|
||||
|
||||
function clearHistory(): void {
|
||||
root.history = [];
|
||||
}
|
||||
|
||||
function addHistory(notification: Notification): void {
|
||||
const entry = {
|
||||
id: notification.id,
|
||||
appName: notification.appName,
|
||||
appIcon: notification.appIcon,
|
||||
desktopEntry: notification.desktopEntry,
|
||||
summary: notification.summary,
|
||||
body: notification.body,
|
||||
image: notification.image,
|
||||
urgency: notification.urgency,
|
||||
transient: notification.transient,
|
||||
time: Date.now(),
|
||||
actions: notification.actions.map(action => ({
|
||||
identifier: action.identifier,
|
||||
text: action.text
|
||||
}))
|
||||
};
|
||||
|
||||
let next = [entry, ...root.history];
|
||||
if (next.length > root.historyLimit)
|
||||
next = next.slice(0, root.historyLimit);
|
||||
root.history = next;
|
||||
}
|
||||
}
|
||||
21
services/PowerProfile.qml
Normal file
@@ -0,0 +1,21 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower as UPower
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string name: {
|
||||
switch (UPower.PowerProfiles.profile) {
|
||||
case UPower.PowerProfile.Performance:
|
||||
return "Performance";
|
||||
case UPower.PowerProfile.PowerSaver:
|
||||
return "Power Saver";
|
||||
default:
|
||||
return "Balanced";
|
||||
}
|
||||
}
|
||||
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/power.svg`
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import Quickshell
|
||||
import qs.windows
|
||||
|
||||
Scope {
|
||||
Bar {}
|
||||
Notification {}
|
||||
Volume {}
|
||||
}
|
||||
|
||||
46
ui/BarButton.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.config
|
||||
|
||||
Button {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property url iconSource: ""
|
||||
property bool iconOnly: false
|
||||
property bool hoverHighlight: true
|
||||
property bool pointerCursor: true
|
||||
|
||||
padding: Theme.padding
|
||||
display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
|
||||
text: label
|
||||
font.family: Theme.font.family
|
||||
font.pointSize: Theme.font.pointSize
|
||||
palette.buttonText: Theme.colors.foreground
|
||||
icon.color: Theme.colors.foreground
|
||||
icon.source: iconSource
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.pointerCursor
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radius
|
||||
color: {
|
||||
if (!root.hoverHighlight)
|
||||
return Theme.colors.inActive;
|
||||
return root.hovered ? Theme.colors.inActive : Theme.colors.background;
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ui/ExclusionZone.qml
Normal file
@@ -0,0 +1,12 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
PanelWindow {
|
||||
property string name
|
||||
|
||||
implicitWidth: 0
|
||||
implicitHeight: 0
|
||||
WlrLayershell.namespace: `quickshell:${name}ExclusionZone`
|
||||
}
|
||||
66
ui/RoundedCorner.qml
Normal file
@@ -0,0 +1,66 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
|
||||
WrapperItem {
|
||||
id: root
|
||||
|
||||
property int corner
|
||||
property real radius: Theme.radius
|
||||
property color color
|
||||
|
||||
Component.onCompleted: {
|
||||
switch (corner) {
|
||||
case 0:
|
||||
anchors.left = parent.left;
|
||||
anchors.top = parent.top;
|
||||
break;
|
||||
case 1:
|
||||
anchors.top = parent.top;
|
||||
anchors.right = parent.right;
|
||||
rotation = 90;
|
||||
break;
|
||||
case 2:
|
||||
anchors.right = parent.right;
|
||||
anchors.bottom = parent.bottom;
|
||||
rotation = 180;
|
||||
break;
|
||||
case 3:
|
||||
anchors.left = parent.left;
|
||||
anchors.bottom = parent.bottom;
|
||||
rotation = -90;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: root.color
|
||||
startX: root.radius
|
||||
|
||||
PathArc {
|
||||
relativeX: -root.radius
|
||||
relativeY: root.radius
|
||||
radiusX: root.radius
|
||||
radiusY: radiusX
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -root.radius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: root.radius
|
||||
relativeY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
windows/Bar.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.ui
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
property color barColor: Theme.colors.background
|
||||
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
mask: Region {
|
||||
item: cornersArea
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
component BarSection: Rectangle {
|
||||
id: section
|
||||
|
||||
property var widgets: []
|
||||
property int alignment: Qt.AlignLeft
|
||||
|
||||
color: "transparent"
|
||||
implicitHeight: parent.height
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
id: secRow
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2
|
||||
spacing: Theme.spacing
|
||||
|
||||
Repeater {
|
||||
model: section.widgets
|
||||
|
||||
Loader {
|
||||
required property var modelData
|
||||
|
||||
source: modelData
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exclusions
|
||||
Scope {
|
||||
ExclusionZone {
|
||||
name: "left"
|
||||
exclusiveZone: leftBar.implicitWidth
|
||||
anchors.left: true
|
||||
}
|
||||
ExclusionZone {
|
||||
name: "top"
|
||||
exclusiveZone: topBar.implicitHeight
|
||||
anchors.top: true
|
||||
}
|
||||
ExclusionZone {
|
||||
name: "right"
|
||||
exclusiveZone: rightBar.implicitWidth
|
||||
anchors.right: true
|
||||
}
|
||||
ExclusionZone {
|
||||
name: "bottom"
|
||||
exclusiveZone: bottomBar.implicitHeight
|
||||
anchors.bottom: true
|
||||
}
|
||||
}
|
||||
|
||||
// Bars
|
||||
Rectangle {
|
||||
id: leftBar
|
||||
|
||||
implicitWidth: Theme.bar.thickness
|
||||
implicitHeight: QsWindow.window?.height ?? 0
|
||||
color: window.barColor
|
||||
anchors.left: parent.left
|
||||
}
|
||||
Rectangle {
|
||||
id: rightBar
|
||||
|
||||
implicitWidth: Theme.bar.thickness
|
||||
implicitHeight: QsWindow.window?.height ?? 0
|
||||
color: window.barColor
|
||||
anchors.right: parent.right
|
||||
}
|
||||
Rectangle {
|
||||
id: bottomBar
|
||||
|
||||
implicitWidth: QsWindow.window?.width ?? 0
|
||||
implicitHeight: Theme.bar.thickness
|
||||
color: window.barColor
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
Rectangle {
|
||||
id: topBar
|
||||
|
||||
implicitWidth: QsWindow.window?.width ?? 0
|
||||
implicitHeight: Theme.bar.height
|
||||
color: window.barColor
|
||||
anchors.top: parent.top
|
||||
|
||||
FlexboxLayout {
|
||||
id: flexLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.margin
|
||||
|
||||
wrap: FlexboxLayout.Wrap
|
||||
direction: FlexboxLayout.Row
|
||||
justifyContent: FlexboxLayout.JustifySpaceBetween
|
||||
|
||||
BarSection {
|
||||
widgets: BarLayout.left
|
||||
alignment: Qt.AlignLeft
|
||||
}
|
||||
BarSection {
|
||||
widgets: BarLayout.center
|
||||
alignment: Qt.AlignHCenter
|
||||
}
|
||||
BarSection {
|
||||
widgets: BarLayout.right
|
||||
alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cornersArea
|
||||
|
||||
implicitWidth: QsWindow.window?.width - (leftBar.implicitWidth + rightBar.implicitWidth)
|
||||
implicitHeight: QsWindow.window?.height - (topBar.implicitHeight + bottomBar.implicitHeight)
|
||||
color: "transparent"
|
||||
x: leftBar.implicitWidth
|
||||
y: topBar.implicitHeight
|
||||
|
||||
Repeater {
|
||||
model: 4
|
||||
|
||||
RoundedCorner {
|
||||
required property int modelData
|
||||
|
||||
corner: modelData
|
||||
color: window.barColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
windows/Notification.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
import "../modules/osd"
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
visible: Notifications.popupCount > 0
|
||||
implicitWidth: Theme.notification.width
|
||||
implicitHeight: stack.implicitHeight
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
margins {
|
||||
top: Theme.bar.height + Theme.margin
|
||||
right: Theme.bar.thickness + Theme.margin
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: stack
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: stack
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.spacing
|
||||
|
||||
Repeater {
|
||||
model: [...Notifications.popups.values].reverse()
|
||||
|
||||
NotificationCard {
|
||||
id: card
|
||||
|
||||
required property var modelData
|
||||
|
||||
notif: modelData
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
windows/Volume.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
import "../modules/osd"
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool shown: false
|
||||
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
visible: shown
|
||||
implicitWidth: card.implicitWidth
|
||||
implicitHeight: card.implicitHeight
|
||||
|
||||
anchors.bottom: true
|
||||
margins.bottom: screen.height * Theme.volume.bottomMarginRatio
|
||||
|
||||
// An empty click mask prevents the window from blocking mouse events.
|
||||
mask: Region {}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
|
||||
function onVolumeChanged() {
|
||||
root.shown = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root.shown = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: Theme.volume.timeout
|
||||
onTriggered: root.shown = false
|
||||
}
|
||||
|
||||
VolumeCard {
|
||||
id: card
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||