mirror of
https://github.com/tuxdotrs/tshell.git
synced 2026-09-19 18:49:02 +05:30
Compare commits
5 Commits
3779974cb2
...
e95f62579b
| Author | SHA1 | Date | |
|---|---|---|---|
|
e95f62579b
|
|||
|
ab15dcf0ac
|
|||
|
3b01362a5a
|
|||
|
1ded80d1c0
|
|||
|
2b93af1a08
|
69
AGENTS.md
Normal file
69
AGENTS.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 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, 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
|
||||||
|
│ ├── 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
|
||||||
|
│ ├── Appearance.qml colors, font, margin/radius/spacing/padding/duration
|
||||||
|
│ └── 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 `Appearance.*` 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): ...`).
|
||||||
1
assets/icons/home.svg
Normal file
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 |
11
assets/icons/notification.svg
Normal file
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 |
1
assets/icons/power.svg
Normal file
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 |
@@ -13,15 +13,18 @@ Singleton {
|
|||||||
|
|
||||||
property var left: [
|
property var left: [
|
||||||
root.widget("LauncherButton"),
|
root.widget("LauncherButton"),
|
||||||
root.widget("BatteryIndicator"),
|
root.widget("PowerProfileWidget"),
|
||||||
root.widget("CavaVisualizer")
|
root.widget("HomeWidget"),
|
||||||
]
|
]
|
||||||
|
|
||||||
property var center: [
|
property var center: [
|
||||||
root.widget("Workspaces")
|
root.widget("CavaVisualizer"),
|
||||||
|
root.widget("Workspaces"),
|
||||||
|
root.widget("CavaVisualizer"),
|
||||||
]
|
]
|
||||||
|
|
||||||
property var right: [
|
property var right: [
|
||||||
|
root.widget("BatteryIndicator"),
|
||||||
root.widget("SystemTrayWidget"),
|
root.widget("SystemTrayWidget"),
|
||||||
root.widget("ClockWidget"),
|
root.widget("ClockWidget"),
|
||||||
root.widget("GhostButton")
|
root.widget("GhostButton")
|
||||||
|
|||||||
11
modules/bar/HomeWidget.qml
Normal file
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/PowerProfileWidget.qml
Normal file
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
|
||||||
|
}
|
||||||
117
modules/osd/NotificationCard.qml
Normal file
117
modules/osd/NotificationCard.qml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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: 360
|
||||||
|
implicitHeight: content.implicitHeight + Appearance.padding * 2
|
||||||
|
radius: Appearance.radius
|
||||||
|
color: Appearance.colors.background
|
||||||
|
border.width: critical ? 1 : 0
|
||||||
|
border.color: Appearance.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: 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 {
|
||||||
|
spacing: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.notif?.appName ?? ""
|
||||||
|
color: Appearance.colors.accent
|
||||||
|
font.family: Appearance.font.family
|
||||||
|
font.pointSize: Appearance.font.pointSize - 2
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.notif?.summary ?? ""
|
||||||
|
color: Appearance.colors.foreground
|
||||||
|
font.family: Appearance.font.family
|
||||||
|
font.pointSize: Appearance.font.pointSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.notif?.body ?? ""
|
||||||
|
visible: text !== ""
|
||||||
|
color: Appearance.colors.foreground
|
||||||
|
opacity: 0.8
|
||||||
|
font.family: Appearance.font.family
|
||||||
|
font.pointSize: Appearance.font.pointSize - 1
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
maximumLineCount: 3
|
||||||
|
elide: Text.ElideRight
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
50
services/Home.qml
Normal file
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
services/Notifications.qml
Normal file
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
services/PowerProfile.qml
Normal file
22
services/PowerProfile.qml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower as UPower
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool performanceAvailable: UPower.PowerProfiles.hasPerformanceProfile
|
||||||
|
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`
|
||||||
|
}
|
||||||
@@ -3,4 +3,6 @@ import qs.windows
|
|||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
Bar {}
|
Bar {}
|
||||||
|
Notification {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ PanelWindow {
|
|||||||
implicitHeight: parent.height
|
implicitHeight: parent.height
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
|
id: secRow
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2
|
x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2
|
||||||
y: (section.height - height) / 2
|
|
||||||
spacing: Appearance.spacing
|
spacing: Appearance.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
@@ -48,6 +50,7 @@ PanelWindow {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
source: modelData
|
source: modelData
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
windows/Notification.qml
Normal file
55
windows/Notification.qml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import "../modules/osd"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property int cardWidth: 420
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
visible: Notifications.popupCount > 0
|
||||||
|
implicitWidth: root.cardWidth
|
||||||
|
implicitHeight: stack.implicitHeight
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
margins {
|
||||||
|
top: 50 + Appearance.margin
|
||||||
|
right: 10 + Appearance.margin
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: stack
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: stack
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.left: parent.left
|
||||||
|
spacing: Appearance.spacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [...Notifications.popups.values].reverse()
|
||||||
|
|
||||||
|
NotificationCard {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
notif: modelData
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user