- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
399 lines
15 KiB
QML
399 lines
15 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "pathspark.js" as Spark
|
|
|
|
// laptop — router — internet, with per-leg latency on the connecting lines.
|
|
// The answer to "is it me or is it them", drawn rather than written.
|
|
Item {
|
|
id: root
|
|
|
|
property var live: null
|
|
// The configured anchor: the far node's fallback label when no WAN
|
|
// address has been proven yet (see middle/far node text below).
|
|
property string anchor: ""
|
|
property color textColor: Color.popups.text
|
|
property color dimColor: Color.muted
|
|
|
|
// A phone sharing its data, as detected by the daemon from the gateway
|
|
// range. `label` is what to call it — iPhone, Phone, Hotspot.
|
|
readonly property var metered: live && live.metered ? live.metered : null
|
|
readonly property bool tethered: !!(metered && metered.tethered)
|
|
|
|
readonly property string middleTitle: tethered ? metered.label : "Router"
|
|
|
|
readonly property string middleDetail: {
|
|
if (!live || !live.link) return ""
|
|
// On a hotspot the gateway is always the same fixed address for the
|
|
// platform, so it carries no information; the phone's own name does.
|
|
// iOS names the hotspot after the device, so the network name IS the
|
|
// handset's name.
|
|
if (tethered) return live.link.ssid || live.link.gateway || ""
|
|
return live.link.gateway || ""
|
|
}
|
|
|
|
// Three minutes of each leg for the connectors. The ISP leg arrives already
|
|
// subtracted per point — see score.wan_point_ms — so nothing here re-derives
|
|
// it and the inversion guard keeps one implementation.
|
|
readonly property var points: panel ? panel.recentPoints : []
|
|
readonly property var localSeries: Spark.slots(points, "local", Spark.SLOTS)
|
|
readonly property var wanSeries: Spark.slots(points, "wan", Spark.SLOTS)
|
|
// One zero-based scale for both, so a 2 ms wobble cannot outdraw the WAN.
|
|
readonly property real sparkMax: Spark.sharedMax([localSeries, wanSeries],
|
|
Spark.SCALE_FLOOR_MS)
|
|
// The panel already knows whether readings are arriving: the daemon writes
|
|
// live.json at 2 Hz and the bar widget calls it stale after five seconds.
|
|
// The ring is that fact drawn, not a second rule with a second clock.
|
|
readonly property bool sparkLive: !!(panel && !panel.stale)
|
|
|
|
readonly property var localMs: live && live.local ? live.local.p50 : null
|
|
readonly property var wanMs: live && live.wan ? live.wan.p50 : null
|
|
readonly property bool localDown: live && live.state === "local-down"
|
|
readonly property bool wanDown: live && live.state === "wan-down"
|
|
|
|
// The address this connection appears from, published by the daemon.
|
|
// Shown masked: Overview screenshots end up on forums, and a screenshot
|
|
// must not carry the poster's IP. Tapping the node reveals it; the
|
|
// reveal is never persisted anywhere.
|
|
readonly property var wanIp: live && live.wan_ip ? live.wan_ip : null
|
|
readonly property var seated: {
|
|
if (!live || !live.instruments) return []
|
|
return live.instruments.filter(function(i) { return i.active })
|
|
}
|
|
|
|
// What the info glyph on the Internet node explains on hover: which
|
|
// instruments hold the scored seats right now. A caption line said
|
|
// "2 probes live" here once — permanent height for a once-read fact.
|
|
function benchTip() {
|
|
var lines = []
|
|
for (var i = 0; i < seated.length; i++) {
|
|
var ins = seated[i]
|
|
lines.push(ins.kind + " \u00b7 " + ins.target
|
|
+ (ins.p50 !== null && ins.p50 !== undefined
|
|
? " \u2014 " + ins.p50.toFixed(1) + " ms" : ""))
|
|
}
|
|
var standby = live && live.instruments
|
|
? live.instruments.length - seated.length : 0
|
|
if (standby > 0)
|
|
lines.push(standby + " on standby \u2014 full bench on the Latency tab")
|
|
lines.push(detailOpen ? "tap to close" : "tap for detail")
|
|
return lines.join("\n")
|
|
}
|
|
|
|
// Opening the detail is the deliberate act that reveals the address, so
|
|
// the two are one gesture. Held by the Panel so switching tabs does not
|
|
// close it; it resets with the shell, which is the right lifetime for a
|
|
// view preference. Never persisted — an Overview screenshot taken
|
|
// without opening this carries a masked address.
|
|
// Held by the Panel, like the other disclosure toggles, so switching
|
|
// tabs does not close it. Null-guarded: without a panel the detail
|
|
// simply never opens rather than throwing on every tap.
|
|
property var panel: null
|
|
readonly property bool detailOpen: !!(panel && panel.wanDetailOpen)
|
|
readonly property bool revealIp: detailOpen
|
|
|
|
function ipLine() {
|
|
if (!wanIp || !wanIp.ip) return ""
|
|
var parts = [String(wanIp.ip)]
|
|
// Country and edge ride along in the response the reachability check
|
|
// already fetches. The edge is Cloudflare's datacentre, not the
|
|
// user's location, so it is labelled as the route and not as a place.
|
|
if (wanIp.country) parts.push(wanIp.country)
|
|
if (wanIp.edge) parts.push("via Cloudflare " + wanIp.edge)
|
|
return parts.join(" \u00b7 ")
|
|
}
|
|
|
|
function legLine() {
|
|
var l = localMs, w = wanMs
|
|
if ((l === null || l === undefined) && (w === null || w === undefined))
|
|
return ""
|
|
var f = function(v) {
|
|
return v === null || v === undefined ? "\u2014" : v.toFixed(2) + " ms"
|
|
}
|
|
return "local " + f(l) + " \u00b7 wan " + f(w)
|
|
}
|
|
|
|
function probeLine() {
|
|
if (seated.length === 0) return ""
|
|
var parts = []
|
|
for (var i = 0; i < seated.length; i++) {
|
|
var ins = seated[i]
|
|
parts.push(ins.kind + " " + ins.target
|
|
+ (ins.p50 !== null && ins.p50 !== undefined
|
|
? " " + ins.p50.toFixed(1) : ""))
|
|
}
|
|
var standby = live && live.instruments
|
|
? live.instruments.length - seated.length : 0
|
|
var out = parts.join(" \u00b7 ")
|
|
if (standby > 0) out += " (+" + standby + " standby)"
|
|
return out
|
|
}
|
|
|
|
function loadLine() {
|
|
var lag = live && live.lag ? live.lag : null
|
|
if (!lag || lag.idle === null || lag.idle === undefined
|
|
|| lag.loaded === null || lag.loaded === undefined) return ""
|
|
// The daemon withholds `inflation` when the two populations are too
|
|
// close to separate or the ratio came out backwards. Absent inflation
|
|
// means the pair is not trustworthy either, so the whole row goes —
|
|
// printing "idle 13.0 -> loaded 10.9" states that the link answers
|
|
// FASTER while busy, which queueing cannot do. A result that is wrong
|
|
// in direction is not a result, and it is not made safe by dropping
|
|
// only the ratio computed from it.
|
|
if (lag.inflation === null || lag.inflation === undefined) return ""
|
|
return "idle " + lag.idle.toFixed(1)
|
|
+ " \u2192 loaded " + lag.loaded.toFixed(1) + " ms"
|
|
+ " (" + lag.inflation.toFixed(2) + "\u00d7)"
|
|
}
|
|
|
|
function appsLine() {
|
|
var s = live && live.sockets ? live.sockets : null
|
|
if (!s || s.queue_p50 === null || s.queue_p50 === undefined) return ""
|
|
var out = "queue " + s.queue_p50.toFixed(1) + " ms typical"
|
|
if (s.queue_p95 !== null && s.queue_p95 !== undefined)
|
|
out += ", " + s.queue_p95.toFixed(1) + " worst"
|
|
if (s.sockets) out += " over " + s.sockets + " connections"
|
|
return out
|
|
}
|
|
|
|
function maskedIp(w) {
|
|
if (!w || !w.ip) return ""
|
|
var full = String(w.ip)
|
|
if (revealIp) return full
|
|
if (w.family === "v6")
|
|
return full.split(":").slice(0, 2).join(":") + ":\u2026"
|
|
return full.split(".").slice(0, 2).join(".") + ".\u2026"
|
|
}
|
|
|
|
function legColor(ms, down) {
|
|
if (down) return Color.urgent
|
|
if (ms === null || ms === undefined) return dimColor
|
|
if (ms <= 15) return "#9ece6a"
|
|
if (ms <= 50) return "#e0af68"
|
|
return Color.urgent
|
|
}
|
|
|
|
// One animator for both legs, stopped the moment the liveness claim stops
|
|
// being true: a panel left open must never keep pulsing over stale data.
|
|
property real ringPhase: 0
|
|
NumberAnimation on ringPhase {
|
|
running: root.sparkLive && root.motionOk
|
|
loops: Animation.Infinite
|
|
from: 0; to: 1; duration: 1400
|
|
}
|
|
// Omarchy's own animation preference; a user who turns the bar's motion off
|
|
// gets a static second circle rather than nothing, so the claim still reads.
|
|
readonly property bool motionOk: {
|
|
var b = panel ? panel.bar : null
|
|
if (!b || !("foregroundAnimationEnabled" in b)) return true
|
|
return b.foregroundAnimationEnabled === true
|
|
}
|
|
|
|
implicitHeight: stack.implicitHeight
|
|
|
|
Column {
|
|
id: stack
|
|
width: parent.width
|
|
spacing: Style.space(10)
|
|
|
|
Row {
|
|
id: row
|
|
width: parent.width
|
|
|
|
component Node: Column {
|
|
property string icon: ""
|
|
property string title: ""
|
|
property string detail: ""
|
|
width: Style.space(84)
|
|
spacing: Style.space(4)
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: parent.icon
|
|
color: root.textColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.iconLarge
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: parent.title
|
|
color: root.textColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.caption
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: parent.detail
|
|
color: root.dimColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.caption
|
|
elide: Text.ElideMiddle
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
}
|
|
|
|
component Leg: Column {
|
|
property var ms: null
|
|
property bool down: false
|
|
property string label: ""
|
|
property var series: []
|
|
width: (row.width - Style.space(84) * 3) / 2
|
|
spacing: Style.space(4)
|
|
// Sits a little above the node centres so the line meets the icons.
|
|
topPadding: Style.space(8)
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: parent.down ? "down"
|
|
: (parent.ms === null || parent.ms === undefined
|
|
? "--" : parent.ms.toFixed(1) + " ms")
|
|
color: root.legColor(parent.ms, parent.down)
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
Canvas {
|
|
id: spark
|
|
width: parent.width - Style.space(12)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
// 16 left the plot 9 px tall with nothing spare, so the ring was
|
|
// cut on every side it could reach. The margin the ring needs is
|
|
// fixed; buying it out of the plot would have left a 3 px band.
|
|
// This keeps the same 9 px of plot and costs six pixels once —
|
|
// the connectors are side by side, so it is six for the panel,
|
|
// not six per leg.
|
|
height: Style.space(22)
|
|
antialiasing: true
|
|
// Repainting on every phase tick is what the ring costs; the series
|
|
// only changes every five seconds.
|
|
property real phase: root.ringPhase
|
|
property var series: parent.series
|
|
property real sparkScale: root.sparkMax
|
|
onPhaseChanged: requestPaint()
|
|
onSeriesChanged: requestPaint()
|
|
onSparkScaleChanged: requestPaint()
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
Spark.draw(ctx, width, height, series, {
|
|
max: sparkScale,
|
|
phase: phase,
|
|
live: root.sparkLive,
|
|
motion: root.motionOk,
|
|
downColor: Color.urgent,
|
|
colorFor: root.legColor
|
|
})
|
|
}
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: parent.label
|
|
color: root.dimColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
}
|
|
|
|
Node {
|
|
icon: "" // nf-md-laptop
|
|
title: "This machine"
|
|
detail: root.live && root.live.link ? (root.live.link.iface || "") : ""
|
|
}
|
|
Leg {
|
|
ms: root.localMs
|
|
down: root.localDown
|
|
label: "LOCAL"
|
|
series: root.localSeries
|
|
}
|
|
// When the connection comes from a phone, this node is the phone. Drawing
|
|
// a router here mislabelled both legs at once: the local leg is the hop
|
|
// to the handset, and everything past it is cellular, not an ISP line.
|
|
Node {
|
|
icon: root.tethered ? "" : "" // nf-md-cellphone / nf-md-router_wireless
|
|
title: root.middleTitle
|
|
detail: root.middleDetail
|
|
}
|
|
Leg {
|
|
ms: root.wanMs
|
|
down: root.wanDown
|
|
label: "WAN"
|
|
series: root.wanSeries
|
|
}
|
|
Node {
|
|
icon: "" // nf-md-web
|
|
title: "Internet"
|
|
// Your address out there, not the probe target: with a bench of
|
|
// instruments there is no single anchor to name, and naming one
|
|
// read as "this monitors Cloudflare". Masked — screenshots end up
|
|
// on forums; tap to reveal, never persisted.
|
|
detail: (root.wanIp ? root.maskedIp(root.wanIp) : root.anchor)
|
|
+ (root.seated.length > 0 ? " " : "")
|
|
|
|
TapHandler {
|
|
onTapped: if (root.panel) root.panel.wanDetailOpen = !root.panel.wanDetailOpen
|
|
}
|
|
HoverHandler { id: inetHover }
|
|
PanelToolTip {
|
|
visible: inetHover.hovered && root.seated.length > 0
|
|
text: root.benchTip()
|
|
}
|
|
}
|
|
}
|
|
|
|
// What the far node knows, shown only when asked for. Everything here
|
|
// is already measured — no extra request, no new destination — and it
|
|
// is deliberately what a single-number internet score cannot show: two
|
|
// legs, which instruments produced them, and what the machine's own
|
|
// connections are experiencing.
|
|
Column {
|
|
id: detail
|
|
width: parent.width
|
|
spacing: Style.space(4)
|
|
visible: root.detailOpen
|
|
// A visible:false child still takes its share of a Column's spacing.
|
|
height: visible ? implicitHeight : 0
|
|
|
|
component DetailRow: Row {
|
|
property string label: ""
|
|
property string value: ""
|
|
visible: value !== ""
|
|
height: visible ? implicitHeight : 0
|
|
spacing: Style.space(8)
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
text: parent.label
|
|
color: root.dimColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
width: Style.space(72)
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
text: parent.value
|
|
color: root.textColor
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.caption
|
|
elide: Text.ElideRight
|
|
width: detail.width - Style.space(80)
|
|
}
|
|
}
|
|
|
|
DetailRow { label: "ADDRESS"; value: root.ipLine() }
|
|
DetailRow { label: "LEGS"; value: root.legLine() }
|
|
DetailRow { label: "PROBES"; value: root.probeLine() }
|
|
DetailRow { label: "UNDER LOAD"; value: root.loadLine() }
|
|
DetailRow { label: "APPS SEE"; value: root.appsLine() }
|
|
}
|
|
|
|
}
|
|
}
|