mirror of
https://github.com/neoromantique/dotfiles.git
synced 2026-03-13 21:53:20 +03:00
1145 lines
40 KiB
Cheetah
1145 lines
40 KiB
Cheetah
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import Quickshell.Services.SystemTray
|
|
import Quickshell.Widgets
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
property color bgColor: "#0f0f0f"
|
|
property color fgColor: "#e0e0e0"
|
|
property color dimColor: "#888888"
|
|
property color accentColor: "#e67e22"
|
|
property color okColor: "#2ecc71"
|
|
property color warnColor: "#f1c40f"
|
|
property color critColor: "#e74c3c"
|
|
property color borderColor: "#333333"
|
|
|
|
Process {
|
|
id: shellExec
|
|
running: false
|
|
}
|
|
|
|
function runShell(cmd) {
|
|
shellExec.command = ["/usr/bin/env", "bash", "-lc", cmd]
|
|
shellExec.running = true
|
|
}
|
|
|
|
function trim(s) {
|
|
return (s || "").toString().trim()
|
|
}
|
|
|
|
property var wsNames: ({"1": "WEB", "2": "CODE", "3": "TERM", "4": "IDE", "5": "VM", "6": "GFX", "7": "DOC", "8": "GAME", "9": "MISC", "10": "TMP", "special:termius": "s:term", "special:org": "s:org", "special:llm": "s:llm"})
|
|
|
|
function setWsLabel(wsId, label) {
|
|
var n = Object.assign({}, wsNames)
|
|
n[wsId] = label
|
|
wsNames = n
|
|
wsNamesSaver.command = ["/usr/bin/env", "bash", "-c", "echo '" + JSON.stringify(n) + "' > ~/.config/quickshell/ws-names.json"]
|
|
wsNamesSaver.running = true
|
|
}
|
|
|
|
Process {
|
|
id: wsNamesLoader
|
|
command: ["/usr/bin/env", "bash", "-c", "cat ~/.config/quickshell/ws-names.json 2>/dev/null || echo '{}'"]
|
|
running: true
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
var loaded = JSON.parse((this.text || "{}").trim())
|
|
root.wsNames = Object.assign({}, root.wsNames, loaded)
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: wsNamesSaver
|
|
running: false
|
|
}
|
|
|
|
function wsIgnored(name) {
|
|
return name === "chrome-sharing-indicator" || name === "special:chrome-sharing-indicator"
|
|
}
|
|
|
|
component ModuleBox: Rectangle {
|
|
color: "#1a1a1a"
|
|
border.width: 1
|
|
border.color: root.borderColor
|
|
radius: 0
|
|
implicitHeight: 22
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
delegate: PanelWindow {
|
|
id: panel
|
|
required property var modelData
|
|
|
|
screen: modelData
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Auto
|
|
exclusiveZone: 34
|
|
aboveWindows: true
|
|
|
|
anchors {
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
implicitHeight: 34
|
|
|
|
property var hyprMonitor: Hyprland.monitorFor(modelData)
|
|
visible: {{- if eq .deviceProfile "desktop" }}hyprMonitor && hyprMonitor.name === "{{ .primaryMonitor }}"{{- else }}true{{- end }}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
color: root.bgColor
|
|
border.width: 1
|
|
border.color: root.borderColor
|
|
|
|
Row {
|
|
id: mainRow
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 6
|
|
anchors.rightMargin: 6
|
|
spacing: 0
|
|
|
|
Row {
|
|
id: leftGroup
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 6
|
|
|
|
ModuleBox {
|
|
id: workspacesBox
|
|
implicitWidth: workspacesRow.implicitWidth + 12
|
|
|
|
Row {
|
|
id: workspacesRow
|
|
anchors.centerIn: parent
|
|
spacing: 6
|
|
|
|
Repeater {
|
|
model: Hyprland.workspaces
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
property bool ignored: root.wsIgnored(modelData.name)
|
|
|
|
visible: !ignored
|
|
color: modelData.urgent
|
|
? root.critColor
|
|
: (modelData.active ? root.accentColor : "transparent")
|
|
border.width: 1
|
|
border.color: modelData.urgent ? root.critColor : root.borderColor
|
|
radius: 0
|
|
implicitHeight: 18
|
|
implicitWidth: wsText.implicitWidth + 10
|
|
|
|
Text {
|
|
id: wsText
|
|
anchors.centerIn: parent
|
|
text: root.wsNames[modelData.name] || "WS"
|
|
color: modelData.active || modelData.urgent ? "#111111" : root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton) {
|
|
wsRenamePopup.wsId = modelData.name
|
|
wsRenamePopup.renameX = mapToItem(null, 0, 0).x
|
|
wsRenamePopup.visible = true
|
|
wsRenameInput.text = root.wsNames[modelData.name] || ""
|
|
wsRenameInput.selectAll()
|
|
wsRenameInput.forceActiveFocus()
|
|
} else {
|
|
modelData.activate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.NoButton
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
Hyprland.dispatch("workspace e+1")
|
|
} else if (wheel.angleDelta.y < 0) {
|
|
Hyprland.dispatch("workspace e-1")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: Math.max(0, panel.width - leftGroup.implicitWidth - rightGroup.implicitWidth - 12)
|
|
height: 1
|
|
}
|
|
|
|
Row {
|
|
id: rightGroup
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 6
|
|
|
|
ModuleBox {
|
|
id: vpnBox
|
|
implicitWidth: vpnText.implicitWidth + 12
|
|
property string vpnClass: "disconnected"
|
|
property string wgRaw: ""
|
|
property string nbRaw: ""
|
|
property string tsRaw: ""
|
|
property int vpnPending: 3
|
|
|
|
function updateVpnStatus() {
|
|
var parts = []
|
|
var hasTunnel = false
|
|
|
|
// Parse WireGuard interfaces (exclude NetBird wt*)
|
|
var wgLines = wgRaw.split("\n")
|
|
for (var i = 0; i < wgLines.length; i++) {
|
|
var m = wgLines[i].match(/^\d+:\s+([^:@\s]+)/)
|
|
if (m && !m[1].startsWith("wt")) {
|
|
parts.push("WG:" + m[1])
|
|
hasTunnel = true
|
|
}
|
|
}
|
|
|
|
// Parse NetBird
|
|
try {
|
|
var nb = JSON.parse(nbRaw)
|
|
if (nb.management && nb.management.connected) {
|
|
var fqdn = nb.fqdn || ""
|
|
var segs = fqdn.split(".")
|
|
var net = segs.length >= 2 ? segs[segs.length - 2] : "nb"
|
|
var conn = (nb.peers && nb.peers.connected) || 0
|
|
var tot = (nb.peers && nb.peers.total) || 0
|
|
parts.push("NB:" + net + "(" + conn + "/" + tot + ")")
|
|
}
|
|
} catch(e) {}
|
|
|
|
// Parse Tailscale
|
|
try {
|
|
var ts = JSON.parse(tsRaw)
|
|
if (ts.BackendState === "Running") {
|
|
var tailnet = (ts.CurrentTailnet && ts.CurrentTailnet.Name) || ""
|
|
var shortNet = tailnet.replace(/\.ts\.net$/, "").replace(/\.tail.*$/, "")
|
|
var exitId = (ts.ExitNodeStatus && ts.ExitNodeStatus.ID) || ""
|
|
var exitOnline = (ts.ExitNodeStatus && ts.ExitNodeStatus.Online) || false
|
|
|
|
if (exitId && exitOnline) {
|
|
var exitHost = "exit"
|
|
if (ts.Peer) {
|
|
var keys = Object.keys(ts.Peer)
|
|
for (var j = 0; j < keys.length; j++) {
|
|
if (ts.Peer[keys[j]].ExitNode) {
|
|
exitHost = ts.Peer[keys[j]].HostName || "exit"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
parts.push("TS:" + shortNet + "→" + exitHost)
|
|
hasTunnel = true
|
|
} else {
|
|
parts.push("TS:" + shortNet)
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
|
|
if (parts.length > 0) {
|
|
vpnText.text = parts.join(" | ")
|
|
vpnClass = hasTunnel ? "connected" : "connected-no-tunnel"
|
|
} else {
|
|
vpnText.text = "VPN off"
|
|
vpnClass = "disconnected"
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: vpnText
|
|
anchors.centerIn: parent
|
|
text: "VPN off"
|
|
color: vpnBox.vpnClass === "connected"
|
|
? root.accentColor
|
|
: (vpnBox.vpnClass === "disconnected" ? root.dimColor : root.fgColor)
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: wgProc
|
|
command: ["/usr/bin/env", "bash", "-c", "ip link show type wireguard 2>/dev/null || true"]
|
|
running: true
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
vpnBox.wgRaw = (this.text || "").trim()
|
|
vpnBox.vpnPending--
|
|
if (vpnBox.vpnPending <= 0) vpnBox.updateVpnStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: nbProc
|
|
command: ["/usr/bin/env", "bash", "-c", "command -v netbird &>/dev/null && netbird status --json 2>/dev/null || echo '{}'"]
|
|
running: true
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
vpnBox.nbRaw = (this.text || "").trim()
|
|
vpnBox.vpnPending--
|
|
if (vpnBox.vpnPending <= 0) vpnBox.updateVpnStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: tsProc
|
|
command: ["/usr/bin/env", "bash", "-c", "command -v tailscale &>/dev/null && tailscale status --json 2>/dev/null || echo '{}'"]
|
|
running: true
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
vpnBox.tsRaw = (this.text || "").trim()
|
|
vpnBox.vpnPending--
|
|
if (vpnBox.vpnPending <= 0) vpnBox.updateVpnStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
vpnBox.vpnPending = 3
|
|
wgProc.running = true
|
|
nbProc.running = true
|
|
tsProc.running = true
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton
|
|
onClicked: {
|
|
vpnPopup.visible = !vpnPopup.visible
|
|
if (vpnPopup.visible) vpnPopup.refresh()
|
|
}
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: spkBox
|
|
implicitWidth: spkText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: spkText
|
|
anchors.centerIn: parent
|
|
text: "VOL --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: spkProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "mute=$(pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | awk '{print $2}'); vol=$(pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | awk 'NR==1{print $5}'); if [ \"${mute}\" = \"yes\" ]; then echo 'VOL muted'; elif [ -n \"${vol}\" ]; then echo \"VOL ${vol}\"; else echo 'VOL --'; fi"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: spkText.text = root.trim(this.text) || "VOL --" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 3000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: spkProc.running = true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton) {
|
|
root.runShell("pactl set-sink-mute @DEFAULT_SINK@ toggle")
|
|
} else {
|
|
root.runShell("pavucontrol -t 3")
|
|
}
|
|
spkProc.running = true
|
|
}
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
root.runShell("~/.local/bin/audio-sink-cycle up")
|
|
} else if (wheel.angleDelta.y < 0) {
|
|
root.runShell("~/.local/bin/audio-sink-cycle down")
|
|
}
|
|
spkProc.running = true
|
|
}
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: micBox
|
|
implicitWidth: micText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: micText
|
|
anchors.centerIn: parent
|
|
text: "MIC --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: micProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "mute=$(pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{print $2}'); vol=$(pactl get-source-volume @DEFAULT_SOURCE@ 2>/dev/null | awk 'NR==1{print $5}'); if [ \"${mute}\" = \"yes\" ]; then echo 'MIC muted'; elif [ -n \"${vol}\" ]; then echo \"MIC ${vol}\"; else echo 'MIC --'; fi"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: micText.text = root.trim(this.text) || "MIC --" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 3000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: micProc.running = true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton) {
|
|
root.runShell("pactl set-source-mute @DEFAULT_SOURCE@ toggle")
|
|
} else {
|
|
root.runShell("pavucontrol -t 4")
|
|
}
|
|
micProc.running = true
|
|
}
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
root.runShell("pactl set-source-volume @DEFAULT_SOURCE@ +2%")
|
|
} else if (wheel.angleDelta.y < 0) {
|
|
root.runShell("pactl set-source-volume @DEFAULT_SOURCE@ -2%")
|
|
}
|
|
micProc.running = true
|
|
}
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: netBox
|
|
implicitWidth: netText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: netText
|
|
anchors.centerIn: parent
|
|
text: "NET down"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: netProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "wifi=$(iwgetid -r 2>/dev/null || true); if [ -n \"$wifi\" ]; then sig=$(awk 'NR==3{gsub(/\./,\"\",$3); q=$3+0; printf \"%d\", int((q/70)*100)}' /proc/net/wireless 2>/dev/null); [ -z \"$sig\" ] && sig=0; echo \"NET wifi $wifi ${sig}%\"; exit; fi; iface=$(ip route | awk '/^default/{print $5; exit}'); if [ -n \"$iface\" ]; then ip4=$(ip -4 addr show dev \"$iface\" | awk '/inet /{print $2; exit}' | cut -d/ -f1); if [ -n \"$ip4\" ]; then echo \"NET eth $ip4\"; else echo 'NET link'; fi; else echo 'NET down'; fi"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: netText.text = root.trim(this.text) || "NET down" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: netProc.running = true
|
|
}
|
|
}
|
|
|
|
{{- if .hasBattery }}
|
|
ModuleBox {
|
|
id: batteryBox
|
|
implicitWidth: batteryText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: batteryText
|
|
anchors.centerIn: parent
|
|
text: "BAT --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: batteryProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "cap=$(cat /sys/class/power_supply/BAT*/capacity 2>/dev/null | head -n1); stat=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1); if [ -z \"$cap\" ]; then echo 'BAT --'; exit; fi; if [ \"$stat\" = 'Charging' ]; then echo \"BAT+ ${cap}%\"; elif [ \"$stat\" = 'Full' ]; then echo 'BAT full'; elif [ \"$stat\" = 'Not charging' ] || [ \"$stat\" = 'Unknown' ]; then echo \"BAT= ${cap}%\"; else echo \"BAT ${cap}%\"; fi"]
|
|
running: true
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const v = root.trim(this.text)
|
|
batteryText.text = v || "BAT --"
|
|
const m = v.match(/(\d+)%/)
|
|
if (v.startsWith("BAT+")) {
|
|
batteryText.color = root.okColor
|
|
} else if (m && Number(m[1]) <= 15) {
|
|
batteryText.color = root.critColor
|
|
} else if (m && Number(m[1]) <= 30) {
|
|
batteryText.color = root.warnColor
|
|
} else {
|
|
batteryText.color = root.fgColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 10000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: batteryProc.running = true
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: brtBox
|
|
implicitWidth: brtText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: brtText
|
|
anchors.centerIn: parent
|
|
text: "BRT --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: brtProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "pct=$(brightnessctl -m 2>/dev/null | awk -F, '{print $4}' | tr -d '%'); if [ -n \"$pct\" ]; then echo \"BRT ${pct}%\"; else echo 'BRT --'; fi"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: brtText.text = root.trim(this.text) || "BRT --" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: brtProc.running = true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.NoButton
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
root.runShell("brightnessctl s 5%+")
|
|
} else if (wheel.angleDelta.y < 0) {
|
|
root.runShell("brightnessctl s 5%-")
|
|
}
|
|
brtProc.running = true
|
|
}
|
|
}
|
|
}
|
|
{{- end }}
|
|
|
|
ModuleBox {
|
|
id: cpuBox
|
|
implicitWidth: cpuText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: cpuText
|
|
anchors.centerIn: parent
|
|
text: "CPU --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: cpuProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "read -r _ u n s i _ < /proc/stat; used=$((u+n+s)); total=$((u+n+s+i)); if [ \"$total\" -gt 0 ]; then printf 'CPU %d%%\\n' $((used*100/total)); else echo 'CPU --'; fi"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: cpuText.text = root.trim(this.text) || "CPU --" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: cpuProc.running = true
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: memBox
|
|
implicitWidth: memText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: memText
|
|
anchors.centerIn: parent
|
|
text: "MEM --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Process {
|
|
id: memProc
|
|
command: ["/usr/bin/env", "bash", "-lc", "free | awk '/Mem:/ { if ($2 > 0) printf \"MEM %d%%\\n\", int($3*100/$2); else print \"MEM --\"; }'"]
|
|
running: true
|
|
stdout: StdioCollector { onStreamFinished: memText.text = root.trim(this.text) || "MEM --" }
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: memProc.running = true
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: clockBox
|
|
implicitWidth: clockText.implicitWidth + 12
|
|
|
|
Text {
|
|
id: clockText
|
|
anchors.centerIn: parent
|
|
text: "TIME --"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
Timer {
|
|
interval: 1000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
const now = new Date()
|
|
clockText.text = Qt.formatDateTime(now, "'TIME' yyyy-MM-dd HH:mm")
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
const now = new Date()
|
|
clockText.text = Qt.formatDateTime(now, "'TIME' yyyy-MM-dd HH:mm")
|
|
}
|
|
}
|
|
|
|
ModuleBox {
|
|
id: trayBox
|
|
implicitWidth: trayRow.implicitWidth + 12
|
|
|
|
Row {
|
|
id: trayRow
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: SystemTray.items
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
color: "transparent"
|
|
border.width: 0
|
|
implicitWidth: 18
|
|
implicitHeight: 18
|
|
|
|
IconImage {
|
|
anchors.fill: parent
|
|
source: modelData.icon
|
|
asynchronous: true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton && modelData.hasMenu) {
|
|
modelData.display(panel, mouse.x, mouse.y)
|
|
} else if (mouse.button === Qt.MiddleButton) {
|
|
modelData.secondaryActivate()
|
|
} else {
|
|
modelData.activate()
|
|
}
|
|
}
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
modelData.scroll(1, false)
|
|
} else if (wheel.angleDelta.y < 0) {
|
|
modelData.scroll(-1, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PanelWindow {
|
|
id: wsRenamePopup
|
|
screen: panel.screen
|
|
visible: false
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Ignore
|
|
aboveWindows: true
|
|
focusable: true
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
property string wsId: ""
|
|
property real renameX: 0
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: wsRenamePopup.visible = false
|
|
}
|
|
|
|
Rectangle {
|
|
x: wsRenamePopup.renameX
|
|
y: parent.height - 72
|
|
width: 180
|
|
height: 26
|
|
color: root.bgColor
|
|
border.width: 1
|
|
border.color: root.accentColor
|
|
radius: 0
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
}
|
|
|
|
TextInput {
|
|
id: wsRenameInput
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 6
|
|
anchors.rightMargin: 6
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
color: root.fgColor
|
|
selectionColor: root.accentColor
|
|
selectedTextColor: "#111111"
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
maximumLength: 12
|
|
z: 1
|
|
|
|
onAccepted: {
|
|
if (text.trim())
|
|
root.setWsLabel(wsRenamePopup.wsId, text.trim())
|
|
wsRenamePopup.visible = false
|
|
}
|
|
|
|
Keys.onEscapePressed: wsRenamePopup.visible = false
|
|
}
|
|
}
|
|
}
|
|
|
|
PanelWindow {
|
|
id: vpnPopup
|
|
screen: panel.screen
|
|
visible: false
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Ignore
|
|
aboveWindows: true
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
property var wgConfigs: []
|
|
property var wgActive: []
|
|
property var tsAccounts: []
|
|
property var tsExits: []
|
|
property bool tsRunning: false
|
|
property string tsExitId: ""
|
|
property int popupPending: 0
|
|
|
|
function refresh() {
|
|
popupPending = 2
|
|
popupWgProc.running = true
|
|
popupTsProc.running = true
|
|
}
|
|
|
|
function parseWgData(raw) {
|
|
var sections = raw.split("---A---")
|
|
var configsRaw = (sections[0] || "").replace("---C---", "").trim()
|
|
var activeRaw = (sections[1] || "").trim()
|
|
wgConfigs = configsRaw ? configsRaw.split("\n").filter(function(s) { return s.trim() }) : []
|
|
wgActive = activeRaw ? activeRaw.split(/\s+/).filter(function(s) { return s.trim() }) : []
|
|
}
|
|
|
|
function parseTsData(raw) {
|
|
var statusSplit = raw.split("---L---")
|
|
var statusRaw = (statusSplit[0] || "").replace("---S---", "").trim()
|
|
var rest = statusSplit[1] || ""
|
|
var exitSplit = rest.split("---E---")
|
|
var accountsRaw = (exitSplit[0] || "").trim()
|
|
var exitsRaw = (exitSplit[1] || "").trim()
|
|
|
|
try {
|
|
var status = JSON.parse(statusRaw)
|
|
tsRunning = status.BackendState === "Running"
|
|
tsExitId = (status.ExitNodeStatus && status.ExitNodeStatus.ID) || ""
|
|
} catch(e) {
|
|
tsRunning = false
|
|
tsExitId = ""
|
|
}
|
|
|
|
var accs = []
|
|
var accLines = accountsRaw.split("\n")
|
|
for (var i = 1; i < accLines.length; i++) {
|
|
var parts = accLines[i].trim().split(/\s+/)
|
|
if (parts.length >= 3) {
|
|
var acct = parts[2]
|
|
var isActive = acct.endsWith("*")
|
|
if (isActive) acct = acct.slice(0, -1)
|
|
accs.push({ id: parts[0], tailnet: parts[1], account: acct, active: isActive })
|
|
}
|
|
}
|
|
tsAccounts = accs
|
|
|
|
var exits = []
|
|
var exitLines = exitsRaw.split("\n")
|
|
for (var j = 0; j < exitLines.length; j++) {
|
|
var line = exitLines[j].trim()
|
|
if (!line || line.startsWith("IP") || line.startsWith("#") || line.startsWith("-")) continue
|
|
var ep = line.split(/\s+/)
|
|
if (ep.length >= 4) {
|
|
var st = ep.slice(4).join(" ")
|
|
if (st.toLowerCase().indexOf("offline") >= 0) continue
|
|
exits.push({ ip: ep[0], hostname: ep[1], short: ep[1].split(".")[0], country: ep[2], city: ep[3] })
|
|
}
|
|
}
|
|
tsExits = exits
|
|
}
|
|
|
|
function vpnAction(cmd) {
|
|
vpnActionProc.command = ["/usr/bin/env", "bash", "-c", cmd]
|
|
vpnActionProc.running = true
|
|
visible = false
|
|
vpnRefreshTimer.running = true
|
|
}
|
|
|
|
Process {
|
|
id: vpnActionProc
|
|
running: false
|
|
}
|
|
|
|
Process {
|
|
id: popupWgProc
|
|
command: ["/usr/bin/env", "bash", "-c", "echo '---C---'; vpndir='{{ .secretsPath }}/vpn'; [ -d \"$vpndir\" ] || vpndir=\"$HOME/cfg/vpn\"; for f in \"$vpndir\"/*.conf; do [ -f \"$f\" ] && basename \"$f\" .conf; done; echo '---A---'; wg show interfaces 2>/dev/null || true"]
|
|
running: false
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
vpnPopup.parseWgData((this.text || "").trim())
|
|
vpnPopup.popupPending--
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: popupTsProc
|
|
command: ["/usr/bin/env", "bash", "-c", "echo '---S---'; command -v tailscale &>/dev/null && tailscale status --json 2>/dev/null || echo '{}'; echo '---L---'; command -v tailscale &>/dev/null && tailscale switch --list 2>/dev/null || true; echo '---E---'; command -v tailscale &>/dev/null && tailscale exit-node list 2>/dev/null || true"]
|
|
running: false
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
vpnPopup.parseTsData((this.text || "").trim())
|
|
vpnPopup.popupPending--
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: vpnRefreshTimer
|
|
interval: 1500
|
|
running: false
|
|
repeat: false
|
|
onTriggered: {
|
|
vpnBox.vpnPending = 3
|
|
wgProc.running = true
|
|
nbProc.running = true
|
|
tsProc.running = true
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: vpnPopup.visible = false
|
|
}
|
|
|
|
Rectangle {
|
|
id: vpnPopupRect
|
|
x: parent.width - 10 - rightGroup.implicitWidth
|
|
y: parent.height - 38 - height
|
|
width: 300
|
|
height: vpnPopupCol.implicitHeight + 16
|
|
color: root.bgColor
|
|
border.width: 1
|
|
border.color: root.borderColor
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
}
|
|
|
|
Column {
|
|
id: vpnPopupCol
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 2
|
|
z: 1
|
|
|
|
Text {
|
|
text: "━━ WireGuard ━━"
|
|
color: root.dimColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 11
|
|
topPadding: 2
|
|
bottomPadding: 2
|
|
}
|
|
|
|
Repeater {
|
|
model: vpnPopup.wgConfigs
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: wgItemMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
property bool active: vpnPopup.wgActive.indexOf(modelData) >= 0
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: (parent.active ? "●" : "○") + " " + modelData
|
|
color: parent.active ? root.accentColor : root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: wgItemMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
var dir = "{{ .secretsPath }}/vpn"
|
|
if (parent.active) {
|
|
vpnPopup.vpnAction("sudo ~/.local/bin/vpn-helper wg-down \"" + dir + "/" + modelData + ".conf\"")
|
|
} else {
|
|
vpnPopup.vpnAction("for i in $(wg show interfaces 2>/dev/null); do sudo ~/.local/bin/vpn-helper wg-down \"$i\" 2>/dev/null; done; tailscale set --exit-node= 2>/dev/null; sudo ~/.local/bin/vpn-helper wg-up \"" + dir + "/" + modelData + ".conf\"")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: "━━ Tailscale ━━"
|
|
color: root.dimColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 11
|
|
topPadding: 6
|
|
bottomPadding: 2
|
|
}
|
|
|
|
Repeater {
|
|
model: vpnPopup.tsAccounts
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: tsAcctMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: (modelData.active ? "●" : "○") + " " + modelData.tailnet + " (" + modelData.account + ")"
|
|
color: modelData.active ? root.accentColor : root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: tsAcctMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
if (!modelData.active)
|
|
vpnPopup.vpnAction("tailscale switch '" + modelData.id + "'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: !vpnPopup.tsRunning
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: tsStartMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: "○ Start Tailscale"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: tsStartMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: vpnPopup.vpnAction("tailscale up")
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: vpnPopup.tsRunning
|
|
text: "━━ Exit Nodes ━━"
|
|
color: root.dimColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 11
|
|
topPadding: 6
|
|
bottomPadding: 2
|
|
}
|
|
|
|
Rectangle {
|
|
visible: vpnPopup.tsRunning && vpnPopup.tsExitId !== ""
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: exitOffMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: "● Disconnect Exit Node"
|
|
color: root.accentColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: exitOffMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: vpnPopup.vpnAction("tailscale set --exit-node=")
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: vpnPopup.tsRunning ? vpnPopup.tsExits : []
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: exitNodeMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: "○ " + modelData.short + " (" + modelData.country + "/" + modelData.city + ")"
|
|
color: root.fgColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: exitNodeMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
vpnPopup.vpnAction("for i in $(wg show interfaces 2>/dev/null); do sudo ~/.local/bin/vpn-helper wg-down \"$i\" 2>/dev/null; done; tailscale set --exit-node='" + modelData.ip + "'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: vpnPopup.tsRunning
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: tsStopMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: "⊘ Stop Tailscale"
|
|
color: root.dimColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: tsStopMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: vpnPopup.vpnAction("tailscale down")
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: vpnPopupCol.width
|
|
height: 1
|
|
color: root.borderColor
|
|
}
|
|
|
|
Rectangle {
|
|
width: vpnPopupCol.width
|
|
height: 24
|
|
color: disconnAllMouse.containsMouse ? "#2a2a2a" : "transparent"
|
|
radius: 2
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
leftPadding: 8
|
|
text: "⊘ Disconnect All"
|
|
color: root.critColor
|
|
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
id: disconnAllMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
vpnPopup.vpnAction("for i in $(wg show interfaces 2>/dev/null); do sudo ~/.local/bin/vpn-helper wg-down \"$i\" 2>/dev/null; done; tailscale set --exit-node= 2>/dev/null || true")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|