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'