add full proj
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Nix build outputs
|
||||
result
|
||||
result-*
|
||||
|
||||
# direnv / nix develop
|
||||
.direnv/
|
||||
.envrc.local
|
||||
|
||||
# Editor / IDE
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
.qmlls.ini
|
||||
|
||||
# OS cruft
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
80
CLAUDE.md
Normal file
80
CLAUDE.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
A Wayland status bar for Sway/i3, written entirely in **QML** and driven by
|
||||
[Quickshell](https://quickshell.outfoxxed.me/), packaged with a Nix flake.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
# run without building (hot-reloads on file save)
|
||||
nix develop # shell with quickshell + qmlls/qmlformat on PATH
|
||||
quickshell --path . # or: qs -p .
|
||||
|
||||
nix run . # run straight from the flake
|
||||
nix build . && ./result/bin/quickshell-bar # build the wrapper, then run
|
||||
```
|
||||
|
||||
There is **no test suite, no linter, and no build step** for the QML itself —
|
||||
it's interpreted and hot-reloaded. The only "build" is the Nix wrapper that
|
||||
pins fonts and runtime tools. `qmlformat`/`qmlls` (from `qt6.qtdeclarative`,
|
||||
available in the dev shell) are the only static tooling. Verify changes by
|
||||
running the bar and looking at it.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Entry point.** `shell.qml` → `ShellRoot` with `Variants { model: Quickshell.screens }`
|
||||
spawns one `widgets/Bar.qml` (`PanelWindow`) per monitor. `Bar.qml` is a fixed
|
||||
three-zone layout: workspaces (left), clock (center), a `RowLayout` of metric
|
||||
modules (right). Changing module order/presence is purely editing that RowLayout.
|
||||
|
||||
**Three import roots, registered via `qmldir`:**
|
||||
- `config/` — `Theme` and `Icons`, both **singletons** (`pragma Singleton` +
|
||||
`singleton` line in `config/qmldir`). Global palette/geometry/fonts and Nerd
|
||||
Font glyphs. Import with `import "../config"`, reference as `Theme.x` / `Icons.x`.
|
||||
- `services/` — `SysStats`, a **singleton** that is the single source of truth
|
||||
for all `/proc` + `/sys` metrics. Import with `import "../services"`.
|
||||
- `widgets/` — the visual modules. `Pill.qml` is the rounded container every
|
||||
module reuses (`default property alias content` lets children sit inside a
|
||||
centered RowLayout); `MetricPill.qml` adds the icon + value convention.
|
||||
|
||||
**The metrics pipeline (`services/SysStats.qml`) is the core design.** One `Timer`
|
||||
(1 Hz, `interval` property) calls `_tickOnce()`, which `reload()`s a set of
|
||||
`FileView`s and parses them into reactive properties (`cpu`, `cpuFreq`, `mem`,
|
||||
`temp`, rates, battery, …). Widgets just bind to those properties. Key conventions:
|
||||
- `FileView { blockLoading: true }` makes `reload()` synchronous so `text()` is
|
||||
fresh on the same tick. Use this for tiny virtual files in `/proc`/`/sys`.
|
||||
- Add a metric by: declaring a property, adding a `FileView` (or reusing one by
|
||||
reassigning its `path`, as `_parseNet` does for the active interface), writing
|
||||
a `_parseX()`, and calling `reload()` + `_parseX()` inside `_tickOnce()`.
|
||||
- Deltas (CPU%, net rates) keep a `_prev*` field and diff against the last tick.
|
||||
- **Two metrics escape the pure-virtual-file rule:** disk usage runs `df` via a
|
||||
`Process` only every 30 ticks (`_tick % 30`), and a one-shot `Process` runs
|
||||
`services/discover.sh` at startup. The bar avoids per-tick subprocess spawns
|
||||
by design — keep new metrics reading virtual files, not shelling out.
|
||||
|
||||
**`discover.sh` handshake.** At startup it prints `TEMP <path>` and `BAT <path>`
|
||||
(hwmon temp sensor + main battery, skipping `scope=Device` peripherals) on stdout;
|
||||
the QML side parses those lines into `tempPath`/`batPath`, which then feed
|
||||
`FileView`s. sysfs paths are stable for a boot, so this runs once, not per tick.
|
||||
|
||||
## Conventions & gotchas
|
||||
|
||||
- **Icons are referenced by Unicode codepoint** (`config/Icons.qml`,
|
||||
`String.fromCharCode(0x...)`) so source stays ASCII. The glyph **must exist in
|
||||
the bundled Nerd Font** — `flake.nix` bundles `nerd-fonts.jetbrains-mono` +
|
||||
`nerd-fonts.symbols-only` (Font Awesome 4 range, plus Material Design). Font
|
||||
Awesome 5+ codepoints are NOT present and render as tofu squares; verify a
|
||||
codepoint is covered before using it. Codepoints above `0xFFFF` (e.g. Material
|
||||
Design `0xf0xxx`) need `String.fromCodePoint`, not `String.fromCharCode`.
|
||||
- **Fonts are pinned via `FONTCONFIG_FILE`** in the flake wrapper, so glyphs
|
||||
render regardless of the host's installed fonts. Adding a font means editing
|
||||
`fontsConf` in `flake.nix`.
|
||||
- **`Bar.qml` remaps its layer surface on geometry change** (`geomKey` →
|
||||
toggle `visible`). This is a deliberate workaround for stale-buffer garbage
|
||||
after output rotation/transform under Sway — don't remove it.
|
||||
- **Quickshell upstream is tracked directly** in `flake.nix` (git, not nixpkgs)
|
||||
to pick up layer-surface fixes ahead of release.
|
||||
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
|
||||
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
|
||||
103
README.md
Normal file
103
README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# quickshell-bar
|
||||
|
||||
A Wayland status bar for [Sway](https://swaywm.org/), built with
|
||||
[Quickshell](https://quickshell.outfoxxed.me/) and packaged with Nix flakes.
|
||||
|
||||

|
||||
|
||||
## Modules
|
||||
|
||||
Left → right:
|
||||
|
||||
| Module | Source |
|
||||
|-------------|------------------------------------------|
|
||||
| Workspaces | Sway/i3 IPC (`Quickshell.I3`) |
|
||||
| Clock | local time (center) |
|
||||
| CPU | `/proc/stat` (per-core load bars + overall %) |
|
||||
| CPU temp | `coretemp`/`k10temp` hwmon, else thermal |
|
||||
| RAM | `/proc/meminfo` |
|
||||
| Disk | `df /` (every 30s) |
|
||||
| Network | `/sys/class/net/<active>/statistics` ↓↑ |
|
||||
| Volume | PipeWire default sink (scroll/click) |
|
||||
| Tray | StatusNotifier (`Quickshell.Services.SystemTray`) |
|
||||
| Battery | first non-peripheral `power_supply` |
|
||||
|
||||
System metrics are read straight from `/proc` and `/sys` in QML via
|
||||
`FileView`, polled once a second (`services/SysStats.qml`) — no persistent
|
||||
helper process and no per-tick `awk`/`df`/`cat` spawns. Two exceptions have
|
||||
no virtual-file equivalent: hardware discovery runs once at startup
|
||||
(`services/discover.sh`, locating the hwmon temp sensor and main battery),
|
||||
and disk usage uses `df` refreshed every 30s (the kernel exposes free space
|
||||
only via the `statvfs` syscall). Workspaces, volume, tray and battery icon
|
||||
use native Quickshell services.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Sway (or i3) — the workspace module talks to `$SWAYSOCK`/`$I3SOCK`.
|
||||
- A running PipeWire session for the volume module.
|
||||
- Nix with flakes enabled (`experimental-features = nix-command flakes`).
|
||||
|
||||
Fonts (JetBrains Mono Nerd Font + Inter) are bundled into the package via
|
||||
`FONTCONFIG_FILE`, so glyphs render even if they aren't installed system-wide.
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
# run directly from the flake
|
||||
nix run .
|
||||
|
||||
# or build a wrapper and run it
|
||||
nix build .
|
||||
./result/bin/quickshell-bar
|
||||
```
|
||||
|
||||
During development you can also run it without building:
|
||||
|
||||
```sh
|
||||
nix develop # drops you into a shell with quickshell on PATH
|
||||
quickshell --path . # or: qs -p .
|
||||
```
|
||||
|
||||
## Use it from Sway
|
||||
|
||||
Add to `~/.config/sway/config`:
|
||||
|
||||
```
|
||||
# hide the built-in swaybar
|
||||
bar { mode invisible }
|
||||
|
||||
# launch the Quickshell bar (adjust the path to this repo)
|
||||
exec nix run /path/to/quickshell_bar
|
||||
```
|
||||
|
||||
Or, if you install the package (e.g. into your system/Home-Manager profile),
|
||||
just `exec quickshell-bar`. The bar anchors to the top edge and reserves an
|
||||
exclusive zone, so windows tile beneath it automatically.
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything is plain QML — edit and the bar hot-reloads.
|
||||
|
||||
- **Colours, sizes, fonts** — `config/Theme.qml` (Catppuccin Mocha by default).
|
||||
- **Icons** — `config/Icons.qml` (Nerd Font codepoints).
|
||||
- **Poll interval** — `interval` in `services/SysStats.qml` (kept in sync with
|
||||
the argument passed to `stats.sh`).
|
||||
- **Module order / layout** — `widgets/Bar.qml`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
shell.qml entry point — one Bar per monitor
|
||||
config/
|
||||
Theme.qml colours, sizes, fonts (singleton)
|
||||
Icons.qml Nerd Font glyphs (singleton)
|
||||
services/
|
||||
SysStats.qml reads /proc & /sys via FileView into reactive props (singleton)
|
||||
discover.sh one-shot hwmon/battery path discovery at startup
|
||||
widgets/
|
||||
Bar.qml the PanelWindow + layout
|
||||
Pill.qml rounded container used by every module
|
||||
MetricPill.qml icon + value helper
|
||||
Workspaces.qml Clock.qml CpuGraph.qml CpuTemp.qml
|
||||
Ram.qml Disk.qml Network.qml Volume.qml Tray.qml Battery.qml
|
||||
```
|
||||
38
config/Icons.qml
Normal file
38
config/Icons.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
// Nerd Font glyphs referenced by codepoint so the source stays ASCII and the
|
||||
// exact glyph is unambiguous (independent of editor/font rendering).
|
||||
// Codepoints are from the Font Awesome (FA) range bundled in Nerd Fonts.
|
||||
Singleton {
|
||||
readonly property string cpu: String.fromCharCode(0xf2db) // microchip
|
||||
readonly property string memory: String.fromCodePoint(0xf035b) // md-memory (FA5 0xf538 is absent from the bundled Nerd Fonts)
|
||||
readonly property string thermo: String.fromCharCode(0xf2c9) // thermometer-half
|
||||
readonly property string disk: String.fromCharCode(0xf0a0) // hdd-o
|
||||
readonly property string wifi: String.fromCharCode(0xf1eb) // wifi
|
||||
readonly property string ethernet: String.fromCharCode(0xf6ff) // network-wired
|
||||
readonly property string down: String.fromCharCode(0xf063) // arrow-down
|
||||
readonly property string up: String.fromCharCode(0xf062) // arrow-up
|
||||
readonly property string clock: String.fromCharCode(0xf017) // clock-o
|
||||
readonly property string volHigh: String.fromCharCode(0xf028) // volume-up
|
||||
readonly property string volLow: String.fromCharCode(0xf027) // volume-down
|
||||
readonly property string volMute: String.fromCharCode(0xf026) // volume-off
|
||||
readonly property string bolt: String.fromCharCode(0xf0e7) // bolt (charging)
|
||||
readonly property string plug: String.fromCharCode(0xf1e6) // plug (AC)
|
||||
|
||||
// battery glyphs full -> empty (FA battery-4 .. battery-0)
|
||||
readonly property var batterySteps: [
|
||||
String.fromCharCode(0xf244), // empty
|
||||
String.fromCharCode(0xf243), // quarter
|
||||
String.fromCharCode(0xf242), // half
|
||||
String.fromCharCode(0xf241), // three-quarters
|
||||
String.fromCharCode(0xf240) // full
|
||||
]
|
||||
|
||||
function battery(pct) {
|
||||
var i = Math.round((pct / 100) * 4);
|
||||
if (i < 0) i = 0; if (i > 4) i = 4;
|
||||
return batterySteps[i];
|
||||
}
|
||||
}
|
||||
49
config/Theme.qml
Normal file
49
config/Theme.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
// --- geometry ---
|
||||
readonly property int barHeight: 34
|
||||
readonly property int radius: 8
|
||||
readonly property int spacing: 6
|
||||
readonly property int padding: 10
|
||||
readonly property int gap: 4
|
||||
|
||||
// --- palette (Catppuccin Mocha, darkened toward OLED black) ---
|
||||
readonly property color base: "#0d0d12"
|
||||
readonly property color mantle: "#08080b"
|
||||
readonly property color surface0: "#1c1c28"
|
||||
readonly property color surface1: "#2a2a3a"
|
||||
readonly property color overlay: "#52526a"
|
||||
readonly property color text: "#cdd6f4"
|
||||
readonly property color subtext: "#a6adc8"
|
||||
|
||||
readonly property color rosewater: "#f5e0dc"
|
||||
readonly property color red: "#f38ba8"
|
||||
readonly property color peach: "#fab387"
|
||||
readonly property color yellow: "#f9e2af"
|
||||
readonly property color green: "#a6e3a1"
|
||||
readonly property color teal: "#94e2d5"
|
||||
readonly property color sky: "#89dceb"
|
||||
readonly property color blue: "#89b4fa"
|
||||
readonly property color mauve: "#cba6f7"
|
||||
readonly property color lavender: "#b4befe"
|
||||
|
||||
// bar background, slightly translucent
|
||||
readonly property color barColor: Qt.rgba(0.051, 0.051, 0.071, 0.94)
|
||||
|
||||
// --- typography ---
|
||||
readonly property string font: "Inter, sans-serif"
|
||||
readonly property string monoFont: "JetBrainsMono Nerd Font, monospace"
|
||||
readonly property int fontSize: 15
|
||||
|
||||
// map a 0..100 load value to a colour (green -> yellow -> red)
|
||||
function loadColor(pct) {
|
||||
if (pct >= 85) return red;
|
||||
if (pct >= 60) return peach;
|
||||
if (pct >= 35) return yellow;
|
||||
return green;
|
||||
}
|
||||
}
|
||||
2
config/qmldir
Normal file
2
config/qmldir
Normal file
@@ -0,0 +1,2 @@
|
||||
singleton Theme 1.0 Theme.qml
|
||||
singleton Icons 1.0 Icons.qml
|
||||
BIN
docs/bar.png
Normal file
BIN
docs/bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
48
flake.lock
generated
Normal file
48
flake.lock
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"quickshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779430452,
|
||||
"narHash": "sha256-zTslhsxLqUlRTML506iougTGzyR38Fzhzn7t4KDEuuE=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "4b4fca3224ab977dc515ac0bb78d00b3dfa71e00",
|
||||
"revCount": 819,
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"quickshell": "quickshell"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
85
flake.nix
Normal file
85
flake.nix
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
description = "A Wayland status bar for Sway, built with Quickshell";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
# Upstream Quickshell, tracked directly so we get layer-surface fixes
|
||||
# (e.g. clean buffers on output transform / rotation) ahead of nixpkgs.
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/quickshell/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, quickshell }:
|
||||
let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
forAllSystems = f:
|
||||
nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (pkgs:
|
||||
let
|
||||
# Quickshell from the upstream flake rather than nixpkgs.
|
||||
qsPkg = quickshell.packages.${pkgs.system}.default;
|
||||
|
||||
# Runtime tools the bar relies on: discover.sh (one-shot hardware
|
||||
# probe) and the periodic `df` for disk usage. Everything else is
|
||||
# read from /proc and /sys directly in QML.
|
||||
runtimeInputs = [
|
||||
qsPkg
|
||||
pkgs.bash
|
||||
pkgs.coreutils # df, cat
|
||||
];
|
||||
|
||||
# Self-contained font set so Nerd Font glyphs always render,
|
||||
# regardless of the host's installed fonts.
|
||||
fontsConf = pkgs.makeFontsConf {
|
||||
fontDirectories = [
|
||||
pkgs.nerd-fonts.jetbrains-mono
|
||||
pkgs.nerd-fonts.symbols-only
|
||||
pkgs.inter
|
||||
];
|
||||
};
|
||||
|
||||
bar = pkgs.writeShellApplication {
|
||||
name = "quickshell-bar";
|
||||
inherit runtimeInputs;
|
||||
text = ''
|
||||
export FONTCONFIG_FILE=${fontsConf}
|
||||
exec quickshell --path ${self} "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
default = bar;
|
||||
quickshell-bar = bar;
|
||||
});
|
||||
|
||||
apps = forAllSystems (pkgs: {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${self.packages.${pkgs.system}.default}/bin/quickshell-bar";
|
||||
};
|
||||
});
|
||||
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
quickshell.packages.${pkgs.system}.default
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.nerd-fonts.jetbrains-mono
|
||||
pkgs.inter
|
||||
pkgs.qt6.qtdeclarative # qmlls / qmlformat for editing
|
||||
];
|
||||
shellHook = ''
|
||||
echo "quickshell $(quickshell --version 2>/dev/null | head -1)"
|
||||
echo "Run the bar with: quickshell --path ."
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
formatter = forAllSystems (pkgs: pkgs.nixpkgs-fmt);
|
||||
};
|
||||
}
|
||||
290
services/SysStats.qml
Normal file
290
services/SysStats.qml
Normal file
@@ -0,0 +1,290 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
// System metrics gathered by reading /proc and /sys directly from QML via
|
||||
// FileView, polled by a Timer. No persistent helper process and no per-tick
|
||||
// subprocess churn (awk/df/cat). Two exceptions that have no virtual-file
|
||||
// equivalent: hardware discovery (one-shot at startup) and disk usage
|
||||
// (df, refreshed infrequently).
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// --- live values, updated each tick ---
|
||||
property real cpu: 0 // %
|
||||
property real cpuFreq: 0 // MHz, max across all cores
|
||||
property real mem: 0 // %
|
||||
property int temp: 0 // °C
|
||||
property int disk: 0 // % used on /
|
||||
property string iface: "" // active interface name
|
||||
property real rxRate: 0 // bytes/sec down
|
||||
property real txRate: 0 // bytes/sec up
|
||||
property int battery: -1 // %, -1 if none
|
||||
property string batteryStatus: "unknown"
|
||||
|
||||
// per-core utilization, 0..100, index = core number
|
||||
property var coreLoads: []
|
||||
property int coreCount: 0
|
||||
|
||||
readonly property bool hasBattery: battery >= 0
|
||||
|
||||
// poll interval in seconds
|
||||
readonly property int interval: 1
|
||||
|
||||
// discovered sysfs paths (filled once by discover.sh)
|
||||
property string tempPath: ""
|
||||
property string batPath: ""
|
||||
|
||||
// --- previous-sample state for delta computations ---
|
||||
property double _prevCpuTotal: 0
|
||||
property double _prevCpuIdle: 0
|
||||
property var _prevCoreTotal: []
|
||||
property var _prevCoreIdle: []
|
||||
property double _prevRx: 0
|
||||
property double _prevTx: 0
|
||||
property string _prevIface: ""
|
||||
property int _tick: 0
|
||||
|
||||
// ---- file readers ------------------------------------------------------
|
||||
// blockLoading makes reload() synchronous so text() is fresh on the
|
||||
// same tick. These are tiny virtual files, so the cost is negligible.
|
||||
|
||||
FileView { id: statView; path: "/proc/stat"; blockLoading: true }
|
||||
FileView { id: cpuInfoView; path: "/proc/cpuinfo"; blockLoading: true }
|
||||
FileView { id: memView; path: "/proc/meminfo"; blockLoading: true }
|
||||
FileView { id: routeView; path: "/proc/net/route"; blockLoading: true }
|
||||
FileView {
|
||||
id: tempView
|
||||
path: root.tempPath
|
||||
blockLoading: true
|
||||
}
|
||||
FileView {
|
||||
id: batCapView
|
||||
path: root.batPath ? root.batPath + "/capacity" : ""
|
||||
blockLoading: true
|
||||
}
|
||||
FileView {
|
||||
id: batStatusView
|
||||
path: root.batPath ? root.batPath + "/status" : ""
|
||||
blockLoading: true
|
||||
}
|
||||
// network counters: path follows the active interface
|
||||
FileView { id: rxView; blockLoading: true }
|
||||
FileView { id: txView; blockLoading: true }
|
||||
|
||||
// ---- parsing -----------------------------------------------------------
|
||||
|
||||
// Utilization from a /proc/stat "cpu..." field array, given the previous
|
||||
// total/idle for that line. Returns { load, total, idle }.
|
||||
function _cpuLineLoad(f, prevTotal, prevIdle) {
|
||||
let total = 0;
|
||||
for (let i = 0; i < f.length; i++) total += f[i];
|
||||
const idle = f[3] + f[4]; // idle + iowait
|
||||
const dt = total - prevTotal;
|
||||
const di = idle - prevIdle;
|
||||
let load = 0;
|
||||
if (prevTotal !== 0 && dt > 0)
|
||||
load = Math.max(0, Math.min(100, (1 - di / dt) * 100));
|
||||
return { load: load, total: total, idle: idle };
|
||||
}
|
||||
|
||||
function _parseCpu() {
|
||||
const text = statView.text();
|
||||
if (!text) return;
|
||||
const lines = text.split("\n");
|
||||
const loads = [];
|
||||
const prevT = root._prevCoreTotal.slice();
|
||||
const prevI = root._prevCoreIdle.slice();
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const ln = lines[li];
|
||||
if (!ln.startsWith("cpu")) break; // cpu lines lead /proc/stat
|
||||
const parts = ln.trim().split(/\s+/);
|
||||
const f = parts.slice(1).map(Number);
|
||||
if (f.length < 5) continue;
|
||||
if (parts[0] === "cpu") { // aggregate -> overall %
|
||||
const r = root._cpuLineLoad(f, root._prevCpuTotal, root._prevCpuIdle);
|
||||
root.cpu = r.load;
|
||||
root._prevCpuTotal = r.total;
|
||||
root._prevCpuIdle = r.idle;
|
||||
} else { // "cpuN" -> per-core
|
||||
const idx = parseInt(parts[0].slice(3));
|
||||
if (isNaN(idx)) continue;
|
||||
const r = root._cpuLineLoad(f, prevT[idx] || 0, prevI[idx] || 0);
|
||||
loads[idx] = Math.round(r.load); // integer %: stable, lets the redraw guard below short-circuit
|
||||
prevT[idx] = r.total;
|
||||
prevI[idx] = r.idle;
|
||||
}
|
||||
}
|
||||
root._prevCoreTotal = prevT;
|
||||
root._prevCoreIdle = prevI;
|
||||
// coreLoads is a var (array): reassigning always fires the change
|
||||
// signal and re-evaluates every bar binding. Only assign when a core's
|
||||
// value actually changed so unchanged ticks trigger no redraw.
|
||||
if (!_sameLoads(loads, root.coreLoads)) {
|
||||
root.coreLoads = loads;
|
||||
root.coreCount = loads.length;
|
||||
}
|
||||
}
|
||||
|
||||
function _sameLoads(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Highest current per-core frequency from /proc/cpuinfo's "cpu MHz" lines.
|
||||
function _parseFreq() {
|
||||
const text = cpuInfoView.text();
|
||||
if (!text) return;
|
||||
let max = 0;
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].startsWith("cpu MHz")) continue;
|
||||
const v = parseFloat(lines[i].split(":")[1]);
|
||||
if (!isNaN(v) && v > max) max = v;
|
||||
}
|
||||
root.cpuFreq = max;
|
||||
}
|
||||
|
||||
function _parseMem() {
|
||||
const text = memView.text();
|
||||
if (!text) return;
|
||||
let mt = 0, ma = 0;
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i];
|
||||
if (ln.startsWith("MemTotal:")) mt = parseInt(ln.split(/\s+/)[1]);
|
||||
else if (ln.startsWith("MemAvailable:")) { ma = parseInt(ln.split(/\s+/)[1]); break; }
|
||||
}
|
||||
if (mt > 0) root.mem = (mt - ma) / mt * 100;
|
||||
}
|
||||
|
||||
function _parseTemp() {
|
||||
if (!root.tempPath) { root.temp = 0; return; }
|
||||
const text = tempView.text();
|
||||
if (!text) return;
|
||||
const v = parseInt(text.trim());
|
||||
if (!isNaN(v)) root.temp = Math.round(v / 1000);
|
||||
}
|
||||
|
||||
function _parseBattery() {
|
||||
if (!root.batPath) { root.battery = -1; root.batteryStatus = "unknown"; return; }
|
||||
const cap = batCapView.text();
|
||||
const st = batStatusView.text();
|
||||
if (cap) { const v = parseInt(cap.trim()); if (!isNaN(v)) root.battery = v; }
|
||||
if (st) root.batteryStatus = st.trim() || "unknown";
|
||||
}
|
||||
|
||||
// Active interface = device of the default route (destination 00000000).
|
||||
function _activeIface() {
|
||||
const text = routeView.text();
|
||||
if (!text) return "";
|
||||
const lines = text.split("\n");
|
||||
for (let i = 1; i < lines.length; i++) { // skip header
|
||||
const cols = lines[i].trim().split(/\s+/);
|
||||
if (cols.length >= 2 && cols[1] === "00000000") return cols[0];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function _parseNet() {
|
||||
const ifc = root._activeIface();
|
||||
root.iface = ifc;
|
||||
if (!ifc) { root.rxRate = 0; root.txRate = 0; root._prevIface = ""; return; }
|
||||
|
||||
const base = "/sys/class/net/" + ifc + "/statistics/";
|
||||
rxView.path = base + "rx_bytes";
|
||||
txView.path = base + "tx_bytes";
|
||||
rxView.reload();
|
||||
txView.reload();
|
||||
|
||||
const rx = parseInt((rxView.text() || "0").trim());
|
||||
const tx = parseInt((txView.text() || "0").trim());
|
||||
if (ifc === root._prevIface && root._prevRx > 0) {
|
||||
root.rxRate = Math.max(0, (rx - root._prevRx) / root.interval);
|
||||
root.txRate = Math.max(0, (tx - root._prevTx) / root.interval);
|
||||
} else {
|
||||
root.rxRate = 0;
|
||||
root.txRate = 0;
|
||||
}
|
||||
root._prevRx = rx;
|
||||
root._prevTx = tx;
|
||||
root._prevIface = ifc;
|
||||
}
|
||||
|
||||
function _tickOnce() {
|
||||
statView.reload();
|
||||
cpuInfoView.reload();
|
||||
memView.reload();
|
||||
routeView.reload();
|
||||
if (root.tempPath) tempView.reload();
|
||||
if (root.batPath) { batCapView.reload(); batStatusView.reload(); }
|
||||
|
||||
_parseCpu();
|
||||
_parseFreq();
|
||||
_parseMem();
|
||||
_parseTemp();
|
||||
_parseBattery();
|
||||
_parseNet();
|
||||
|
||||
// disk has no virtual file; refresh via df every 30 ticks.
|
||||
if (root._tick % 30 === 0) dfProc.running = true;
|
||||
root._tick++;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: root.interval * 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root._tickOnce()
|
||||
}
|
||||
|
||||
// ---- one-shot hardware discovery --------------------------------------
|
||||
|
||||
Process {
|
||||
id: discover
|
||||
command: ["bash", Quickshell.shellPath("services/discover.sh")]
|
||||
running: true
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (!line) return;
|
||||
const sp = line.indexOf(" ");
|
||||
if (sp < 0) return;
|
||||
const key = line.slice(0, sp);
|
||||
const val = line.slice(sp + 1).trim();
|
||||
if (key === "TEMP") root.tempPath = val;
|
||||
else if (key === "BAT") root.batPath = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- disk usage (df, low cadence) -------------------------------------
|
||||
|
||||
Process {
|
||||
id: dfProc
|
||||
command: ["df", "-P", "/"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = (this.text || "").trim().split("\n");
|
||||
if (lines.length < 2) return;
|
||||
const cols = lines[lines.length - 1].trim().split(/\s+/);
|
||||
if (cols.length >= 5) {
|
||||
const v = parseInt(cols[4].replace("%", ""));
|
||||
if (!isNaN(v)) root.disk = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
||||
function fmtRate(bytes) {
|
||||
const u = ["K", "M", "G"];
|
||||
let i = 0, v = bytes / 1024; // start in kB
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
||||
return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + u[i];
|
||||
}
|
||||
}
|
||||
59
services/discover.sh
Executable file
59
services/discover.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot hardware discovery for the Quickshell bar.
|
||||
# Prints stable sysfs paths the QML side then reads directly via FileView,
|
||||
# so no per-tick subprocess is needed. Output (one key per line):
|
||||
#
|
||||
# TEMP <path to hwmon/thermal temp*_input>
|
||||
# BAT <path to power_supply battery dir>
|
||||
#
|
||||
# Re-run only when hardware topology might change (rare); paths are stable
|
||||
# across a boot.
|
||||
|
||||
set -u
|
||||
|
||||
# CPU package temperature sensor (coretemp / k10temp / zenpower ...).
|
||||
discover_temp_input() {
|
||||
local hw name lbl base
|
||||
for hw in /sys/class/hwmon/hwmon*; do
|
||||
name=$(cat "$hw/name" 2>/dev/null) || continue
|
||||
case "$name" in
|
||||
coretemp|k10temp|zenpower)
|
||||
# Prefer the package/Tctl/Tdie label if present.
|
||||
for lbl in "$hw"/temp*_label; do
|
||||
[ -e "$lbl" ] || continue
|
||||
case "$(cat "$lbl" 2>/dev/null)" in
|
||||
"Package id 0"|Tctl|Tdie)
|
||||
base="${lbl%_label}"
|
||||
echo "${base}_input"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ -e "$hw/temp1_input" ] && { echo "$hw/temp1_input"; return 0; }
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Fallback: x86_pkg_temp thermal zone, else first thermal zone.
|
||||
local z t
|
||||
for z in /sys/class/thermal/thermal_zone*; do
|
||||
t=$(cat "$z/type" 2>/dev/null)
|
||||
[ "$t" = "x86_pkg_temp" ] && { echo "$z/temp"; return 0; }
|
||||
done
|
||||
[ -e /sys/class/thermal/thermal_zone0/temp ] && echo /sys/class/thermal/thermal_zone0/temp
|
||||
}
|
||||
|
||||
# Main system battery (skip peripherals like wacom/mouse which set scope=Device).
|
||||
discover_battery() {
|
||||
local ps t scope
|
||||
for ps in /sys/class/power_supply/*; do
|
||||
t=$(cat "$ps/type" 2>/dev/null)
|
||||
[ "$t" = "Battery" ] || continue
|
||||
scope=$(cat "$ps/scope" 2>/dev/null)
|
||||
[ "$scope" = "Device" ] && continue
|
||||
echo "$ps"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
printf 'TEMP %s\n' "$(discover_temp_input)"
|
||||
printf 'BAT %s\n' "$(discover_battery)"
|
||||
1
services/qmldir
Normal file
1
services/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton SysStats 1.0 SysStats.qml
|
||||
13
shell.qml
Normal file
13
shell.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
//@ pragma UseQApplication
|
||||
|
||||
import Quickshell
|
||||
import "widgets"
|
||||
|
||||
// Entry point: spawn one Bar on every connected monitor.
|
||||
ShellRoot {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Bar {}
|
||||
}
|
||||
}
|
||||
69
widgets/Bar.qml
Normal file
69
widgets/Bar.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../config"
|
||||
|
||||
// One bar instance per monitor.
|
||||
PanelWindow {
|
||||
id: panel
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
color: "transparent"
|
||||
|
||||
// Auto-rotation flips the output transform and resizes this layer surface
|
||||
// in place; the old buffer bleeds through the area that isn't fully
|
||||
// repainted, leaving stale "garbage" pixels. Remapping the surface on any
|
||||
// geometry change forces Sway to hand us a fresh, fully-painted buffer.
|
||||
readonly property string geomKey: modelData
|
||||
? (modelData.width + "x" + modelData.height)
|
||||
: ""
|
||||
onGeomKeyChanged: Qt.callLater(function () {
|
||||
if (!panel.visible) return;
|
||||
panel.visible = false;
|
||||
panel.visible = true;
|
||||
})
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
// background strip
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.barColor
|
||||
}
|
||||
|
||||
// left: workspaces
|
||||
Workspaces {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.gap + 2
|
||||
screen: panel.modelData
|
||||
}
|
||||
|
||||
// center: clock
|
||||
Clock {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
// right: battery, system metrics, volume, tray
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.gap + 2
|
||||
spacing: Theme.gap
|
||||
|
||||
Battery {}
|
||||
CpuGraph {}
|
||||
CpuTemp {}
|
||||
Ram {}
|
||||
Disk {}
|
||||
Network {}
|
||||
Volume {}
|
||||
Tray { panelWindow: panel }
|
||||
}
|
||||
}
|
||||
17
widgets/Battery.qml
Normal file
17
widgets/Battery.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
MetricPill {
|
||||
readonly property bool charging: SysStats.batteryStatus === "Charging"
|
||||
|| SysStats.batteryStatus === "Full"
|
||||
|
||||
visible: SysStats.hasBattery
|
||||
|
||||
icon: charging ? Icons.bolt : Icons.battery(SysStats.battery)
|
||||
iconColor: charging ? Theme.green
|
||||
: SysStats.battery <= 15 ? Theme.red
|
||||
: SysStats.battery <= 30 ? Theme.peach
|
||||
: Theme.green
|
||||
value: SysStats.battery + "%"
|
||||
reserve: "100%"
|
||||
}
|
||||
52
widgets/Clock.qml
Normal file
52
widgets/Clock.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../config"
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
background: Theme.surface1
|
||||
property var now: new Date()
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Icons.clock
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.lavender
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: dateMetrics
|
||||
font: dateText.font
|
||||
// widest day/month abbreviations in this locale
|
||||
text: "Wed 00 MMM"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: dateText
|
||||
text: Qt.formatDateTime(root.now, "ddd dd MMM")
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.subtext
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: dateMetrics.advanceWidth
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(root.now, "HH:mm")
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.bold: true
|
||||
color: Theme.text
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
66
widgets/CpuGraph.qml
Normal file
66
widgets/CpuGraph.qml
Normal file
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
// CPU usage as a live filled-area graph plus the current percentage.
|
||||
Pill {
|
||||
id: root
|
||||
spacing: Theme.spacing
|
||||
|
||||
Text {
|
||||
text: Icons.cpu
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.loadColor(SysStats.cpu)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// One vertical bar per core, filled from the bottom by that core's load.
|
||||
Row {
|
||||
id: cores
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 1
|
||||
readonly property int barH: Theme.barHeight - Theme.gap * 2 - 12
|
||||
|
||||
Repeater {
|
||||
model: SysStats.coreCount
|
||||
delegate: Rectangle {
|
||||
readonly property real load: SysStats.coreLoads[index] || 0
|
||||
width: 3
|
||||
height: cores.barH
|
||||
radius: 1
|
||||
color: Qt.rgba(1, 1, 1, 0.08) // unfilled track
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
radius: 1
|
||||
height: Math.max(1, parent.height * (parent.load / 100))
|
||||
color: Theme.loadColor(parent.load)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SysStats.cpu.toFixed(0) + "%"
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 42 // fits "100%" at fontSize 15
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
// Max current core frequency, shown in GHz.
|
||||
Text {
|
||||
text: (SysStats.cpuFreq / 1000).toFixed(1) + "GHz"
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.subtext
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 56 // fits "0.0GHz" at fontSize 15
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
13
widgets/CpuTemp.qml
Normal file
13
widgets/CpuTemp.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
MetricPill {
|
||||
icon: Icons.thermo
|
||||
// temperature thresholds differ from load: warm >70, hot >85
|
||||
iconColor: SysStats.temp >= 85 ? Theme.red
|
||||
: SysStats.temp >= 70 ? Theme.peach
|
||||
: SysStats.temp >= 55 ? Theme.yellow
|
||||
: Theme.green
|
||||
value: SysStats.temp + "°C"
|
||||
reserve: "100°C"
|
||||
}
|
||||
9
widgets/Disk.qml
Normal file
9
widgets/Disk.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
MetricPill {
|
||||
icon: Icons.disk
|
||||
iconColor: Theme.loadColor(SysStats.disk)
|
||||
value: SysStats.disk + "%"
|
||||
reserve: "100%"
|
||||
}
|
||||
40
widgets/MetricPill.qml
Normal file
40
widgets/MetricPill.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../config"
|
||||
|
||||
// A pill showing a coloured Nerd Font icon followed by a value string.
|
||||
Pill {
|
||||
id: root
|
||||
property string icon: ""
|
||||
property string value: ""
|
||||
property color iconColor: Theme.text
|
||||
property alias label: valueText
|
||||
// Widest value the pill will ever show, e.g. "100%". When set, the value
|
||||
// column reserves that width so changing values never shift other widgets.
|
||||
property string reserve: ""
|
||||
|
||||
Text {
|
||||
text: root.icon
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: root.iconColor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: reserveMetrics
|
||||
font: valueText.font
|
||||
text: root.reserve
|
||||
}
|
||||
|
||||
Text {
|
||||
id: valueText
|
||||
text: root.value
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: root.reserve ? reserveMetrics.advanceWidth : implicitWidth
|
||||
}
|
||||
}
|
||||
43
widgets/Network.qml
Normal file
43
widgets/Network.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
// Down/up throughput on the active interface, with a wifi/ethernet icon.
|
||||
Pill {
|
||||
Text {
|
||||
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.sky
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// reserve the width of the widest rate string so values never shift
|
||||
TextMetrics {
|
||||
id: rateMetrics
|
||||
font: rxText.font
|
||||
text: Icons.down + " 99.9M"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: rxText
|
||||
text: Icons.down + " " + SysStats.fmtRate(SysStats.rxRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.green
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Icons.up + " " + SysStats.fmtRate(SysStats.txRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.peach
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
}
|
||||
}
|
||||
23
widgets/Pill.qml
Normal file
23
widgets/Pill.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../config"
|
||||
|
||||
// Rounded background container used by every bar module for a consistent look.
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
default property alias content: row.data
|
||||
property int spacing: Theme.spacing
|
||||
property color background: Theme.surface0
|
||||
|
||||
implicitWidth: row.implicitWidth + Theme.padding * 2
|
||||
implicitHeight: Theme.barHeight - Theme.gap * 2
|
||||
radius: Theme.radius
|
||||
color: background
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: pill.spacing
|
||||
}
|
||||
}
|
||||
9
widgets/Ram.qml
Normal file
9
widgets/Ram.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
MetricPill {
|
||||
icon: Icons.memory
|
||||
iconColor: Theme.loadColor(SysStats.mem)
|
||||
value: SysStats.mem.toFixed(0) + "%"
|
||||
reserve: "100%"
|
||||
}
|
||||
62
widgets/Tray.qml
Normal file
62
widgets/Tray.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import "../config"
|
||||
|
||||
// StatusNotifier system tray. Left-click activates, middle-click does the
|
||||
// secondary action, right-click opens the item's native menu.
|
||||
Pill {
|
||||
id: root
|
||||
// the PanelWindow this tray lives in, needed to anchor context menus
|
||||
property var panelWindow
|
||||
|
||||
visible: SystemTray.items.values.length > 0
|
||||
spacing: Theme.spacing + 2
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
Item {
|
||||
id: entry
|
||||
required property var modelData
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: entry.modelData.icon
|
||||
sourceSize.width: 18
|
||||
sourceSize.height: 18
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
const item = entry.modelData;
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (item.onlyMenu) openMenu();
|
||||
else item.activate();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
item.secondaryActivate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
openMenu();
|
||||
}
|
||||
}
|
||||
function openMenu() {
|
||||
const item = entry.modelData;
|
||||
if (!item.hasMenu || !root.panelWindow) return;
|
||||
const p = entry.mapToItem(null, 0, entry.height + Theme.gap);
|
||||
item.display(root.panelWindow, p.x, p.y);
|
||||
}
|
||||
onWheel: wheel =>
|
||||
entry.modelData.scroll(wheel.angleDelta.y, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
widgets/Volume.qml
Normal file
63
widgets/Volume.qml
Normal file
@@ -0,0 +1,63 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../config"
|
||||
|
||||
// PipeWire default sink volume. Scroll to adjust, click to mute.
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var audio: sink ? sink.audio : null
|
||||
readonly property bool muted: audio ? audio.muted : true
|
||||
readonly property int volPct: audio ? Math.round(audio.volume * 100) : 0
|
||||
|
||||
implicitWidth: pill.implicitWidth
|
||||
implicitHeight: pill.implicitHeight
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
onClicked: { if (audio) audio.muted = !audio.muted; }
|
||||
onWheel: wheel => {
|
||||
if (!audio) return;
|
||||
audio.volume = Math.max(0, Math.min(1,
|
||||
audio.volume + (wheel.angleDelta.y > 0 ? 0.05 : -0.05)));
|
||||
}
|
||||
|
||||
// keep the sink's audio properties live
|
||||
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
|
||||
|
||||
Pill {
|
||||
id: pill
|
||||
anchors.fill: parent
|
||||
|
||||
Text {
|
||||
text: root.muted ? Icons.volMute
|
||||
: root.volPct < 50 ? Icons.volLow
|
||||
: Icons.volHigh
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: root.muted ? Theme.overlay : Theme.teal
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Theme.fontSize + 4
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: volMetrics
|
||||
font: volText.font
|
||||
text: "muted"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: volText
|
||||
text: root.muted ? "muted" : root.volPct + "%"
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: volMetrics.advanceWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
54
widgets/Workspaces.qml
Normal file
54
widgets/Workspaces.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.I3
|
||||
import "../config"
|
||||
|
||||
// Sway/i3 workspace switcher for a single monitor.
|
||||
Row {
|
||||
id: root
|
||||
required property var screen // ShellScreen this bar lives on
|
||||
spacing: Theme.gap
|
||||
|
||||
// Name of the i3/sway output this bar is on, used to filter workspaces.
|
||||
readonly property string monitorName: screen ? screen.name : ""
|
||||
|
||||
Repeater {
|
||||
model: I3.workspaces
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
required property var modelData
|
||||
// only show workspaces belonging to this monitor
|
||||
// I3Workspace.monitor is an I3Monitor object, so compare its name
|
||||
visible: modelData.monitor && modelData.monitor.name === root.monitorName
|
||||
width: visible ? Math.max(height, label.implicitWidth + 16) : 0
|
||||
height: Theme.barHeight - Theme.gap * 2
|
||||
radius: Theme.radius
|
||||
|
||||
readonly property bool isFocused: modelData.focused
|
||||
readonly property bool isUrgent: modelData.urgent
|
||||
|
||||
color: isUrgent ? Theme.red
|
||||
: isFocused ? Theme.blue
|
||||
: modelData.active ? Theme.surface1
|
||||
: Theme.surface0
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: chip.modelData.name
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.bold: chip.isFocused || chip.isUrgent
|
||||
color: (chip.isFocused || chip.isUrgent) ? Theme.mantle : Theme.text
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: chip.modelData.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user