quickshell: add initial bar config with per-monitor workspaces
- Vertical bar on DP-2 with rounded-square pills throughout - Per-monitor workspace groups sorted by screen x position, with Nerd Font icons for named workspaces and apex-neon red active indicator - Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom - Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system) - Lock command switched from hyprlock to swaylock - Hyprland blur/ignore_alpha layerrules for quickshell namespace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -128,3 +128,7 @@ windowrule = match:class com.gabm.satty, min_size 700 400
|
||||
#layerrule = blur on, match:namespace swaync-notification-window
|
||||
#layerrule = ignore_alpha 0, match:namespace swaync-control-center
|
||||
#layerrule = ignore_alpha 0, match:namespace swaync-notification-window
|
||||
|
||||
# Quickshell
|
||||
layerrule = blur on, match:namespace quickshell:.*
|
||||
layerrule = ignore_alpha 0.79, match:namespace quickshell:.*
|
||||
|
||||
273
dot_config/quickshell/Bar.qml
Normal file
273
dot_config/quickshell/Bar.qml
Normal file
@@ -0,0 +1,273 @@
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "shared" as Shared
|
||||
import "bar" as BarComponents
|
||||
import "bar/popouts" as Popouts
|
||||
|
||||
Scope {
|
||||
property var notifModel: null
|
||||
property var notifDaemon: null
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
|
||||
}
|
||||
|
||||
property bool popoutOpen: Shared.PopoutState.active !== ""
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// BAR — fixed width, never resizes
|
||||
// ═══════════════════════════════════════
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
id: barWindow
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
// Screens sorted left→right by x position (negative x = leftmost)
|
||||
property var sortedScreens: {
|
||||
let s = [];
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) s.push(Quickshell.screens[i]);
|
||||
s.sort((a, b) => a.x - b.x);
|
||||
return s;
|
||||
}
|
||||
|
||||
visible: modelData.name === Shared.Config.monitor
|
||||
WlrLayershell.namespace: "quickshell:bar"
|
||||
surfaceFormat { opaque: !Shared.Theme.transparencyEnabled }
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
implicitWidth: Shared.Theme.barWidth
|
||||
exclusiveZone: Shared.Theme.barWidth
|
||||
color: Shared.Theme.barBackground
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Shared.Theme.barPadding + 2
|
||||
anchors.bottomMargin: Shared.Theme.barPadding + 2
|
||||
anchors.leftMargin: Shared.Theme.barPadding
|
||||
anchors.rightMargin: Shared.Theme.barPadding
|
||||
spacing: Shared.Theme.spacing
|
||||
|
||||
// ── Top ──────────────────────────────
|
||||
BarComponents.DateTimePill { id: datetimeBtn }
|
||||
|
||||
BarComponents.WeatherPill { id: weatherBtn }
|
||||
|
||||
// ── Center (workspaces) ───────────────
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// Per-monitor workspace groups — sorted left→right by screen position
|
||||
Repeater {
|
||||
model: barWindow.sortedScreens.length
|
||||
|
||||
delegate: Column {
|
||||
required property int index
|
||||
spacing: Shared.Theme.spacing
|
||||
|
||||
Rectangle {
|
||||
visible: index > 0
|
||||
width: Shared.Theme.barInnerWidth
|
||||
height: 1
|
||||
color: Shared.Theme.overlay0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
BarComponents.Workspaces {
|
||||
monitorName: barWindow.sortedScreens[index].name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// ── Bottom ────────────────────────────
|
||||
BarComponents.GamemodePill {}
|
||||
|
||||
BarComponents.MediaPill { id: mediaBtn }
|
||||
|
||||
BarComponents.BarPill {
|
||||
id: notifBtn
|
||||
groupName: "notifications"
|
||||
accentColor: Shared.Theme.mauve
|
||||
|
||||
property int count: notifModel ? notifModel.values.length : 0
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: notifDaemon?.dnd ? "\u{f009b}" : "\u{f0f3}"
|
||||
color: notifDaemon?.dnd ? Shared.Theme.danger : Shared.Theme.mauve
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.iconFont
|
||||
},
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: notifBtn.count > 0
|
||||
text: notifBtn.count.toString()
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BarComponents.SystemPill { id: systemBtn }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// POPOUT WINDOW — separate overlay, only exists when open
|
||||
// ═══════════════════════════════════════
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
id: popoutWindow
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
visible: modelData.name === Shared.Config.monitor && (popoutOpen || notifSlot.animating || mediaSlot.animating || weatherSlot.animating || datetimeSlot.animating || systemSlot.animating)
|
||||
WlrLayershell.namespace: "quickshell:popout"
|
||||
surfaceFormat { opaque: false }
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth + 12
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
focusable: popoutOpen
|
||||
color: "transparent"
|
||||
|
||||
margins {
|
||||
right: Shared.Theme.barWidth
|
||||
}
|
||||
|
||||
// Escape key = close
|
||||
Keys.onEscapePressed: Shared.PopoutState.close()
|
||||
|
||||
// Click on empty area = close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: Shared.PopoutState.close()
|
||||
}
|
||||
|
||||
// Popout slots
|
||||
Item {
|
||||
id: popoutArea
|
||||
anchors.fill: parent
|
||||
|
||||
PopoutSlot {
|
||||
id: notifSlot
|
||||
name: "notifications"
|
||||
verticalAnchor: "bottom"
|
||||
sourceComponent: Popouts.NotificationCenter { trackedNotifications: notifModel; daemon: notifDaemon }
|
||||
}
|
||||
|
||||
PopoutSlot {
|
||||
id: mediaSlot
|
||||
name: "media"
|
||||
verticalAnchor: "bottom"
|
||||
sourceComponent: Popouts.MediaPopout {}
|
||||
}
|
||||
|
||||
PopoutSlot {
|
||||
id: weatherSlot
|
||||
name: "weather"
|
||||
verticalAnchor: "top"
|
||||
sourceComponent: Popouts.WeatherPopout {}
|
||||
}
|
||||
|
||||
PopoutSlot {
|
||||
id: datetimeSlot
|
||||
name: "datetime"
|
||||
verticalAnchor: "top"
|
||||
sourceComponent: Popouts.CalendarPopout {}
|
||||
}
|
||||
|
||||
PopoutSlot {
|
||||
id: systemSlot
|
||||
name: "system"
|
||||
verticalAnchor: "bottom"
|
||||
sourceComponent: Popouts.SystemPopout { panelWindow: popoutWindow }
|
||||
}
|
||||
}
|
||||
|
||||
// PopoutSlot — anchored to triggering icon, MD3 animation
|
||||
component PopoutSlot: Item {
|
||||
id: slot
|
||||
required property string name
|
||||
property alias sourceComponent: loader.sourceComponent
|
||||
property string verticalAnchor: "center" // "top" | "bottom" | "center"
|
||||
|
||||
readonly property bool isOpen: Shared.PopoutState.active === name
|
||||
readonly property bool animating: fadeAnim.running || scaleAnim.running || slideAnim.running
|
||||
readonly property real cardH: loader.item?.implicitHeight ?? 400
|
||||
readonly property real cardW: loader.item?.implicitWidth ?? Shared.Theme.popoutWidth
|
||||
|
||||
readonly property real centerY: Math.max(16, Math.min(
|
||||
popoutArea.height - cardH - 16,
|
||||
(popoutArea.height - cardH) / 2
|
||||
))
|
||||
|
||||
anchors.right: parent.right
|
||||
y: {
|
||||
if (verticalAnchor === "top")
|
||||
return Math.max(16, Shared.PopoutState.triggerY);
|
||||
if (verticalAnchor === "bottom")
|
||||
return Math.min(popoutArea.height - cardH - 16,
|
||||
Shared.PopoutState.triggerY - cardH);
|
||||
return centerY;
|
||||
}
|
||||
width: cardW
|
||||
height: cardH
|
||||
|
||||
visible: isOpen || animating
|
||||
|
||||
opacity: isOpen ? 1.0 : 0.0
|
||||
scale: isOpen ? 1.0 : 0.97
|
||||
transformOrigin: Item.Right
|
||||
property real slideX: isOpen ? 0 : 8
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { id: fadeAnim; duration: 180; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation { id: scaleAnim; duration: 220; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on slideX {
|
||||
NumberAnimation { id: slideAnim; duration: 220; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
transform: Translate { x: slot.slideX }
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
active: slot.isOpen || slot.animating
|
||||
width: slot.cardW
|
||||
height: slot.cardH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
dot_config/quickshell/CLAUDE.md
Normal file
85
dot_config/quickshell/CLAUDE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What This Is
|
||||
|
||||
A Quickshell (v0.2.x) desktop shell configuration for Hyprland on Arch Linux. It renders a vertical bar on the right edge of the screen with popout panels. Written entirely in QML using Quickshell's extended Qt Quick modules.
|
||||
|
||||
## Running / Reloading
|
||||
|
||||
```bash
|
||||
quickshell # launch (reads shell.qml from ~/.config/quickshell/)
|
||||
quickshell -c /path/to/dir # launch from a different config dir
|
||||
```
|
||||
|
||||
Quickshell hot-reloads on file save — no restart needed during development. If the shell crashes or gets stuck, `killall quickshell && quickshell &` to restart.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry Point
|
||||
|
||||
`shell.qml` — `ShellRoot` that instantiates three top-level components:
|
||||
1. `NotificationDaemon` — D-Bus notification server
|
||||
2. `Bar` — the main bar + popout overlay
|
||||
3. `Osd` — volume/mic OSD overlay
|
||||
|
||||
### Bar (`Bar.qml`)
|
||||
|
||||
Uses `Variants` over `Quickshell.screens` to create per-monitor `PanelWindow` instances (filtered to `Config.monitor`). Two window layers:
|
||||
- **Bar window** — fixed-width right-anchored panel with pill-shaped widgets
|
||||
- **Popout window** — transparent overlay with animated `PopoutSlot` components that appear left of the bar
|
||||
|
||||
Bar pills (notification, workspaces, weather, datetime, system) toggle popouts via the `PopoutState` singleton.
|
||||
|
||||
### Shared Singletons (`shared/`)
|
||||
|
||||
All four are `pragma Singleton`:
|
||||
- **Config** — user settings (monitor=DP-2, weatherLocation=Nospelt, power commands, disk mounts, formats); `.tmpl` file driven by chezmoi data
|
||||
- **Theme** — Apex-neon/aeon colors (replaces Catppuccin), layout constants, typography (GeistMono Nerd Font), animation durations
|
||||
- **PopoutState** — which popout is active (`active` string + `triggerY`), with `toggle()`/`close()`
|
||||
- **Time** — `SystemClock`-backed formatted time strings
|
||||
- **Weather** — fetches from `wttr.in`, parses JSON, exposes current + forecast data
|
||||
|
||||
### Popout Pattern
|
||||
|
||||
Each popout follows the same structure:
|
||||
1. A `BarPill` in the bar column (toggles `PopoutState`)
|
||||
2. A `PopoutSlot` in `Bar.qml` (handles animation — fade, scale, slide)
|
||||
3. A popout component in `bar/popouts/` wrapped in `PopoutBackground` (rounded left, flush right)
|
||||
|
||||
### Data Fetching
|
||||
|
||||
`SystemPopout` polls system stats via `Process` + `StdioCollector`:
|
||||
- CPU, memory, temperature, GPU (via `scripts/gpu.sh`), disk, network, updates
|
||||
- Different refresh intervals: 5s (CPU/mem/temp/GPU), 30s (disk/net), 5min (updates)
|
||||
|
||||
Weather uses `curl wttr.in` with a 30-minute refresh timer.
|
||||
|
||||
### Quickshell-Specific Patterns
|
||||
|
||||
- `Variants { model: Quickshell.screens }` — per-screen window instantiation
|
||||
- `PanelWindow` with `anchors`/`margins`/`exclusiveZone` — Wayland layer-shell positioning
|
||||
- `Scope` — non-visual Quickshell container for grouping logic
|
||||
- `Process` + `StdioCollector { onStreamFinished }` — async shell command execution
|
||||
- `PwObjectTracker` — required to track PipeWire default sink/source changes
|
||||
- `Quickshell.Hyprland` — workspace state and `Hyprland.dispatch()` for IPC
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **Hyprland** — compositor (workspace switching, IPC)
|
||||
- **PipeWire** — audio control (volume, mute)
|
||||
- **wttr.in** — weather data (no API key needed)
|
||||
- **lm_sensors** (`sensors`) — CPU temperature
|
||||
- **Nerd Fonts** (GeistMono Nerd Font) — all icons are Unicode Nerd Font glyphs
|
||||
- **hyprlock** / **hypridle** / **hyprshutdown** — lock, idle, power actions
|
||||
|
||||
## Conventions
|
||||
|
||||
- Icons: Nerd Font Unicode escapes (`"\u{f057e}"`) — not icon names or image files
|
||||
- Colors: always reference `Shared.Theme.*` (apex-neon palette by default; apex-aeon for light mode)
|
||||
- Layout: use `Shared.Theme.*` constants for sizing, spacing, radii
|
||||
- Config: user-facing settings go in `Config.qml`, not hardcoded in components
|
||||
- Animations: use `Behavior on <prop>` with `Shared.Theme.animFast/animNormal/animSlow`
|
||||
- Inline components: `component Foo: ...` inside a file for file-local reusable types (see `SystemPopout`)
|
||||
- Popout panels consume mouse clicks with a root `MouseArea` to prevent click-through closing
|
||||
70
dot_config/quickshell/README.md
Normal file
70
dot_config/quickshell/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Quickshell Desktop Shell
|
||||
|
||||
A vertical bar + popout panel shell for Hyprland, built with Quickshell v0.2.x and themed with Catppuccin.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
quickshell # launch (reads from ~/.config/quickshell/)
|
||||
```
|
||||
|
||||
Quickshell hot-reloads on file save. If it crashes: `killall quickshell && quickshell &`
|
||||
|
||||
## Configuration
|
||||
|
||||
All user settings live in **`shared/Config.qml`**. Edit this file to customize your setup:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `catppuccinFlavor` | `"mocha"` | Color theme: `"mocha"`, `"macchiato"`, `"frappe"`, `"latte"` |
|
||||
| `transparency` | `true` | Semi-transparent bar/popouts (requires Hyprland layerrules below) |
|
||||
| `monitor` | `"DP-1"` | Which monitor to display the bar on |
|
||||
| `workspaceCount` | `5` | Number of workspace indicators |
|
||||
| `weatherLocation` | `"Munich"` | City name for wttr.in weather data |
|
||||
| `useCelsius` | `true` | Temperature unit (`false` for Fahrenheit) |
|
||||
| `use24h` | `true` | Clock format (`false` for 12-hour) |
|
||||
| `weekStartsMonday` | `true` | Calendar week start day |
|
||||
| `diskMount1` / `diskMount2` | `"/"` / `~/data` | Disk mounts shown in system popout |
|
||||
| `idleProcess` | `"hypridle"` | Idle daemon to toggle |
|
||||
| `lockCommand` | `"hyprlock"` | Lock screen command |
|
||||
|
||||
## Hyprland Layerrules (required for blur/transparency)
|
||||
|
||||
Add to your `hyprland.conf`:
|
||||
|
||||
```ini
|
||||
# Enable blur on all Quickshell windows
|
||||
layerrule = match:namespace quickshell:.*, blur on
|
||||
layerrule = match:namespace quickshell:.*, ignore_alpha 0.79
|
||||
```
|
||||
|
||||
The shell registers these namespaces:
|
||||
|
||||
| Namespace | Window |
|
||||
|-----------|--------|
|
||||
| `quickshell:bar` | The main bar panel |
|
||||
| `quickshell:popout` | Popout overlay (notifications, media, weather, etc.) |
|
||||
| `quickshell:osd` | Volume/brightness OSD |
|
||||
| `quickshell:notifications` | Toast notification popups |
|
||||
| `quickshell:idle` | Idle screen overlay |
|
||||
|
||||
To target specific windows, replace `quickshell:.*` with the exact namespace:
|
||||
|
||||
```ini
|
||||
# Only blur the bar, not popouts
|
||||
layerrule = match:namespace quickshell:bar, blur on
|
||||
layerrule = match:namespace quickshell:bar, ignore_alpha 0.79
|
||||
```
|
||||
|
||||
If you set `transparency: false` in Config.qml, layerrules are not needed.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Hyprland** — compositor
|
||||
- **PipeWire** — audio control
|
||||
- **lm_sensors** (`sensors`) — CPU temperature
|
||||
- **Nerd Fonts** (Inconsolata Go Nerd Font) — all icons
|
||||
- **hyprlock** / **hypridle** / **hyprshutdown** — lock, idle, power
|
||||
- **brightnessctl** — brightness OSD (optional, auto-detected)
|
||||
- **wf-recorder** — screen recording (optional)
|
||||
- **hyprpicker** — color picker (optional)
|
||||
43
dot_config/quickshell/bar/ActiveWindow.qml
Normal file
43
dot_config/quickshell/bar/ActiveWindow.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import "../shared" as Shared
|
||||
|
||||
// Compact active window title, rotated vertically to fit the narrow bar
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string title: {
|
||||
let w = Hyprland.focusedWindow;
|
||||
if (!w) return "";
|
||||
let t = w.title || "";
|
||||
let c = w.wlClass || "";
|
||||
// Show class if title is too long or empty
|
||||
if (t.length === 0) return c;
|
||||
if (t.length > 30) return c || t.substring(0, 20);
|
||||
return t;
|
||||
}
|
||||
|
||||
visible: title !== ""
|
||||
implicitWidth: Shared.Theme.barInnerWidth
|
||||
implicitHeight: Math.min(label.implicitWidth + 8, 120)
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
rotation: -90
|
||||
width: root.implicitHeight
|
||||
text: root.title
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
Behavior on text {
|
||||
enabled: false // no animation on text change
|
||||
}
|
||||
}
|
||||
|
||||
opacity: title !== "" ? 0.7 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: Shared.Theme.animFast } }
|
||||
}
|
||||
52
dot_config/quickshell/bar/BarPill.qml
Normal file
52
dot_config/quickshell/bar/BarPill.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
// Unified segmented-control pill for bar group buttons
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string groupName: ""
|
||||
property color accentColor: Shared.Theme.text
|
||||
property alias content: contentArea.data
|
||||
|
||||
readonly property bool isActive: Shared.PopoutState.active === groupName
|
||||
readonly property bool isHovered: mouse.containsMouse
|
||||
|
||||
implicitWidth: Shared.Theme.barInnerWidth
|
||||
implicitHeight: pill.height
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: parent.width
|
||||
height: contentArea.implicitHeight + Shared.Theme.barPadding * 2 + 6
|
||||
radius: Shared.Theme.radiusNormal
|
||||
color: root.isActive
|
||||
? Qt.alpha(root.accentColor, Shared.Theme.opacityLight)
|
||||
: root.isHovered
|
||||
? Shared.Theme.surface1
|
||||
: Shared.Theme.surface0
|
||||
border.width: root.isActive ? 1 : 0
|
||||
border.color: Qt.alpha(root.accentColor, Shared.Theme.opacityMedium)
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
Behavior on border.color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
ColumnLayout {
|
||||
id: contentArea
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
let globalPos = root.mapToItem(null, 0, root.height / 2);
|
||||
Shared.PopoutState.toggle(root.groupName, globalPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
dot_config/quickshell/bar/DateTimePill.qml
Normal file
37
dot_config/quickshell/bar/DateTimePill.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
BarPill {
|
||||
groupName: "datetime"
|
||||
accentColor: Shared.Theme.teal
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Qt.formatDateTime(Shared.Time.date, Shared.Config.pillTimeFormat)
|
||||
color: Shared.Theme.teal
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
lineHeight: 1.1
|
||||
},
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: Shared.Theme.barInnerWidth * 0.5
|
||||
height: 1
|
||||
color: Shared.Theme.teal
|
||||
opacity: Shared.Theme.opacityLight
|
||||
},
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Qt.formatDateTime(Shared.Time.date, Shared.Config.pillDateFormat)
|
||||
color: Shared.Theme.teal
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
]
|
||||
}
|
||||
48
dot_config/quickshell/bar/GamemodePill.qml
Normal file
48
dot_config/quickshell/bar/GamemodePill.qml
Normal file
@@ -0,0 +1,48 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
// Gamemode indicator — visible only when gamemode is active
|
||||
// Polls `gamemoded --status` every 5 seconds
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
groupName: "" // no popout
|
||||
accentColor: Shared.Theme.green
|
||||
visible: isActive
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "\u{f0522}" // nf-md-controller_classic
|
||||
color: Shared.Theme.green
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
]
|
||||
|
||||
Process {
|
||||
id: gameProc
|
||||
command: ["gamemoded", "--status"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.isActive = this.text.indexOf("is active") >= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function poll() { gameProc.running = false; gameProc.running = true; }
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.poll()
|
||||
}
|
||||
|
||||
Component.onCompleted: root.poll()
|
||||
}
|
||||
35
dot_config/quickshell/bar/MediaPill.qml
Normal file
35
dot_config/quickshell/bar/MediaPill.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
readonly property var player: {
|
||||
let players = Mpris.players.values;
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
if (players[i].isPlaying) return players[i];
|
||||
}
|
||||
return players.length > 0 ? players[0] : null;
|
||||
}
|
||||
|
||||
visible: player !== null
|
||||
groupName: "media"
|
||||
accentColor: Shared.Theme.green
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
if (!root.player) return "\u{f075a}";
|
||||
if (root.player.isPlaying) return "\u{f040a}";
|
||||
return "\u{f03e4}";
|
||||
}
|
||||
color: Shared.Theme.green
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
]
|
||||
}
|
||||
19
dot_config/quickshell/bar/SystemPill.qml
Normal file
19
dot_config/quickshell/bar/SystemPill.qml
Normal file
@@ -0,0 +1,19 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
BarPill {
|
||||
groupName: "system"
|
||||
accentColor: Shared.Theme.lavender
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "\u{f303}"
|
||||
color: Shared.Theme.lavender
|
||||
font.pixelSize: Shared.Theme.fontLarge + 2
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
]
|
||||
}
|
||||
33
dot_config/quickshell/bar/WeatherPill.qml
Normal file
33
dot_config/quickshell/bar/WeatherPill.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
BarPill {
|
||||
groupName: "weather"
|
||||
accentColor: Shared.Weather.status === "error" ? Shared.Theme.overlay0 : Shared.Theme.peach
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Shared.Weather.status === "error" ? "\u{f0599}" : Shared.Weather.icon
|
||||
color: Shared.Weather.status === "error" ? Shared.Theme.overlay0
|
||||
: Shared.Weather.status === "stale" ? Shared.Theme.warning
|
||||
: Shared.Theme.peach
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.iconFont
|
||||
},
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Shared.Weather.status === "error" ? "N/A"
|
||||
: Shared.Weather.status === "loading" ? "…"
|
||||
: Shared.Weather.temp
|
||||
color: Shared.Weather.status === "error" ? Shared.Theme.overlay0
|
||||
: Shared.Weather.status === "stale" ? Shared.Theme.warning
|
||||
: Shared.Theme.peach
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
]
|
||||
}
|
||||
164
dot_config/quickshell/bar/Workspaces.qml
Normal file
164
dot_config/quickshell/bar/Workspaces.qml
Normal file
@@ -0,0 +1,164 @@
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import "../shared" as Shared
|
||||
|
||||
// Rounded-square workspace slots with sliding active highlight.
|
||||
// Set monitorName to show only workspaces on a specific monitor.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string monitorName: ""
|
||||
property int wsCount: Shared.Config.workspaceCount
|
||||
|
||||
readonly property int cellSize: Shared.Theme.barInnerWidth - Shared.Theme.barPadding * 2
|
||||
|
||||
readonly property var iconMap: ({
|
||||
"mail": "\u{eb1c}",
|
||||
"comms": "\u{ee59}",
|
||||
"element": "\u{f1d7}",
|
||||
"joplin": "\u{f249}",
|
||||
"steam": "\u{f1b6}",
|
||||
"spotify": "\u{f1bc}"
|
||||
})
|
||||
|
||||
// { id, name } objects for workspaces on this monitor, sorted by name numerically.
|
||||
property var monitorWsData: {
|
||||
if (!monitorName) return [];
|
||||
let all = Hyprland.workspaces, data = [];
|
||||
for (let i = 0; i < all.values.length; i++) {
|
||||
let ws = all.values[i];
|
||||
if (ws.monitor && ws.monitor.name === monitorName)
|
||||
data.push({ id: ws.id, name: ws.name });
|
||||
}
|
||||
data.sort((a, b) => parseInt(a.name) - parseInt(b.name));
|
||||
return data;
|
||||
}
|
||||
|
||||
// Active workspace ID for this monitor specifically
|
||||
property int activeWsId: {
|
||||
if (monitorName) {
|
||||
let mons = Hyprland.monitors;
|
||||
for (let i = 0; i < mons.values.length; i++) {
|
||||
if (mons.values[i].name === monitorName)
|
||||
return mons.values[i].activeWorkspace?.id ?? -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
let fw = Hyprland.focusedWorkspace;
|
||||
return fw ? fw.id : 1;
|
||||
}
|
||||
|
||||
implicitWidth: Shared.Theme.barInnerWidth
|
||||
implicitHeight: container.height
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
width: parent.width
|
||||
height: wsCol.height + padding * 2
|
||||
radius: Shared.Theme.radiusNormal
|
||||
color: Shared.Theme.surface0
|
||||
|
||||
property int padding: Shared.Theme.barPadding + 2
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y > 0)
|
||||
Hyprland.dispatch("workspace m-1");
|
||||
else
|
||||
Hyprland.dispatch("workspace m+1");
|
||||
}
|
||||
}
|
||||
|
||||
// Sliding active indicator
|
||||
Rectangle {
|
||||
id: activeIndicator
|
||||
|
||||
property int targetIndex: {
|
||||
if (root.monitorName) {
|
||||
for (let i = 0; i < root.monitorWsData.length; i++) {
|
||||
if (root.monitorWsData[i].id === root.activeWsId) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.min(root.activeWsId - 1, root.wsCount - 1));
|
||||
}
|
||||
property var targetItem: wsRepeater.itemAt(targetIndex)
|
||||
|
||||
x: (container.width - width) / 2
|
||||
y: targetItem ? targetItem.y + wsCol.y : container.padding
|
||||
width: root.cellSize
|
||||
height: root.cellSize
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: Shared.Theme.red
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Shared.Theme.animSlow
|
||||
easing.type: Easing.InOutQuart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: wsCol
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: container.padding
|
||||
spacing: Math.floor(Shared.Theme.spacing / 2)
|
||||
|
||||
Repeater {
|
||||
id: wsRepeater
|
||||
model: root.monitorName ? root.monitorWsData.length : root.wsCount
|
||||
|
||||
delegate: Item {
|
||||
id: wsItem
|
||||
required property int index
|
||||
|
||||
width: root.cellSize
|
||||
height: root.cellSize
|
||||
|
||||
property int wsId: root.monitorName ? root.monitorWsData[index].id : index + 1
|
||||
property string wsName: root.monitorName ? root.monitorWsData[index].name : (index + 1).toString()
|
||||
property bool isActive: root.activeWsId === wsId
|
||||
property bool isOccupied: {
|
||||
let all = Hyprland.workspaces;
|
||||
for (let i = 0; i < all.values.length; i++) {
|
||||
if (all.values[i].id === wsId)
|
||||
return all.values[i].lastIpcObject?.windows > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
|
||||
property string icon: root.iconMap[wsItem.wsName] ?? ""
|
||||
property bool hasIcon: icon !== ""
|
||||
|
||||
text: hasIcon ? icon : wsItem.wsName
|
||||
color: wsItem.isActive
|
||||
? Shared.Theme.crust
|
||||
: wsItem.isOccupied
|
||||
? Shared.Theme.text
|
||||
: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: hasIcon ? Shared.Theme.iconFont : Shared.Theme.fontFamily
|
||||
font.bold: !hasIcon && wsItem.isActive
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Shared.Theme.animFast }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Hyprland.dispatch("workspace name:" + wsItem.wsName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
dot_config/quickshell/bar/popouts/CalendarPopout.qml
Normal file
189
dot_config/quickshell/bar/popouts/CalendarPopout.qml
Normal file
@@ -0,0 +1,189 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: col.implicitHeight + Shared.Theme.popoutPadding * 2
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
|
||||
property int viewMonth: new Date().getMonth()
|
||||
property int viewYear: new Date().getFullYear()
|
||||
property int todayDay: Shared.Time.date.getDate()
|
||||
property int todayMonth: Shared.Time.date.getMonth()
|
||||
property int todayYear: Shared.Time.date.getFullYear()
|
||||
|
||||
readonly property bool isViewingToday: viewMonth === todayMonth && viewYear === todayYear
|
||||
|
||||
function prevMonth() { if (viewMonth === 0) { viewMonth = 11; viewYear--; } else viewMonth--; }
|
||||
function nextMonth() { if (viewMonth === 11) { viewMonth = 0; viewYear++; } else viewMonth++; }
|
||||
function goToday() { viewMonth = todayMonth; viewYear = todayYear; }
|
||||
function daysInMonth(m, y) { return new Date(y, m + 1, 0).getDate(); }
|
||||
function firstDayOfWeek(m, y) {
|
||||
let d = new Date(y, m, 1).getDay();
|
||||
if (Shared.Config.weekStartsMonday)
|
||||
return d === 0 ? 6 : d - 1;
|
||||
return d;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y > 0) root.prevMonth();
|
||||
else root.nextMonth();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
spacing: Shared.Theme.popoutSpacing
|
||||
|
||||
Text {
|
||||
text: Shared.Time.clockSeconds
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 28
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 2
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(Shared.Time.date, Shared.Config.dateFormat)
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.rightMargin: 4
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
height: 1
|
||||
color: Shared.Theme.surface0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: "\u{f0141}"
|
||||
color: navPrev.containsMouse ? Shared.Theme.text : Shared.Theme.subtext0
|
||||
font.pixelSize: 16
|
||||
font.family: Shared.Theme.iconFont
|
||||
MouseArea { id: navPrev; anchors.fill: parent; hoverEnabled: true; onClicked: root.prevMonth() }
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: new Date(root.viewYear, root.viewMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
MouseArea { anchors.fill: parent; onClicked: root.goToday() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !root.isViewingToday
|
||||
implicitWidth: todayLabel.implicitWidth + 10
|
||||
implicitHeight: 20
|
||||
radius: 10
|
||||
color: todayMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
Text {
|
||||
id: todayLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Today"
|
||||
color: Shared.Theme.blue
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
MouseArea { id: todayMouse; anchors.fill: parent; hoverEnabled: true; onClicked: root.goToday() }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "\u{f0142}"
|
||||
color: navNext.containsMouse ? Shared.Theme.text : Shared.Theme.subtext0
|
||||
font.pixelSize: 16
|
||||
font.family: Shared.Theme.iconFont
|
||||
MouseArea { id: navNext; anchors.fill: parent; hoverEnabled: true; onClicked: root.nextMonth() }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: Shared.Config.dayHeaders
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: calGrid
|
||||
Layout.fillWidth: true
|
||||
columns: 7
|
||||
spacing: 0
|
||||
|
||||
property int numDays: root.daysInMonth(root.viewMonth, root.viewYear)
|
||||
property int startDay: root.firstDayOfWeek(root.viewMonth, root.viewYear)
|
||||
|
||||
Repeater {
|
||||
model: calGrid.startDay + calGrid.numDays + (7 - (calGrid.startDay + calGrid.numDays) % 7) % 7
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
property int day: index - calGrid.startDay + 1
|
||||
property bool isValid: day >= 1 && day <= calGrid.numDays
|
||||
property bool isToday: isValid && day === root.todayDay && root.viewMonth === root.todayMonth && root.viewYear === root.todayYear
|
||||
property bool isWeekend: {
|
||||
let col = index % 7;
|
||||
if (Shared.Config.weekStartsMonday)
|
||||
return col >= 5; // Sa=5, Su=6
|
||||
return col === 0 || col === 6; // Su=0, Sa=6
|
||||
}
|
||||
|
||||
width: calGrid.width / 7
|
||||
height: 28
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 24; height: 24; radius: 12
|
||||
color: parent.isToday ? Shared.Theme.blue : "transparent"
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: parent.isValid ? parent.day.toString() : ""
|
||||
color: parent.isToday ? Shared.Theme.crust
|
||||
: parent.isWeekend && parent.isValid ? Shared.Theme.overlay0
|
||||
: parent.isValid ? Shared.Theme.text
|
||||
: "transparent"
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: parent.isToday
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
240
dot_config/quickshell/bar/popouts/MediaPopout.qml
Normal file
240
dot_config/quickshell/bar/popouts/MediaPopout.qml
Normal file
@@ -0,0 +1,240 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: col.implicitHeight + Shared.Theme.popoutPadding * 2
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
readonly property var player: {
|
||||
let players = Mpris.players.values;
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
if (players[i].isPlaying) return players[i];
|
||||
}
|
||||
return players.length > 0 ? players[0] : null;
|
||||
}
|
||||
|
||||
readonly property bool isPlaying: player?.isPlaying ?? false
|
||||
readonly property string trackTitle: player?.trackTitle ?? ""
|
||||
readonly property string trackArtist: player?.trackArtist ?? ""
|
||||
readonly property string trackAlbum: player?.trackAlbum ?? ""
|
||||
readonly property string artUrl: player?.trackArtUrl ?? ""
|
||||
readonly property real trackLength: player?.length ?? 0
|
||||
readonly property real trackPosition: player?.position ?? 0
|
||||
|
||||
// Position doesn't auto-update — emit positionChanged periodically while playing
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.isPlaying && root.player !== null
|
||||
repeat: true
|
||||
onTriggered: { if (root.player) root.player.positionChanged(); }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
spacing: 10
|
||||
|
||||
// Album art
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: artImg.hasArt ? width * 0.6 : 48
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: Shared.Theme.surface0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight { NumberAnimation { duration: Shared.Theme.animNormal; easing.type: Easing.OutCubic } }
|
||||
|
||||
Image {
|
||||
id: artImg
|
||||
anchors.fill: parent
|
||||
source: root.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: hasArt
|
||||
property bool hasArt: status === Image.Ready && root.artUrl !== ""
|
||||
}
|
||||
|
||||
// Gradient overlay at bottom for readability
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: parent.height * 0.4
|
||||
visible: artImg.hasArt
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "transparent" }
|
||||
GradientStop { position: 1.0; color: Qt.alpha(Shared.Theme.mantle, 0.8) }
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder icon when no art
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !artImg.hasArt
|
||||
text: "\u{f075a}"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: 24
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
}
|
||||
|
||||
// Track info
|
||||
Text {
|
||||
text: root.trackTitle || "No track"
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.trackArtist
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
Layout.topMargin: -6
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 4
|
||||
radius: 2
|
||||
color: Shared.Theme.surface0
|
||||
visible: root.trackLength > 0
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.trackLength > 0 ? parent.width * Math.min(1, root.trackPosition / root.trackLength) : 0
|
||||
radius: 2
|
||||
color: Shared.Theme.green
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: event => {
|
||||
if (root.player?.canSeek && root.trackLength > 0) {
|
||||
let ratio = event.x / parent.width;
|
||||
let target = ratio * root.trackLength;
|
||||
root.player.position = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time labels
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.trackLength > 0
|
||||
|
||||
Text {
|
||||
text: formatTime(root.trackPosition)
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: formatTime(root.trackLength)
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 16
|
||||
|
||||
// Previous
|
||||
Text {
|
||||
text: "\u{f04ae}"
|
||||
color: prevMouse.containsMouse ? Shared.Theme.text : Shared.Theme.subtext0
|
||||
font.pixelSize: 20
|
||||
font.family: Shared.Theme.iconFont
|
||||
opacity: root.player?.canGoPrevious ? 1.0 : 0.3
|
||||
MouseArea {
|
||||
id: prevMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: { if (root.player?.canGoPrevious) root.player.previous(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Play/Pause
|
||||
Rectangle {
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
radius: 20
|
||||
color: playMouse.containsMouse ? Shared.Theme.green : Qt.alpha(Shared.Theme.green, Shared.Theme.opacityMedium)
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.isPlaying ? "\u{f03e4}" : "\u{f040a}"
|
||||
color: Shared.Theme.crust
|
||||
font.pixelSize: 20
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: { if (root.player?.canTogglePlaying) root.player.togglePlaying(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Next
|
||||
Text {
|
||||
text: "\u{f04ad}"
|
||||
color: nextMouse.containsMouse ? Shared.Theme.text : Shared.Theme.subtext0
|
||||
font.pixelSize: 20
|
||||
font.family: Shared.Theme.iconFont
|
||||
opacity: root.player?.canGoNext ? 1.0 : 0.3
|
||||
MouseArea {
|
||||
id: nextMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: { if (root.player?.canGoNext) root.player.next(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player name
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.player?.identity ?? ""
|
||||
color: Shared.Theme.surface2
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(secs) {
|
||||
let m = Math.floor(secs / 60);
|
||||
let s = Math.floor(secs % 60);
|
||||
return m + ":" + (s < 10 ? "0" : "") + s;
|
||||
}
|
||||
}
|
||||
352
dot_config/quickshell/bar/popouts/NotificationCenter.qml
Normal file
352
dot_config/quickshell/bar/popouts/NotificationCenter.qml
Normal file
@@ -0,0 +1,352 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var trackedNotifications
|
||||
required property var daemon
|
||||
|
||||
function relativeTime(id) {
|
||||
let ts = root.daemon?.timestamps?.[id];
|
||||
if (!ts) return "";
|
||||
let diff = Math.floor((Date.now() - ts) / 1000);
|
||||
if (diff < 60) return "now";
|
||||
if (diff < 3600) return Math.floor(diff / 60) + "m";
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + "h";
|
||||
return Math.floor(diff / 86400) + "d";
|
||||
}
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: Math.min(600, col.implicitHeight + Shared.Theme.popoutPadding * 2)
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
// Drives relative timestamp re-evaluation
|
||||
Timer { id: tsRefresh; property int tick: 0; interval: 30000; running: true; repeat: true; onTriggered: tick++ }
|
||||
|
||||
// DnD auto-off timer
|
||||
Timer {
|
||||
id: dndTimer
|
||||
property int remaining: 0 // seconds, 0 = indefinite
|
||||
interval: 1000
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
remaining--;
|
||||
if (remaining <= 0) {
|
||||
running = false;
|
||||
if (root.daemon) root.daemon.dnd = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
spacing: 8
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Notifications"
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// DnD toggle
|
||||
Rectangle {
|
||||
implicitWidth: dndRow.implicitWidth + 12
|
||||
implicitHeight: 24
|
||||
radius: 12
|
||||
color: root.daemon?.dnd ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (dndMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
|
||||
border.width: root.daemon?.dnd ? 1 : 0
|
||||
border.color: Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityMedium)
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
RowLayout {
|
||||
id: dndRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Text { text: root.daemon?.dnd ? "\u{f009b}" : "\u{f009a}"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: 12; font.family: Shared.Theme.iconFont }
|
||||
Text { text: "DnD"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dndMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: { if (root.daemon) root.daemon.dnd = !root.daemon.dnd; }
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all
|
||||
Rectangle {
|
||||
visible: root.trackedNotifications.values.length > 0
|
||||
implicitWidth: clearRow.implicitWidth + 12
|
||||
implicitHeight: 24
|
||||
radius: 12
|
||||
color: clearMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
RowLayout {
|
||||
id: clearRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Text { text: "Clear all"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
let tracked = root.trackedNotifications;
|
||||
for (let i = tracked.values.length - 1; i >= 0; i--)
|
||||
tracked.values[i].dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DnD schedule (visible when DnD is active)
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
active: root.daemon?.dnd === true
|
||||
visible: active
|
||||
sourceComponent: RowLayout {
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: [
|
||||
{ label: "30m", mins: 30 },
|
||||
{ label: "1h", mins: 60 },
|
||||
{ label: "2h", mins: 120 },
|
||||
{ label: "\u{f0026}", mins: 0 } // infinity = until manual off
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool isActive: {
|
||||
if (modelData.mins === 0) return dndTimer.remaining <= 0;
|
||||
return dndTimer.remaining > 0 && dndTimer.remaining <= modelData.mins * 60;
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
radius: 11
|
||||
color: isActive ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (schedMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: parent.modelData.label
|
||||
color: parent.isActive ? Shared.Theme.danger : Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: schedMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (parent.modelData.mins === 0) {
|
||||
dndTimer.remaining = 0; // indefinite
|
||||
} else {
|
||||
dndTimer.remaining = parent.modelData.mins * 60;
|
||||
dndTimer.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0 }
|
||||
|
||||
// Empty state
|
||||
Text {
|
||||
visible: root.trackedNotifications.values.length === 0
|
||||
text: "No notifications"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
}
|
||||
|
||||
// Notification list
|
||||
Flickable {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(480, notifList.implicitHeight)
|
||||
contentHeight: notifList.implicitHeight
|
||||
clip: true
|
||||
visible: root.trackedNotifications.values.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: notifList
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.trackedNotifications
|
||||
|
||||
Rectangle {
|
||||
id: notifCard
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: notifContent.implicitHeight + 16
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: notifMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
||||
border.width: modelData.urgency === NotificationUrgency.Critical ? 1 : 0
|
||||
border.color: Shared.Theme.danger
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
MouseArea {
|
||||
id: notifMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
let actions = notifCard.modelData.actions;
|
||||
if (actions.length > 0)
|
||||
actions[0].invoke();
|
||||
else
|
||||
notifCard.modelData.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: notifContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 4
|
||||
|
||||
// App name + close
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
IconImage {
|
||||
source: notifCard.modelData.appIcon
|
||||
implicitSize: 14
|
||||
visible: notifCard.modelData.appIcon !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: notifCard.modelData.appName || "App"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
property int tick: tsRefresh.tick
|
||||
text: root.relativeTime(notifCard.modelData.id)
|
||||
color: Shared.Theme.surface2
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "\u{f0156}"
|
||||
color: dismissMouse.containsMouse ? Shared.Theme.danger : Shared.Theme.overlay0
|
||||
font.pixelSize: 12
|
||||
font.family: Shared.Theme.iconFont
|
||||
MouseArea {
|
||||
id: dismissMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: notifCard.modelData.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: notifCard.modelData.summary
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: notifCard.modelData.body
|
||||
textFormat: Text.PlainText
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Actions
|
||||
RowLayout {
|
||||
visible: notifCard.modelData.actions.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: notifCard.modelData.actions
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 24
|
||||
radius: 4
|
||||
color: actMouse.containsMouse ? Shared.Theme.surface2 : Shared.Theme.surface1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.text
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
dot_config/quickshell/bar/popouts/PopoutBackground.qml
Normal file
19
dot_config/quickshell/bar/popouts/PopoutBackground.qml
Normal file
@@ -0,0 +1,19 @@
|
||||
import QtQuick
|
||||
import "../../shared" as Shared
|
||||
|
||||
// Rounded left corners, square right (flush against bar)
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width + 22
|
||||
radius: 22
|
||||
color: Shared.Theme.popoutBackground
|
||||
border.width: 1
|
||||
border.color: Shared.Theme.borderSubtle
|
||||
}
|
||||
}
|
||||
840
dot_config/quickshell/bar/popouts/SystemPopout.qml
Normal file
840
dot_config/quickshell/bar/popouts/SystemPopout.qml
Normal file
@@ -0,0 +1,840 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: Math.min(flickable.contentHeight + Shared.Theme.popoutPadding * 2, Screen.height - 32)
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
|
||||
// --- State ---
|
||||
readonly property int historySize: 30
|
||||
property var cpuHistory: []
|
||||
property var memHistory: []
|
||||
property var gpuHistory: []
|
||||
|
||||
function pushHistory(arr, val) {
|
||||
let a = arr.slice();
|
||||
a.push(val);
|
||||
if (a.length > historySize) a.shift();
|
||||
return a;
|
||||
}
|
||||
|
||||
property real cpuVal: 0
|
||||
property string memText: "--"
|
||||
property real memVal: 0
|
||||
property string tempText: "--"
|
||||
property int tempVal: 0
|
||||
property string gpuText: "--"
|
||||
property real gpuUsage: 0
|
||||
property int gpuTemp: 0
|
||||
property string nvmeTempText: "--"
|
||||
property int nvmeTempVal: 0
|
||||
property string diskRootText: "--"
|
||||
property real diskRootVal: 0
|
||||
property string diskDataText: "--"
|
||||
property real diskDataVal: 0
|
||||
property string updatesText: ""
|
||||
property string updatesClass: ""
|
||||
property string alhpText: ""
|
||||
property string alhpClass: ""
|
||||
property string networkIp: "--"
|
||||
property string networkIface: "--"
|
||||
property bool idleActive: false
|
||||
property var panelWindow: null
|
||||
property string audioDrawer: "" // "" = closed, "sink" or "source"
|
||||
|
||||
function thresholdColor(val, warn, crit) {
|
||||
if (val >= crit) return Shared.Theme.danger;
|
||||
if (val >= warn) return Shared.Theme.warning;
|
||||
return Shared.Theme.success;
|
||||
}
|
||||
|
||||
function tempColor(t) {
|
||||
if (t >= 85) return Shared.Theme.danger;
|
||||
if (t >= 70) return Shared.Theme.warning;
|
||||
return Shared.Theme.success;
|
||||
}
|
||||
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
contentHeight: col.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
width: flickable.width
|
||||
spacing: 6
|
||||
|
||||
// ─── UPDATES ─────────────────────────
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
active: root.updatesClass === "pending" || root.updatesClass === "many"
|
||||
visible: active
|
||||
sourceComponent: Rectangle {
|
||||
implicitHeight: updRow.implicitHeight + 14
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: Shared.Theme.surface0
|
||||
|
||||
RowLayout {
|
||||
id: updRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 10
|
||||
Text { text: "\u{f0ab7}"; color: Shared.Theme.warning; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
|
||||
Text { text: root.updatesText + " updates available"; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; Layout.fillWidth: true }
|
||||
Text { visible: root.alhpText !== ""; text: "ALHP " + root.alhpText; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
active: root.updatesClass !== "pending" && root.updatesClass !== "many" && root.updatesClass !== ""
|
||||
visible: active
|
||||
sourceComponent: RowLayout {
|
||||
spacing: 10
|
||||
Text { text: "\u{f05e0}"; color: Shared.Theme.success; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
|
||||
Text { text: "System up to date"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
||||
|
||||
// ─── STORAGE & PERFORMANCE ───────────
|
||||
|
||||
MetricBar { label: Shared.Config.diskMount1Label; value: root.diskRootText; fill: root.diskRootVal; barColor: thresholdColor(root.diskRootVal * 100, 70, 90); valueColor: thresholdColor(root.diskRootVal * 100, 70, 90) }
|
||||
MetricBar { label: Shared.Config.diskMount2Label; value: root.diskDataText; fill: root.diskDataVal; barColor: thresholdColor(root.diskDataVal * 100, 70, 90); valueColor: thresholdColor(root.diskDataVal * 100, 70, 90) }
|
||||
|
||||
Item { implicitHeight: 2 }
|
||||
|
||||
MetricBar {
|
||||
label: "CPU"
|
||||
value: root.cpuVal.toFixed(0) + "%"
|
||||
fill: root.cpuVal / 100
|
||||
barColor: thresholdColor(root.cpuVal, 50, 80)
|
||||
valueColor: thresholdColor(root.cpuVal, 50, 80)
|
||||
suffix: root.tempText
|
||||
suffixColor: tempColor(root.tempVal)
|
||||
history: root.cpuHistory
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
label: "Mem"
|
||||
value: root.memText
|
||||
fill: root.memVal
|
||||
barColor: thresholdColor(root.memVal * 100, 60, 85)
|
||||
valueColor: thresholdColor(root.memVal * 100, 60, 85)
|
||||
history: root.memHistory
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
label: "GPU"
|
||||
value: root.gpuText
|
||||
fill: root.gpuUsage / 100
|
||||
barColor: thresholdColor(root.gpuUsage, 50, 80)
|
||||
valueColor: thresholdColor(root.gpuUsage, 50, 80)
|
||||
history: root.gpuHistory
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
label: "NVMe"
|
||||
value: root.nvmeTempText
|
||||
fill: root.nvmeTempVal / 100
|
||||
barColor: tempColor(root.nvmeTempVal)
|
||||
valueColor: tempColor(root.nvmeTempVal)
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
||||
|
||||
// ─── AUDIO ───────────────────────────
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
VolumeSlider {
|
||||
Layout.fillWidth: true
|
||||
audio: Pipewire.defaultAudioSink?.audio ?? null
|
||||
icon: (!audio || audio.muted) ? "\u{f057f}" : "\u{f057e}"
|
||||
label: "Out"
|
||||
accentColor: Shared.Theme.sky
|
||||
drawerActive: root.audioDrawer === "sink"
|
||||
onDrawerToggled: root.audioDrawer = root.audioDrawer === "sink" ? "" : "sink"
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
Layout.fillWidth: true
|
||||
audio: Pipewire.defaultAudioSource?.audio ?? null
|
||||
icon: (!audio || audio.muted) ? "\u{f036d}" : "\u{f036c}"
|
||||
label: "Mic"
|
||||
accentColor: Shared.Theme.pink
|
||||
drawerActive: root.audioDrawer === "source"
|
||||
onDrawerToggled: root.audioDrawer = root.audioDrawer === "source" ? "" : "source"
|
||||
}
|
||||
}
|
||||
|
||||
// Device drawer
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
active: root.audioDrawer !== ""
|
||||
visible: active
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 3
|
||||
|
||||
Repeater {
|
||||
model: Pipewire.nodes
|
||||
|
||||
Rectangle {
|
||||
id: devItem
|
||||
required property var modelData
|
||||
|
||||
readonly property bool isSinkDrawer: root.audioDrawer === "sink"
|
||||
readonly property bool matchesFilter: modelData.audio && !modelData.isStream
|
||||
&& modelData.isSink === isSinkDrawer
|
||||
readonly property bool isDefault: isSinkDrawer
|
||||
? Pipewire.defaultAudioSink === modelData
|
||||
: Pipewire.defaultAudioSource === modelData
|
||||
|
||||
visible: matchesFilter
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: matchesFilter ? 28 : 0
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: devMouse.containsMouse ? Shared.Theme.surface1 : (isDefault ? Shared.Theme.surface0 : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: devItem.isDefault ? "\u{f012c}" : "\u{f0765}"
|
||||
color: devItem.isDefault ? (devItem.isSinkDrawer ? Shared.Theme.sky : Shared.Theme.pink) : Shared.Theme.overlay0
|
||||
font.pixelSize: 12
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
Text {
|
||||
text: devItem.modelData.description || devItem.modelData.name
|
||||
color: devItem.isDefault ? Shared.Theme.text : Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: devItem.isDefault
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: devMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (devItem.isSinkDrawer)
|
||||
Pipewire.preferredDefaultAudioSink = devItem.modelData;
|
||||
else
|
||||
Pipewire.preferredDefaultAudioSource = devItem.modelData;
|
||||
root.audioDrawer = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
||||
|
||||
// ─── NETWORK ─────────────────────────
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
Text { text: "\u{f0bf0}"; color: Shared.Theme.overlay0; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
|
||||
Text { text: root.networkIface; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
|
||||
Item { Layout.fillWidth: true }
|
||||
Text { text: root.networkIp; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; font.bold: true }
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
||||
|
||||
// ─── SYSTRAY ─────────────────────────
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
Rectangle {
|
||||
id: trayItem
|
||||
required property var modelData
|
||||
|
||||
visible: modelData.status !== Status.Passive
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: trayMouse.containsMouse ? Shared.Theme.surface1 : "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
source: trayItem.modelData.icon
|
||||
implicitSize: 22
|
||||
}
|
||||
|
||||
// Tooltip — positioned above icon, clamped to popout width
|
||||
Rectangle {
|
||||
id: tooltip
|
||||
visible: trayMouse.containsMouse && tipText.text !== ""
|
||||
width: Math.min(tipText.implicitWidth + 12, Shared.Theme.popoutWidth - Shared.Theme.popoutPadding * 2)
|
||||
height: tipText.implicitHeight + 8
|
||||
radius: 6
|
||||
color: Shared.Theme.surface0
|
||||
z: 10
|
||||
|
||||
// Position above the icon, clamp horizontally within the popout
|
||||
y: -height - 4
|
||||
x: {
|
||||
let centered = (trayItem.width - width) / 2;
|
||||
let globalX = trayItem.mapToItem(col, centered, 0).x;
|
||||
let maxX = col.width - width;
|
||||
let clampedGlobalX = Math.max(0, Math.min(globalX, maxX));
|
||||
return centered + (clampedGlobalX - globalX);
|
||||
}
|
||||
|
||||
Text {
|
||||
id: tipText
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 12
|
||||
text: trayItem.modelData.tooltipTitle || trayItem.modelData.title || ""
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
onClicked: event => {
|
||||
let item = trayItem.modelData;
|
||||
if (event.button === Qt.RightButton || item.onlyMenu) {
|
||||
if (item.hasMenu && root.panelWindow) {
|
||||
let pos = trayItem.mapToItem(null, trayItem.width / 2, trayItem.height / 2);
|
||||
item.display(root.panelWindow, pos.x, pos.y);
|
||||
}
|
||||
} else if (event.button === Qt.MiddleButton) {
|
||||
item.secondaryActivate();
|
||||
} else {
|
||||
item.activate();
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
trayItem.modelData.scroll(event.angleDelta.y, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 6; Layout.bottomMargin: 6 }
|
||||
|
||||
// ─── ACTIONS ─────────────────────────
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
ActionIcon {
|
||||
icon: "\u{f1436}"
|
||||
label: "Idle"
|
||||
iconColor: root.idleActive ? Shared.Theme.green : Shared.Theme.overlay0
|
||||
onActivated: {
|
||||
if (root.idleActive) idleKill.running = true;
|
||||
else idleStart.running = true;
|
||||
root.idleActive = !root.idleActive;
|
||||
}
|
||||
}
|
||||
|
||||
ActionIcon {
|
||||
icon: "\u{f033e}"
|
||||
label: "Lock"
|
||||
iconColor: Shared.Theme.subtext0
|
||||
onActivated: { Shared.PopoutState.close(); lockProc.running = true; }
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
HoldAction {
|
||||
icon: "\u{f0425}"
|
||||
label: "Logout"
|
||||
iconColor: Shared.Theme.subtext0
|
||||
holdColor: Shared.Theme.green
|
||||
onConfirmed: { Shared.PopoutState.close(); logoutProc.running = true; }
|
||||
}
|
||||
|
||||
HoldAction {
|
||||
icon: "\u{f0709}"
|
||||
label: "Reboot"
|
||||
iconColor: Shared.Theme.peach
|
||||
holdColor: Shared.Theme.peach
|
||||
onConfirmed: { Shared.PopoutState.close(); rebootProc.running = true; }
|
||||
}
|
||||
|
||||
HoldAction {
|
||||
icon: "\u{f0425}"
|
||||
label: "Off"
|
||||
iconColor: Shared.Theme.danger
|
||||
holdColor: Shared.Theme.danger
|
||||
onConfirmed: { Shared.PopoutState.close(); offProc.running = true; }
|
||||
}
|
||||
}
|
||||
} // ColumnLayout
|
||||
} // Flickable
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// COMPONENTS
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
component Sparkline: Canvas {
|
||||
id: spark
|
||||
property var history: []
|
||||
property color lineColor: Shared.Theme.overlay0
|
||||
|
||||
onHistoryChanged: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
let ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
if (history.length < 2) return;
|
||||
ctx.strokeStyle = Qt.alpha(lineColor, 0.5);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
let step = width / (root.historySize - 1);
|
||||
let offset = root.historySize - history.length;
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
let x = (offset + i) * step;
|
||||
let y = height - history[i] * height;
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
component MetricBar: RowLayout {
|
||||
property string label
|
||||
property string value
|
||||
property real fill: 0
|
||||
property color barColor: Shared.Theme.green
|
||||
property color valueColor: Shared.Theme.text
|
||||
property string suffix: ""
|
||||
property color suffixColor: Shared.Theme.overlay0
|
||||
property var history: null
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: parent.label
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.preferredWidth: 40
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 10
|
||||
radius: 5
|
||||
color: Shared.Theme.surface0
|
||||
|
||||
Sparkline {
|
||||
anchors.fill: parent
|
||||
visible: history && history.length > 1
|
||||
history: parent.parent.history ?? []
|
||||
lineColor: barColor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * Math.min(1, Math.max(0, fill))
|
||||
radius: 5
|
||||
color: barColor
|
||||
opacity: Shared.Theme.opacityFill
|
||||
Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animNormal } }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: parent.value
|
||||
color: parent.valueColor
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.preferredWidth: 86
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: parent.suffix !== ""
|
||||
visible: active
|
||||
sourceComponent: Text {
|
||||
text: suffix
|
||||
color: suffixColor
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component VolumeSlider: Rectangle {
|
||||
id: vs
|
||||
property var audio: null
|
||||
property string icon
|
||||
property string label
|
||||
property color accentColor
|
||||
property bool drawerActive: false
|
||||
signal drawerToggled()
|
||||
|
||||
property real vol: 0
|
||||
property bool muted: audio?.muted ?? false
|
||||
|
||||
function updateVol() {
|
||||
if (audio && !isNaN(audio.volume) && audio.volume >= 0)
|
||||
vol = audio.volume;
|
||||
}
|
||||
onAudioChanged: { updateVol(); bindRetry.retries = 0; bindRetry.running = true; }
|
||||
Connections {
|
||||
target: vs.audio
|
||||
function onVolumeChanged() { vs.updateVol(); }
|
||||
}
|
||||
|
||||
// Retry binding pickup after async PwObjectTracker re-bind
|
||||
Timer {
|
||||
id: bindRetry
|
||||
interval: 300
|
||||
running: false
|
||||
repeat: true
|
||||
property int retries: 0
|
||||
onTriggered: {
|
||||
vs.updateVol();
|
||||
retries++;
|
||||
if (vs.vol > 0 || retries >= 5) running = false;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 36
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: drawerActive ? Qt.alpha(accentColor, Shared.Theme.opacityLight) : Shared.Theme.surface0
|
||||
border.width: drawerActive ? 1 : 0
|
||||
border.color: Qt.alpha(accentColor, Shared.Theme.opacityMedium)
|
||||
opacity: muted ? 0.45 : 1.0
|
||||
clip: true
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: Shared.Theme.animFast } }
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: vs.muted ? 0 : parent.width * Math.min(1, vs.vol)
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: Qt.alpha(vs.accentColor, Shared.Theme.opacityMedium)
|
||||
Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
Text { text: vs.icon; color: vs.accentColor; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
|
||||
Text { text: vs.label; color: vs.accentColor; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: vs.muted ? "Muted" : Math.round(vs.vol * 100) + "%"
|
||||
color: vs.muted ? Shared.Theme.overlay0 : Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: !vs.muted
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
property bool dragging: false
|
||||
property real startX: 0
|
||||
property int pressedBtn: Qt.NoButton
|
||||
onClicked: event => {
|
||||
if (event.button === Qt.MiddleButton) { pavuProc.running = true; return; }
|
||||
if (event.button === Qt.RightButton) { if (vs.audio) vs.audio.muted = !vs.audio.muted; return; }
|
||||
if (!dragging) vs.drawerToggled();
|
||||
}
|
||||
onPressed: event => { dragging = false; startX = event.x; pressedBtn = event.button; }
|
||||
onPositionChanged: event => {
|
||||
if (pressed && pressedBtn === Qt.LeftButton) {
|
||||
if (Math.abs(event.x - startX) > 5) dragging = true;
|
||||
if (dragging && vs.audio) vs.audio.volume = Math.max(0, Math.min(1, event.x / vs.width));
|
||||
}
|
||||
}
|
||||
onWheel: event => {
|
||||
if (!vs.audio) return;
|
||||
let step = 0.05;
|
||||
if (event.angleDelta.y > 0) vs.audio.volume = Math.min(1.0, vs.vol + step);
|
||||
else vs.audio.volume = Math.max(0.0, vs.vol - step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ActionIcon: Rectangle {
|
||||
property string icon
|
||||
property string label
|
||||
property color iconColor
|
||||
signal activated()
|
||||
|
||||
implicitWidth: actCol.implicitWidth + 16
|
||||
implicitHeight: actCol.implicitHeight + 14
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: actMouse.containsMouse ? Shared.Theme.surface1 : "transparent"
|
||||
border.width: actMouse.containsMouse ? 1 : 0
|
||||
border.color: Shared.Theme.surface2
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
Column {
|
||||
id: actCol
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
Text { anchors.horizontalCenter: parent.horizontalCenter; text: icon; color: iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont }
|
||||
Text { anchors.horizontalCenter: parent.horizontalCenter; text: label; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
|
||||
MouseArea { id: actMouse; anchors.fill: parent; hoverEnabled: true; onClicked: parent.activated() }
|
||||
}
|
||||
|
||||
component HoldAction: Rectangle {
|
||||
id: ha
|
||||
property string icon
|
||||
property string label
|
||||
property color iconColor
|
||||
property color holdColor: Shared.Theme.red
|
||||
signal confirmed()
|
||||
|
||||
readonly property real holdDuration: 800
|
||||
property real holdProgress: 0
|
||||
property bool holding: false
|
||||
|
||||
implicitWidth: haCol.implicitWidth + 16
|
||||
implicitHeight: haCol.implicitHeight + 14
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: haMouse.containsMouse ? Shared.Theme.surface1 : "transparent"
|
||||
border.width: haMouse.containsMouse || holding ? 1 : 0
|
||||
border.color: holding ? Qt.alpha(holdColor, Shared.Theme.opacityStrong) : Shared.Theme.surface2
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }
|
||||
|
||||
// Fill overlay
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * ha.holdProgress
|
||||
radius: Shared.Theme.radiusSmall
|
||||
color: Qt.alpha(ha.holdColor, Shared.Theme.opacityLight)
|
||||
}
|
||||
|
||||
Column {
|
||||
id: haCol
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.icon; color: ha.holding ? ha.holdColor : ha.iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont }
|
||||
Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.holding ? "Hold…" : ha.label; color: ha.holding ? ha.holdColor : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: holdTimer
|
||||
interval: 16
|
||||
running: ha.holding
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
ha.holdProgress += interval / ha.holdDuration;
|
||||
if (ha.holdProgress >= 1.0) {
|
||||
ha.holding = false;
|
||||
ha.holdProgress = 0;
|
||||
ha.confirmed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: haMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onPressed: { ha.holding = true; ha.holdProgress = 0; }
|
||||
onReleased: { ha.holding = false; ha.holdProgress = 0; }
|
||||
onCanceled: { ha.holding = false; ha.holdProgress = 0; }
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// DATA FETCHING
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
property real prevCpuActive: -1
|
||||
property real prevCpuTotal: -1
|
||||
Process { id: cpuProc; command: ["sh", "-c", "awk '/^cpu / {print $2+$4, $2+$4+$5}' /proc/stat"]; stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let p = this.text.trim().split(" ");
|
||||
let active = parseFloat(p[0]), total = parseFloat(p[1]);
|
||||
if (!isNaN(active) && root.prevCpuTotal > 0) {
|
||||
let da = active - root.prevCpuActive;
|
||||
let dt = total - root.prevCpuTotal;
|
||||
if (dt > 0) { root.cpuVal = (da / dt) * 100; root.cpuHistory = root.pushHistory(root.cpuHistory, root.cpuVal / 100); }
|
||||
}
|
||||
root.prevCpuActive = active;
|
||||
root.prevCpuTotal = total;
|
||||
}
|
||||
}}
|
||||
|
||||
Process { id: memProc; command: ["sh", "-c", "free -m | awk '/Mem:/ {printf \"%.1f/%.0fG %.4f\", $3/1024, $2/1024, $3/$2}'"]; stdout: StdioCollector {
|
||||
onStreamFinished: { let p = this.text.trim().split(" "); if (p.length >= 2) { root.memText = p[0]; root.memVal = parseFloat(p[1]) || 0; root.memHistory = root.pushHistory(root.memHistory, root.memVal); } }
|
||||
}}
|
||||
|
||||
Process { id: tempProc; command: ["sh", "-c", "sensors 2>/dev/null | awk '/Tctl:|Tdie:|Package id 0:/ {gsub(/\\+|°C/,\"\",$2); printf \"%d\", $2; exit}'"]; stdout: StdioCollector {
|
||||
onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v)) { root.tempVal = v; root.tempText = v + "°C"; } }
|
||||
}}
|
||||
|
||||
Process { id: gpuProc; command: [Shared.Config.gpuScript]; stdout: StdioCollector {
|
||||
onStreamFinished: { try {
|
||||
let d = JSON.parse(this.text);
|
||||
let tt = d.tooltip || "";
|
||||
let lines = tt.split("\n");
|
||||
let usage = 0, temp = 0, power = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let v = parseInt(lines[i].replace(/[^0-9]/g, ""));
|
||||
if (isNaN(v)) continue;
|
||||
if (lines[i].indexOf("Usage") >= 0) usage = v;
|
||||
else if (lines[i].indexOf("Temp") >= 0) temp = v;
|
||||
else if (lines[i].indexOf("Power") >= 0) power = v;
|
||||
}
|
||||
root.gpuUsage = usage; root.gpuTemp = temp;
|
||||
root.gpuText = usage + "% " + temp + "°C " + power + "W";
|
||||
root.gpuHistory = root.pushHistory(root.gpuHistory, usage / 100);
|
||||
} catch(e) {} }
|
||||
}}
|
||||
|
||||
Process { id: nvmeTempProc; command: ["sh", "-c", "for d in /sys/class/hwmon/hwmon*; do if grep -q nvme \"$d/name\" 2>/dev/null; then awk '{printf \"%d\", $1/1000}' \"$d/temp1_input\" 2>/dev/null; break; fi; done"]; stdout: StdioCollector {
|
||||
onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v) && v > 0) { root.nvmeTempVal = v; root.nvmeTempText = v + "°C"; } }
|
||||
}}
|
||||
|
||||
Process { id: diskProc; command: ["sh", "-c",
|
||||
"df -h " + Shared.Config.diskMount1 + " --output=pcent,size 2>/dev/null | tail -1 && " +
|
||||
"df -h " + Shared.Config.diskMount2 + " --output=pcent,size 2>/dev/null | tail -1 && " +
|
||||
"df " + Shared.Config.diskMount1 + " --output=pcent 2>/dev/null | tail -1 && " +
|
||||
"df " + Shared.Config.diskMount2 + " --output=pcent 2>/dev/null | tail -1"
|
||||
]; stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let lines = this.text.trim().split("\n");
|
||||
if (lines.length >= 1) root.diskRootText = lines[0].trim().replace(/\s+/g, " of ");
|
||||
if (lines.length >= 2) root.diskDataText = lines[1].trim().replace(/\s+/g, " of ");
|
||||
if (lines.length >= 3) root.diskRootVal = parseInt(lines[2]) / 100 || 0;
|
||||
if (lines.length >= 4) root.diskDataVal = parseInt(lines[3]) / 100 || 0;
|
||||
}
|
||||
}}
|
||||
|
||||
Process { id: updateProc; command: ["bash", "-c", "set -o pipefail; checkupdates 2>/dev/null | wc -l; echo \":$?\""]; stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let text = this.text.trim();
|
||||
let exitMatch = text.match(/:(\d+)$/);
|
||||
let exit = exitMatch ? parseInt(exitMatch[1]) : 1;
|
||||
let n = parseInt(text);
|
||||
// checkupdates: 0 = updates available, 2 = no updates, anything else = error
|
||||
if (exit !== 0 && exit !== 2) return; // error — keep previous state
|
||||
if (isNaN(n) || n === 0) { root.updatesText = ""; root.updatesClass = "uptodate"; }
|
||||
else if (n > 50) { root.updatesText = n.toString(); root.updatesClass = "many"; }
|
||||
else { root.updatesText = n.toString(); root.updatesClass = "pending"; }
|
||||
}
|
||||
}}
|
||||
|
||||
Process { id: alhpProc; command: ["sh", "-c", "alhp.utils -j 2>/dev/null || echo '{}'"]; stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
let d = JSON.parse(this.text);
|
||||
let total = d.total || 0;
|
||||
let stale = d.mirror_out_of_date || false;
|
||||
if (stale) { root.alhpText = "stale"; root.alhpClass = "stale"; }
|
||||
else if (total > 0) { root.alhpText = total.toString(); root.alhpClass = "bad"; }
|
||||
else { root.alhpText = ""; root.alhpClass = "good"; }
|
||||
} catch(e) { root.alhpText = "?"; root.alhpClass = "down"; }
|
||||
}
|
||||
}}
|
||||
|
||||
Process { id: netProc; command: ["sh", "-c", "ip -4 -o addr show | grep -v '" + Shared.Config.netExcludePattern + "' | head -1 | awk '{split($4,a,\"/\"); print $2 \":\" a[1]}'"]; stdout: StdioCollector {
|
||||
onStreamFinished: { let l = this.text.trim(); if (l) { let p = l.split(":"); root.networkIface = p[0] || "--"; root.networkIp = p[1] || "--"; } else { root.networkIface = "Network"; root.networkIp = "Offline"; } }
|
||||
}}
|
||||
|
||||
Process { id: pavuProc; command: ["pavucontrol"] }
|
||||
Process { id: idleProc; command: ["pgrep", "-x", Shared.Config.idleProcess]; stdout: StdioCollector { onStreamFinished: root.idleActive = this.text.trim().length > 0 } }
|
||||
Process { id: idleKill; command: ["killall", Shared.Config.idleProcess] }
|
||||
Process { id: idleStart; command: ["sh", "-c", "setsid " + Shared.Config.idleProcess + " > /dev/null 2>&1 &"] }
|
||||
Process { id: lockProc; command: [Shared.Config.lockCommand] }
|
||||
Process { id: logoutProc; command: Shared.Config.powerActions[1].command }
|
||||
Process { id: rebootProc; command: Shared.Config.powerActions[2].command }
|
||||
Process { id: offProc; command: Shared.Config.powerActions[3].command }
|
||||
|
||||
function rerun(proc) { proc.running = false; proc.running = true; }
|
||||
Timer { interval: 5000; running: true; repeat: true; onTriggered: { rerun(cpuProc); rerun(memProc); rerun(tempProc); rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); } }
|
||||
Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); } }
|
||||
Timer { interval: 300000; running: true; repeat: true; onTriggered: { rerun(updateProc); rerun(alhpProc); } }
|
||||
|
||||
// Stagger initial launches to avoid 9 concurrent process spawns
|
||||
Component.onCompleted: {
|
||||
rerun(cpuProc); rerun(memProc); rerun(tempProc);
|
||||
staggerTimer.running = true;
|
||||
}
|
||||
Timer {
|
||||
id: staggerTimer
|
||||
interval: 200
|
||||
onTriggered: { rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); rerun(diskProc); rerun(netProc); rerun(updateProc); rerun(alhpProc); }
|
||||
}
|
||||
}
|
||||
153
dot_config/quickshell/bar/popouts/WeatherPopout.qml
Normal file
153
dot_config/quickshell/bar/popouts/WeatherPopout.qml
Normal file
@@ -0,0 +1,153 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: col.implicitHeight + Shared.Theme.popoutPadding * 2
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
spacing: 6
|
||||
|
||||
// ─── Header ───
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Text {
|
||||
text: Shared.Weather.location
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 18
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: Shared.Weather.icon
|
||||
color: Shared.Theme.peach
|
||||
font.pixelSize: 24
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
Text {
|
||||
text: Shared.Weather.temp
|
||||
color: Shared.Theme.peach
|
||||
font.pixelSize: 18
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (Shared.Weather.status === "error") return "Unable to fetch weather data";
|
||||
if (Shared.Weather.status === "stale") return Shared.Weather.description + " · stale";
|
||||
if (Shared.Weather.status === "loading") return "Loading…";
|
||||
return Shared.Weather.description;
|
||||
}
|
||||
color: Shared.Weather.status === "error" ? Shared.Theme.danger
|
||||
: Shared.Weather.status === "stale" ? Shared.Theme.warning
|
||||
: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
||||
|
||||
// ─── Conditions ───
|
||||
ConditionRow { label: "Feels like"; value: Shared.Weather.feelsLike }
|
||||
ConditionRow { label: "Humidity"; value: Shared.Weather.humidity }
|
||||
ConditionRow { label: "Wind"; value: Shared.Weather.wind }
|
||||
|
||||
// ─── separator ───
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.rightMargin: 4
|
||||
Layout.topMargin: 2
|
||||
Layout.bottomMargin: 6
|
||||
height: 1
|
||||
color: Shared.Theme.surface0
|
||||
}
|
||||
|
||||
// ─── Forecast ───
|
||||
Text {
|
||||
text: "Forecast"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.bottomMargin: 2
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Shared.Weather.forecast
|
||||
|
||||
RowLayout {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: modelData.day
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.preferredWidth: 36
|
||||
}
|
||||
Text {
|
||||
text: Shared.Weather.weatherIcon(modelData.code)
|
||||
color: Shared.Theme.peach
|
||||
font.pixelSize: 14
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
Text {
|
||||
text: modelData.desc
|
||||
color: Shared.Theme.surface2
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
text: modelData.low + "°"
|
||||
color: Shared.Theme.sky
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.preferredWidth: 28
|
||||
}
|
||||
Text {
|
||||
text: modelData.high + "°"
|
||||
color: Shared.Theme.peach
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.preferredWidth: 28
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ConditionRow: RowLayout {
|
||||
property string label
|
||||
property string value
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
spacing: 10
|
||||
Text { text: parent.label; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; Layout.fillWidth: true }
|
||||
Text { text: parent.value; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; font.bold: true }
|
||||
}
|
||||
}
|
||||
0
dot_config/quickshell/launcher/.keep
Normal file
0
dot_config/quickshell/launcher/.keep
Normal file
0
dot_config/quickshell/lock/.keep
Normal file
0
dot_config/quickshell/lock/.keep
Normal file
104
dot_config/quickshell/lock/IdleScreen.qml
Normal file
104
dot_config/quickshell/lock/IdleScreen.qml
Normal file
@@ -0,0 +1,104 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
// Custom apex-neon idle screen
|
||||
// Activated by hypridle via: quickshell -c ~/.config/quickshell/lock/
|
||||
// Dismissed by any key/mouse input → triggers hyprlock
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
visible: root.active && modelData.name === Shared.Config.monitor
|
||||
WlrLayershell.namespace: "quickshell:idle"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
surfaceFormat { opaque: true }
|
||||
focusable: true
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
// Dismiss on any input
|
||||
Keys.onPressed: root.dismiss()
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: false
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
|
||||
// Full black backdrop
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#050505"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
// Large clock — apex-neon cyan, breathing opacity
|
||||
Text {
|
||||
id: clockText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Qt.formatDateTime(new Date(), Shared.Config.clockSecondsFormat)
|
||||
color: "#00eaff"
|
||||
font.pixelSize: 96
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.weight: Font.Light
|
||||
font.letterSpacing: -2
|
||||
|
||||
// Breathing animation on opacity
|
||||
SequentialAnimation on opacity {
|
||||
running: root.active
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.55; duration: 2800; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { to: 1.0; duration: 2800; easing.type: Easing.InOutSine }
|
||||
}
|
||||
}
|
||||
|
||||
// Date — dim grey, understated
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Qt.formatDateTime(new Date(), "dddd, d MMMM")
|
||||
color: "#404040"
|
||||
font.pixelSize: 18
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.letterSpacing: 2
|
||||
}
|
||||
}
|
||||
|
||||
// Clock update timer
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.active
|
||||
repeat: true
|
||||
onTriggered: clockText.text = Qt.formatDateTime(new Date(), Shared.Config.clockSecondsFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() { active = true; }
|
||||
function dismiss() {
|
||||
active = false;
|
||||
lockProc.running = true;
|
||||
}
|
||||
|
||||
Process { id: lockProc; command: [Shared.Config.lockCommand] }
|
||||
}
|
||||
159
dot_config/quickshell/notifications/NotificationDaemon.qml
Normal file
159
dot_config/quickshell/notifications/NotificationDaemon.qml
Normal file
@@ -0,0 +1,159 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
// Expose for NC popout
|
||||
property alias trackedNotifications: server.trackedNotifications
|
||||
property bool dnd: false
|
||||
|
||||
// Toast IDs currently showing as popups (capped to avoid overflow)
|
||||
readonly property int maxToasts: 4
|
||||
property var toastIds: []
|
||||
property var timestamps: ({}) // notification id → arrival epoch ms
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
keepOnReload: true
|
||||
bodySupported: true
|
||||
bodyMarkupSupported: false
|
||||
actionsSupported: true
|
||||
imageSupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
root.timestamps[notification.id] = Date.now();
|
||||
root.timestampsChanged();
|
||||
|
||||
// Suppress toasts in DnD mode (still tracked for history)
|
||||
if (root.dnd) return;
|
||||
|
||||
// Add to toast list, evict oldest if at capacity
|
||||
let ids = root.toastIds.slice();
|
||||
if (ids.length >= root.maxToasts) {
|
||||
let evicted = ids.shift();
|
||||
delete toastTimer.pending[evicted];
|
||||
}
|
||||
ids.push(notification.id);
|
||||
root.toastIds = ids;
|
||||
|
||||
// Schedule toast removal (not notification removal)
|
||||
let timeout = notification.expireTimeout > 0 ? notification.expireTimeout * 1000 : 5000;
|
||||
if (notification.urgency !== NotificationUrgency.Critical) {
|
||||
toastTimer.createTimer(notification.id, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toast timer manager
|
||||
QtObject {
|
||||
id: toastTimer
|
||||
property var pending: ({})
|
||||
|
||||
function createTimer(id, timeout) {
|
||||
pending[id] = Date.now() + timeout;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: root.toastIds.length > 0
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
let now = Date.now();
|
||||
let changed = false;
|
||||
let ids = root.toastIds.slice();
|
||||
|
||||
for (let id in toastTimer.pending) {
|
||||
if (now >= toastTimer.pending[id]) {
|
||||
let idx = ids.indexOf(parseInt(id));
|
||||
if (idx >= 0) { ids.splice(idx, 1); changed = true; }
|
||||
delete toastTimer.pending[id];
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) root.toastIds = ids;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune stale timestamps to prevent unbounded growth
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
let tracked = server.trackedNotifications;
|
||||
let live = new Set();
|
||||
for (let i = 0; i < tracked.values.length; i++)
|
||||
live.add(tracked.values[i].id);
|
||||
let ts = root.timestamps;
|
||||
let pruned = false;
|
||||
for (let id in ts) {
|
||||
if (!live.has(parseInt(id))) { delete ts[id]; pruned = true; }
|
||||
}
|
||||
if (pruned) root.timestampsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Toast popup window — shows only active toasts
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell:notifications"
|
||||
surfaceFormat { opaque: false }
|
||||
|
||||
visible: modelData.name === Shared.Config.monitor && root.toastIds.length > 0
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: 380
|
||||
implicitHeight: toastColumn.implicitHeight + 20
|
||||
color: "transparent"
|
||||
|
||||
margins {
|
||||
right: Shared.Theme.barWidth + 12
|
||||
top: 8
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toastColumn
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
width: 360
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: server.trackedNotifications
|
||||
|
||||
Loader {
|
||||
required property var modelData
|
||||
active: root.toastIds.indexOf(modelData.id) >= 0
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: NotificationPopup {
|
||||
notification: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
dot_config/quickshell/notifications/NotificationPopup.qml
Normal file
163
dot_config/quickshell/notifications/NotificationPopup.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
// Single notification card
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var notification
|
||||
|
||||
implicitWidth: 360
|
||||
implicitHeight: content.implicitHeight + 20
|
||||
radius: Shared.Theme.radiusNormal
|
||||
color: Shared.Theme.popoutBackground
|
||||
border.width: 1
|
||||
border.color: notification.urgency === NotificationUrgency.Critical ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityMedium) : Shared.Theme.borderSubtle
|
||||
|
||||
// Entrance animation
|
||||
opacity: 0
|
||||
scale: 0.95
|
||||
Component.onCompleted: { opacity = 1; scale = 1; }
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
Behavior on scale { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
|
||||
|
||||
// Dismiss on click
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.notification.dismiss()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 6
|
||||
|
||||
// Header: icon + app name + close
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
IconImage {
|
||||
source: root.notification.appIcon
|
||||
implicitSize: 18
|
||||
visible: root.notification.appIcon !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.notification.appName || "Notification"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "\u{f0156}"
|
||||
color: closeMouse.containsMouse ? Shared.Theme.danger : Shared.Theme.overlay0
|
||||
font.pixelSize: 14
|
||||
font.family: Shared.Theme.iconFont
|
||||
MouseArea {
|
||||
id: closeMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.notification.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary (title)
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.notification.summary
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.notification.body
|
||||
textFormat: Text.PlainText
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 4
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Image
|
||||
Image {
|
||||
visible: root.notification.image !== ""
|
||||
source: root.notification.image
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 120
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
Layout.topMargin: 2
|
||||
}
|
||||
|
||||
// Actions
|
||||
RowLayout {
|
||||
visible: root.notification.actions.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 2
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.notification.actions
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 28
|
||||
radius: 6
|
||||
color: actMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.text
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Urgency accent bar on left edge
|
||||
Rectangle {
|
||||
visible: root.notification.urgency === NotificationUrgency.Critical
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 1
|
||||
anchors.topMargin: root.radius
|
||||
anchors.bottomMargin: root.radius
|
||||
width: 3
|
||||
color: Shared.Theme.danger
|
||||
}
|
||||
}
|
||||
178
dot_config/quickshell/osd/Osd.qml
Normal file
178
dot_config/quickshell/osd/Osd.qml
Normal file
@@ -0,0 +1,178 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
PwObjectTracker { objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource] }
|
||||
|
||||
property string osdIcon: ""
|
||||
property real osdValue: 0
|
||||
property bool osdMuted: false
|
||||
property string osdLabel: ""
|
||||
property bool osdVisible: false
|
||||
|
||||
function showOsd(icon, value, muted, label) {
|
||||
osdIcon = icon;
|
||||
osdValue = value;
|
||||
osdMuted = muted;
|
||||
osdLabel = label;
|
||||
osdVisible = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 1500
|
||||
onTriggered: root.osdVisible = false
|
||||
}
|
||||
|
||||
// Event-driven audio change detection
|
||||
property var sinkAudio: Pipewire.defaultAudioSink?.audio ?? null
|
||||
property var sourceAudio: Pipewire.defaultAudioSource?.audio ?? null
|
||||
|
||||
Connections {
|
||||
target: root.sinkAudio
|
||||
function onVolumeChanged() {
|
||||
let a = root.sinkAudio;
|
||||
if (a) root.showOsd("\u{f057e}", a.volume, a.muted, "Volume");
|
||||
}
|
||||
function onMutedChanged() {
|
||||
let a = root.sinkAudio;
|
||||
if (a) root.showOsd(a.muted ? "\u{f057f}" : "\u{f057e}", a.volume, a.muted, a.muted ? "Muted" : "Volume");
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.sourceAudio
|
||||
function onVolumeChanged() {
|
||||
let a = root.sourceAudio;
|
||||
if (a) root.showOsd("\u{f036c}", a.volume, a.muted, "Mic");
|
||||
}
|
||||
function onMutedChanged() {
|
||||
let a = root.sourceAudio;
|
||||
if (a) root.showOsd(a.muted ? "\u{f036d}" : "\u{f036c}", a.volume, a.muted, a.muted ? "Mic muted" : "Mic");
|
||||
}
|
||||
}
|
||||
|
||||
// Brightness monitoring via brightnessctl (auto-disables if no backlight device)
|
||||
property real lastBrightness: -1
|
||||
property bool hasBrightness: false
|
||||
Process {
|
||||
id: brightProc
|
||||
command: ["brightnessctl", "-m"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let parts = this.text.trim().split(",");
|
||||
if (parts.length >= 5) {
|
||||
root.hasBrightness = true;
|
||||
let pct = parseInt(parts[4]) / 100;
|
||||
if (root.lastBrightness >= 0 && Math.abs(pct - root.lastBrightness) > 0.005)
|
||||
root.showOsd("\u{f00df}", pct, false, "Brightness");
|
||||
root.lastBrightness = pct;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 500
|
||||
running: root.hasBrightness
|
||||
repeat: true
|
||||
onTriggered: { brightProc.running = false; brightProc.running = true; }
|
||||
}
|
||||
Component.onCompleted: { brightProc.running = true; }
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell:osd"
|
||||
surfaceFormat { opaque: false }
|
||||
|
||||
visible: modelData.name === Shared.Config.monitor && root.osdVisible
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: Shared.Theme.barWidth + Shared.Theme.popoutWidth + 12
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Shared.Theme.barWidth + 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 44
|
||||
height: 180
|
||||
radius: 22
|
||||
color: Shared.Theme.popoutBackground
|
||||
border.width: 1
|
||||
border.color: Shared.Theme.borderSubtle
|
||||
|
||||
opacity: root.osdVisible ? 1.0 : 0.0
|
||||
scale: root.osdVisible ? 1.0 : 0.95
|
||||
transformOrigin: Item.Right
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
|
||||
Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 12
|
||||
anchors.bottomMargin: 12
|
||||
spacing: 8
|
||||
|
||||
// Percentage
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.osdMuted ? "M" : Math.round(root.osdValue * 100)
|
||||
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
// Vertical bar (fills bottom-up)
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: 6
|
||||
radius: 3
|
||||
color: Shared.Theme.surface0
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: root.osdMuted ? 0 : parent.height * Math.min(1, root.osdValue)
|
||||
radius: 3
|
||||
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.sky
|
||||
Behavior on height { NumberAnimation { duration: 80; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
// Icon
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.osdIcon
|
||||
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.sky
|
||||
font.pixelSize: 16
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
dot_config/quickshell/scripts/executable_gpu.sh
Normal file
110
dot_config/quickshell/scripts/executable_gpu.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# AMD GPU stats for Waybar (RDNA 4 / amdgpu)
|
||||
|
||||
set -o pipefail
|
||||
|
||||
read_numeric_file() {
|
||||
local path="$1"
|
||||
local value
|
||||
[[ -r "$path" ]] || return 1
|
||||
value=$(<"$path")
|
||||
[[ "$value" =~ ^[0-9]+$ ]] || return 1
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
# Find AMD GPU hwmon
|
||||
GPU_HWMON=""
|
||||
for hwmon in /sys/class/hwmon/hwmon*; do
|
||||
if [[ -f "$hwmon/name" ]] && grep -q "amdgpu" "$hwmon/name" 2>/dev/null; then
|
||||
GPU_HWMON="$hwmon"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$GPU_HWMON" ]]; then
|
||||
echo '{"text":"","class":"disconnected","tooltip":"AMD GPU not found"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GPU_DEVICE=$(readlink -f "$GPU_HWMON/device" 2>/dev/null || true)
|
||||
|
||||
# Read GPU stats
|
||||
temp_raw=0
|
||||
for temp_sensor in temp1_input temp2_input temp3_input; do
|
||||
if temp_candidate=$(read_numeric_file "$GPU_HWMON/$temp_sensor"); then
|
||||
temp_raw="$temp_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
temp=$((temp_raw / 1000))
|
||||
|
||||
# GPU usage from /sys/class/drm
|
||||
gpu_busy=0
|
||||
if [[ -n "$GPU_DEVICE" ]] && gpu_busy_candidate=$(read_numeric_file "$GPU_DEVICE/gpu_busy_percent"); then
|
||||
gpu_busy="$gpu_busy_candidate"
|
||||
else
|
||||
for card in /sys/class/drm/card*/device/gpu_busy_percent; do
|
||||
if gpu_busy_candidate=$(read_numeric_file "$card"); then
|
||||
gpu_busy="$gpu_busy_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# VRAM usage
|
||||
vram_used=0
|
||||
vram_total=0
|
||||
if [[ -n "$GPU_DEVICE" ]] && [[ -r "$GPU_DEVICE/mem_info_vram_used" ]]; then
|
||||
vram_used_raw=$(read_numeric_file "$GPU_DEVICE/mem_info_vram_used" || echo 0)
|
||||
vram_total_raw=$(read_numeric_file "$GPU_DEVICE/mem_info_vram_total" || echo 0)
|
||||
vram_used=$((vram_used_raw / 1024 / 1024))
|
||||
vram_total=$((vram_total_raw / 1024 / 1024))
|
||||
else
|
||||
for card in /sys/class/drm/card*/device; do
|
||||
if [[ -r "$card/mem_info_vram_used" ]]; then
|
||||
vram_used_raw=$(read_numeric_file "$card/mem_info_vram_used" || echo 0)
|
||||
vram_total_raw=$(read_numeric_file "$card/mem_info_vram_total" || echo 0)
|
||||
vram_used=$((vram_used_raw / 1024 / 1024))
|
||||
vram_total=$((vram_total_raw / 1024 / 1024))
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Power usage (watts)
|
||||
power_raw=0
|
||||
for power_sensor in power1_average power1_input; do
|
||||
if power_candidate=$(read_numeric_file "$GPU_HWMON/$power_sensor"); then
|
||||
power_raw="$power_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
power=$((power_raw / 1000000))
|
||||
|
||||
# Determine class based on temperature
|
||||
if [[ $temp -ge 90 ]]; then
|
||||
class="critical"
|
||||
elif [[ $temp -ge 75 ]]; then
|
||||
class="warning"
|
||||
elif [[ $gpu_busy -ge 90 ]]; then
|
||||
class="high"
|
||||
else
|
||||
class="normal"
|
||||
fi
|
||||
|
||||
# Format text
|
||||
text=" ${temp}°C"
|
||||
|
||||
# Build tooltip with actual newlines
|
||||
NL=$'\n'
|
||||
tooltip="AMD GPU${NL}Usage: ${gpu_busy}%${NL}Temp: ${temp}°C${NL}Power: ${power}W"
|
||||
if [[ $vram_total -gt 0 ]]; then
|
||||
vram_pct=$((vram_used * 100 / vram_total))
|
||||
tooltip="${tooltip}${NL}VRAM: ${vram_used}/${vram_total} MB (${vram_pct}%)"
|
||||
fi
|
||||
|
||||
jq -nc \
|
||||
--arg text "$text" \
|
||||
--arg class "$class" \
|
||||
--arg tooltip "$tooltip" \
|
||||
'{text: $text, class: $class, tooltip: $tooltip}'
|
||||
62
dot_config/quickshell/shared/Config.qml.tmpl
Normal file
62
dot_config/quickshell/shared/Config.qml.tmpl
Normal file
@@ -0,0 +1,62 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property string home: Quickshell.env("HOME")
|
||||
|
||||
// Appearance
|
||||
// apexFlavor: "neon" (dark) or "aeon" (light) — synced from chezmoi data.theme
|
||||
readonly property string apexFlavor: "{{ trimPrefix "apex-" .chezmoi.config.data.theme }}"
|
||||
readonly property bool transparency: true // semi-transparent backgrounds (needs Hyprland blur layerrule)
|
||||
|
||||
// Monitor — quickshell bar lives on the rightmost monitor (portrait, DP-2)
|
||||
readonly property string monitor: "DP-2"
|
||||
|
||||
// Workspaces — show all 10; Hyprland IPC tracks per-monitor active workspace
|
||||
readonly property int workspaceCount: 10
|
||||
|
||||
// Weather
|
||||
readonly property string weatherLocation: "Nospelt"
|
||||
readonly property bool useCelsius: true
|
||||
readonly property string tempUnit: useCelsius ? "°C" : "°F"
|
||||
readonly property string windUnit: useCelsius ? "km/h" : "mph"
|
||||
readonly property int forecastDays: 5
|
||||
readonly property int weatherRefreshMs: 1800000
|
||||
|
||||
// Disk mounts to monitor
|
||||
readonly property string diskMount1: "/"
|
||||
readonly property string diskMount1Label: "/"
|
||||
readonly property string diskMount2: home + "/data"
|
||||
readonly property string diskMount2Label: "/data"
|
||||
|
||||
// Scripts
|
||||
readonly property string scriptsDir: home + "/.config/quickshell/scripts"
|
||||
readonly property string gpuScript: scriptsDir + "/gpu.sh"
|
||||
|
||||
// Idle daemon
|
||||
readonly property string idleProcess: "hypridle"
|
||||
readonly property string lockCommand: "swaylock"
|
||||
|
||||
// Power commands
|
||||
readonly property var powerActions: [
|
||||
{ command: ["swaylock"] },
|
||||
{ command: ["hyprshutdown"] },
|
||||
{ command: ["hyprshutdown", "-t", "Restarting...", "--post-cmd", "systemctl reboot"] },
|
||||
{ command: ["hyprshutdown", "-t", "Powering off...", "--post-cmd", "systemctl poweroff"] }
|
||||
]
|
||||
|
||||
// Network filter (interfaces to exclude from IP display)
|
||||
readonly property string netExcludePattern: "127.0.0\\|docker\\|br-\\|veth"
|
||||
|
||||
// DateTime / Calendar
|
||||
readonly property bool use24h: true
|
||||
readonly property string clockFormat: use24h ? "HH:mm" : "hh:mm A"
|
||||
readonly property string clockSecondsFormat: use24h ? "HH:mm:ss" : "hh:mm:ss A"
|
||||
readonly property string dateFormat: "dddd, d MMMM yyyy"
|
||||
readonly property string pillDateFormat: "ddd\nd"
|
||||
readonly property string pillTimeFormat: use24h ? "HH\nmm" : "hh\nmm"
|
||||
readonly property bool weekStartsMonday: true
|
||||
readonly property var dayHeaders: weekStartsMonday ? ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] : ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
||||
}
|
||||
22
dot_config/quickshell/shared/PopoutState.qml
Normal file
22
dot_config/quickshell/shared/PopoutState.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
property string active: ""
|
||||
property real triggerY: 0
|
||||
|
||||
function toggle(name: string, y: real): void {
|
||||
if (active === name) {
|
||||
active = "";
|
||||
} else {
|
||||
active = name;
|
||||
triggerY = y;
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
active = "";
|
||||
}
|
||||
}
|
||||
124
dot_config/quickshell/shared/Theme.qml
Normal file
124
dot_config/quickshell/shared/Theme.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
// ─── Apex palettes ───────────────────────
|
||||
readonly property var palettes: ({
|
||||
neon: {
|
||||
// Backgrounds
|
||||
base: "#050505", mantle: "#0a0a0a", crust: "#000000",
|
||||
surface0: "#141414", surface1: "#1e1e1e", surface2: "#262626",
|
||||
overlay0: "#404040", overlay1: "#555555",
|
||||
// Text
|
||||
text: "#ededed", subtext0: "#b0b0b0", subtext1: "#d0d0d0",
|
||||
// Accent colors — apex-neon palette
|
||||
lavender: "#9d00ff", // sacred purple
|
||||
mauve: "#9d00ff", // sacred purple
|
||||
pink: "#ff0044", // razor red
|
||||
red: "#ff0044", // razor red
|
||||
peach: "#ff8899", // alert salmon
|
||||
yellow: "#ffb700", // amber warning
|
||||
green: "#00ff99", // toxic green
|
||||
teal: "#00ff99", // toxic green
|
||||
blue: "#00eaff", // tech cyan
|
||||
sky: "#00eaff" // tech cyan
|
||||
},
|
||||
aeon: {
|
||||
// Backgrounds
|
||||
base: "#f5f5f5", mantle: "#e8e8e8", crust: "#d0d0d0",
|
||||
surface0: "#e0e0e0", surface1: "#d4d4d4", surface2: "#c8c8c8",
|
||||
overlay0: "#737373", overlay1: "#555555",
|
||||
// Text
|
||||
text: "#0a0a0a", subtext0: "#444444", subtext1: "#333333",
|
||||
// Accent colors — apex-aeon palette
|
||||
lavender: "#7a3cff", // indigo purple
|
||||
mauve: "#7a3cff", // indigo purple
|
||||
pink: "#ff0044", // razor red
|
||||
red: "#ff0044", // razor red
|
||||
peach: "#ff4d6d", // rose error
|
||||
yellow: "#d18f00", // dark amber
|
||||
green: "#00b377", // forest green
|
||||
teal: "#00b377", // forest green
|
||||
blue: "#007a88", // deep teal
|
||||
sky: "#007a88" // deep teal
|
||||
}
|
||||
})
|
||||
|
||||
readonly property var p: palettes[Config.apexFlavor] || palettes.neon
|
||||
|
||||
// ─── Palette colors ──────────────────────
|
||||
readonly property color base: p.base
|
||||
readonly property color mantle: p.mantle
|
||||
readonly property color crust: p.crust
|
||||
readonly property color surface0: p.surface0
|
||||
readonly property color surface1: p.surface1
|
||||
readonly property color surface2: p.surface2
|
||||
readonly property color overlay0: p.overlay0
|
||||
readonly property color text: p.text
|
||||
readonly property color subtext0: p.subtext0
|
||||
readonly property color subtext1: p.subtext1
|
||||
readonly property color lavender: p.lavender
|
||||
readonly property color mauve: p.mauve
|
||||
readonly property color pink: p.pink
|
||||
readonly property color red: p.red
|
||||
readonly property color peach: p.peach
|
||||
readonly property color yellow: p.yellow
|
||||
readonly property color green: p.green
|
||||
readonly property color teal: p.teal
|
||||
readonly property color blue: p.blue
|
||||
readonly property color sky: p.sky
|
||||
|
||||
// ─── Semantic aliases ────────────────────
|
||||
readonly property color accent: blue // tech cyan / deep teal
|
||||
readonly property color success: green // toxic / forest
|
||||
readonly property color warning: yellow // amber
|
||||
readonly property color danger: red // razor red
|
||||
readonly property color info: sky // cyan / teal
|
||||
readonly property color muted: overlay0 // dim grey
|
||||
|
||||
// ─── Opacity tokens ──────────────────────
|
||||
readonly property real opacitySubtle: 0.08 // borders, faint dividers
|
||||
readonly property real opacityLight: 0.15 // tinted backgrounds
|
||||
readonly property real opacityMedium: 0.3 // active borders, overlays
|
||||
readonly property real opacityStrong: 0.45 // muted/disabled elements
|
||||
readonly property real opacityFill: 0.7 // progress bar fills
|
||||
|
||||
// ─── Border token ────────────────────────
|
||||
readonly property bool isDark: Config.apexFlavor === "neon"
|
||||
readonly property color borderSubtle: isDark ? Qt.rgba(1, 1, 1, opacitySubtle) : Qt.rgba(0, 0, 0, opacitySubtle)
|
||||
|
||||
// ─── Transparency ────────────────────────
|
||||
readonly property bool transparencyEnabled: Config.transparency
|
||||
readonly property color barBackground: transparencyEnabled ? Qt.alpha(mantle, 0.75) : mantle
|
||||
readonly property color popoutBackground: transparencyEnabled ? Qt.alpha(mantle, 0.82) : mantle
|
||||
|
||||
// ─── Layout ──────────────────────────────
|
||||
readonly property int barWidth: 52
|
||||
readonly property int barInnerWidth: 40
|
||||
readonly property int barPadding: 6
|
||||
readonly property int spacing: 10
|
||||
|
||||
// Popouts
|
||||
readonly property int popoutWidth: 320
|
||||
readonly property int popoutPadding: 16
|
||||
readonly property int popoutSpacing: 10
|
||||
|
||||
// Rounding
|
||||
readonly property int radiusSmall: 8
|
||||
readonly property int radiusNormal: 14
|
||||
readonly property int radiusPill: 1000
|
||||
|
||||
// ─── Typography ──────────────────────────
|
||||
readonly property int fontSmall: 11
|
||||
readonly property int fontSize: 13
|
||||
readonly property int fontLarge: 15
|
||||
readonly property string fontFamily: "GeistMono Nerd Font"
|
||||
readonly property string iconFont: "GeistMono Nerd Font"
|
||||
|
||||
// ─── Animation ───────────────────────────
|
||||
readonly property int animFast: 150
|
||||
readonly property int animNormal: 300
|
||||
readonly property int animSlow: 500
|
||||
}
|
||||
15
dot_config/quickshell/shared/Time.qml
Normal file
15
dot_config/quickshell/shared/Time.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property string clock: Qt.formatDateTime(systemClock.date, Config.clockFormat)
|
||||
readonly property string clockSeconds: Qt.formatDateTime(systemClock.date, Config.clockSecondsFormat)
|
||||
readonly property date date: systemClock.date
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
}
|
||||
85
dot_config/quickshell/shared/Weather.qml
Normal file
85
dot_config/quickshell/shared/Weather.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool useCelsius: Config.useCelsius
|
||||
|
||||
property string location: Config.weatherLocation
|
||||
property string temp: "--"
|
||||
property string icon: "\u{f0590}"
|
||||
property string description: "--"
|
||||
property string feelsLike: "--"
|
||||
property string humidity: "--"
|
||||
property string wind: "--"
|
||||
property var forecast: []
|
||||
property string status: "loading" // "loading", "ok", "error"
|
||||
|
||||
function weatherIcon(code) {
|
||||
code = parseInt(code);
|
||||
if (code === 113) return "\u{f0599}";
|
||||
if (code === 116) return "\u{f0595}";
|
||||
if (code <= 122) return "\u{f0590}";
|
||||
if (code <= 260) return "\u{f0591}";
|
||||
if ([176,263,266,293,296,299,302,305,308,353,356,359].includes(code)) return "\u{f0597}";
|
||||
return "\u{f0598}";
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherProc
|
||||
command: ["sh", "-c", "curl -sf 'wttr.in/" + Config.weatherLocation + "?format=j1' 2>/dev/null"]
|
||||
running: true
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
let data = JSON.parse(this.text);
|
||||
let cur = data.current_condition[0];
|
||||
let tempKey = root.useCelsius ? "temp_C" : "temp_F";
|
||||
let feelsKey = root.useCelsius ? "FeelsLikeC" : "FeelsLikeF";
|
||||
let windKey = root.useCelsius ? "windspeedKmph" : "windspeedMiles";
|
||||
let maxKey = root.useCelsius ? "maxtempC" : "maxtempF";
|
||||
let minKey = root.useCelsius ? "mintempC" : "mintempF";
|
||||
|
||||
root.temp = cur[tempKey] + Config.tempUnit;
|
||||
root.feelsLike = cur[feelsKey] + Config.tempUnit;
|
||||
root.humidity = cur.humidity + "%";
|
||||
root.wind = cur[windKey] + " " + Config.windUnit;
|
||||
root.description = cur.weatherDesc[0].value;
|
||||
root.location = data.nearest_area[0].areaName[0].value;
|
||||
root.icon = root.weatherIcon(cur.weatherCode);
|
||||
|
||||
let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
let fc = [];
|
||||
let weather = data.weather || [];
|
||||
for (let i = 0; i < Math.min(weather.length, Config.forecastDays); i++) {
|
||||
let w = weather[i];
|
||||
let d = new Date(w.date);
|
||||
fc.push({
|
||||
day: days[d.getDay()],
|
||||
high: w[maxKey],
|
||||
low: w[minKey],
|
||||
code: w.hourly[4].weatherCode,
|
||||
desc: w.hourly[4].weatherDesc[0].value
|
||||
});
|
||||
}
|
||||
root.forecast = fc;
|
||||
root.status = "ok";
|
||||
} catch(e) {
|
||||
console.warn("Weather: failed to parse response:", e);
|
||||
root.status = root.temp === "--" ? "error" : "stale";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.weatherRefreshMs
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: weatherProc.running = true
|
||||
}
|
||||
}
|
||||
13
dot_config/quickshell/shell.qml
Normal file
13
dot_config/quickshell/shell.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
//@ pragma UseQApplication
|
||||
|
||||
import Quickshell
|
||||
import "notifications"
|
||||
import "osd"
|
||||
import "lock"
|
||||
|
||||
ShellRoot {
|
||||
NotificationDaemon { id: notifDaemon }
|
||||
Bar { notifModel: notifDaemon.trackedNotifications; notifDaemon: notifDaemon }
|
||||
Osd {}
|
||||
IdleScreen { id: idleScreen }
|
||||
}
|
||||
Reference in New Issue
Block a user