Files
neoromantique-dotfiles/home/private_dot_config/quickshell/shell.qml.tmpl
David Aizenberg 9d239329c0 sync
2026-02-13 13:27:05 +01:00

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")
}
}
}
}
}
}
}
}
}