Compare commits
10 Commits
eecf1a1b6a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
3a98bc08e9
|
|||
|
f6796f3726
|
|||
|
6655c9c500
|
|||
|
b7ea18c751
|
|||
|
796be77341
|
|||
|
7a95288f81
|
|||
|
fa3e41ecc2
|
|||
|
e97a2b65c2
|
|||
|
c62e8f10ea
|
|||
|
dfb16cce4e
|
@@ -11,7 +11,8 @@ Singleton {
|
|||||||
readonly property string thermo: String.fromCharCode(0xf2c9) // thermometer-half
|
readonly property string thermo: String.fromCharCode(0xf2c9) // thermometer-half
|
||||||
readonly property string disk: String.fromCharCode(0xf0a0) // hdd-o
|
readonly property string disk: String.fromCharCode(0xf0a0) // hdd-o
|
||||||
readonly property string wifi: String.fromCharCode(0xf1eb) // wifi
|
readonly property string wifi: String.fromCharCode(0xf1eb) // wifi
|
||||||
readonly property string ethernet: String.fromCharCode(0xf6ff) // network-wired
|
readonly property string ethernet: String.fromCodePoint(0xf0200) // md-ethernet (FA5 network-wired 0xf6ff is absent from the bundled Nerd Fonts)
|
||||||
|
readonly property string netOff: String.fromCodePoint(0xf05aa) // md-wifi-off (shown when there is no active interface)
|
||||||
readonly property string down: String.fromCharCode(0xf063) // arrow-down
|
readonly property string down: String.fromCharCode(0xf063) // arrow-down
|
||||||
readonly property string up: String.fromCharCode(0xf062) // arrow-up
|
readonly property string up: String.fromCharCode(0xf062) // arrow-up
|
||||||
readonly property string clock: String.fromCharCode(0xf017) // clock-o
|
readonly property string clock: String.fromCharCode(0xf017) // clock-o
|
||||||
|
|||||||
@@ -11,28 +11,28 @@ Singleton {
|
|||||||
readonly property int padding: 10
|
readonly property int padding: 10
|
||||||
readonly property int gap: 4
|
readonly property int gap: 4
|
||||||
|
|
||||||
// --- palette (Catppuccin Mocha, darkened toward OLED black) ---
|
// --- palette (deep near-black base, high-contrast foreground) ---
|
||||||
readonly property color base: "#0d0d12"
|
readonly property color base: "#05060a"
|
||||||
readonly property color mantle: "#08080b"
|
readonly property color mantle: "#000000"
|
||||||
readonly property color surface0: "#1c1c28"
|
readonly property color surface0: "#15161f"
|
||||||
readonly property color surface1: "#2a2a3a"
|
readonly property color surface1: "#232532"
|
||||||
readonly property color overlay: "#52526a"
|
readonly property color overlay: "#4a4d63"
|
||||||
readonly property color text: "#cdd6f4"
|
readonly property color text: "#eef1f8"
|
||||||
readonly property color subtext: "#a6adc8"
|
readonly property color subtext: "#b4bbd0"
|
||||||
|
|
||||||
readonly property color rosewater: "#f5e0dc"
|
readonly property color rosewater: "#ffdfd6"
|
||||||
readonly property color red: "#f38ba8"
|
readonly property color red: "#ff7a93"
|
||||||
readonly property color peach: "#fab387"
|
readonly property color peach: "#ffb074"
|
||||||
readonly property color yellow: "#f9e2af"
|
readonly property color yellow: "#ffe07a"
|
||||||
readonly property color green: "#a6e3a1"
|
readonly property color green: "#8ff09a"
|
||||||
readonly property color teal: "#94e2d5"
|
readonly property color teal: "#7ff0df"
|
||||||
readonly property color sky: "#89dceb"
|
readonly property color sky: "#74d9ff"
|
||||||
readonly property color blue: "#89b4fa"
|
readonly property color blue: "#7aa9ff"
|
||||||
readonly property color mauve: "#cba6f7"
|
readonly property color mauve: "#c799ff"
|
||||||
readonly property color lavender: "#b4befe"
|
readonly property color lavender: "#aebcff"
|
||||||
|
|
||||||
// bar background, slightly translucent
|
// bar background, near-opaque true black
|
||||||
readonly property color barColor: Qt.rgba(0.051, 0.051, 0.071, 0.94)
|
readonly property color barColor: Qt.rgba(0.02, 0.024, 0.039, 0.96)
|
||||||
|
|
||||||
// --- typography ---
|
// --- typography ---
|
||||||
readonly property string font: "Inter, sans-serif"
|
readonly property string font: "Inter, sans-serif"
|
||||||
|
|||||||
17
flake.nix
17
flake.nix
@@ -21,15 +21,17 @@
|
|||||||
packages = forAllSystems (pkgs:
|
packages = forAllSystems (pkgs:
|
||||||
let
|
let
|
||||||
# Quickshell from the upstream flake rather than nixpkgs.
|
# Quickshell from the upstream flake rather than nixpkgs.
|
||||||
qsPkg = quickshell.packages.${pkgs.system}.default;
|
qsPkg = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
|
||||||
# Runtime tools the bar relies on: discover.sh (one-shot hardware
|
# Runtime tools the bar relies on: discover.sh (one-shot hardware
|
||||||
# probe) and the periodic `df` for disk usage. Everything else is
|
# probe), the periodic `df` for disk usage, and `ss` (iproute2) for
|
||||||
# read from /proc and /sys directly in QML.
|
# the per-process network drawer (topnet.sh). Everything else is read
|
||||||
|
# from /proc and /sys directly in QML.
|
||||||
runtimeInputs = [
|
runtimeInputs = [
|
||||||
qsPkg
|
qsPkg
|
||||||
pkgs.bash
|
pkgs.bash
|
||||||
pkgs.coreutils # df, cat
|
pkgs.coreutils # df, cat
|
||||||
|
pkgs.iproute2 # ss, for topnet.sh per-process network usage
|
||||||
];
|
];
|
||||||
|
|
||||||
# Self-contained font set so Nerd Font glyphs always render,
|
# Self-contained font set so Nerd Font glyphs always render,
|
||||||
@@ -47,6 +49,10 @@
|
|||||||
inherit runtimeInputs;
|
inherit runtimeInputs;
|
||||||
text = ''
|
text = ''
|
||||||
export FONTCONFIG_FILE=${fontsConf}
|
export FONTCONFIG_FILE=${fontsConf}
|
||||||
|
# Force the Wayland QPA plugin: Qt defaults to xcb (X11) on Linux,
|
||||||
|
# so without this the bar comes up under XWayland. Honour an
|
||||||
|
# explicit override if the caller already set one.
|
||||||
|
export QT_QPA_PLATFORM="''${QT_QPA_PLATFORM:-wayland}"
|
||||||
exec quickshell --path ${self} "$@"
|
exec quickshell --path ${self} "$@"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -59,16 +65,17 @@
|
|||||||
apps = forAllSystems (pkgs: {
|
apps = forAllSystems (pkgs: {
|
||||||
default = {
|
default = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${self.packages.${pkgs.system}.default}/bin/quickshell-bar";
|
program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/quickshell-bar";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forAllSystems (pkgs: {
|
devShells = forAllSystems (pkgs: {
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
quickshell.packages.${pkgs.system}.default
|
quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default
|
||||||
pkgs.bash
|
pkgs.bash
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
|
pkgs.iproute2 # ss, for topnet.sh
|
||||||
pkgs.nerd-fonts.jetbrains-mono
|
pkgs.nerd-fonts.jetbrains-mono
|
||||||
pkgs.inter
|
pkgs.inter
|
||||||
pkgs.qt6.qtdeclarative # qmlls / qmlformat for editing
|
pkgs.qt6.qtdeclarative # qmlls / qmlformat for editing
|
||||||
|
|||||||
@@ -37,11 +37,27 @@ Singleton {
|
|||||||
property bool procPollEnabled: false
|
property bool procPollEnabled: false
|
||||||
readonly property int procInterval: 2 // seconds between frames
|
readonly property int procInterval: 2 // seconds between frames
|
||||||
|
|
||||||
|
// --- top network processes (same opt-in streaming model as topProcs) ---
|
||||||
|
// Each entry: { name, rx (bytes/sec down), tx (bytes/sec up) }, highest
|
||||||
|
// combined throughput first. Set netProcPollEnabled true (Network drawer
|
||||||
|
// open) to start the streamer; only TCP traffic is attributable (see
|
||||||
|
// topnet.sh), so UDP/QUIC-heavy apps may underreport.
|
||||||
|
property var topNetProcs: []
|
||||||
|
readonly property int netProcCount: 6
|
||||||
|
property bool netProcPollEnabled: false
|
||||||
|
readonly property int netProcInterval: 2 // seconds between frames
|
||||||
|
|
||||||
readonly property bool hasBattery: battery >= 0
|
readonly property bool hasBattery: battery >= 0
|
||||||
|
|
||||||
// poll interval in seconds
|
// poll interval in seconds
|
||||||
readonly property int interval: 1
|
readonly property int interval: 1
|
||||||
|
|
||||||
|
// cpuFreq comes from /proc/cpuinfo, which the kernel regenerates in full on
|
||||||
|
// every read (cost grows with core count) — the priciest per-tick read here.
|
||||||
|
// Frequency on a status bar doesn't need 1 Hz precision, so only refresh it
|
||||||
|
// every Nth tick instead of on the CPU%/mem hot path.
|
||||||
|
readonly property int freqEveryTicks: 3
|
||||||
|
|
||||||
// discovered sysfs paths (filled once by discover.sh)
|
// discovered sysfs paths (filled once by discover.sh)
|
||||||
property string tempPath: ""
|
property string tempPath: ""
|
||||||
property string batPath: ""
|
property string batPath: ""
|
||||||
@@ -242,16 +258,36 @@ Singleton {
|
|||||||
root.topProcs = out;
|
root.topProcs = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build topNetProcs from one run of topnet.sh: lines of
|
||||||
|
// "N <rx_bytes/s> <tx_bytes/s> <name>", already ranked by the script.
|
||||||
|
function _parseNetProcFrame(text) {
|
||||||
|
if (!text) return;
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const p = lines[i].split(" ");
|
||||||
|
if (p[0] !== "N" || p.length < 4) continue;
|
||||||
|
out.push({
|
||||||
|
rx: Number(p[1]),
|
||||||
|
tx: Number(p[2]),
|
||||||
|
name: p.slice(3).join(" ")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.topNetProcs = out;
|
||||||
|
}
|
||||||
|
|
||||||
function _tickOnce() {
|
function _tickOnce() {
|
||||||
|
const doFreq = root._tick % root.freqEveryTicks === 0;
|
||||||
|
|
||||||
statView.reload();
|
statView.reload();
|
||||||
cpuInfoView.reload();
|
if (doFreq) cpuInfoView.reload();
|
||||||
memView.reload();
|
memView.reload();
|
||||||
routeView.reload();
|
routeView.reload();
|
||||||
if (root.tempPath) tempView.reload();
|
if (root.tempPath) tempView.reload();
|
||||||
if (root.batPath) { batCapView.reload(); batStatusView.reload(); }
|
if (root.batPath) { batCapView.reload(); batStatusView.reload(); }
|
||||||
|
|
||||||
_parseCpu();
|
_parseCpu();
|
||||||
_parseFreq();
|
if (doFreq) _parseFreq();
|
||||||
_parseMem();
|
_parseMem();
|
||||||
_parseTemp();
|
_parseTemp();
|
||||||
_parseBattery();
|
_parseBattery();
|
||||||
@@ -343,6 +379,35 @@ Singleton {
|
|||||||
else root.topProcs = [];
|
else root.topProcs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- top network processes (opt-in, drawer-driven) --------------------
|
||||||
|
// Mirror of procScan: services/topnet.sh prints one ranked frame and exits;
|
||||||
|
// we re-run it every ~netProcInterval while netProcPollEnabled (the Network
|
||||||
|
// drawer is open). Each run sleeps netProcInterval between its two ss
|
||||||
|
// snapshots, so the bar pays nothing at rest.
|
||||||
|
|
||||||
|
property var _netFrame: [] // stdout lines accumulated for the current run
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: netScan
|
||||||
|
command: ["bash", Quickshell.shellPath("services/topnet.sh"),
|
||||||
|
String(root.netProcInterval), String(root.netProcCount)]
|
||||||
|
onStarted: root._netFrame = [];
|
||||||
|
onExited: {
|
||||||
|
root._parseNetProcFrame(root._netFrame.join("\n"));
|
||||||
|
if (root.netProcPollEnabled) // re-arm for the next frame
|
||||||
|
Qt.callLater(function () { netScan.running = true; });
|
||||||
|
}
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: line => { if (line) root._netFrame.push(line); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNetProcPollEnabledChanged: {
|
||||||
|
if (netProcPollEnabled) netScan.running = true;
|
||||||
|
else root.topNetProcs = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
||||||
function fmtRate(bytes) {
|
function fmtRate(bytes) {
|
||||||
const u = ["K", "M", "G"];
|
const u = ["K", "M", "G"];
|
||||||
|
|||||||
97
services/topnet.sh
Executable file
97
services/topnet.sh
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Print one frame of the top network-consuming processes, then exit.
|
||||||
|
#
|
||||||
|
# Single-shot by design, exactly like topproc.sh: SysStats re-runs it every
|
||||||
|
# netProcInterval while the Network drawer is open, and exiting per frame
|
||||||
|
# guarantees stdout is flushed (a long-running loop would block-buffer its
|
||||||
|
# frame and the QML side would never see it). topproc.sh is the reference for
|
||||||
|
# the bash-script + single-shot + SplitParser-parse pattern.
|
||||||
|
#
|
||||||
|
# How per-process bandwidth is derived: `ss` reports, per TCP socket, the
|
||||||
|
# cumulative bytes_sent / bytes_received plus the owning pid (users:) and a
|
||||||
|
# stable socket inode (ino:). We take two snapshots netProcInterval apart, diff
|
||||||
|
# each socket's byte counters by inode, sum the positive deltas per process,
|
||||||
|
# and divide by the interval to get bytes/sec. Only TCP carries these counters,
|
||||||
|
# so UDP/QUIC traffic is not attributed — that is a kernel/ss limitation, not a
|
||||||
|
# bug here.
|
||||||
|
#
|
||||||
|
# IMPORTANT: needs `ss` (iproute2) on PATH — added to runtimeInputs in flake.nix
|
||||||
|
# alongside bash + coreutils. No awk/getconf (absent at runtime); bash builtins
|
||||||
|
# and coreutils (sort, head, sleep) only.
|
||||||
|
#
|
||||||
|
# Output: one "N <rx_bytes_per_sec> <tx_bytes_per_sec> <name>" line per process,
|
||||||
|
# highest combined throughput first.
|
||||||
|
|
||||||
|
intv="${1:-2}"
|
||||||
|
n="${2:-6}"
|
||||||
|
|
||||||
|
# Snapshot every TCP socket's cumulative rx/tx bytes into the rx/tx assoc
|
||||||
|
# arrays keyed by socket inode, and remember each inode's owning process in
|
||||||
|
# pidOf/nameOf. `ss -O` prints one socket per line so a single read loop works.
|
||||||
|
# -t TCP -i info(byte counters) -n numeric -H no header
|
||||||
|
# -O oneline -p process -e extended(ino:)
|
||||||
|
snapshot() {
|
||||||
|
local -n _rx="$1" _tx="$2" _pid="$3" _name="$4"
|
||||||
|
local line w ino bs br pid name tmp
|
||||||
|
set -f # no globbing when word-splitting $line below
|
||||||
|
while IFS= read -r line; do
|
||||||
|
ino=""; bs=0; br=0
|
||||||
|
# ino: / bytes_*: tokens never contain spaces, so word-splitting is safe
|
||||||
|
# even when a process name does (e.g. "Web Content").
|
||||||
|
for w in $line; do
|
||||||
|
case "$w" in
|
||||||
|
ino:*) ino="${w#ino:}" ;;
|
||||||
|
bytes_sent:*) bs="${w#bytes_sent:}" ;;
|
||||||
|
bytes_received:*) br="${w#bytes_received:}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$ino" ] || continue
|
||||||
|
_rx["$ino"]="$br"
|
||||||
|
_tx["$ino"]="$bs"
|
||||||
|
# name + pid from the users:(("name",pid=N,fd=M)) token; parse off the
|
||||||
|
# whole line so a space inside the quoted name can't break it.
|
||||||
|
tmp="${line#*users:((\"}"; name="${tmp%%\"*}"
|
||||||
|
tmp="${line#*pid=}"; pid="${tmp%%,*}"
|
||||||
|
_pid["$ino"]="$pid"
|
||||||
|
_name["$ino"]="$name"
|
||||||
|
done < <(ss -tinHOpe 2>/dev/null)
|
||||||
|
set +f
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -A rx1 tx1 rx2 tx2 pidOf nameOf
|
||||||
|
snapshot rx1 tx1 pidOf nameOf
|
||||||
|
sleep "$intv"
|
||||||
|
snapshot rx2 tx2 pidOf nameOf
|
||||||
|
|
||||||
|
# Per-process byte deltas over the interval. A socket present in both snapshots
|
||||||
|
# contributes its counter delta; a socket new in the second snapshot contributes
|
||||||
|
# its full byte count (all of it landed within the window). Aggregate by pid.
|
||||||
|
declare -A prx ptx pname
|
||||||
|
for ino in "${!rx2[@]}"; do
|
||||||
|
pid="${pidOf[$ino]}"
|
||||||
|
[ -n "$pid" ] || continue
|
||||||
|
if [ -n "${rx1[$ino]:-}" ]; then
|
||||||
|
drx=$(( ${rx2[$ino]} - ${rx1[$ino]} ))
|
||||||
|
dtx=$(( ${tx2[$ino]} - ${tx1[$ino]} ))
|
||||||
|
else
|
||||||
|
drx="${rx2[$ino]}"
|
||||||
|
dtx="${tx2[$ino]}"
|
||||||
|
fi
|
||||||
|
[ "$drx" -gt 0 ] || drx=0
|
||||||
|
[ "$dtx" -gt 0 ] || dtx=0
|
||||||
|
prx["$pid"]=$(( ${prx[$pid]:-0} + drx ))
|
||||||
|
ptx["$pid"]=$(( ${ptx[$pid]:-0} + dtx ))
|
||||||
|
pname["$pid"]="${nameOf[$ino]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Emit "<total/s> <rx/s> <tx/s> <name>" per process, rank by total throughput,
|
||||||
|
# take the top n, then reformat to the N-line protocol the QML side parses.
|
||||||
|
for pid in "${!prx[@]}"; do
|
||||||
|
r=$(( ${prx[$pid]} / intv ))
|
||||||
|
t=$(( ${ptx[$pid]} / intv ))
|
||||||
|
tot=$(( r + t ))
|
||||||
|
[ "$tot" -gt 0 ] || continue
|
||||||
|
printf '%d %d %d %s\n' "$tot" "$r" "$t" "${pname[$pid]}"
|
||||||
|
done | sort -rn | head -n "$n" | while read -r tot r t name; do
|
||||||
|
printf 'N %s %s %s\n' "$r" "$t" "$name"
|
||||||
|
done
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
//@ pragma UseQApplication
|
//@ pragma UseQApplication
|
||||||
|
// Required: Quickshell renders the system-tray context menu (right-click in
|
||||||
|
// Tray.qml -> item.display()) via QtWidgets' QMenu. Dropping this loads the
|
||||||
|
// lighter QGuiApplication but breaks tray menus. Verified 2026-06-08.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import "widgets"
|
import "widgets"
|
||||||
|
|||||||
@@ -29,7 +29,20 @@ PanelWindow {
|
|||||||
left: true
|
left: true
|
||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
implicitHeight: Theme.barHeight
|
|
||||||
|
// Wrap onto two rows when a single row can't hold everything (portrait
|
||||||
|
// outputs). Needed width is computed from implicit sizes, which don't
|
||||||
|
// depend on which row anything sits in, so this can't bind-loop.
|
||||||
|
readonly property real neededWidth:
|
||||||
|
(Theme.gap + 2) * 2 // outer margins
|
||||||
|
+ workspaces.implicitWidth
|
||||||
|
+ Theme.gap * 4 // breathing room between clusters
|
||||||
|
+ metricsRow.implicitWidth
|
||||||
|
+ Theme.gap
|
||||||
|
+ endRow.implicitWidth
|
||||||
|
readonly property bool twoRows: width > 0 && neededWidth > width
|
||||||
|
|
||||||
|
implicitHeight: twoRows ? Theme.barHeight * 2 : Theme.barHeight
|
||||||
|
|
||||||
// background strip
|
// background strip
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -37,24 +50,35 @@ PanelWindow {
|
|||||||
color: Theme.barColor
|
color: Theme.barColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// left: workspaces
|
// row 1 left: workspaces
|
||||||
Workspaces {
|
Workspaces {
|
||||||
|
id: workspaces
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Theme.gap + 2
|
anchors.leftMargin: Theme.gap + 2
|
||||||
|
y: (Theme.barHeight - height) / 2
|
||||||
screen: panel.modelData
|
screen: panel.modelData
|
||||||
}
|
}
|
||||||
|
|
||||||
// center: clock
|
// row 1 right: clock, tray
|
||||||
Clock {
|
RowLayout {
|
||||||
anchors.centerIn: parent
|
id: endRow
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.gap + 2
|
||||||
|
y: (Theme.barHeight - height) / 2
|
||||||
|
spacing: Theme.gap
|
||||||
|
|
||||||
|
Clock {}
|
||||||
|
Tray { panelWindow: panel }
|
||||||
}
|
}
|
||||||
|
|
||||||
// right: battery, system metrics, volume, tray
|
// metrics: left of the clock in one-row mode, own second row when narrow
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.right: parent.right
|
id: metricsRow
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.right: panel.twoRows ? parent.right : endRow.left
|
||||||
anchors.rightMargin: Theme.gap + 2
|
anchors.rightMargin: panel.twoRows ? Theme.gap + 2 : Theme.gap
|
||||||
|
y: panel.twoRows
|
||||||
|
? Theme.barHeight + (Theme.barHeight - height) / 2
|
||||||
|
: (Theme.barHeight - height) / 2
|
||||||
spacing: Theme.gap
|
spacing: Theme.gap
|
||||||
|
|
||||||
Battery {}
|
Battery {}
|
||||||
@@ -64,6 +88,5 @@ PanelWindow {
|
|||||||
Disk {}
|
Disk {}
|
||||||
Network {}
|
Network {}
|
||||||
Volume {}
|
Volume {}
|
||||||
Tray { panelWindow: panel }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import "../config"
|
import "../config"
|
||||||
import "../services"
|
import "../services"
|
||||||
|
|
||||||
// Down/up throughput on the active interface, with a wifi/ethernet icon.
|
// Down/up throughput on the active interface, with a wifi/ethernet icon.
|
||||||
Pill {
|
// Clicking the pill toggles a drawer listing the top network-consuming
|
||||||
|
// processes, refreshed live (SysStats only scans sockets while the drawer is
|
||||||
|
// open). CpuGraph.qml is the reference for the drawer pattern.
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: pill.implicitWidth
|
||||||
|
implicitHeight: pill.implicitHeight
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
id: pill
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Theme.spacing
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
text: SysStats.iface === "" ? Icons.netOff
|
||||||
|
: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||||
font.family: Theme.monoFont
|
font.family: Theme.monoFont
|
||||||
font.pixelSize: Theme.fontSize + 1
|
font.pixelSize: Theme.fontSize + 1
|
||||||
color: Theme.sky
|
color: SysStats.iface === "" ? Theme.overlay : Theme.sky
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,4 +55,185 @@ Pill {
|
|||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: drawer.visible = !drawer.visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- top network-processes drawer -------------------------------------
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: drawer
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
implicitWidth: panel.implicitWidth
|
||||||
|
implicitHeight: panel.implicitHeight
|
||||||
|
|
||||||
|
// Hang below the pill, top-right corner aligned to the pill's
|
||||||
|
// bottom-right (same anchoring rationale as CpuGraph's drawer): anchor
|
||||||
|
// to the item so the rect is in the pill's own coordinates and tracks
|
||||||
|
// it wherever the RowLayout places it.
|
||||||
|
anchor {
|
||||||
|
item: root
|
||||||
|
rect.x: 0
|
||||||
|
rect.y: root.height + Theme.gap
|
||||||
|
rect.width: root.width
|
||||||
|
rect.height: 1
|
||||||
|
edges: Edges.Bottom | Edges.Right
|
||||||
|
gravity: Edges.Bottom | Edges.Left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pay for the socket scan while the drawer is actually showing.
|
||||||
|
onVisibleChanged: SysStats.netProcPollEnabled = visible
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: panel
|
||||||
|
readonly property int rowH: 22
|
||||||
|
implicitWidth: 340
|
||||||
|
// Fixed height (header + netProcCount rows): the panel must NOT
|
||||||
|
// resize when rows replace "sampling…", or the popup's layer surface
|
||||||
|
// shows a stale/blank buffer under Sway on first open (same class of
|
||||||
|
// bug as Bar.qml's geomKey remap). A constant size sidesteps it.
|
||||||
|
implicitHeight: col.implicitHeight + Theme.padding * 2
|
||||||
|
radius: Theme.radius
|
||||||
|
color: Theme.barColor
|
||||||
|
border.color: Theme.surface1
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
// Largest combined rate in the current frame, used to scale the
|
||||||
|
// per-row background bar (network has no fixed ceiling like CPU%).
|
||||||
|
readonly property real maxRate: {
|
||||||
|
let m = 1;
|
||||||
|
const ps = SysStats.topNetProcs;
|
||||||
|
for (let i = 0; i < ps.length; i++)
|
||||||
|
m = Math.max(m, ps[i].rx + ps[i].tx);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: col
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.padding
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
// header: icon + title + total throughput
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Theme.spacing
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: SysStats.iface === "" ? Icons.netOff
|
||||||
|
: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||||
|
font.family: Theme.monoFont
|
||||||
|
font.pixelSize: Theme.fontSize + 1
|
||||||
|
color: SysStats.iface === "" ? Theme.overlay : Theme.sky
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "Network by process"
|
||||||
|
font.family: Theme.font
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
color: Theme.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Icons.down + SysStats.fmtRate(SysStats.rxRate)
|
||||||
|
+ " " + Icons.up + SysStats.fmtRate(SysStats.txRate)
|
||||||
|
font.family: Theme.monoFont
|
||||||
|
font.pixelSize: Theme.fontSize - 3
|
||||||
|
color: Theme.subtext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { // divider
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
implicitHeight: 1
|
||||||
|
color: Theme.surface1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed-height body so the panel stays one constant size whether
|
||||||
|
// it shows the placeholder or the rows (see panel.implicitHeight).
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
implicitHeight: SysStats.netProcCount * panel.rowH
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: SysStats.topNetProcs.length === 0
|
||||||
|
text: "sampling…"
|
||||||
|
font.family: Theme.font
|
||||||
|
font.pixelSize: Theme.fontSize - 2
|
||||||
|
color: Theme.subtext
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: SysStats.topNetProcs
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: rowItem
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
height: panel.rowH
|
||||||
|
|
||||||
|
// throughput bar behind the text, scaled to the
|
||||||
|
// busiest process in the frame.
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
height: parent.height - 4
|
||||||
|
radius: 3
|
||||||
|
width: parent.width *
|
||||||
|
Math.min(1, (rowItem.modelData.rx + rowItem.modelData.tx) / panel.maxRate)
|
||||||
|
color: Qt.rgba(Theme.sky.r, Theme.sky.g, Theme.sky.b, 0.16)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
spacing: Theme.spacing
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: rowItem.modelData.name
|
||||||
|
font.family: Theme.font
|
||||||
|
font.pixelSize: Theme.fontSize - 1
|
||||||
|
color: Theme.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Icons.down + " " + SysStats.fmtRate(rowItem.modelData.rx)
|
||||||
|
font.family: Theme.monoFont
|
||||||
|
font.pixelSize: Theme.fontSize - 2
|
||||||
|
color: Theme.green
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
Layout.preferredWidth: 62
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Icons.up + " " + SysStats.fmtRate(rowItem.modelData.tx)
|
||||||
|
font.family: Theme.monoFont
|
||||||
|
font.pixelSize: Theme.fontSize - 2
|
||||||
|
color: Theme.peach
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
Layout.preferredWidth: 62
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,20 @@ MouseArea {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
// accumulate raw wheel deltas so high-res / inertial scrolling doesn't
|
||||||
|
// fire a full step per micro-event; one notch (120 units) == one step
|
||||||
|
property real _wheelAccum: 0
|
||||||
|
readonly property real _wheelStep: 0.02
|
||||||
|
|
||||||
onClicked: { if (audio) audio.muted = !audio.muted; }
|
onClicked: { if (audio) audio.muted = !audio.muted; }
|
||||||
onWheel: wheel => {
|
onWheel: wheel => {
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
|
_wheelAccum += wheel.angleDelta.y;
|
||||||
|
const notches = Math.trunc(_wheelAccum / 120);
|
||||||
|
if (notches === 0) return;
|
||||||
|
_wheelAccum -= notches * 120;
|
||||||
audio.volume = Math.max(0, Math.min(1,
|
audio.volume = Math.max(0, Math.min(1,
|
||||||
audio.volume + (wheel.angleDelta.y > 0 ? 0.05 : -0.05)));
|
audio.volume + notches * _wheelStep));
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep the sink's audio properties live
|
// keep the sink's audio properties live
|
||||||
|
|||||||
Reference in New Issue
Block a user