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