Initial commit: hypr-rdp Control bar widget

Toggles the hypr-rdp systemd --user service, isolates its headless
monitor's workspace via Omarchy's toggle-flag convention, shows what's
running on that workspace, and manages hypr-rdp's connection credentials.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLfSWFEMyY85ZDWEYicdaF
This commit is contained in:
2026-09-08 15:57:18 +01:00
commit 82d76b726e
7 changed files with 909 additions and 0 deletions

327
Service.qml Normal file
View File

@@ -0,0 +1,327 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "Model.js" as Model
Item {
id: root
property var settings: ({})
readonly property string homeDir: Quickshell.env("HOME")
readonly property string serviceUnitPath: homeDir + "/.config/systemd/user/hypr-rdp.service"
readonly property string rdpConfigDir: homeDir + "/.config/hypr-rdp"
readonly property string rdpConfigPath: rdpConfigDir + "/config.toml"
readonly property string toggleDir: homeDir + "/.local/state/omarchy/toggles/hypr"
readonly property string isolationTogglePath: toggleDir + "/hypr-rdp-isolation.lua"
readonly property string outputName: "hypr-rdp"
readonly property int pollIntervalSec: intSetting("pollIntervalSec", 5, 2, 60)
readonly property int isolatedWorkspace: intSetting("isolatedWorkspace", 11, 1, 99)
property bool installed: false
property bool serviceUnitExists: false
property bool running: false
// Optimistic desired state so the switch reacts instantly; -1 means
// "just follow reality", 0/1 mean "still catching up to a click".
property int _desired: -1
readonly property bool active: _desired === -1 ? running : (_desired === 1)
property bool isolationFileExists: false
property int _isolationDesired: -1
readonly property bool isolated: _isolationDesired === -1 ? isolationFileExists : (_isolationDesired === 1)
readonly property bool busy: startStopProc.running || installUnitProc.running
|| isolationRemoveProc.running || reloadProc.running
property string statusText: "Checking…"
property string lastError: ""
property string actionStatus: ""
property string username: ""
property string password: ""
property string bindAddress: ""
property string rawConfigText: ""
property bool configLoaded: false
property var windowsOnIsolated: []
property int previousWorkspace: -1
property bool peeking: false
function setting(name, fallback) {
var v = settings ? settings[name] : undefined
return (v === undefined || v === null) ? fallback : v
}
function intSetting(name, fallback, min, max) {
var n = parseInt(String(setting(name, fallback)), 10)
if (!isFinite(n)) n = fallback
if (n < min) n = min
if (n > max) n = max
return n
}
function clearActionStatus() { root.actionStatus = "" }
Timer {
id: actionStatusTimer
interval: 3000
repeat: false
onTriggered: root.clearActionStatus()
}
function flashStatus(message) {
root.actionStatus = message
actionStatusTimer.restart()
}
function refresh() {
if (!whichProc.running) whichProc.running = true
if (!statusProc.running) statusProc.running = true
if (root.running && !clientsProc.running) clientsProc.running = true
}
Timer {
interval: root.pollIntervalSec * 1000
running: true
repeat: true
onTriggered: root.refresh()
}
Component.onCompleted: {
unitDirProc.running = true
isolationDirProc.running = true
configDirProc.running = true
root.refresh()
}
// ---------------------------------------------------------- install
Process {
id: whichProc
command: ["which", "hypr-rdp"]
onExited: function(code) { root.installed = (code === 0) }
}
function installHyprRdp() {
Quickshell.execDetached(["omarchy", "launch", "terminal", "bash", "-lc",
"omarchy pkg aur add hypr-rdp-git; echo; read -p 'Press Enter to close...' _"])
}
// ---------------------------------------------------------- service unit
property FileView serviceUnitFile: FileView {
path: root.serviceUnitPath
watchChanges: true
printErrors: false
onLoaded: root.serviceUnitExists = true
onLoadFailed: root.serviceUnitExists = false
onFileChanged: reload()
}
readonly property string serviceUnitContent:
"[Unit]\n" +
"Description=Native RDP server for Hyprland\n" +
"Documentation=https://github.com/MuNeNICK/hypr-rdp\n" +
"PartOf=graphical-session.target\n" +
"Requires=graphical-session.target\n" +
"After=graphical-session.target\n" +
"ConditionEnvironment=WAYLAND_DISPLAY\n" +
"\n" +
"[Service]\n" +
"Type=simple\n" +
"ExecStart=/usr/bin/hypr-rdp\n" +
"Slice=session.slice\n" +
"Restart=on-failure\n" +
"\n" +
"[Install]\n" +
"WantedBy=graphical-session.target\n"
Process { id: unitDirProc; command: ["mkdir", "-p", root.homeDir + "/.config/systemd/user"] }
Process {
id: installUnitProc
command: []
onExited: function(code) {
if (code !== 0) root.flashStatus("Failed to enable hypr-rdp.service")
root.refresh()
}
}
function ensureServiceUnit(thenStart) {
if (root.serviceUnitExists) { thenStart(); return }
root.serviceUnitFile.setText(root.serviceUnitContent)
root.serviceUnitExists = true
installUnitProc.command = ["bash", "-lc",
"systemctl --user daemon-reload && systemctl --user enable hypr-rdp.service"]
installUnitProc.running = true
thenStart()
}
// ---------------------------------------------------------- status/start/stop
Process {
id: statusProc
command: ["systemctl", "--user", "is-active", "hypr-rdp.service"]
onExited: function(code) {
root.running = (code === 0)
if (root._desired !== -1 && root.running === (root._desired === 1)) root._desired = -1
root.statusText = !root.installed ? "Not installed" : (root.running ? "Running" : "Stopped")
if (root.running && !clientsProc.running) clientsProc.running = true
if (!root.running) root.windowsOnIsolated = []
}
}
Process {
id: startStopProc
command: []
onExited: function(code) {
if (code !== 0) {
var wasStarting = root._desired === 1
root._desired = -1
root.flashStatus(wasStarting ? "hypr-rdp failed to start" : "hypr-rdp failed to stop")
}
root.refresh()
}
}
function toggleService() {
if (!root.installed || startStopProc.running) return
if (root.active) {
root._desired = 0
startStopProc.command = ["systemctl", "--user", "stop", "hypr-rdp.service"]
startStopProc.running = true
} else {
root._desired = 1
ensureServiceUnit(function() {
startStopProc.command = ["systemctl", "--user", "start", "hypr-rdp.service"]
startStopProc.running = true
})
}
}
// ---------------------------------------------------------- isolation toggle
property FileView isolationFile: FileView {
path: root.isolationTogglePath
watchChanges: true
printErrors: false
onLoaded: {
root.isolationFileExists = true
if (root._isolationDesired === 1) root._isolationDesired = -1
}
onLoadFailed: {
root.isolationFileExists = false
if (root._isolationDesired === 0) root._isolationDesired = -1
}
onFileChanged: reload()
}
Process { id: isolationDirProc; command: ["mkdir", "-p", root.toggleDir] }
Process {
id: isolationRemoveProc
command: ["rm", "-f", root.isolationTogglePath]
onExited: function(code) { root.applyReload() }
}
Process {
id: reloadProc
command: ["bash", "-lc", "hyprctl reload >/dev/null 2>&1; hyprctl configerrors"]
stdout: StdioCollector { id: reloadOut; waitForEnd: true }
onExited: function(code) {
var text = String(reloadOut.text || "").trim()
root.lastError = (text !== "" && text.toLowerCase() !== "ok") ? ("Hyprland config error: " + text) : ""
}
}
function applyReload() { reloadProc.running = true }
function toggleIsolation() {
if (isolationRemoveProc.running || reloadProc.running) return
if (root.isolated) {
root._isolationDesired = 0
root.isolationFileExists = false
isolationRemoveProc.running = true
} else {
root._isolationDesired = 1
root.isolationFile.setText(Model.isolationLuaContent(root.isolatedWorkspace, root.outputName))
// FileView does not re-emit onLoaded for its own write.
root.isolationFileExists = true
root.applyReload()
}
}
// ---------------------------------------------------------- credentials
property FileView configFile: FileView {
path: root.rdpConfigPath
watchChanges: true
printErrors: false
onLoaded: {
root.rawConfigText = text()
var parsed = Model.parseConfig(root.rawConfigText)
root.username = parsed.username
root.password = parsed.password
root.bindAddress = parsed.bind
root.configLoaded = true
}
onLoadFailed: root.configLoaded = false
onFileChanged: reload()
}
Process { id: configDirProc; command: ["mkdir", "-p", root.rdpConfigDir] }
function saveCredentials(newUsername, newPassword, newBind) {
var patch = { username: newUsername, password: newPassword, bind: newBind }
var text = Model.serializeConfig(root.rawConfigText, patch)
root.configFile.setText(text)
root.rawConfigText = text
root.username = newUsername
root.password = newPassword
root.bindAddress = newBind
root.flashStatus(root.running ? "Saved — restart hypr-rdp to apply" : "Saved")
}
// ---------------------------------------------------------- isolated workspace peek
Process {
id: activeWorkspaceProc
command: []
stdout: StdioCollector { id: activeWsOut; waitForEnd: true }
onExited: function(code) {
var id = Model.parseActiveWorkspaceId(activeWsOut.text)
if (id >= 0) root.previousWorkspace = id
dispatchProc.command = ["hyprctl", "dispatch", "workspace", String(root.isolatedWorkspace)]
dispatchProc.running = true
root.peeking = true
}
}
Process { id: dispatchProc; command: [] }
function peekIsolatedWorkspace() {
if (root.peeking || activeWorkspaceProc.running) return
activeWorkspaceProc.command = ["hyprctl", "-j", "activeworkspace"]
activeWorkspaceProc.running = true
}
function returnFromPeek() {
if (!root.peeking || root.previousWorkspace < 0) return
dispatchProc.command = ["hyprctl", "dispatch", "workspace", String(root.previousWorkspace)]
dispatchProc.running = true
root.peeking = false
}
// ---------------------------------------------------------- windows on the isolated workspace
Process {
id: clientsProc
command: ["hyprctl", "-j", "clients"]
stdout: StdioCollector {
id: clientsOut
waitForEnd: true
onStreamFinished: root.windowsOnIsolated = Model.parseClients(text, root.isolatedWorkspace)
}
}
}