Compare commits

..

21 Commits

Author SHA1 Message Date
tux
352eb99568 style: format code 2026-09-11 02:10:39 +05:30
tux
f2268f65e7 feat(cava): update bar opacity using level 2026-09-04 22:56:32 +05:30
tux
6357b949b3 feat(bar): add smooth sliding highlight to workspaces 2026-09-04 22:55:22 +05:30
tux
77c56f9945 feat(bar): add network widget and service 2026-09-04 01:36:50 +05:30
tux
570eea59ae feat(cava): enhance visualizer sensitivity and response 2026-09-04 00:10:11 +05:30
tux
c9310e802b feat(gpu): add cardwire integration and gpu widget 2026-08-25 00:51:36 +05:30
tux
4453c280bf build(nix): add package derivation for tshell 2026-08-24 20:38:17 +05:30
tux
51906373aa refactor(flake): inline qmlformat program config 2026-08-24 20:05:36 +05:30
tux
8f8239727e style(theme): update poimandres base16 dark colors 2026-08-24 19:26:54 +05:30
tux
5299945cfb feat(bar): add music widget to bar layout 2026-08-24 18:32:02 +05:30
tux
abd48a9c66 feat(osd): add volume osd popup window 2026-08-24 18:18:10 +05:30
tux
28aa79dd13 style(bar): disable hover highlight on icon buttons 2026-08-24 17:58:46 +05:30
tux
dfab12482a style(cava): update bar count and remove animation 2026-08-24 17:54:58 +05:30
tux
33c19e35a5 refactor(audio): truncate device names 2026-08-24 17:47:38 +05:30
tux
198ce4ff67 feat(theme): add base16 theme support 2026-08-24 17:47:16 +05:30
tux
e1906e5528 refactor(theme): centralize theme 2026-08-24 16:42:33 +05:30
tux
5d84d68e26 refactor(battery): format energy rate output 2026-08-24 15:53:21 +05:30
tux
d53f375fa7 refactor(power): remove unused performanceAvailable property 2026-08-24 15:51:16 +05:30
tux
adfd353e85 style: format qml files 2026-08-24 15:45:02 +05:30
tux
ca7ac0696b chore(flake): enable qmlformat formatter 2026-08-24 15:44:27 +05:30
tux
de3c9274b5 feat(bar): add audio indicator widget 2026-08-24 15:43:43 +05:30
44 changed files with 1835 additions and 578 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
result
result-bin
plugin/build/
plugin/dist/

View File

@@ -32,17 +32,19 @@ shell.qml Entry point: Scope { Bar {}; Notification {} }
│ └── Notification.qml Popup notification stack window │ └── Notification.qml Popup notification stack window
├── modules/ Feature widgets, grouped by surface ├── modules/ Feature widgets, grouped by surface
│ ├── bar/ Widgets placed in the bar (Clock, Workspaces, │ ├── bar/ Widgets placed in the bar (Clock, Workspaces,
│ │ BatteryIndicator, CavaVisualizer, SystemTray, │ │ BatteryIndicator, AudioWidget, CavaVisualizer,
│ │ LauncherButton, PowerProfileWidget, GhostButton) │ │ SystemTray, LauncherButton, PowerProfileWidget,
│ │ GhostButton)
│ └── osd/ NotificationCard.qml │ └── osd/ NotificationCard.qml
├── services/ QML Singletons exposing system state ├── services/ QML Singletons exposing system state
│ ├── Time.qml SystemClock → formatted time string │ ├── Time.qml SystemClock → formatted time string
│ ├── Battery.qml UPower display device → percentage/icon │ ├── Battery.qml UPower display device → percentage/icon
│ ├── Audio.qml PipeWire default sink/source → device names/icons
│ ├── PowerProfile.qml UPower power profiles → name/icon │ ├── PowerProfile.qml UPower power profiles → name/icon
│ ├── Cava.qml Spawns `cava`, parses stdout into `values[]` │ ├── Cava.qml Spawns `cava`, parses stdout into `values[]`
│ └── Notifications.qml NotificationServer: popups, DND flag, history │ └── Notifications.qml NotificationServer: popups, DND flag, history
├── config/ QML Singletons for configuration/theming ├── config/ QML Singletons for configuration/theming
│ ├── Appearance.qml colors, font, margin/radius/spacing/padding/duration │ ├── 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 │ └── BarLayout.qml Which widgets go in left/center/right bar sections
├── ui/ Shared reusable primitives ├── ui/ Shared reusable primitives
│ ├── BarButton.qml Themed button used by bar widgets │ ├── BarButton.qml Themed button used by bar widgets
@@ -64,6 +66,6 @@ shell.qml Entry point: Scope { Bar {}; Notification {} }
- Start every QML file with `pragma ComponentBehavior: Bound` where bindings reference outer ids. - 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. - 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 `Appearance.*` rather than hardcoding colors/sizes/fonts. - 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. - 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): ...`). - Commit messages follow Conventional Commits style (`feat(notifications): ...`, `refactor(bar): ...`).

View File

@@ -1,6 +1,7 @@
[general] [general]
bars = 12 bars = 14
framerate = 60 framerate = 60
sensitivity=140
[input] [input]
method = pulse method = pulse

View 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
View 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

35
assets/icons/nvidia.svg Normal file
View 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

5
assets/icons/speaker.svg Normal file
View 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

View File

@@ -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"
}
}

View File

@@ -5,28 +5,15 @@ import qs.ui
import qs.services import qs.services
Singleton { Singleton {
id: root id: root
function widget(name: string): url { function widget(name: string): url {
return `${Quickshell.shellPath("modules/bar")}/${name}.qml`; return `${Quickshell.shellPath("modules/bar")}/${name}.qml`;
} }
property var left: [ property var left: [root.widget("LauncherButton"), root.widget("NetworkWidget"), root.widget("PowerProfileWidget"), root.widget("GPUWidget"), root.widget("HomeWidget"), root.widget("MusicWidget")]
root.widget("LauncherButton"),
root.widget("PowerProfileWidget"),
root.widget("HomeWidget"),
]
property var center: [ property var center: [root.widget("CavaVisualizer"), root.widget("Workspaces"), root.widget("CavaVisualizer"),]
root.widget("CavaVisualizer"),
root.widget("Workspaces"),
root.widget("CavaVisualizer"),
]
property var right: [ property var right: [root.widget("AudioWidget"), root.widget("BatteryIndicator"), root.widget("SystemTrayWidget"), root.widget("ClockWidget"), root.widget("GhostButton")]
root.widget("BatteryIndicator"),
root.widget("SystemTrayWidget"),
root.widget("ClockWidget"),
root.widget("GhostButton")
]
} }

150
config/Theme.qml Normal file
View 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
}
}

View File

@@ -15,41 +15,63 @@
}; };
}; };
outputs = outputs = {
{ self,
self, nixpkgs,
nixpkgs, quickshell,
quickshell, treefmt-nix,
treefmt-nix, }: let
}: system = "x86_64-linux";
let pkgs = nixpkgs.legacyPackages.${system};
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
quickshellWithModules = quickshell.packages.${system}.default.withModules [ quickshellWithModules = quickshell.packages.${system}.default.withModules [
pkgs.qt6.qtbase pkgs.qt6.qtbase
pkgs.qt6.qtdeclarative pkgs.qt6.qtdeclarative
pkgs.qt6.qtmultimedia pkgs.qt6.qtmultimedia
pkgs.qt6.qttranslations pkgs.qt6.qttranslations
]; ];
treefmtEval = treefmt-nix.lib.evalModule pkgs { treefmtEval = treefmt-nix.lib.evalModule pkgs {
projectRootFile = "flake.nix"; projectRootFile = "flake.nix";
programs = { programs = {
nixfmt.enable = true; alejandra.enable = true;
}; qmlformat.enable = true;
}; clang-format.enable = true;
in
{
formatter.${system} = treefmtEval.config.build.wrapper;
devShells.${system}.default = pkgs.mkShell {
packages = [
quickshellWithModules
pkgs.cava
pkgs.wallust
];
}; };
}; };
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
'';
};
};
} }

View 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
}
}

View File

@@ -4,8 +4,8 @@ import qs.ui
import qs.services import qs.services
BarButton { BarButton {
hoverHighlight: false hoverHighlight: false
pointerCursor: false pointerCursor: false
label: `${Battery.percentage}% ${Battery.energyRate}W` label: `${Battery.percentage}% ${Battery.energyRate}W`
iconSource: Battery.icon iconSource: Battery.icon
} }

View File

@@ -5,47 +5,55 @@ import qs.config
import qs.services import qs.services
Rectangle { Rectangle {
id: root id: root
property int barCount: 14 property int barCount: 14
property real maxValue: 100 property real maxValue: 100
readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001) readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001)
implicitWidth: 96 implicitWidth: Theme.cava.width
implicitHeight: 32 implicitHeight: Theme.cava.height
radius: Appearance.radius radius: Theme.radius
color: Appearance.colors.inActive color: Theme.colors.inActive
visible: shouldVisualize visible: shouldVisualize
Row { Row {
id: barRow id: barRow
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: Theme.cava.innerMargin
spacing: 3 spacing: Theme.cava.barSpacing
Repeater { Repeater {
model: root.barCount model: root.barCount
Rectangle { Rectangle {
id: bar id: bar
required property int index required property int index
readonly property real value: Cava.values[index] ?? 0 readonly property real level: Math.min(1, (Cava.values[index] ?? 0) / root.maxValue)
width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount
height: Math.max(2, (bar.value / root.maxValue) * barRow.height) height: Math.max(Theme.cava.barMinHeight, Math.pow(bar.level, 0.6) * barRow.height)
anchors.bottom: parent.bottom anchors.verticalCenter: parent.verticalCenter
radius: bar.width / 2 radius: bar.width / 2
color: Appearance.colors.accent color: Theme.colors.accent
antialiasing: true antialiasing: true
opacity: 0.45 + (bar.level * 0.55)
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Appearance.duration duration: 90
} easing.type: Easing.OutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
} }
}
} }
}
} }

View File

@@ -4,7 +4,7 @@ import qs.ui
import qs.services import qs.services
BarButton { BarButton {
hoverHighlight: false hoverHighlight: false
pointerCursor: false pointerCursor: false
label: Time.time label: Time.time
} }

23
modules/bar/GPUWidget.qml Normal file
View 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";
}
}

View File

@@ -4,6 +4,7 @@ import Quickshell
import qs.ui import qs.ui
BarButton { BarButton {
iconOnly: true iconOnly: true
iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg` hoverHighlight: false
iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg`
} }

View File

@@ -4,8 +4,8 @@ import qs.ui
import qs.services import qs.services
BarButton { BarButton {
hoverHighlight: false hoverHighlight: false
pointerCursor: false pointerCursor: false
label: Home.latency >= 0 ? `${Home.latency} ms` : "NA" label: Home.latency >= 0 ? `${Home.latency} ms` : "NA"
iconSource: Home.icon iconSource: Home.icon
} }

View File

@@ -4,7 +4,8 @@ import Quickshell
import qs.ui import qs.ui
BarButton { BarButton {
iconOnly: true iconOnly: true
iconSource: `${Quickshell.shellPath("assets")}/icons/nix.svg` hoverHighlight: false
onClicked: Quickshell.execDetached(["vicinae", "toggle"]) iconSource: `${Quickshell.shellPath("assets")}/icons/nix.svg`
onClicked: Quickshell.execDetached(["vicinae", "toggle"])
} }

View 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}`
}

View 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
}

View File

@@ -4,8 +4,8 @@ import qs.ui
import qs.services import qs.services
BarButton { BarButton {
hoverHighlight: false hoverHighlight: false
pointerCursor: false pointerCursor: false
label: PowerProfile.name label: PowerProfile.name
iconSource: PowerProfile.icon iconSource: PowerProfile.icon
} }

View File

@@ -6,27 +6,25 @@ import qs.config
import qs.ui import qs.ui
BarButton { BarButton {
id: root id: root
property int iconSize: 16 hoverHighlight: false
pointerCursor: false
visible: SystemTray.items.values.length > 0
hoverHighlight: false contentItem: Row {
pointerCursor: false spacing: Theme.spacing
visible: SystemTray.items.values.length > 0
contentItem: Row { Repeater {
spacing: Appearance.spacing model: SystemTray.items.values
Repeater { Image {
model: SystemTray.items.values required property var modelData
Image { source: modelData.icon
required property var modelData width: Theme.tray.iconSize
height: Theme.tray.iconSize
source: modelData.icon }
width: root.iconSize }
height: root.iconSize
}
} }
}
} }

View File

@@ -4,57 +4,141 @@ import QtQuick
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.config import qs.config
Row { Item {
id: root id: root
property int count: 7 property int count: 7
spacing: Appearance.spacing implicitWidth: pillsRow.implicitWidth
implicitHeight: Theme.workspaces.height
Behavior on implicitWidth {
NumberAnimation {
duration: Theme.duration
easing.type: Easing.OutQuint
}
}
Repeater { Row {
model: root.count 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 { Rectangle {
id: pill id: highlight
required property int index z: 2
property var ws: Hyprland.workspaces.values.find(w => w.id === pill.index + 1) radius: Theme.workspaces.radius
property bool isActive: Hyprland.focusedWorkspace?.id === (pill.index + 1) color: Theme.colors.accent
radius: 1000 readonly property real activeW: Theme.workspaces.height * Theme.workspaces.activeWidthRatio
implicitHeight: 15 readonly property real inactiveW: Theme.workspaces.height
implicitWidth: pill.isActive ? pill.implicitHeight * 2.3 : pill.implicitHeight property int curIdx: Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id - 1 : -1
color: { property int prevIdx: 0
if (handler.hovered)
return Appearance.colors.accent;
if (pill.isActive || pill.ws)
return Appearance.colors.accent;
return Appearance.colors.inActive;
}
MouseArea { function getX(index, activeIndex) {
hoverEnabled: true if (index < 0)
anchors.fill: parent return 0;
cursorShape: Qt.PointingHandCursor let x = 0;
onClicked: Hyprland.dispatch("workspace " + (pill.index + 1)) for (let i = 0; i < index; i++)
} x += (i === activeIndex ? activeW : inactiveW) + pillsRow.spacing;
return x;
HoverHandler {
id: handler
}
Behavior on color {
ColorAnimation {
duration: Appearance.duration
} }
}
Behavior on implicitWidth { property real targetLeft: curIdx >= 0 ? getX(curIdx, curIdx) : 0
NumberAnimation { property real targetRight: curIdx >= 0 ? targetLeft + activeW : 0
duration: Appearance.duration property real actualLeft: targetLeft
easing.type: Easing.OutQuad 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
}
} }
}
} }
}
} }

View File

@@ -7,111 +7,110 @@ import Quickshell.Services.Notifications
import qs.config import qs.config
Rectangle { Rectangle {
id: root id: root
property Notification notif property Notification notif
readonly property bool critical: notif?.urgency === NotificationUrgency.Critical readonly property bool critical: notif?.urgency === NotificationUrgency.Critical
readonly property int timeoutDuration: { readonly property int timeoutDuration: {
const seconds = Math.round((notif?.expireTimeout ?? 0) / 1000); const seconds = Math.round((notif?.expireTimeout ?? 0) / 1000);
return seconds > 0 ? seconds : 5; return seconds > 0 ? seconds : 5;
} }
readonly property string iconName: { readonly property string iconName: {
if (!notif) if (!notif)
return ""; return "";
const hints = notif.hints ?? {}; const hints = notif.hints ?? {};
const hinted = String(hints["image-path"] ?? hints["image_path"] ?? hints["icon_path"] ?? ""); const hinted = String(hints["image-path"] ?? hints["image_path"] ?? hints["icon_path"] ?? "");
return notif.appIcon || notif.desktopEntry || hinted; return notif.appIcon || notif.desktopEntry || hinted;
} }
readonly property url themedIconUrl: iconName !== "" ? Quickshell.iconPath(iconName, false) : "" readonly property url themedIconUrl: iconName !== "" ? Quickshell.iconPath(iconName, false) : ""
readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/notification.svg` readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/notification.svg`
implicitWidth: 360 implicitWidth: Theme.notification.width
implicitHeight: content.implicitHeight + Appearance.padding * 2 implicitHeight: content.implicitHeight + Theme.padding * 2
radius: Appearance.radius radius: Theme.radius
color: Appearance.colors.background color: Theme.colors.background
border.width: critical ? 1 : 0 border.width: critical ? Theme.notification.borderWidth : 0
border.color: Appearance.colors.accent border.color: Theme.colors.accent
Timer { Timer {
id: countdown id: countdown
interval: root.timeoutDuration * 1000 interval: root.timeoutDuration * 1000
repeat: false repeat: false
running: !root.critical && !hovered.hovered && visible running: !root.critical && !hovered.hovered && visible
onTriggered: root.notif.expire() onTriggered: root.notif.expire()
}
HoverHandler {
id: hovered
}
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding
spacing: Appearance.spacing
Item {
width: 40
height: 40
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 { HoverHandler {
spacing: 2 id: hovered
Layout.fillWidth: true }
Layout.alignment: Qt.AlignVCenter
RowLayout {
Text { id: content
text: root.notif?.appName ?? ""
color: Appearance.colors.accent anchors.left: parent.left
font.family: Appearance.font.family anchors.right: parent.right
font.pointSize: Appearance.font.pointSize - 2 anchors.top: parent.top
elide: Text.ElideRight anchors.margins: Theme.padding
Layout.fillWidth: true spacing: Theme.spacing
}
Text { Item {
text: root.notif?.summary ?? "" width: Theme.notification.iconSize
color: Appearance.colors.foreground height: Theme.notification.iconSize
font.family: Appearance.font.family Layout.alignment: Qt.AlignTop
font.pointSize: Appearance.font.pointSize
font.weight: Font.DemiBold Image {
elide: Text.ElideRight anchors.fill: parent
Layout.fillWidth: true source: root.iconUrl
} fillMode: Image.PreserveAspectFit
Text { smooth: true
text: root.notif?.body ?? "" }
visible: text !== ""
color: Appearance.colors.foreground Image {
opacity: 0.8 anchors.fill: parent
font.family: Appearance.font.family source: root.themedIconUrl
font.pointSize: Appearance.font.pointSize - 1 visible: root.themedIconUrl !== "" && status === Image.Ready
wrapMode: Text.Wrap fillMode: Image.PreserveAspectFit
maximumLineCount: 3 smooth: true
elide: Text.ElideRight }
textFormat: Text.PlainText }
Layout.fillWidth: 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
}
}
} }
}
} }

View 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
View 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
View 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
View 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
View 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
View 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]
}
}

View File

@@ -7,7 +7,7 @@ import Quickshell.Services.UPower
Singleton { Singleton {
id: root id: root
readonly property int percentage: Math.round(UPower.displayDevice.percentage * 100) readonly property int percentage: Math.round(UPower.displayDevice.percentage * 100)
readonly property real energyRate: UPower.displayDevice.changeRate 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 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}` readonly property url icon: `${Quickshell.shellPath("assets")}/icons/${root.iconName}`
} }

View File

@@ -5,22 +5,22 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
Singleton { Singleton {
id: root id: root
readonly property string configPath: { readonly property string configPath: {
const url = Quickshell.shellPath("assets/cava.conf").toString(); const url = Quickshell.shellPath("assets/cava.conf").toString();
return url.startsWith("file://") ? decodeURIComponent(url.substring(7)) : url; return url.startsWith("file://") ? decodeURIComponent(url.substring(7)) : url;
} }
property var values: [] property var values: []
Process { Process {
running: true running: true
command: ["cava", "-p", root.configPath] command: ["cava", "-p", root.configPath]
stdout: SplitParser { stdout: SplitParser {
onRead: line => { onRead: line => {
root.values = line.split(";").map(Number).filter(v => !Number.isNaN(v)); root.values = line.split(";").map(Number).filter(v => !Number.isNaN(v));
} }
}
} }
}
} }

12
services/GPU.qml Normal file
View 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`
}

43
services/Media.qml Normal file
View 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
View 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();
}
}
}

View File

@@ -7,7 +7,6 @@ import Quickshell.Services.UPower as UPower
Singleton { Singleton {
id: root id: root
readonly property bool performanceAvailable: UPower.PowerProfiles.hasPerformanceProfile
readonly property string name: { readonly property string name: {
switch (UPower.PowerProfiles.profile) { switch (UPower.PowerProfiles.profile) {
case UPower.PowerProfile.Performance: case UPower.PowerProfile.Performance:

View File

@@ -4,5 +4,5 @@ import qs.windows
Scope { Scope {
Bar {} Bar {}
Notification {} Notification {}
Volume {}
} }

View File

@@ -5,42 +5,42 @@ import QtQuick.Controls
import qs.config import qs.config
Button { Button {
id: root id: root
property string label: "" property string label: ""
property url iconSource: "" property url iconSource: ""
property bool iconOnly: false property bool iconOnly: false
property bool hoverHighlight: true property bool hoverHighlight: true
property bool pointerCursor: true property bool pointerCursor: true
padding: Appearance.padding padding: Theme.padding
display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
text: label text: label
font.family: Appearance.font.family font.family: Theme.font.family
font.pointSize: Appearance.font.pointSize font.pointSize: Theme.font.pointSize
palette.buttonText: Appearance.colors.foreground palette.buttonText: Theme.colors.foreground
icon.color: Appearance.colors.foreground icon.color: Theme.colors.foreground
icon.source: iconSource icon.source: iconSource
HoverHandler { HoverHandler {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: root.pointerCursor enabled: root.pointerCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: {
if (!root.hoverHighlight)
return Appearance.colors.inActive;
return root.hovered ? Appearance.colors.inActive : Appearance.colors.background;
} }
Behavior on color { background: Rectangle {
ColorAnimation { anchors.fill: parent
duration: Appearance.duration 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
}
}
} }
}
} }

View File

@@ -4,9 +4,9 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
PanelWindow { PanelWindow {
property string name property string name
implicitWidth: 0 implicitWidth: 0
implicitHeight: 0 implicitHeight: 0
WlrLayershell.namespace: `quickshell:${name}ExclusionZone` WlrLayershell.namespace: `quickshell:${name}ExclusionZone`
} }

View File

@@ -6,61 +6,61 @@ import Quickshell.Widgets
import qs.config import qs.config
WrapperItem { WrapperItem {
id: root id: root
property int corner property int corner
property real radius: Appearance.radius property real radius: Theme.radius
property color color property color color
Component.onCompleted: { Component.onCompleted: {
switch (corner) { switch (corner) {
case 0: case 0:
anchors.left = parent.left; anchors.left = parent.left;
anchors.top = parent.top; anchors.top = parent.top;
break; break;
case 1: case 1:
anchors.top = parent.top; anchors.top = parent.top;
anchors.right = parent.right; anchors.right = parent.right;
rotation = 90; rotation = 90;
break; break;
case 2: case 2:
anchors.right = parent.right; anchors.right = parent.right;
anchors.bottom = parent.bottom; anchors.bottom = parent.bottom;
rotation = 180; rotation = 180;
break; break;
case 3: case 3:
anchors.left = parent.left; anchors.left = parent.left;
anchors.bottom = parent.bottom; anchors.bottom = parent.bottom;
rotation = -90; rotation = -90;
break; break;
}
} }
}
Shape { Shape {
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
strokeWidth: 0 strokeWidth: 0
fillColor: root.color fillColor: root.color
startX: root.radius startX: root.radius
PathArc { PathArc {
relativeX: -root.radius relativeX: -root.radius
relativeY: root.radius relativeY: root.radius
radiusX: root.radius radiusX: root.radius
radiusY: radiusX radiusY: radiusX
direction: PathArc.Counterclockwise direction: PathArc.Counterclockwise
} }
PathLine { PathLine {
relativeX: 0 relativeX: 0
relativeY: -root.radius relativeY: -root.radius
} }
PathLine { PathLine {
relativeX: root.radius relativeX: root.radius
relativeY: 0 relativeY: 0
} }
}
} }
}
} }

View File

@@ -8,154 +8,154 @@ import qs.config
import qs.ui import qs.ui
PanelWindow { PanelWindow {
id: window id: window
property color barColor: Appearance.colors.background 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" color: "transparent"
implicitHeight: parent.height exclusionMode: ExclusionMode.Ignore
Layout.fillWidth: true mask: Region {
item: cornersArea
intersection: Intersection.Subtract
}
RowLayout { anchors {
id: secRow left: true
top: true
right: true
bottom: true
}
anchors.verticalCenter: parent.verticalCenter component BarSection: Rectangle {
x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2 id: section
spacing: Appearance.spacing
Repeater { property var widgets: []
model: section.widgets property int alignment: Qt.AlignLeft
Loader { color: "transparent"
required property var modelData implicitHeight: parent.height
Layout.fillWidth: true
source: modelData RowLayout {
Layout.alignment: Qt.AlignVCenter 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 // Exclusions
Scope { Scope {
ExclusionZone { ExclusionZone {
name: "left" name: "left"
exclusiveZone: leftBar.implicitWidth exclusiveZone: leftBar.implicitWidth
anchors.left: true 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
}
} }
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 // Bars
Rectangle { Rectangle {
id: leftBar id: leftBar
implicitWidth: 10 implicitWidth: Theme.bar.thickness
implicitHeight: QsWindow.window?.height ?? 0 implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor
anchors.left: parent.left
}
Rectangle {
id: rightBar
implicitWidth: 10
implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor
anchors.right: parent.right
}
Rectangle {
id: bottomBar
implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 10
color: window.barColor
anchors.bottom: parent.bottom
}
Rectangle {
id: topBar
implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 50
color: window.barColor
anchors.top: parent.top
FlexboxLayout {
id: flexLayout
anchors.fill: parent
anchors.margins: Appearance.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 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
}
}
} }
}
} }

View File

@@ -8,48 +8,46 @@ import qs.services
import "../modules/osd" import "../modules/osd"
PanelWindow { PanelWindow {
id: root id: root
readonly property int cardWidth: 420 color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: Notifications.popupCount > 0
implicitWidth: Theme.notification.width
implicitHeight: stack.implicitHeight
color: "transparent" anchors {
exclusionMode: ExclusionMode.Ignore top: true
visible: Notifications.popupCount > 0 right: true
implicitWidth: root.cardWidth }
implicitHeight: stack.implicitHeight margins {
top: Theme.bar.height + Theme.margin
anchors { right: Theme.bar.thickness + Theme.margin
top: true }
right: true
} mask: Region {
margins { item: stack
top: 50 + Appearance.margin }
right: 10 + Appearance.margin
} ColumnLayout {
id: stack
mask: Region {
item: stack anchors.top: parent.top
} anchors.right: parent.right
anchors.left: parent.left
ColumnLayout { spacing: Theme.spacing
id: stack
Repeater {
anchors.top: parent.top model: [...Notifications.popups.values].reverse()
anchors.right: parent.right
anchors.left: parent.left NotificationCard {
spacing: Appearance.spacing id: card
Repeater { required property var modelData
model: [...Notifications.popups.values].reverse()
notif: modelData
NotificationCard { Layout.fillWidth: true
id: card }
}
required property var modelData
notif: modelData
Layout.fillWidth: true
}
} }
}
} }

52
windows/Volume.qml Normal file
View 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
}
}