diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml index a37b34a..d2074cb 100644 --- a/config/dot/quickshell/config/PreferenceSchema.qml +++ b/config/dot/quickshell/config/PreferenceSchema.qml @@ -702,6 +702,14 @@ Singleton { detail: "Moves the magnified view in increments rather than gliding with the pointer", hypr: { path: ["cursor", "zoom_rigid"], option: "cursor:zoom_rigid", readAs: "bool" } }, + { + key: "highContrast", type: "bool", def: false, group: "accessibility", + label: "High contrast", + detail: "Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here.", + // No hypr mapping: this is a GNOME interface setting the portal + // republishes as org.freedesktop.appearance contrast, which is what + // libadwaita actually reads. DesktopStyle applies it. + }, { key: "dimInactive", type: "bool", def: false, group: "accessibility", label: "Dim inactive windows", diff --git a/config/dot/quickshell/modules/settings/AccessibilityPage.qml b/config/dot/quickshell/modules/settings/AccessibilityPage.qml index 2add626..6fe201a 100644 --- a/config/dot/quickshell/modules/settings/AccessibilityPage.qml +++ b/config/dot/quickshell/modules/settings/AccessibilityPage.qml @@ -27,7 +27,12 @@ SettingsPage { title: "Text" subtitle: "Scales text in applications. The shell's own panels are drawn at their design size, so they are unaffected." - SliderRow { setting: "textScale"; divider: false } + SliderRow { setting: "textScale" } + // Reaches GTK4 applications through the desktop portal, which + // republishes it as org.freedesktop.appearance contrast. No + // high-contrast theme is involved, and none is installed here -- older + // GTK3 applications will not change. + ToggleRow { setting: "highContrast"; divider: false } } SettingsCard { diff --git a/config/dot/quickshell/modules/settings/SharingPage.qml b/config/dot/quickshell/modules/settings/SharingPage.qml index b0e504c..6e2d17c 100644 --- a/config/dot/quickshell/modules/settings/SharingPage.qml +++ b/config/dot/quickshell/modules/settings/SharingPage.qml @@ -114,14 +114,49 @@ SettingsPage { value: String(Sharing.remoteDesktop?.port ?? "") } - TextRow { + SwitchRow { + visible: Sharing.remoteDesktop?.available === true + label: "View only" + detail: "Let someone watch this desktop without controlling the pointer or keyboard" + checked: Sharing.remoteDesktop?.viewOnly === true + enabled: !Sharing.busy + onToggled: value => Sharing.setRdpViewOnly(value) + } + + TextFieldRow { + visible: Sharing.remoteDesktop?.available === true + label: "Port" + detail: "The port other machines connect to" + text: String(Sharing.remoteDesktop?.port ?? "") + placeholder: "3389" + enabled: !Sharing.busy + onAccepted: value => Sharing.setRdpPort(value) + } + + // The password is typed into gnome-remote-desktop's own tool in a + // terminal, never into this page. grdctl prompts for it on a terminal + // and crashes without one, and passing it as an argument would publish + // it through /proc to every process on this machine. + ActionRow { visible: Sharing.remoteDesktop?.available === true label: "Credentials" detail: Sharing.remoteDesktop?.hasCredentials === true - ? "Stored in the login keyring, where Privacy & Security can manage them" - : "None stored yet" - value: Sharing.remoteDesktop?.hasCredentials === true ? "Stored" : "Not set" + ? "Stored in the login keyring · setting new ones opens a terminal to type into" + : "None stored yet · remote desktop cannot be turned on without them" + action: "Set…" + enabled: !Sharing.busy + onTriggered: Sharing.setRdpCredentials(Quickshell.env("USER") || "") + } + + ActionRow { + visible: Sharing.remoteDesktop?.available === true + && Sharing.remoteDesktop?.hasCredentials === true + label: "Forget the stored credentials" + detail: "Remote desktop cannot be turned on again until new ones are set" + action: "Clear" + enabled: !Sharing.busy divider: false + onTriggered: Sharing.clearRdpCredentials() } } diff --git a/config/dot/quickshell/scripts/panama-sharing b/config/dot/quickshell/scripts/panama-sharing index 6a9cc5a..678961b 100755 --- a/config/dot/quickshell/scripts/panama-sharing +++ b/config/dot/quickshell/scripts/panama-sharing @@ -80,6 +80,7 @@ def remote_desktop() -> dict: state["rdpEnabled"] = False state["port"] = "" state["hasCredentials"] = False + state["viewOnly"] = False if not state["available"]: return state @@ -93,6 +94,7 @@ def remote_desktop() -> dict: # grdctl prints "(hidden)" when a credential is stored and nothing when # it is not, so this reads presence without ever reading the value. state["hasCredentials"] = "(hidden)" in block + state["viewOnly"] = re.search(r"View-only:\s*yes", block) is not None return state @@ -144,6 +146,27 @@ def set_remote_desktop(enabled: bool) -> None: raise BoundaryError(_refusal(result, "The remote desktop service could not be changed.")) +def set_rdp_port(port: str) -> None: + if not port.isdigit() or not (1 <= int(port) <= 65535): + raise BoundaryError("That is not a port number.") + result = run(["grdctl", "rdp", "set-port", port]) + if result.returncode != 0: + raise BoundaryError(_refusal(result, "The port could not be changed.")) + + +def set_rdp_view_only(view_only: bool) -> None: + result = run(["grdctl", "rdp", + "enable-view-only" if view_only else "disable-view-only"]) + if result.returncode != 0: + raise BoundaryError(_refusal(result, "That could not be changed.")) + + +def clear_rdp_credentials() -> None: + result = run(["grdctl", "rdp", "clear-credentials"]) + if result.returncode != 0: + raise BoundaryError(_refusal(result, "The credentials could not be cleared.")) + + def set_hostname(name: str) -> None: if not HOSTNAME.fullmatch(name or ""): raise BoundaryError("A name may use letters, digits and hyphens.") @@ -170,10 +193,18 @@ def main(arguments: list[str]) -> int: set_remote_desktop(arguments[1] == "true") elif len(arguments) == 2 and arguments[0] == "set-hostname": set_hostname(arguments[1]) + elif len(arguments) == 2 and arguments[0] == "set-rdp-port": + set_rdp_port(arguments[1]) + elif len(arguments) == 2 and arguments[0] == "set-rdp-view-only": + set_rdp_view_only(arguments[1] == "true") + elif arguments == ["clear-rdp-credentials"]: + clear_rdp_credentials() else: raise BoundaryError( "Usage: panama-sharing snapshot | set-remote-login true|false | " - "set-remote-desktop true|false | set-hostname NAME") + "set-remote-desktop true|false | set-hostname NAME | " + "set-rdp-port PORT | set-rdp-view-only true|false | " + "clear-rdp-credentials") except BoundaryError as error: state = snapshot() state["error"] = str(error) diff --git a/config/dot/quickshell/services/DesktopStyle.qml b/config/dot/quickshell/services/DesktopStyle.qml index c47b5b0..5e439ae 100644 --- a/config/dot/quickshell/services/DesktopStyle.qml +++ b/config/dot/quickshell/services/DesktopStyle.qml @@ -208,7 +208,12 @@ Singleton { DesktopPreferences.get("middleClickPaste")), root.setting("org.gnome.desktop.wm.preferences", "button-layout", root.buttonLayout()), root.setting("org.gnome.desktop.wm.preferences", "action-double-click-titlebar", - DesktopPreferences.get("titlebarDoubleClick")) + DesktopPreferences.get("titlebarDoubleClick")), + // The portal republishes this as org.freedesktop.appearance + // contrast, which is what libadwaita reads -- so this reaches GTK4 + // applications without any high-contrast theme being installed. + root.setting("org.gnome.desktop.a11y.interface", "high-contrast", + DesktopPreferences.get("highContrast")) ]); } diff --git a/config/dot/quickshell/services/Sharing.qml b/config/dot/quickshell/services/Sharing.qml index ec59bdb..905f9ac 100644 --- a/config/dot/quickshell/services/Sharing.qml +++ b/config/dot/quickshell/services/Sharing.qml @@ -93,6 +93,31 @@ Singleton { root.run(["set-hostname", name]); } + function setRdpPort(port: string): void { + root.run(["set-rdp-port", port]); + } + + 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) } diff --git a/config/local/share/vicinae/scripts/settings-accessibility.sh b/config/local/share/vicinae/scripts/settings-accessibility.sh index 79d399e..328538d 100755 --- a/config/local/share/vicinae/scripts/settings-accessibility.sh +++ b/config/local/share/vicinae/scripts/settings-accessibility.sh @@ -5,6 +5,6 @@ # @vicinae.mode silent # @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg # @vicinae.description Open Accessibility in Settings. -# @vicinae.keywords ["settings", "magnifier", "magnifier follows in steps", "dim inactive windows", "dim amount", "pointer size", "text size"] +# @vicinae.keywords ["settings", "magnifier", "magnifier follows in steps", "high contrast", "dim inactive windows", "dim amount", "pointer size", "text size"] exec "$HOME/.config/quickshell/scripts/panama-action" settings-page accessibility diff --git a/docs/settings.md b/docs/settings.md index 917defc..07af177 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -4,7 +4,7 @@ Do not edit this file. Run `quickshell/scripts/panama-settings-docs` after changing the schema; a contract fails when this copy is stale. -127 settings across 26 groups. 67 of them are applied to the compositor and confirmed by reading the value back. +128 settings across 26 groups. 67 of them are applied to the compositor and confirmed by reading the value back. ## accessibility @@ -14,6 +14,7 @@ Found on **Accessibility**. |---|---|---| | **Magnifier**
`magnifierFactor` `cursor:zoom_factor` | 1.0 | Magnifies the screen around the pointer. 1.0 is off. Range 1.0–5.0. | | **Magnifier follows in steps**
`magnifierRigid` `cursor:zoom_rigid` | false | Moves the magnified view in increments rather than gliding with the pointer | +| **High contrast**
`highContrast` | false | Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here. | | **Dim inactive windows**
`dimInactive` `decoration:dim_inactive` | false | Darkens every window except the focused one, so the active window is unmistakable | | **Dim amount**
`dimStrength` `decoration:dim_strength` | 0.5 | How much darker unfocused windows are. Range 0.05–0.9. | | **Pointer size**
`cursorSize` | 24 px | Applies to the compositor and to applications. Range 16–64. | diff --git a/docs/superpowers/plans/2026-08-19-desktop-integration.md b/docs/superpowers/plans/2026-08-19-desktop-integration.md index 26e1687..5dc5191 100644 --- a/docs/superpowers/plans/2026-08-19-desktop-integration.md +++ b/docs/superpowers/plans/2026-08-19-desktop-integration.md @@ -233,7 +233,29 @@ wrong driver produces a printer that accepts jobs, reports success, and prints nothing, which is the worst failure this page could ship because it looks like it worked. A printer old enough to need a PPD stays a system-config-printer job. -**Still handed to GNOME.** Color, Wacom, and Universal Access. Each +**Universal Access is ours, and mostly already was.** The magnifier +(`cursor:zoom_factor`), pointer size, text scale, motion and dimming were all +present; high contrast was the real gap and is now covered. It reaches GTK4 +applications through the desktop portal, which republishes GNOME's a11y setting +as `org.freedesktop.appearance contrast` -- no high-contrast theme involved, and +none is installed. Sticky, slow and bounce keys stay absent: there is no Wayland +or Hyprland implementation, and the compositor would happily store the XKB option +while nothing acted on it. + +**Remote desktop is configurable now.** Port, view-only, and clearing stored +credentials go through grdctl. SETTING credentials opens a terminal running +grdctl itself, because it prompts for the password on a terminal and core-dumps +without one -- and the alternative, passing it as an argument, would publish it +through /proc to every process on the machine. Typed into grdctl directly, it +never passes through Panama. + +**Still handed to GNOME: Color and Wacom.** Color is not a matter of effort. colord +runs here with seven profiles and ZERO devices registered, because the daemons +that register displays (gsd-color, colord-kde) do not run under this session, and +Hyprland exposes no ICC, gamma, or color-management option at all. A Color page +could import a profile and attach it to nothing, and nothing would load a curve +into the display: it would look like it worked and change nothing. That is the +same failure refused for snapshot rollback and printer drivers. Each wraps hardware-specific tooling with no D-Bus surface worth reimplementing, and each launches with XDG_CURRENT_DESKTOP=GNOME because gnome-control-center refuses to run otherwise. Digital Wellbeing is deliberately absent: it configures diff --git a/tests/quickshell/sharing-contract.sh b/tests/quickshell/sharing-contract.sh index b66e138..ce340ba 100755 --- a/tests/quickshell/sharing-contract.sh +++ b/tests/quickshell/sharing-contract.sh @@ -74,6 +74,21 @@ grep -q 'pkexec' <<<"$desktop_body" \ grep -q '"--user"' <<<"$desktop_body" \ || fail 'remote desktop is not managed as a user service' +# ── The remote desktop password never passes through Panama ──────────────── +# grdctl takes it on a terminal and core-dumps without one, so the only two +# options were a terminal hand-off or an argument -- and an argument publishes +# it through /proc to every process on this machine. +grep -q 'set-credentials' "$service" \ + || fail 'the service cannot set remote desktop credentials at all' +grep -qE 'set-credentials".*(password|secret)' "$service" \ + && fail 'the service puts a password on the command line' +grep -q 'set-credentials' "$helper" \ + && fail 'the helper handles credentials; that path cannot prompt and must stay in a terminal' +grep -q 'kitty' "$service" \ + || fail 'credentials are not handed to a terminal, so nothing can prompt for them' +grep -q 'clear-rdp-credentials' "$helper" \ + || fail 'stored credentials cannot be cleared' + # ── The snapshot reflects the machine ─────────────────────────────────────── command -v jq >/dev/null 2>&1 || { printf 'sharing contract: SKIP (no jq)\n'; exit 0; } snapshot="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'