mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 07:00:27 +02:00
clipboard-sync: dedupe on switch; vr-keyboard: revert without reboot
- clipboard-sync: stop stale builds and duplicates, prefer the instance started in the nested desktop, only start a new one when switching from the nested desktop (an instance inherits the switching shell's session) - vr-keyboard: the helper reverts the patch on SIGTERM (vrkbd-unpatch.js); disabling the module stops the service via the new steamFrame.userServices.stop, so the stock keyboard is back immediately
This commit is contained in:
1 parent
2dc9ccb83e
commit
4b2885a992
6 files changed
+113
-19
No files matched your search
@@ -115,6 +115,7 @@ menu list every desktop entry, including terminals such as Konsole.
|
||||
| `steamFrame.outerBusEnv` | str, read-only | `"env DBUS_SESSION_BUS_ADDRESS=${userBus}"` | Prefix for launchers that must use the outer bus. |
|
||||
| `steamFrame.userServices.start` | list of str | `[ ]` | User units started on switch if not running (outer user manager). |
|
||||
| `steamFrame.userServices.restart` | list of str | `[ ]` | User units restarted on every switch (outer user manager). |
|
||||
| `steamFrame.userServices.stop` | list of str | `[ ]` | User units stopped on switch if still running, e.g. of a feature just disabled. |
|
||||
| `steamFrame.portalFix.enable` | bool | `true` | Working portal config for the Steam session (OpenURI). |
|
||||
| `steamFrame.keyboardLayout` | null or str | `null` | XKB layout for the Steam session, e.g. `"de"`. `null` = no drop-in (US). |
|
||||
| `steamFrame.keyboardVariant` | null or str | `null` | XKB variant for the Steam session. |
|
||||
@@ -138,7 +139,8 @@ running"): new or changed user units are neither reloaded nor started.
|
||||
**Fix:** the activation entry `steamFrameUserServices` exports the outer
|
||||
session's `XDG_RUNTIME_DIR`/`DBUS_SESSION_BUS_ADDRESS` and runs
|
||||
`/usr/bin/systemctl --user daemon-reload`, then `start` for
|
||||
`userServices.start` and `restart` for `userServices.restart`. The entry
|
||||
`userServices.start`, `stop` for `userServices.stop` and `restart` for
|
||||
`userServices.restart`. The entry
|
||||
always runs (also with both lists empty), so unit files you define yourself
|
||||
are at least reloaded.
|
||||
|
||||
@@ -185,8 +187,11 @@ keymap (`| @ { [ ] } \ ~ ^`, backtick, `ä ö ü €`) come out as `1`.
|
||||
`vrkbd-patch.js` into Steam's UI at runtime through Steam's CEF DevTools port
|
||||
(`127.0.0.1:8080`; SteamOS starts Steam with `-cef-enable-debugging`) and
|
||||
re-injects it after Steam restarts or the keyboard popup is recreated.
|
||||
Steam's files are never modified; without the service, a restart of Steam
|
||||
gives the stock keyboard. The service is restarted on every switch (via
|
||||
Steam's files are never modified. Enabling and disabling take effect on
|
||||
`home-manager switch`, no reboot or Steam restart needed: when the helper
|
||||
stops (service stopped, or the option disabled, which stops it via
|
||||
`userServices.stop`) it reverts the patch in Steam's running UI
|
||||
(`vrkbd-unpatch.js`). The service is restarted on every switch (via
|
||||
`userServices.restart`) so a changed patch is re-injected.
|
||||
|
||||
- Bottom row becomes `Esc Ctrl Alt [Space] AltGr ← ↑ ↓ → Close`.
|
||||
|
||||
@@ -13,7 +13,7 @@ in {
|
||||
options.steamFrame.clipboardSync = {
|
||||
enable = lib.mkEnableOption ''
|
||||
clipboard-sync between the Steam session and the nested desktop
|
||||
(KDE autostart; restarted on switch when the build changed)
|
||||
(KDE autostart; on switch, stale builds and duplicates are stopped)
|
||||
'';
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
@@ -42,15 +42,44 @@ in {
|
||||
NoDisplay=true
|
||||
'';
|
||||
|
||||
# (Re)start on switch if not running the current build.
|
||||
# Run `home-manager switch` from a desktop terminal so it gets the desktop env.
|
||||
# Keep exactly one instance of the current build, preferably one started in
|
||||
# the nested desktop (XDG_CURRENT_DESKTOP=KDE): it inherits the process
|
||||
# environment, so a copy started from a Steam-session terminal runs with
|
||||
# the wrong session's env. Stale builds and duplicate instances are
|
||||
# stopped (workers of an instance are left alone). A new
|
||||
# instance is only started when switching from the nested desktop;
|
||||
# otherwise the autostart entry starts it with the desktop.
|
||||
home.activation.startClipboardSync = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
want="${cfg.package}/bin/clipboard-sync"
|
||||
pid="$(${pkgs.procps}/bin/pgrep -x clipboard-sync | head -n1 || true)"
|
||||
have="$( [ -n "$pid" ] && readlink "/proc/$pid/exe" || true )"
|
||||
if [ "$have" != "$want" ]; then
|
||||
${pkgs.procps}/bin/pkill -x clipboard-sync || true
|
||||
run ${pkgs.util-linux}/bin/setsid -f "$want" >/dev/null 2>&1
|
||||
desktop=() other=()
|
||||
for pid in $(${pkgs.procps}/bin/pgrep -x clipboard-sync || true); do
|
||||
# clipboard-sync forks workers; only look at top-level instances.
|
||||
ppid="$(${pkgs.procps}/bin/ps -o ppid= -p "$pid" | tr -d ' ')"
|
||||
[ "$(cat "/proc/$ppid/comm" 2>/dev/null)" = clipboard-sync ] && continue
|
||||
if [ "$(readlink "/proc/$pid/exe" 2>/dev/null)" != "$want" ]; then
|
||||
run kill "$pid" || true # stale build
|
||||
elif tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -qx 'XDG_CURRENT_DESKTOP=KDE'; then
|
||||
desktop+=("$pid")
|
||||
else
|
||||
other+=("$pid")
|
||||
fi
|
||||
done
|
||||
if [ ''${#desktop[@]} -gt 0 ]; then
|
||||
keep=("''${desktop[0]}")
|
||||
elif [ "''${XDG_CURRENT_DESKTOP:-}" = KDE ]; then
|
||||
keep=()
|
||||
else
|
||||
keep=("''${other[@]:0:1}")
|
||||
fi
|
||||
for pid in "''${desktop[@]}" "''${other[@]}"; do
|
||||
[[ " ''${keep[*]} " == *" $pid "* ]] || run kill "$pid" || true
|
||||
done
|
||||
if [ ''${#keep[@]} -eq 0 ]; then
|
||||
if [ "''${XDG_CURRENT_DESKTOP:-}" = KDE ]; then
|
||||
run ${pkgs.util-linux}/bin/setsid -f "$want" >/dev/null 2>&1
|
||||
else
|
||||
echo "clipboard-sync: not running; it starts with the nested desktop (autostart)"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
+12
-2
@@ -12,8 +12,8 @@
|
||||
# running"). The `steamFrameUserServices` activation entry does it instead,
|
||||
# pointed at the outer session: it always runs `systemctl --user
|
||||
# daemon-reload` (so changed unit files are picked up even when both lists are
|
||||
# empty), then starts the units in `userServices.start` and restarts those in
|
||||
# `userServices.restart`.
|
||||
# empty), then starts the units in `userServices.start`, stops those in
|
||||
# `userServices.stop` and restarts those in `userServices.restart`.
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.steamFrame;
|
||||
@@ -58,6 +58,14 @@ in {
|
||||
example = [ "vr-keyboard.service" ];
|
||||
description = "User units restarted on every switch (outer user manager).";
|
||||
};
|
||||
stop = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
User units stopped on switch if still running, e.g. the service of a
|
||||
feature that was just disabled (its unit file is already gone).
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,6 +74,8 @@ in {
|
||||
run /usr/bin/systemctl --user daemon-reload
|
||||
'' + lib.optionalString (cfg.userServices.start != [ ]) ''
|
||||
run /usr/bin/systemctl --user start ${units cfg.userServices.start}
|
||||
'' + lib.optionalString (cfg.userServices.stop != [ ]) ''
|
||||
run /usr/bin/systemctl --user stop ${units cfg.userServices.stop} || true
|
||||
'' + lib.optionalString (cfg.userServices.restart != [ ]) ''
|
||||
run /usr/bin/systemctl --user restart ${units cfg.userServices.restart}
|
||||
'');
|
||||
|
||||
+11
-2
@@ -24,7 +24,8 @@
|
||||
Steam's UI through its CEF DevTools port, plus an xdotool helper service)
|
||||
'';
|
||||
|
||||
config = lib.mkIf config.steamFrame.vrKeyboard.enable {
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf config.steamFrame.vrKeyboard.enable {
|
||||
systemd.user.services.vr-keyboard = {
|
||||
Unit.Description = "Modifier keys for Steam's VR keyboard (CEF patch + xdotool)";
|
||||
Service = {
|
||||
@@ -32,11 +33,13 @@
|
||||
"${pkgs.nodejs}/bin/node"
|
||||
"${./vr-keyboard/vrkbd-helper.mjs}"
|
||||
"${./vr-keyboard/vrkbd-patch.js}"
|
||||
"${./vr-keyboard/vrkbd-unpatch.js}"
|
||||
"${pkgs.xdotool}/bin/xdotool"
|
||||
];
|
||||
Environment = "VRKBD_DISPLAY=:0";
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
TimeoutStopSec = 5; # the helper unpatches Steam's UI on SIGTERM
|
||||
};
|
||||
Install.WantedBy = [ "default.target" ];
|
||||
};
|
||||
@@ -44,5 +47,11 @@
|
||||
# Restart on every switch so a changed patch is re-injected (it replaces
|
||||
# the older version).
|
||||
steamFrame.userServices.restart = [ "vr-keyboard.service" ];
|
||||
};
|
||||
})
|
||||
# Disabled: stop a still-running helper, which reverts the patch, so the
|
||||
# stock keyboard is back right away (no Steam restart or reboot).
|
||||
(lib.mkIf (!config.steamFrame.vrKeyboard.enable) {
|
||||
steamFrame.userServices.stop = [ "vr-keyboard.service" ];
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
// vrkbd-helper: injects vrkbd-patch.js into Steam's UI via CEF DevTools and
|
||||
// performs the key requests of the patched VR keyboard with xdotool on :0.
|
||||
// usage: node vrkbd-helper.mjs <patch.js> [xdotool]
|
||||
// On SIGTERM/SIGINT it reverts the patch (unpatch.js), so stopping the service
|
||||
// restores Steam's stock keyboard without restarting Steam.
|
||||
// usage: node vrkbd-helper.mjs <patch.js> <unpatch.js> [xdotool]
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execFile } from 'node:child_process';
|
||||
|
||||
const [, , patchPath, xdotool = 'xdotool'] = process.argv;
|
||||
const [, , patchPath, unpatchPath, xdotool = 'xdotool'] = process.argv;
|
||||
const PATCH = readFileSync(patchPath, 'utf8');
|
||||
const UNPATCH = readFileSync(unpatchPath, 'utf8');
|
||||
let current = null; // CDP `call` of the live session, if any
|
||||
const CDP = 'http://127.0.0.1:8080/json/list';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const ENV = { ...process.env, DISPLAY: process.env.VRKBD_DISPLAY || ':0', LC_ALL: 'C.UTF-8' };
|
||||
@@ -78,7 +82,8 @@ async function session() {
|
||||
else if (m.method === 'Runtime.bindingCalled' && m.params.name === '__vrkbdKey') handle(m.params.payload);
|
||||
else if (m.method === 'Runtime.executionContextCreated') setTimeout(inject, 3000);
|
||||
};
|
||||
const closed = new Promise((res) => { ws.onclose = res; });
|
||||
const closed = new Promise((res) => { ws.onclose = () => { current = null; res(); }; });
|
||||
current = call;
|
||||
await call('Runtime.enable');
|
||||
await call('Runtime.addBinding', { name: '__vrkbdKey' });
|
||||
// Nothing is held by this helper instance; make the page re-send holds.
|
||||
@@ -92,7 +97,14 @@ async function session() {
|
||||
releaseAll();
|
||||
}
|
||||
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => { releaseAll(); setTimeout(() => process.exit(0), 200); });
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, async () => {
|
||||
releaseAll();
|
||||
if (current) {
|
||||
const r = await Promise.race([current('Runtime.evaluate', { expression: UNPATCH, returnByValue: true }), sleep(2000)]);
|
||||
console.log('unpatch:', r?.result?.result?.value ?? 'timeout');
|
||||
}
|
||||
setTimeout(() => process.exit(0), 200);
|
||||
});
|
||||
|
||||
for (;;) {
|
||||
try { await session(); console.log('disconnected'); }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Reverts vrkbd-patch.js in Steam's SharedJSContext; evaluated by vrkbd-helper
|
||||
// when it stops (service stopped, module disabled). Every patched function
|
||||
// keeps its original as __vrkbdOrig. Safe to run when nothing is patched.
|
||||
(() => {
|
||||
const wr = window.__vrkbdWr;
|
||||
if (!wr) return 'not patched';
|
||||
const unwrap = (obj, name) => {
|
||||
const f = obj?.[name];
|
||||
if (f && f.__vrkbdOrig) obj[name] = f.__vrkbdOrig;
|
||||
};
|
||||
const Layouts = wr(40222);
|
||||
for (const l of [...(Layouts.G$() || []), Layouts.r_()]) unwrap(l, 'rgLayout');
|
||||
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
||||
unwrap(wr(5363).PE.prototype, 'DispatchKeypress');
|
||||
|
||||
clearInterval(window.__vrkbdHoldTimer);
|
||||
const inst = window.__vrkbdInst;
|
||||
if (inst) {
|
||||
let proto = Object.getPrototypeOf(inst);
|
||||
while (proto && !Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
||||
unwrap(proto, 'TypeKeyInternal');
|
||||
delete inst.__vrkbd;
|
||||
inst.setState({ standardLayout: Layouts.r_() });
|
||||
inst.forceUpdate();
|
||||
}
|
||||
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.document.getElementById('vrkbd-style')?.remove();
|
||||
for (const k of ['__vrkbdHoldTimer', '__vrkbdHeld', '__vrkbdInst', '__vrkbdPatched']) delete window[k];
|
||||
return 'unpatched';
|
||||
})()
|
||||
Reference in new issue
Block a user