mirror of
https://github.com/neoromantique/dotfiles.git
synced 2026-03-13 21:53:20 +03:00
2672 lines
89 KiB
Cheetah
2672 lines
89 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", "11": "AUX1", "12": "AUX2", "13": "AUX3", "14": "AUX4", "15": "AUX5", "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 --" }
|
||
}
|
||
|
||
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 {
|
||
sinkPopup.visible = !sinkPopup.visible
|
||
if (sinkPopup.visible) sinkPopup.refresh()
|
||
}
|
||
}
|
||
onWheel: function(wheel) {
|
||
if (wheel.angleDelta.y > 0) {
|
||
root.runShell("pactl set-sink-volume @DEFAULT_SINK@ +2%")
|
||
} else if (wheel.angleDelta.y < 0) {
|
||
root.runShell("pactl set-sink-volume @DEFAULT_SINK@ -2%")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 --" }
|
||
}
|
||
|
||
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 {
|
||
sourcePopup.visible = !sourcePopup.visible
|
||
if (sourcePopup.visible) sourcePopup.refresh()
|
||
}
|
||
}
|
||
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%")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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: pomBox
|
||
implicitWidth: pomText.implicitWidth + 12
|
||
|
||
property string pomState: "idle"
|
||
property int pomSeconds: 0
|
||
property int pomCount: 0
|
||
property int totalPoms: 0
|
||
property int totalFocusMins: 0
|
||
property bool pomBlink: false
|
||
property int workMins: 25
|
||
property int breakMins: 5
|
||
property int longBreakMins: 15
|
||
|
||
function fmt(s) {
|
||
var m = Math.floor(s / 60)
|
||
var ss = s % 60
|
||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss
|
||
}
|
||
|
||
function update() {
|
||
if (pomState === "idle") {
|
||
pomText.text = "POM off"
|
||
pomText.color = root.dimColor
|
||
return
|
||
}
|
||
var c = pomState === "work" ? " " + (pomCount + 1) + "/4" : pomCount > 0 ? " " + pomCount + "/4" : ""
|
||
if (pomState === "work") {
|
||
pomText.text = "POM " + fmt(pomSeconds) + c
|
||
pomText.color = pomSeconds <= 60 ? root.critColor : root.accentColor
|
||
} else if (pomState === "break" || pomState === "longbreak") {
|
||
pomText.text = "BRK " + fmt(pomSeconds) + c
|
||
pomText.color = pomSeconds <= 60 ? root.warnColor : root.okColor
|
||
} else if (pomState === "done") {
|
||
pomText.text = pomBlink ? ">>> GO <<<" : " GO "
|
||
pomText.color = root.critColor
|
||
}
|
||
}
|
||
|
||
function startWork() {
|
||
pomState = "work"
|
||
pomSeconds = workMins * 60
|
||
pomBlinkTimer.running = false
|
||
pomBlink = false
|
||
pomTimer.running = true
|
||
pomFlash.dismiss()
|
||
update()
|
||
}
|
||
|
||
function startBreak() {
|
||
pomCount++
|
||
totalPoms++
|
||
totalFocusMins += workMins
|
||
if (pomCount >= 4) {
|
||
pomState = "longbreak"
|
||
pomSeconds = longBreakMins * 60
|
||
pomCount = 0
|
||
pomFlash.show("LONG BREAK", root.okColor)
|
||
} else {
|
||
pomState = "break"
|
||
pomSeconds = breakMins * 60
|
||
pomFlash.show("BREAK", root.okColor)
|
||
}
|
||
pomTimer.running = true
|
||
update()
|
||
root.runShell("notify-send -u normal 'Pomodoro' 'Take a break!'")
|
||
}
|
||
|
||
function goDone() {
|
||
pomState = "done"
|
||
pomTimer.running = false
|
||
pomBlinkTimer.running = true
|
||
pomFlash.show("GET BACK TO WORK", root.critColor)
|
||
update()
|
||
root.runShell("notify-send -u critical 'Pomodoro' 'Break over. Get back to work.'")
|
||
}
|
||
|
||
function reset() {
|
||
pomState = "idle"
|
||
pomSeconds = 0
|
||
pomCount = 0
|
||
pomBlink = false
|
||
pomTimer.running = false
|
||
pomBlinkTimer.running = false
|
||
pomFlash.dismiss()
|
||
update()
|
||
}
|
||
|
||
function saveSettings() {
|
||
pomSettingsSaver.command = ["/usr/bin/env", "bash", "-c",
|
||
"echo '" + JSON.stringify({work: workMins, brk: breakMins, lng: longBreakMins}) + "' > ~/.config/quickshell/pom-settings.json"]
|
||
pomSettingsSaver.running = true
|
||
}
|
||
|
||
Process {
|
||
id: pomSettingsLoader
|
||
command: ["/usr/bin/env", "bash", "-c", "cat ~/.config/quickshell/pom-settings.json 2>/dev/null || echo '{}'"]
|
||
running: true
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
try {
|
||
var s = JSON.parse((this.text || "{}").trim())
|
||
if (s.work) pomBox.workMins = s.work
|
||
if (s.brk) pomBox.breakMins = s.brk
|
||
if (s.lng) pomBox.longBreakMins = s.lng
|
||
} catch(e) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: pomSettingsSaver
|
||
running: false
|
||
}
|
||
|
||
Timer {
|
||
id: pomTimer
|
||
interval: 1000
|
||
repeat: true
|
||
onTriggered: {
|
||
pomBox.pomSeconds--
|
||
if (pomBox.pomSeconds <= 0) {
|
||
if (pomBox.pomState === "work") pomBox.startBreak()
|
||
else pomBox.goDone()
|
||
} else {
|
||
pomBox.update()
|
||
}
|
||
}
|
||
}
|
||
|
||
Timer {
|
||
id: pomBlinkTimer
|
||
interval: 500
|
||
repeat: true
|
||
onTriggered: { pomBox.pomBlink = !pomBox.pomBlink; pomBox.update() }
|
||
}
|
||
|
||
Text {
|
||
id: pomText
|
||
anchors.centerIn: parent
|
||
text: "POM off"
|
||
color: root.dimColor
|
||
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) {
|
||
pomPopup.visible = !pomPopup.visible
|
||
} else {
|
||
if (pomBox.pomState === "idle" || pomBox.pomState === "done")
|
||
pomBox.startWork()
|
||
else
|
||
pomTimer.running = !pomTimer.running
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ModuleBox {
|
||
id: memoBox
|
||
implicitWidth: memoBoxText.implicitWidth + 12
|
||
property int todayCount: 0
|
||
|
||
Text {
|
||
id: memoBoxText
|
||
anchors.centerIn: parent
|
||
text: memoBox.todayCount > 0 ? "MEMO " + memoBox.todayCount : "MEMO"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
Process {
|
||
id: memoCountProc
|
||
command: ["/usr/bin/env", "bash", "-c", "today=$(date +%Y-%m-%d); grep -c \"$today\" ~/.local/share/quickmemo/notes.jsonl 2>/dev/null || echo 0"]
|
||
running: true
|
||
stdout: StdioCollector {
|
||
onStreamFinished: memoBox.todayCount = parseInt(root.trim(this.text)) || 0
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||
onClicked: function(mouse) {
|
||
if (mouse.button === Qt.RightButton) {
|
||
memoReview.refresh()
|
||
memoReview.visible = !memoReview.visible
|
||
} else {
|
||
memoInput.visible = !memoInput.visible
|
||
if (memoInput.visible) {
|
||
memoInputField.text = ""
|
||
memoInputField.forceActiveFocus()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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")
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.LeftButton
|
||
onClicked: {
|
||
calPopup.visible = !calPopup.visible
|
||
if (calPopup.visible) calPopup.resetToToday()
|
||
}
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: paSubscribe
|
||
command: ["/usr/bin/env", "bash", "-c", "pactl subscribe 2>/dev/null"]
|
||
running: true
|
||
stdout: SplitParser {
|
||
onRead: function(line) {
|
||
if (line.indexOf("sink") >= 0 || line.indexOf("server") >= 0)
|
||
spkDebounce.restart()
|
||
if (line.indexOf("source") >= 0 || line.indexOf("server") >= 0)
|
||
micDebounce.restart()
|
||
}
|
||
}
|
||
onRunningChanged: {
|
||
if (!running) paResubscribe.running = true
|
||
}
|
||
}
|
||
|
||
Timer {
|
||
id: paResubscribe
|
||
interval: 2000
|
||
repeat: false
|
||
onTriggered: paSubscribe.running = true
|
||
}
|
||
|
||
Timer {
|
||
id: spkDebounce
|
||
interval: 50
|
||
repeat: false
|
||
onTriggered: spkProc.running = true
|
||
}
|
||
|
||
Timer {
|
||
id: micDebounce
|
||
interval: 50
|
||
repeat: false
|
||
onTriggered: micProc.running = true
|
||
}
|
||
|
||
Timer {
|
||
interval: 10000
|
||
running: true
|
||
repeat: true
|
||
onTriggered: { spkProc.running = true; micProc.running = true }
|
||
}
|
||
|
||
Timer {
|
||
interval: 150
|
||
running: true
|
||
repeat: true
|
||
onTriggered: memoTriggerCheck.running = true
|
||
}
|
||
|
||
Process {
|
||
id: memoTriggerCheck
|
||
command: ["/usr/bin/env", "bash", "-c", "r=''; [ -f /tmp/qs-memo-input ] && rm /tmp/qs-memo-input && r=\"input\n\"; [ -f /tmp/qs-memo-clip ] && rm /tmp/qs-memo-clip && r=\"${r}clipboard\"; printf '%s' \"$r\""]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
var t = (this.text || "").trim()
|
||
if (t.indexOf("input") >= 0) {
|
||
memoInput.visible = true
|
||
memoInputField.text = ""
|
||
memoInputField.forceActiveFocus()
|
||
}
|
||
if (t.indexOf("clipboard") >= 0)
|
||
memoClipCapture.running = true
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: memoClipCapture
|
||
command: ["/usr/bin/env", "bash", "-c", "wl-paste 2>/dev/null || true"]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
var text = (this.text || "").trim()
|
||
if (text) {
|
||
panel.memoSave(text, "clipboard")
|
||
root.runShell("notify-send -r 91192 -t 2000 'Quick Memo' 'Clipboard saved'")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function memoSave(text, src) {
|
||
var entry = JSON.stringify({ts: new Date().toISOString(), text: text, src: src || "input"})
|
||
var b64 = Qt.btoa(entry)
|
||
memoSaveProc.command = ["/usr/bin/env", "bash", "-c",
|
||
"mkdir -p ~/.local/share/quickmemo && printf '%s' " + b64 + " | base64 -d >> ~/.local/share/quickmemo/notes.jsonl && printf '\\n' >> ~/.local/share/quickmemo/notes.jsonl"]
|
||
memoSaveProc.running = true
|
||
memoBox.todayCount++
|
||
}
|
||
|
||
Process {
|
||
id: memoSaveProc
|
||
running: false
|
||
}
|
||
|
||
PanelWindow {
|
||
id: sinkPopup
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property var sinks: []
|
||
property string defaultSink: ""
|
||
|
||
function refresh() {
|
||
sinkListProc.running = true
|
||
}
|
||
|
||
function parseSinks(raw) {
|
||
var sections = raw.split("---DEF---")
|
||
var listRaw = (sections[0] || "").replace("---LIST---", "").trim()
|
||
var defRaw = (sections[1] || "").trim()
|
||
defaultSink = defRaw
|
||
|
||
var items = []
|
||
var lines = listRaw.split("\n")
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i].trim()
|
||
if (!line) continue
|
||
var tabIdx = line.indexOf("\t")
|
||
if (tabIdx < 0) continue
|
||
var name = line.substring(0, tabIdx)
|
||
var desc = line.substring(tabIdx + 1)
|
||
items.push({ name: name, desc: desc, active: name === defRaw })
|
||
}
|
||
sinks = items
|
||
}
|
||
|
||
function setDefault(name) {
|
||
sinkActionProc.command = ["/usr/bin/env", "bash", "-c", "pactl set-default-sink '" + name + "'"]
|
||
sinkActionProc.running = true
|
||
visible = false
|
||
sinkRefreshTimer.running = true
|
||
}
|
||
|
||
Process {
|
||
id: sinkListProc
|
||
command: ["/usr/bin/env", "bash", "-c", "echo '---LIST---'; pactl list sinks 2>/dev/null | awk '/^\\tName:/{name=$2} /^\\tDescription:/{sub(/^\\tDescription: /,\"\"); print name\"\\t\"$0}'; echo '---DEF---'; pactl get-default-sink 2>/dev/null"]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: sinkPopup.parseSinks((this.text || "").trim())
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: sinkActionProc
|
||
running: false
|
||
}
|
||
|
||
Timer {
|
||
id: sinkRefreshTimer
|
||
interval: 500
|
||
running: false
|
||
repeat: false
|
||
onTriggered: spkProc.running = true
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: sinkPopup.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
x: parent.width - 10 - rightGroup.implicitWidth + vpnBox.implicitWidth + 6
|
||
y: parent.height - 38 - height
|
||
width: Math.max(250, sinkCol.implicitWidth + 16)
|
||
height: sinkCol.implicitHeight + 16
|
||
color: root.bgColor
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
}
|
||
|
||
Column {
|
||
id: sinkCol
|
||
anchors.fill: parent
|
||
anchors.margins: 8
|
||
spacing: 2
|
||
z: 1
|
||
|
||
Text {
|
||
text: "━━ Output Devices ━━"
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
topPadding: 2
|
||
bottomPadding: 2
|
||
}
|
||
|
||
Repeater {
|
||
model: sinkPopup.sinks
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
width: sinkCol.width
|
||
height: 24
|
||
color: sinkItemMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: (modelData.active ? "●" : "○") + " " + modelData.desc
|
||
color: modelData.active ? root.accentColor : root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
elide: Text.ElideRight
|
||
width: sinkCol.width - 8
|
||
}
|
||
|
||
MouseArea {
|
||
id: sinkItemMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: sinkPopup.setDefault(modelData.name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
PanelWindow {
|
||
id: sourcePopup
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property var sources: []
|
||
property string defaultSource: ""
|
||
|
||
function refresh() {
|
||
sourceListProc.running = true
|
||
}
|
||
|
||
function parseSources(raw) {
|
||
var sections = raw.split("---DEF---")
|
||
var listRaw = (sections[0] || "").replace("---LIST---", "").trim()
|
||
var defRaw = (sections[1] || "").trim()
|
||
defaultSource = defRaw
|
||
|
||
var items = []
|
||
var lines = listRaw.split("\n")
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i].trim()
|
||
if (!line) continue
|
||
var tabIdx = line.indexOf("\t")
|
||
if (tabIdx < 0) continue
|
||
var name = line.substring(0, tabIdx)
|
||
var desc = line.substring(tabIdx + 1)
|
||
items.push({ name: name, desc: desc, active: name === defRaw })
|
||
}
|
||
sources = items
|
||
}
|
||
|
||
function setDefault(name) {
|
||
sourceActionProc.command = ["/usr/bin/env", "bash", "-c", "pactl set-default-source '" + name + "'"]
|
||
sourceActionProc.running = true
|
||
visible = false
|
||
sourceRefreshTimer.running = true
|
||
}
|
||
|
||
Process {
|
||
id: sourceListProc
|
||
command: ["/usr/bin/env", "bash", "-c", "echo '---LIST---'; pactl list sources 2>/dev/null | awk '/^\\tName:/{name=$2} /^\\tDescription:/{sub(/^\\tDescription: /,\"\"); print name\"\\t\"$0}' | grep -v '\\.monitor'; echo '---DEF---'; pactl get-default-source 2>/dev/null"]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: sourcePopup.parseSources((this.text || "").trim())
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: sourceActionProc
|
||
running: false
|
||
}
|
||
|
||
Timer {
|
||
id: sourceRefreshTimer
|
||
interval: 500
|
||
running: false
|
||
repeat: false
|
||
onTriggered: micProc.running = true
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: sourcePopup.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
x: parent.width - 10 - rightGroup.implicitWidth + vpnBox.implicitWidth + spkBox.implicitWidth + 12
|
||
y: parent.height - 38 - height
|
||
width: Math.max(250, sourceCol.implicitWidth + 16)
|
||
height: sourceCol.implicitHeight + 16
|
||
color: root.bgColor
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
}
|
||
|
||
Column {
|
||
id: sourceCol
|
||
anchors.fill: parent
|
||
anchors.margins: 8
|
||
spacing: 2
|
||
z: 1
|
||
|
||
Text {
|
||
text: "━━ Input Devices ━━"
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
topPadding: 2
|
||
bottomPadding: 2
|
||
}
|
||
|
||
Repeater {
|
||
model: sourcePopup.sources
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
width: sourceCol.width
|
||
height: 24
|
||
color: srcItemMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: (modelData.active ? "●" : "○") + " " + modelData.desc
|
||
color: modelData.active ? root.accentColor : root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
elide: Text.ElideRight
|
||
width: sourceCol.width - 8
|
||
}
|
||
|
||
MouseArea {
|
||
id: srcItemMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: sourcePopup.setDefault(modelData.name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
PanelWindow {
|
||
id: calPopup
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property int calYear: new Date().getFullYear()
|
||
property int calMonth: new Date().getMonth()
|
||
property var calDays: []
|
||
|
||
function resetToToday() {
|
||
var now = new Date()
|
||
calYear = now.getFullYear()
|
||
calMonth = now.getMonth()
|
||
buildDays()
|
||
}
|
||
|
||
function changeMonth(delta) {
|
||
var m = calMonth + delta
|
||
var y = calYear
|
||
while (m < 0) { m += 12; y-- }
|
||
while (m > 11) { m -= 12; y++ }
|
||
calMonth = m
|
||
calYear = y
|
||
buildDays()
|
||
}
|
||
|
||
function buildDays() {
|
||
var first = new Date(calYear, calMonth, 1)
|
||
var startDow = (first.getDay() + 6) % 7
|
||
var daysInMonth = new Date(calYear, calMonth + 1, 0).getDate()
|
||
var prevMonthDays = new Date(calYear, calMonth, 0).getDate()
|
||
|
||
var today = new Date()
|
||
var isCurrentMonth = (today.getFullYear() === calYear && today.getMonth() === calMonth)
|
||
var todayDate = today.getDate()
|
||
|
||
var cells = []
|
||
for (var i = 0; i < startDow; i++)
|
||
cells.push({ day: prevMonthDays - startDow + 1 + i, current: false, today: false })
|
||
for (var d = 1; d <= daysInMonth; d++)
|
||
cells.push({ day: d, current: true, today: isCurrentMonth && d === todayDate })
|
||
var remainder = 42 - cells.length
|
||
for (var r = 1; r <= remainder; r++)
|
||
cells.push({ day: r, current: false, today: false })
|
||
|
||
calDays = cells
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: calPopup.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
id: calRect
|
||
x: parent.width - 16 - clockBox.implicitWidth - trayBox.implicitWidth
|
||
y: parent.height - 38 - height
|
||
width: 7 * 30 + 16
|
||
height: calCol.implicitHeight + 16
|
||
color: root.bgColor
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onWheel: function(wheel) {
|
||
if (wheel.angleDelta.y > 0) calPopup.changeMonth(-1)
|
||
else if (wheel.angleDelta.y < 0) calPopup.changeMonth(1)
|
||
}
|
||
}
|
||
|
||
Column {
|
||
id: calCol
|
||
anchors.fill: parent
|
||
anchors.margins: 8
|
||
spacing: 4
|
||
z: 1
|
||
|
||
Row {
|
||
width: parent.width
|
||
height: 24
|
||
|
||
Rectangle {
|
||
width: 30
|
||
height: 24
|
||
color: calPrevMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "◄"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
id: calPrevMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: calPopup.changeMonth(-1)
|
||
}
|
||
}
|
||
|
||
Item {
|
||
width: parent.width - 60
|
||
height: 24
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: Qt.formatDate(new Date(calPopup.calYear, calPopup.calMonth, 1), "MMMM yyyy")
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: 30
|
||
height: 24
|
||
color: calNextMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "►"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
id: calNextMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: calPopup.changeMonth(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
Grid {
|
||
columns: 7
|
||
spacing: 0
|
||
width: parent.width
|
||
|
||
Repeater {
|
||
model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
||
|
||
delegate: Item {
|
||
required property var modelData
|
||
width: 30
|
||
height: 20
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: modelData
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Grid {
|
||
columns: 7
|
||
spacing: 0
|
||
width: parent.width
|
||
|
||
Repeater {
|
||
model: calPopup.calDays
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
width: 30
|
||
height: 24
|
||
radius: 2
|
||
color: modelData.today ? root.accentColor : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: modelData.day
|
||
color: modelData.today
|
||
? "#111111"
|
||
: (modelData.current ? root.fgColor : root.dimColor)
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
PanelWindow {
|
||
id: pomFlash
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property string flashText: ""
|
||
property color flashColor: root.critColor
|
||
property bool flashOn: true
|
||
|
||
function show(text, col) {
|
||
flashText = text
|
||
flashColor = col
|
||
flashOn = true
|
||
visible = true
|
||
flashAnim.running = true
|
||
flashAutoClose.running = true
|
||
}
|
||
|
||
function dismiss() {
|
||
visible = false
|
||
flashAnim.running = false
|
||
flashAutoClose.running = false
|
||
}
|
||
|
||
Timer {
|
||
id: flashAnim
|
||
interval: 400
|
||
repeat: true
|
||
onTriggered: pomFlash.flashOn = !pomFlash.flashOn
|
||
}
|
||
|
||
Timer {
|
||
id: flashAutoClose
|
||
interval: 10000
|
||
repeat: false
|
||
onTriggered: pomFlash.dismiss()
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: pomFlash.flashOn ? "#000000E0" : "#000000A0"
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: {
|
||
pomFlash.dismiss()
|
||
if (pomBox.pomState === "done") pomBox.startWork()
|
||
}
|
||
}
|
||
|
||
Column {
|
||
anchors.centerIn: parent
|
||
spacing: 16
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: pomFlash.flashText
|
||
color: pomFlash.flashOn ? pomFlash.flashColor : "#333333"
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 72
|
||
}
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: pomBox.pomState === "done" ? "click anywhere to start" : "click to dismiss"
|
||
color: "#666666"
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 14
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
PanelWindow {
|
||
id: pomPopup
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: pomPopup.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
x: parent.width - 10 - rightGroup.implicitWidth + vpnBox.implicitWidth + spkBox.implicitWidth + micBox.implicitWidth + netBox.implicitWidth + 24
|
||
{{- if .hasBattery }}
|
||
+ batteryBox.implicitWidth + brtBox.implicitWidth + 12
|
||
{{- end }}
|
||
y: parent.height - 38 - height
|
||
width: 220
|
||
height: pomPopupCol.implicitHeight + 16
|
||
color: root.bgColor
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
}
|
||
|
||
Column {
|
||
id: pomPopupCol
|
||
anchors.fill: parent
|
||
anchors.margins: 8
|
||
spacing: 2
|
||
z: 1
|
||
|
||
Text {
|
||
text: "━━ Settings ━━"
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
topPadding: 2
|
||
bottomPadding: 2
|
||
}
|
||
|
||
Rectangle {
|
||
width: pomPopupCol.width
|
||
height: 24
|
||
color: "transparent"
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: "Work " + pomBox.workMins + " min"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.NoButton
|
||
onWheel: function(wheel) {
|
||
if (wheel.angleDelta.y > 0) pomBox.workMins = Math.min(90, pomBox.workMins + 5)
|
||
else pomBox.workMins = Math.max(5, pomBox.workMins - 5)
|
||
pomBox.saveSettings()
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: pomPopupCol.width
|
||
height: 24
|
||
color: "transparent"
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: "Break " + pomBox.breakMins + " min"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.NoButton
|
||
onWheel: function(wheel) {
|
||
if (wheel.angleDelta.y > 0) pomBox.breakMins = Math.min(30, pomBox.breakMins + 1)
|
||
else pomBox.breakMins = Math.max(1, pomBox.breakMins - 1)
|
||
pomBox.saveSettings()
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: pomPopupCol.width
|
||
height: 24
|
||
color: "transparent"
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: "Long Break " + pomBox.longBreakMins + " min"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.NoButton
|
||
onWheel: function(wheel) {
|
||
if (wheel.angleDelta.y > 0) pomBox.longBreakMins = Math.min(45, pomBox.longBreakMins + 5)
|
||
else pomBox.longBreakMins = Math.max(5, pomBox.longBreakMins - 5)
|
||
pomBox.saveSettings()
|
||
}
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: "━━ Session ━━"
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
topPadding: 6
|
||
bottomPadding: 2
|
||
}
|
||
|
||
Text {
|
||
leftPadding: 8
|
||
text: "Completed " + pomBox.totalPoms + " poms"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
height: 24
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
Text {
|
||
leftPadding: 8
|
||
text: "Focus time " + pomBox.totalFocusMins + " min"
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
height: 24
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
Text {
|
||
leftPadding: 8
|
||
text: "Current " + (pomBox.pomState === "idle" ? "---" : pomBox.pomState)
|
||
color: pomBox.pomState === "work" ? root.accentColor : pomBox.pomState === "idle" ? root.dimColor : root.okColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
height: 24
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
Item {
|
||
width: pomPopupCol.width
|
||
height: 9
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
width: parent.width
|
||
height: 1
|
||
color: root.borderColor
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: pomPopupCol.width
|
||
height: 24
|
||
color: pomResetMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
text: "⊘ Reset"
|
||
color: root.critColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
MouseArea {
|
||
id: pomResetMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: {
|
||
pomBox.reset()
|
||
pomBox.totalPoms = 0
|
||
pomBox.totalFocusMins = 0
|
||
pomPopup.visible = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
PanelWindow {
|
||
id: memoInput
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
focusable: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property var notes: []
|
||
property bool inDepthMode: false
|
||
|
||
function refresh() {
|
||
memoInlineLoadProc.running = true
|
||
}
|
||
|
||
function parseNotes(raw) {
|
||
var lines = raw.split("\n").filter(function(l) { return l.trim() })
|
||
var items = []
|
||
for (var i = lines.length - 1; i >= 0 && items.length < 8; i--) {
|
||
try { items.push(JSON.parse(lines[i])) } catch(e) {}
|
||
}
|
||
if (items.length === 0 && raw.trim().length > 0) {
|
||
var stitched = raw.replace(/}\s*{/g, "}\n{")
|
||
var fallbackLines = stitched.split("\n").filter(function(l) { return l.trim() })
|
||
for (var j = fallbackLines.length - 1; j >= 0 && items.length < 8; j--) {
|
||
try { items.push(JSON.parse(fallbackLines[j])) } catch(e2) {}
|
||
}
|
||
}
|
||
notes = items
|
||
}
|
||
|
||
function fmtRelative(iso) {
|
||
var d = new Date(iso)
|
||
var now = new Date()
|
||
var secs = Math.floor((now - d) / 1000)
|
||
if (isNaN(secs) || secs < 0) return ""
|
||
if (secs < 60) return "now"
|
||
var mins = Math.floor(secs / 60)
|
||
if (mins < 60) return mins + "m ago"
|
||
var hrs = Math.floor(mins / 60)
|
||
if (hrs < 48) return hrs + "h ago"
|
||
var days = Math.floor(hrs / 24)
|
||
return days + "d ago"
|
||
}
|
||
|
||
onVisibleChanged: {
|
||
if (visible) {
|
||
memoInputField.text = ""
|
||
memoInputArea.text = ""
|
||
memoInput.inDepthMode = false
|
||
memoInput.refresh()
|
||
memoInputField.forceActiveFocus()
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: memoInlineLoadProc
|
||
command: ["/usr/bin/env", "bash", "-c", "cat ~/.local/share/quickmemo/notes.jsonl 2>/dev/null || true"]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: memoInput.parseNotes((this.text || "").trim())
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: memoInput.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
width: Math.min(parent.width - 128, 784)
|
||
height: Math.min(parent.height - 60, 432)
|
||
color: "#111214"
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
clip: true
|
||
|
||
MouseArea { anchors.fill: parent }
|
||
|
||
Item {
|
||
anchors.fill: parent
|
||
anchors.margins: 22
|
||
z: 1
|
||
|
||
Rectangle {
|
||
id: memoEntryBox
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
y: {
|
||
if (!memoInput.inDepthMode && memoNotesPanel.visible) return 10
|
||
var base = Math.floor((parent.height - memoEntryBox.height - (memoNotesPanel.visible ? (memoNotesPanel.height + 16) : 0)) / 2)
|
||
return Math.max(0, base)
|
||
}
|
||
width: Math.floor(parent.width * 0.9)
|
||
height: memoInput.inDepthMode ? Math.min(parent.height - 8, 340) : 56
|
||
color: "#111214"
|
||
border.width: 1
|
||
border.color: "#303236"
|
||
|
||
Row {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 14
|
||
anchors.rightMargin: 14
|
||
spacing: 10
|
||
|
||
Text {
|
||
id: memoPrompt
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "›"
|
||
color: root.accentColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: memoInput.inDepthMode ? 18 : 22
|
||
}
|
||
|
||
Item {
|
||
width: Math.max(0, parent.width - memoPrompt.width - 10)
|
||
height: parent.height
|
||
|
||
TextInput {
|
||
id: memoInputField
|
||
anchors.fill: parent
|
||
anchors.topMargin: 12
|
||
anchors.bottomMargin: 12
|
||
anchors.leftMargin: 6
|
||
anchors.rightMargin: 6
|
||
color: root.fgColor
|
||
selectionColor: root.accentColor
|
||
selectedTextColor: "#111111"
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 22
|
||
clip: true
|
||
visible: !memoInput.inDepthMode
|
||
|
||
Keys.onEscapePressed: memoInput.visible = false
|
||
Keys.onTabPressed: function(event) {
|
||
memoInput.inDepthMode = true
|
||
memoInputArea.forceActiveFocus()
|
||
event.accepted = true
|
||
}
|
||
Keys.onReturnPressed: {
|
||
var text = memoInputField.text.trim()
|
||
if (text) {
|
||
panel.memoSave(text, "input")
|
||
memoInputField.text = ""
|
||
memoInput.refresh()
|
||
}
|
||
}
|
||
}
|
||
|
||
Flickable {
|
||
id: memoInputAreaFlick
|
||
anchors.fill: parent
|
||
anchors.topMargin: 8
|
||
anchors.bottomMargin: 8
|
||
anchors.leftMargin: 6
|
||
anchors.rightMargin: 6
|
||
contentWidth: width
|
||
contentHeight: memoInputArea.implicitHeight
|
||
clip: true
|
||
flickableDirection: Flickable.VerticalFlick
|
||
visible: memoInput.inDepthMode
|
||
|
||
TextEdit {
|
||
id: memoInputArea
|
||
width: memoInputAreaFlick.width
|
||
color: root.fgColor
|
||
selectionColor: root.accentColor
|
||
selectedTextColor: "#111111"
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 16
|
||
wrapMode: TextEdit.Wrap
|
||
|
||
Keys.onEscapePressed: memoInput.visible = false
|
||
Keys.onTabPressed: function(event) {
|
||
memoInput.inDepthMode = false
|
||
memoInputField.forceActiveFocus()
|
||
event.accepted = true
|
||
}
|
||
Keys.onPressed: function(event) {
|
||
if (event.key === Qt.Key_Return && (event.modifiers & Qt.ControlModifier)) {
|
||
var text = memoInputArea.text.trim()
|
||
if (text) {
|
||
panel.memoSave(text, "input")
|
||
memoInputArea.text = ""
|
||
memoInput.refresh()
|
||
}
|
||
event.accepted = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Text {
|
||
anchors.fill: parent
|
||
anchors.topMargin: 12
|
||
anchors.bottomMargin: 12
|
||
anchors.leftMargin: 6
|
||
anchors.rightMargin: 6
|
||
text: memoInput.inDepthMode ? "In-depth note (Ctrl+Enter to save, Tab for quick mode)" : "Add memo"
|
||
color: root.dimColor
|
||
visible: memoInput.inDepthMode ? memoInputArea.text.length === 0 : memoInputField.text.length === 0
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: memoInput.inDepthMode ? 14 : 22
|
||
horizontalAlignment: Text.AlignLeft
|
||
verticalAlignment: memoInput.inDepthMode ? Text.AlignTop : Text.AlignVCenter
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
id: memoNotesPanel
|
||
anchors.left: memoEntryBox.left
|
||
anchors.right: memoEntryBox.right
|
||
anchors.top: memoEntryBox.bottom
|
||
anchors.topMargin: 16
|
||
anchors.bottom: parent.bottom
|
||
color: "#15171a"
|
||
border.width: 1
|
||
border.color: "#2b2d31"
|
||
radius: 0
|
||
visible: !memoInput.inDepthMode && memoInput.notes.length > 0
|
||
|
||
Flickable {
|
||
id: memoNotesFlick
|
||
anchors.fill: parent
|
||
anchors.margins: 9
|
||
contentWidth: width
|
||
contentHeight: memoNotesCol.implicitHeight
|
||
clip: true
|
||
boundsBehavior: Flickable.StopAtBounds
|
||
flickableDirection: Flickable.VerticalFlick
|
||
|
||
Column {
|
||
id: memoNotesCol
|
||
width: memoNotesFlick.width
|
||
spacing: 6
|
||
|
||
Repeater {
|
||
model: memoInput.notes
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
width: memoNotesCol.width
|
||
height: 32
|
||
color: memoItemHover.containsMouse ? "#1c1f23" : "transparent"
|
||
radius: 0
|
||
|
||
Text {
|
||
anchors.left: parent.left
|
||
anchors.right: noteAge.left
|
||
anchors.rightMargin: 10
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 4
|
||
text: "→ " + ((modelData.text || "").replace(/\n+/g, " ").trim())
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 16
|
||
elide: Text.ElideRight
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
Text {
|
||
id: noteAge
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
rightPadding: 4
|
||
text: memoInput.fmtRelative(modelData.ts)
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 14
|
||
horizontalAlignment: Text.AlignRight
|
||
}
|
||
|
||
MouseArea {
|
||
id: memoItemHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
PanelWindow {
|
||
id: memoReview
|
||
screen: panel.screen
|
||
visible: false
|
||
color: "transparent"
|
||
exclusionMode: ExclusionMode.Ignore
|
||
aboveWindows: true
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
property var notes: []
|
||
|
||
function refresh() {
|
||
memoLoadProc.running = true
|
||
}
|
||
|
||
function parseNotes(raw) {
|
||
var lines = raw.split("\n").filter(function(l) { return l.trim() })
|
||
var items = []
|
||
for (var i = lines.length - 1; i >= 0 && items.length < 50; i--) {
|
||
try { items.push(JSON.parse(lines[i])) } catch(e) {}
|
||
}
|
||
notes = items
|
||
}
|
||
|
||
function fmtTs(iso) {
|
||
var d = new Date(iso)
|
||
return Qt.formatDateTime(d, "yyyy-MM-dd HH:mm")
|
||
}
|
||
|
||
Process {
|
||
id: memoLoadProc
|
||
command: ["/usr/bin/env", "bash", "-c", "cat ~/.local/share/quickmemo/notes.jsonl 2>/dev/null || true"]
|
||
running: false
|
||
stdout: StdioCollector {
|
||
onStreamFinished: memoReview.parseNotes((this.text || "").trim())
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: memoClipCopy
|
||
running: false
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: memoReview.visible = false
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
width: 600
|
||
height: Math.min(memoReviewFlick.contentHeight + 50, parent.height - 80)
|
||
color: root.bgColor
|
||
border.width: 1
|
||
border.color: root.borderColor
|
||
clip: true
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onWheel: function(wheel) {}
|
||
}
|
||
|
||
Column {
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: parent.top
|
||
anchors.margins: 8
|
||
z: 1
|
||
|
||
Text {
|
||
text: "━━ Recent Memos ━━"
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 11
|
||
topPadding: 2
|
||
bottomPadding: 4
|
||
}
|
||
|
||
Flickable {
|
||
id: memoReviewFlick
|
||
width: parent.width
|
||
height: Math.min(contentHeight, panel.height - 130)
|
||
contentHeight: memoReviewCol.implicitHeight
|
||
clip: true
|
||
flickableDirection: Flickable.VerticalFlick
|
||
boundsBehavior: Flickable.StopAtBounds
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.NoButton
|
||
onWheel: function(wheel) {
|
||
memoReviewFlick.contentY = Math.max(0,
|
||
Math.min(memoReviewFlick.contentHeight - memoReviewFlick.height,
|
||
memoReviewFlick.contentY - wheel.angleDelta.y))
|
||
}
|
||
}
|
||
|
||
Column {
|
||
id: memoReviewCol
|
||
width: parent.width
|
||
spacing: 0
|
||
|
||
Repeater {
|
||
model: memoReview.notes
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
width: memoReviewCol.width
|
||
height: memoNoteCol.implicitHeight + 12
|
||
color: memoItemMouse.containsMouse ? "#2a2a2a" : "transparent"
|
||
radius: 2
|
||
|
||
Column {
|
||
id: memoNoteCol
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
leftPadding: 8
|
||
rightPadding: 8
|
||
spacing: 2
|
||
|
||
Text {
|
||
text: memoReview.fmtTs(modelData.ts) + (modelData.src === "clipboard" ? " [clip]" : "")
|
||
color: root.dimColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 10
|
||
}
|
||
|
||
Text {
|
||
width: parent.width - 16
|
||
text: (modelData.text || "").substring(0, 200)
|
||
color: root.fgColor
|
||
font.family: "Terminus, IBM Plex Mono, JetBrainsMono Nerd Font, monospace"
|
||
font.pixelSize: 12
|
||
wrapMode: Text.Wrap
|
||
maximumLineCount: 3
|
||
elide: Text.ElideRight
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: memoItemMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
onClicked: {
|
||
memoClipCopy.command = ["/usr/bin/env", "bash", "-c",
|
||
"echo " + Qt.btoa(modelData.text) + " | base64 -d | wl-copy"]
|
||
memoClipCopy.running = true
|
||
root.runShell("notify-send -r 91192 -t 1500 'Quick Memo' 'Copied to clipboard'")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|