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:
327
Service.qml
Normal file
327
Service.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user