pragma Singleton // What this machine offers to other machines: remote login, remote desktop, // and the two services that would provide file and media sharing if they were // installed. // // A service that is absent is reported as absent rather than shown as a switch // that would do nothing -- which is the failure mode of the panel this replaces. // // Turning remote login on is a system-wide change and goes through pkexec, so // it prompts. Remote desktop is a user service and does not. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-sharing" property string hostname: "" property string prettyHostname: "" property var remoteLogin: ({}) property var remoteDesktop: ({}) property var fileSharing: ({}) property var mediaSharing: ({}) property bool scanned: false property string lastError: "" // Guards read the Process objects directly; a derived binding is stale // inside the handler that changes it. See DefaultApps.qml. readonly property bool busy: query.running || mutation.running // The name someone types to reach this machine. readonly property string networkName: root.hostname !== "" ? root.hostname : "this machine" readonly property bool remoteLoginOn: root.remoteLogin?.active === true // People signed in from another machine right now. Empty is the normal // case; the list exists so that "someone is on this machine" is something // the page can state rather than something you have to go and check. readonly property var remoteSessions: Array.isArray(root.remoteLogin?.sessions) ? root.remoteLogin.sessions : [] readonly property bool remoteDesktopOn: root.remoteDesktop?.active === true // sshd's configuration only sometimes states this. Saying "keys only" when // the file is silent would be a security claim that cannot be backed up. function passwordLoginSummary(): string { const stated = String(root.remoteLogin?.passwordAuthentication ?? ""); if (stated === "") return "Not configured, so the system default applies"; return stated.toLowerCase() === "no" ? "Refused; keys only" : "Allowed"; } function refresh(): void { if (query.running) return; query.command = [root.helperPath, "snapshot"]; query.running = true; } function absorb(text: string): void { try { const parsed = JSON.parse(text); root.hostname = String(parsed.hostname ?? ""); root.prettyHostname = String(parsed.prettyHostname ?? ""); root.remoteLogin = parsed.remoteLogin ?? ({}); root.remoteDesktop = parsed.remoteDesktop ?? ({}); root.fileSharing = parsed.fileSharing ?? ({}); root.mediaSharing = parsed.mediaSharing ?? ({}); root.lastError = String(parsed.error ?? ""); } catch (error) { root.lastError = "Could not read the sharing helper's answer."; console.warn("Sharing: could not parse helper output:", error); } root.scanned = true; } function run(arguments: var): void { if (mutation.running) return; root.lastError = ""; mutation.command = [root.helperPath].concat(arguments); mutation.running = true; } function setRemoteLogin(enabled: bool): void { root.run(["set-remote-login", enabled ? "true" : "false"]); } function setRemoteDesktop(enabled: bool): void { root.run(["set-remote-desktop", enabled ? "true" : "false"]); } function setHostname(name: string): void { root.run(["set-hostname", name]); } function setRdpPort(port: string): void { root.run(["set-rdp-port", port]); } // Rygel serves media to devices over DLNA. Turning it on publishes media // directories to every device on the network, so the page says that next to // the switch rather than after the fact. function setMediaSharing(enabled: bool): void { root.run(["set-media-sharing", enabled ? "true" : "false"]); } function setRdpViewOnly(viewOnly: bool): void { root.run(["set-rdp-view-only", viewOnly ? "true" : "false"]); } function clearRdpCredentials(): void { root.run(["clear-rdp-credentials"]); } // Setting credentials opens a terminal running gnome-remote-desktop's own // tool, which prompts for the password itself. // // That is not a cop-out, it is the only safe path: grdctl takes the // password on a terminal and CRASHES without one, and the alternative -- // passing it as an argument -- would publish it through /proc to every // process on this machine. Typed into grdctl directly, it never passes // through Panama at all. function setRdpCredentials(userName: string): void { Quickshell.execDetached(["kitty", "--hold", "-e", "grdctl", "rdp", "set-credentials", userName]); } Process { id: query stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: mutation // Answers with the fresh state, so the page updates from the change // itself rather than asking again afterwards. stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } }