Files
omarchy/plugins/io.github.x3me.nexthop/WifiTab.qml
T
asepharyana 1cdb82a76f Sync config from arch
- 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
2026-09-23 15:19:12 +07:00

621 lines
20 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pragma ComponentBehavior: Bound
import QtQuick
import qs.Commons
import qs.Ui
import "readout.js" as Readout
// The local leg in detail: signal on a labelled scale, link facts, and the
// airtime counters that explain "signal looks fine but Wi-Fi feels slow".
Column {
id: tab
required property var panel
Component.onCompleted: panel.requestEvents("7d")
readonly property var linkEvents: {
var all = panel.eventsData && panel.eventsData.events
? panel.eventsData.events : []
var kinds = {"roam": 1, "kick": 1, "drop": 1, "associate": 1,
"rate-drop": 1, "channel-change": 1}
var cut = Date.now() / 1000 - 86400
var out = []
for (var i = 0; i < all.length && out.length < 6; i++)
if (kinds[all[i].kind] && all[i].ts >= cut) out.push(all[i])
return out
}
// Every change of access point inside the chart window, with who ended
// the previous association: a roam was this machine's choice, a kick the
// access point's, a drop a link that fell over.
readonly property var roamMarks: {
var all = panel.eventsData && panel.eventsData.events
? panel.eventsData.events : []
var kinds = {"roam": 1, "kick": 1, "drop": 1}
var cut = Date.now() / 1000 - 1800
var out = []
for (var i = 0; i < all.length; i++)
if (kinds[all[i].kind] && all[i].ts >= cut)
out.push({ts: all[i].ts, kind: all[i].kind})
return out
}
readonly property var link: panel.live ? panel.live.link : null
readonly property var station: link ? link.station : null
readonly property bool isWifi: link && link.kind === "wifi"
spacing: Style.space(12)
Text {
textFormat: Text.PlainText
visible: !tab.isWifi
text: tab.link && tab.link.kind === "ethernet"
? "Wired connection — no radio to report on. The local leg lives on the Latency tab."
: "Not connected."
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
width: parent.width
}
// ---- signal -------------------------------------------------------------
Column {
width: parent.width
visible: tab.isWifi
spacing: Style.space(12)
Text {
textFormat: Text.PlainText
text: "THE LOCAL LEG · THIS MACHINE TO THE ROUTER"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
Row {
width: parent.width
spacing: Style.space(20)
readonly property var dbm: tab.link ? tab.link.signal_dbm : null
readonly property color tone: {
if (dbm === null || dbm === undefined) return tab.panel.dim
if (dbm >= -60) return tab.panel.okTone
if (dbm >= -70) return tab.panel.warnTone
return Color.urgent
}
Column {
spacing: Style.space(2)
anchors.bottom: parent.bottom
Text {
textFormat: Text.PlainText
text: parent.parent.dbm !== null && parent.parent.dbm !== undefined
? parent.parent.dbm + " dBm" : "--"
color: parent.parent.tone
font.family: tab.panel.fontFamily
font.pixelSize: Style.fontPx(2.0)
font.weight: Font.Bold
}
Text {
textFormat: Text.PlainText
text: "SIGNAL"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
}
Column {
width: parent.width - Style.space(130)
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.space(4)
spacing: Style.space(5)
Rectangle {
width: parent.width
height: Style.space(8)
color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.10)
Rectangle {
readonly property var dbm: tab.link ? tab.link.signal_dbm : null
height: parent.height
// -90 dBm is unusable, -30 is rail: map onto 0..1.
width: parent.width * (dbm === null || dbm === undefined
? 0 : Math.max(0, Math.min(1, (dbm + 90) / 60)))
color: parent.parent.parent.tone
Behavior on width { NumberAnimation { duration: 300 } }
}
// The marginal mark at -67 dBm, where video calls start to suffer.
Rectangle {
x: parent.width * ((-67 + 90) / 60)
y: -Style.space(3)
width: 1
height: parent.height + Style.space(6)
color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.35)
}
}
Item {
width: parent.width
height: scaleLeft.implicitHeight
Text {
id: scaleLeft
textFormat: Text.PlainText
text: "−90 unusable"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
Text {
textFormat: Text.PlainText
x: parent.width * ((-67 + 90) / 60) - implicitWidth / 2
text: "−67 marginal"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
Text {
textFormat: Text.PlainText
anchors.right: parent.right
text: "−30 max"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
}
}
}
// ---- signal & local lag history ---------------------------------------
Item {
width: parent.width
height: sigLabel.implicitHeight
Text {
id: sigLabel
textFormat: Text.PlainText
text: "SIGNAL & LOCAL LAG · LAST 30 MIN"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
Text {
textFormat: Text.PlainText
anchors.right: parent.right
text: {
var marks = tab.roamMarks, n = {roam: 0, kick: 0, drop: 0}
for (var i = 0; i < marks.length; i++) n[marks[i].kind] += 1
var parts = []
if (n.roam) parts.push(n.roam === 1 ? "one roam" : n.roam + " roams")
if (n.kick) parts.push(n.kick === 1 ? "kicked once" : "kicked " + n.kick + "\u00d7")
if (n.drop) parts.push(n.drop === 1 ? "one drop" : n.drop + " drops")
return parts.join(" \u00b7 ")
}
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
}
Canvas {
id: sigChart
width: parent.width
height: Style.space(78)
property real hoverX: -1
onHoverXChanged: requestPaint()
HoverHandler {
onPointChanged: sigChart.hoverX = hovered ? point.position.x : -1
onHoveredChanged: if (!hovered) sigChart.hoverX = -1
}
readonly property var pts: tab.panel.recentPoints
readonly property var roams: tab.roamMarks
onPtsChanged: requestPaint()
onRoamsChanged: requestPaint()
onWidthChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
var bottom = height - 1
ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g,
tab.panel.fg.b, 0.16)
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, bottom + 0.5)
ctx.lineTo(width, bottom + 0.5)
ctx.stroke()
var p = pts
if (!p || p.length < 2) return
var t0 = p[0].t, span = Math.max(1, p[p.length - 1].t - t0)
function xAt(t) { return (t - t0) * (width - 1) / span }
// Access-point changes first, dashed, under the series: purple
// when this machine chose to move, amber when it was kicked or
// the link dropped.
ctx.setLineDash([2, 3])
for (var m = 0; m < roams.length; m++) {
var rx = xAt(roams[m].ts)
if (rx < 0 || rx > width) continue
ctx.strokeStyle = roams[m].kind === "roam"
? Qt.rgba(0.73, 0.6, 0.97, 0.55) : Qt.rgba(0.88, 0.69, 0.41, 0.7)
ctx.beginPath()
ctx.moveTo(rx, 0)
ctx.lineTo(rx, height)
ctx.stroke()
}
ctx.setLineDash([])
function runs(key, yAt) {
var out = [], cur = []
for (var i = 0; i < p.length; i++) {
var v = p[i][key]
if (v === null || v === undefined) {
if (cur.length > 1) out.push(cur)
cur = []
} else {
cur.push([xAt(p[i].t), yAt(v)])
}
}
if (cur.length > 1) out.push(cur)
return out
}
// Signal on the fixed -90..-30 dBm scale, as a filled band.
var ok = tab.panel.okTone
var sigRuns = runs("sig", function(v) {
var f = Math.max(0, Math.min(1, (v + 90) / 60))
return bottom - (bottom - 3) * f
})
for (var r = 0; r < sigRuns.length; r++) {
var run = sigRuns[r]
ctx.beginPath()
ctx.moveTo(run[0][0], bottom)
for (var j = 0; j < run.length; j++) ctx.lineTo(run[j][0], run[j][1])
ctx.lineTo(run[run.length - 1][0], bottom)
ctx.closePath()
ctx.fillStyle = Qt.rgba(ok.r, ok.g, ok.b, 0.14)
ctx.fill()
ctx.beginPath()
for (j = 0; j < run.length; j++) {
if (j === 0) ctx.moveTo(run[j][0], run[j][1])
else ctx.lineTo(run[j][0], run[j][1])
}
ctx.strokeStyle = ok
ctx.lineWidth = 1.4
ctx.stroke()
}
// Local lag on its own scale, a thin dim line.
var lagPeak = 8
for (var i = 0; i < p.length; i++) {
if (p[i].local !== null && p[i].local !== undefined)
lagPeak = Math.max(lagPeak, p[i].local)
}
lagPeak *= 1.15
var lagRuns = runs("local", function(v) {
return bottom - (bottom - 3) * Math.min(1, v / lagPeak)
})
ctx.strokeStyle = tab.panel.dim
ctx.lineWidth = 1
for (r = 0; r < lagRuns.length; r++) {
run = lagRuns[r]
ctx.beginPath()
for (j = 0; j < run.length; j++) {
if (j === 0) ctx.moveTo(run[j][0], run[j][1])
else ctx.lineTo(run[j][0], run[j][1])
}
ctx.stroke()
}
if (hoverX >= 0) {
var tAt = t0 + hoverX * span / (width - 1)
var best = null, bestD = Infinity
for (i = 0; i < p.length; i++) {
var d = Math.abs(p[i].t - tAt)
if (d < bestD) { bestD = d; best = p[i] }
}
if (best) {
var cx = xAt(best.t)
ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g,
tab.panel.fg.b, 0.4)
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(cx + 0.5, 0)
ctx.lineTo(cx + 0.5, height)
ctx.stroke()
var parts = [Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss")]
parts.push(best.sig !== null && best.sig !== undefined
? best.sig + " dBm" : "no signal data")
if (best.local !== null && best.local !== undefined)
parts.push("lag " + best.local.toFixed(1) + " ms")
var label = parts.join(" · ")
Readout.draw(ctx, tab.panel.fontFamily, label, cx, width,
tab.panel.fg, Color.popups.background)
}
}
}
}
Row {
spacing: Style.space(16)
component ChartKey: Row {
property color tint: "white"
property string label: ""
property bool dashed: false
spacing: Style.space(6)
Rectangle {
width: parent.dashed ? 1 : Style.space(10)
height: parent.dashed ? Style.space(9) : 2
color: parent.tint
anchors.verticalCenter: parent.verticalCenter
}
Text {
textFormat: Text.PlainText
text: parent.label
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
anchors.verticalCenter: parent.verticalCenter
}
}
ChartKey { tint: tab.panel.okTone; label: "signal" }
ChartKey { tint: tab.panel.dim; label: "local lag" }
ChartKey { tint: "#bb9af7"; label: "roam"; dashed: true }
ChartKey { tint: tab.panel.warnTone; label: "kicked / dropped"; dashed: true }
}
PanelSeparator { width: parent.width }
// ---- link facts -------------------------------------------------------
Grid {
width: parent.width
columns: 2
columnSpacing: Style.space(24)
rowSpacing: Style.space(6)
readonly property real cell: (width - Style.space(24)) / 2
component KvRow: Item {
property string k: ""
property string v: ""
height: kText.implicitHeight
Text {
id: kText
textFormat: Text.PlainText
text: parent.k
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
anchors.right: parent.right
text: parent.v
color: tab.panel.fg
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.bodySmall
elide: Text.ElideLeft
width: parent.width - kText.implicitWidth - Style.space(8)
horizontalAlignment: Text.AlignRight
}
}
KvRow { width: parent.cell; k: "Band"
v: tab.link && tab.link.band ? tab.link.band : "--" }
KvRow { width: parent.cell; k: "Channel"
v: tab.link && tab.link.channel
? tab.link.channel + (tab.link.width_mhz ? " · " + tab.link.width_mhz + " MHz" : "")
: "--" }
KvRow { width: parent.cell; k: "Tx rate"
v: tab.link && tab.link.tx_mbps ? tab.link.tx_mbps + " Mbps" : "--" }
KvRow { width: parent.cell; k: "Rx rate"
v: tab.link && tab.link.rx_mbps ? tab.link.rx_mbps + " Mbps" : "--" }
KvRow { width: parent.cell; k: "Standard"
v: tab.link && tab.link.standard ? tab.link.standard : "--" }
KvRow { width: parent.cell; k: "Interface"
v: tab.link && tab.link.iface ? tab.link.iface : "--" }
KvRow { width: parent.cell; k: "BSSID"
v: tab.link && tab.link.bssid ? tab.link.bssid : "--" }
KvRow { width: parent.cell; k: "Gateway"
v: tab.link && tab.link.gateway ? tab.link.gateway : "--" }
}
PanelSeparator { width: parent.width }
// ---- airtime health ---------------------------------------------------
Text {
textFormat: Text.PlainText
text: "AIRTIME · WHY WI-FI FEELS SLOW WHEN SIGNAL LOOKS FINE"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
Row {
width: parent.width
spacing: Style.space(14)
readonly property real cell: (width - Style.space(14) * 2) / 3
component AirStat: Column {
property string label: ""
property string display: "--"
property real frac: 0 // 0..1 meter fill
property bool bad: false
spacing: Style.space(4)
Text {
textFormat: Text.PlainText
text: parent.display
color: parent.bad ? tab.panel.warnTone : tab.panel.okTone
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.heading
font.weight: Font.Bold
}
Text {
textFormat: Text.PlainText
text: parent.label
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
Rectangle {
width: parent.width
height: Math.max(2, Style.space(3))
color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.12)
Rectangle {
height: parent.height
width: parent.width * Math.max(0, Math.min(1, parent.parent.frac))
color: parent.parent.bad ? tab.panel.warnTone : tab.panel.okTone
}
}
}
AirStat {
width: parent.cell
label: "TX RETRIES"
display: tab.station && tab.station.retry_pct !== undefined
? tab.station.retry_pct.toFixed(1) + " %"
: (tab.station && tab.station.tx_retries !== undefined
? String(tab.station.tx_retries) : "--")
// 10% retries fills the meter; past ~5% the air is genuinely busy.
frac: tab.station && tab.station.retry_pct !== undefined
? tab.station.retry_pct / 10 : 0
bad: tab.station && tab.station.retry_pct > 5
}
AirStat {
width: parent.cell
label: "TX FAILED"
display: tab.station && tab.station.tx_failed !== undefined
? String(tab.station.tx_failed) : "--"
frac: tab.station && tab.station.tx_failed !== undefined
? tab.station.tx_failed / 50 : 0
bad: tab.station && tab.station.tx_failed > 10
}
AirStat {
width: parent.cell
label: "BEACON LOSS"
display: tab.station && tab.station.beacon_loss !== undefined
? String(tab.station.beacon_loss) : "--"
frac: tab.station && tab.station.beacon_loss !== undefined
? tab.station.beacon_loss / 10 : 0
bad: tab.station && tab.station.beacon_loss > 0
}
}
Text {
textFormat: Text.PlainText
width: parent.width
text: "Counters since association. Retries mean a noisy channel; failures mean "
+ "frames given up on; beacon loss means the router's heartbeat went missing."
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
PanelSeparator { width: parent.width }
// ---- link events ------------------------------------------------------
Text {
textFormat: Text.PlainText
text: "LINK EVENTS · LAST 24 H"
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
}
Text {
textFormat: Text.PlainText
visible: tab.linkEvents.length === 0
text: "None. The link has been steady."
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.bodySmall
}
Column {
width: parent.width
spacing: Style.space(8)
Repeater {
model: tab.linkEvents
Row {
id: linkRow
required property var modelData
width: parent.width
spacing: Style.space(10)
readonly property color tone: {
var k = linkRow.modelData.kind
if (k === "roam") return "#bb9af7"
if (k === "kick" || k === "drop") return tab.panel.warnTone
if (k === "rate-drop") return tab.panel.warnTone
if (k === "associate") return tab.panel.okTone
return Color.accent
}
Text {
textFormat: Text.PlainText
width: Style.space(42)
text: {
var d = new Date(linkRow.modelData.ts * 1000)
return d.toLocaleString(Qt.locale(), "HH:mm")
}
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
Rectangle {
width: Style.space(5)
height: Style.space(5)
color: linkRow.tone
anchors.verticalCenter: parent.verticalCenter
}
Text {
textFormat: Text.PlainText
width: parent.width - Style.space(42) - Style.space(5)
- Style.space(58) - Style.space(10) * 3
text: linkRow.modelData.detail
color: tab.panel.fg
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
Text {
textFormat: Text.PlainText
width: Style.space(58)
horizontalAlignment: Text.AlignRight
text: {
var e = linkRow.modelData
if (!e.ended_ts || e.ended_ts === e.ts) return ""
var s = e.ended_ts - e.ts
return s < 60 ? "for " + s + " s"
: "for " + Math.round(s / 60) + " m"
}
color: tab.panel.dim
font.family: tab.panel.fontFamily
font.pixelSize: Style.font.caption
}
}
}
}
}
}