- 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
196 lines
6.6 KiB
QML
196 lines
6.6 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Commons
|
|
import "readout.js" as Readout
|
|
|
|
// The two-leg latency chart: local leg as a dim band from the baseline, wan
|
|
// leg stacked on top in the accent colour, loss as ticks along the floor.
|
|
// Stacking is the point — the top line is the latency you feel, and the
|
|
// split says which side of the router owns it.
|
|
//
|
|
// `points` is an array of {t, local, total, loss} — local/total in ms or
|
|
// null, loss 0..1 or null. X is time, so a suspend shows as a gap.
|
|
Canvas {
|
|
id: chart
|
|
|
|
property var points: []
|
|
property color wanColor: Color.accent
|
|
property color localColor: Color.muted
|
|
property color lossColor: Color.urgent
|
|
property color axisColor: Qt.rgba(Color.popups.text.r, Color.popups.text.g,
|
|
Color.popups.text.b, 0.15)
|
|
property real minScaleMs: 20
|
|
// Larger charts label their scale; the compact overview chart stays clean.
|
|
property bool showScale: false
|
|
property string fontFamily: Style.font.family
|
|
// Hover crosshair with the values under the cursor. -1 = no hover.
|
|
property real hoverX: -1
|
|
property color readoutBg: Color.popups.background
|
|
property color readoutFg: Color.popups.text
|
|
|
|
readonly property real peakMs: {
|
|
var peak = 0
|
|
for (var i = 0; i < points.length; i++) {
|
|
var p = points[i]
|
|
if (p.total !== null && p.total !== undefined) peak = Math.max(peak, p.total)
|
|
}
|
|
return Math.max(minScaleMs, peak) * 1.08
|
|
}
|
|
|
|
onPointsChanged: requestPaint()
|
|
onWidthChanged: requestPaint()
|
|
onHeightChanged: requestPaint()
|
|
onWanColorChanged: requestPaint()
|
|
onHoverXChanged: requestPaint()
|
|
|
|
HoverHandler {
|
|
onPointChanged: chart.hoverX = hovered ? point.position.x : -1
|
|
onHoveredChanged: if (!hovered) chart.hoverX = -1
|
|
}
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.reset()
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
var top = 2, bottom = height - 1
|
|
|
|
ctx.strokeStyle = axisColor
|
|
ctx.lineWidth = 1
|
|
ctx.beginPath()
|
|
ctx.moveTo(0, bottom + 0.5)
|
|
ctx.lineTo(width, bottom + 0.5)
|
|
ctx.stroke()
|
|
|
|
if (showScale) {
|
|
// Quarter gridlines plus the top-of-scale label, so a quiet line
|
|
// reads as "3 ms on a 10 ms scale" rather than as an empty box.
|
|
// Barely-there on purpose: they are reference, not content — and
|
|
// Qt.rgba here sets an absolute alpha, so this must be far below
|
|
// the baseline's 0.15, not a multiplier of it.
|
|
ctx.strokeStyle = Qt.rgba(axisColor.r, axisColor.g, axisColor.b, 0.05)
|
|
for (var g = 1; g <= 3; g++) {
|
|
var gy = Math.round(bottom - (bottom - top) * g / 4) + 0.5
|
|
ctx.beginPath()
|
|
ctx.moveTo(0, gy)
|
|
ctx.lineTo(width, gy)
|
|
ctx.stroke()
|
|
}
|
|
ctx.fillStyle = Qt.rgba(localColor.r, localColor.g, localColor.b, 0.9)
|
|
ctx.font = "10px " + fontFamily
|
|
ctx.textBaseline = "top"
|
|
ctx.fillText(Math.round(peakMs) + " ms", 4, 3)
|
|
}
|
|
|
|
var pts = points
|
|
if (!pts || pts.length < 2) return
|
|
|
|
var t0 = pts[0].t, t1 = pts[pts.length - 1].t
|
|
var span = Math.max(1, t1 - t0)
|
|
var peak = peakMs
|
|
|
|
function xAt(t) { return (t - t0) * (width - 1) / span }
|
|
function yAt(ms) { return bottom - (bottom - top) * Math.max(0, ms) / peak }
|
|
|
|
// Runs of consecutive non-null samples paint as separate segments so a
|
|
// gap in the data is a gap on screen, not a line drawn through it.
|
|
function runs(key) {
|
|
var out = [], current = []
|
|
for (var i = 0; i < pts.length; i++) {
|
|
var v = pts[i][key]
|
|
if (v === null || v === undefined) {
|
|
if (current.length > 1) out.push(current)
|
|
current = []
|
|
} else {
|
|
current.push([xAt(pts[i].t), yAt(v)])
|
|
}
|
|
}
|
|
if (current.length > 1) out.push(current)
|
|
return out
|
|
}
|
|
|
|
function area(run, tint, alpha) {
|
|
ctx.beginPath()
|
|
ctx.moveTo(run[0][0], bottom)
|
|
for (var i = 0; i < run.length; i++) ctx.lineTo(run[i][0], run[i][1])
|
|
ctx.lineTo(run[run.length - 1][0], bottom)
|
|
ctx.closePath()
|
|
ctx.fillStyle = Qt.rgba(tint.r, tint.g, tint.b, alpha)
|
|
ctx.fill()
|
|
}
|
|
|
|
function line(run, tint, w) {
|
|
ctx.beginPath()
|
|
for (var i = 0; i < run.length; i++) {
|
|
if (i === 0) ctx.moveTo(run[i][0], run[i][1])
|
|
else ctx.lineTo(run[i][0], run[i][1])
|
|
}
|
|
ctx.strokeStyle = tint
|
|
ctx.lineWidth = w
|
|
ctx.stroke()
|
|
}
|
|
|
|
var i, r
|
|
var totalRuns = runs("total")
|
|
for (i = 0; i < totalRuns.length; i++) {
|
|
r = totalRuns[i]
|
|
area(r, wanColor, 0.20)
|
|
line(r, wanColor, 1.5)
|
|
}
|
|
var localRuns = runs("local")
|
|
for (i = 0; i < localRuns.length; i++) {
|
|
r = localRuns[i]
|
|
area(r, localColor, 0.5)
|
|
line(r, localColor, 1)
|
|
}
|
|
|
|
ctx.fillStyle = lossColor
|
|
for (i = 0; i < pts.length; i++) {
|
|
if (pts[i].loss !== null && pts[i].loss !== undefined && pts[i].loss > 0)
|
|
ctx.fillRect(xAt(pts[i].t) - 1, bottom - 8, 2, 8)
|
|
}
|
|
|
|
// Hover crosshair: nearest sample by time, values in a readout box.
|
|
if (hoverX >= 0) {
|
|
var tAt = t0 + hoverX * span / (width - 1)
|
|
var best = null, bestD = Infinity
|
|
for (i = 0; i < pts.length; i++) {
|
|
var d = Math.abs(pts[i].t - tAt)
|
|
if (d < bestD) { bestD = d; best = pts[i] }
|
|
}
|
|
if (best) {
|
|
var cx = xAt(best.t)
|
|
ctx.strokeStyle = Qt.rgba(readoutFg.r, readoutFg.g, readoutFg.b, 0.4)
|
|
ctx.lineWidth = 1
|
|
ctx.beginPath()
|
|
ctx.moveTo(cx + 0.5, 0)
|
|
ctx.lineTo(cx + 0.5, bottom)
|
|
ctx.stroke()
|
|
|
|
var parts = [Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss")]
|
|
if (best.total !== null && best.total !== undefined) {
|
|
var local = best.local !== null && best.local !== undefined ? best.local : 0
|
|
// Same rule the daemon applies to the leg figures: if the router
|
|
// answered slower than the internet behind it, total = local + wan
|
|
// does not hold and the subtraction says nothing. Clamping the
|
|
// negative to zero printed "wan 0.0 ms" — a perfect ISP leg from
|
|
// an invalid measurement.
|
|
parts.push(local > best.total + 1.0
|
|
? "wan \u2014"
|
|
: "wan " + Math.max(0, best.total - local).toFixed(1) + " ms")
|
|
parts.push("local " + local.toFixed(1) + " ms")
|
|
} else {
|
|
parts.push("no data")
|
|
}
|
|
if (best.loss !== null && best.loss !== undefined && best.loss > 0)
|
|
parts.push("loss " + Math.round(best.loss * 100) + "%")
|
|
var label = parts.join(" · ")
|
|
|
|
Readout.draw(ctx, fontFamily, label, cx, width,
|
|
readoutFg, readoutBg)
|
|
}
|
|
}
|
|
}
|
|
}
|