mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-09-06 10:07:15 +02:00
feat(sunshine): add clipboard bridge for streamed sessions
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
# Temporary remote session recovery
|
||||
|
||||
When a remote-only machine is stuck at the `greetd` TUI, run the Nushell helper from an
|
||||
SSH session to start one complete PAM/logind Wayland session without changing
|
||||
the persistent NixOS configuration:
|
||||
When a remote-only machine is stuck at the `greetd` TUI, run the Nushell helper from an SSH session
|
||||
to start one complete PAM/logind Wayland session without changing the persistent NixOS
|
||||
configuration:
|
||||
|
||||
```bash
|
||||
sudo nu ./remote-session.nu
|
||||
```
|
||||
|
||||
The helper validates its dependencies before changing the current session. It
|
||||
then stops the normal `greetd` service, starts a transient systemd-managed
|
||||
`greetd` configuration on `/dev/tty1`, and runs Niri directly. It returns after
|
||||
greetd, Niri, and Sunshine are ready. Tailscale is not affected.
|
||||
The helper validates its dependencies before changing the current session. It then stops the normal
|
||||
`greetd` service, starts a transient systemd-managed `greetd` configuration on `/dev/tty1`, and runs
|
||||
Niri directly. It returns after greetd, Niri, and Sunshine are ready. Tailscale is not affected.
|
||||
|
||||
The `Virtual-1` output is created persistently by the `vkms` configuration in
|
||||
`hosts/idols-ai/hardware-intel.nix` and configured by
|
||||
`hosts/idols-ai/niri-hardware.kdl`. It gives Niri and Sunshine a capture target
|
||||
when both physical monitors attached to the NVIDIA GPU are off. The output also
|
||||
exists during normal physical sessions; keeping an idle virtual framebuffer has
|
||||
`hosts/idols-ai/hardware-intel.nix` and configured by `hosts/idols-ai/niri-hardware.kdl`. It gives
|
||||
Niri and Sunshine a capture target when both physical monitors attached to the NVIDIA GPU are off.
|
||||
The output also exists during normal physical sessions; keeping an idle virtual framebuffer has
|
||||
minor GPU and memory overhead.
|
||||
|
||||
Options:
|
||||
@@ -30,5 +28,36 @@ Options:
|
||||
--check validate prerequisites without changing service state
|
||||
```
|
||||
|
||||
A reboot or a manual restart of `greetd.service` restores the normal TUI
|
||||
configuration.
|
||||
A reboot or a manual restart of `greetd.service` restores the normal TUI configuration.
|
||||
|
||||
## Clipboard bridge (client -> host paste)
|
||||
|
||||
Moonlight's `Ctrl+Alt+Shift+V` pastes by asking Sunshine to _type_ the text back as
|
||||
`Ctrl+Shift+U <hex> <Enter>` key events per character. Only GTK widgets interpret that sequence, so
|
||||
pasting into a browser address bar or a terminal garbles the content (a URL turns into its literal
|
||||
hex form). To paste reliably, set the _real_ clipboard of the streamed session instead and then use
|
||||
a normal `Ctrl+V`.
|
||||
|
||||
- `clipboard.nu` (host side): reads text from stdin and writes it to the clipboard of the compositor
|
||||
Sunshine is currently streaming to. The streamed session is found via the running `sunshine`
|
||||
process environment (`WAYLAND_DISPLAY`/`XDG_RUNTIME_DIR`), so this works for both the
|
||||
`greetd-sunshine` remote session and a regular desktop session.
|
||||
- `send-clipboard.nu` (client side, Linux): pipes the client clipboard to the host over SSH.
|
||||
|
||||
Host usage (input must be piped, not typed):
|
||||
|
||||
```bash
|
||||
printf 'https://example.com/path?q=1' | nu ./clipboard.nu
|
||||
```
|
||||
|
||||
Client usage, from the Moonlight client (requires key-based SSH to the host and `wl-clipboard` on
|
||||
the client):
|
||||
|
||||
```bash
|
||||
wl-paste -n | ssh ryan@ai "nu ~/nix-config/modules/nixos/desktop/networking/sunshine/clipboard.nu"
|
||||
# or, from this repo:
|
||||
nu ./send-clipboard.nu ryan@ai
|
||||
```
|
||||
|
||||
`--verify` makes the host script read the clipboard back and fail on mismatch. An empty input, or a
|
||||
host with no active `sunshine` process, exits with an error.
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env nu
|
||||
|
||||
# Sunshine clipboard bridge (host side)
|
||||
#
|
||||
# Reads raw text from stdin and sets it as the clipboard of the compositor that
|
||||
# is currently streaming via Sunshine, so pasting inside the stream is a plain
|
||||
# Ctrl+V. Moonlight's built-in "type clipboard text" (Ctrl+Alt+Shift+V) is not
|
||||
# usable here: Sunshine re-types the payload as Ctrl+Shift+U hex key events,
|
||||
# which only GTK widgets understand, so browsers and terminals garble it.
|
||||
#
|
||||
# The streamed compositor is discovered through the running `sunshine` process
|
||||
# (its WAYLAND_DISPLAY / XDG_RUNTIME_DIR). This works for both a headless
|
||||
# greetd-sunshine remote session and a regular desktop session.
|
||||
#
|
||||
# Usage (input must be a pipe, not a tty):
|
||||
#
|
||||
# printf 'https://example.com/path?q=1' | nu clipboard.nu
|
||||
# wl-paste -n | nu clipboard.nu
|
||||
#
|
||||
# Usage from a Moonlight client over SSH (see send-clipboard.nu):
|
||||
#
|
||||
# wl-paste -n | ssh ryan@ai "nu ~/nix-config/modules/nixos/desktop/networking/sunshine/clipboard.nu"
|
||||
|
||||
def main [
|
||||
--user (-u): string = "" # Sunshine session user (default: SUDO_USER or current user)
|
||||
--display (-d): string = "" # Override WAYLAND_DISPLAY (auto-detected from the sunshine process)
|
||||
--verify (-v) # Read the clipboard back and fail if it differs from the input
|
||||
] {
|
||||
if (is-terminal --stdin) {
|
||||
error make {
|
||||
msg: 'no input: pipe text on stdin, e.g. `printf "text" | nu clipboard.nu`'
|
||||
}
|
||||
}
|
||||
|
||||
let raw = (open --raw /dev/stdin)
|
||||
if ($raw | str trim) == '' {
|
||||
error make { msg: 'empty input: nothing to copy' }
|
||||
}
|
||||
|
||||
let user = (effective-user $user)
|
||||
let pid = (streaming-sunshine-pid $user)
|
||||
if $pid < 0 {
|
||||
error make {
|
||||
msg: $"no running `sunshine` process for user `($user)`"
|
||||
help: 'start the Sunshine session first (headless: `sudo nu ./remote-session.nu`)'
|
||||
}
|
||||
}
|
||||
|
||||
let runtime = (proc-env $pid 'XDG_RUNTIME_DIR' $"/run/user/(^id -u $user | str trim)")
|
||||
let display = if $display != '' {
|
||||
$display
|
||||
} else {
|
||||
let detected = (proc-env $pid 'WAYLAND_DISPLAY' '')
|
||||
if $detected != '' { $detected } else { (single-wayland-display $runtime) }
|
||||
}
|
||||
|
||||
let target_env = {
|
||||
WAYLAND_DISPLAY: $display
|
||||
XDG_RUNTIME_DIR: $runtime
|
||||
}
|
||||
|
||||
# wl-copy forks a server that must outlive this process and serve the
|
||||
# selection; redirect its stdio so the daemon does not hold our pipes open.
|
||||
with-env $target_env {
|
||||
$raw | ^wl-copy out> /dev/null err> /dev/null
|
||||
}
|
||||
if $env.LAST_EXIT_CODE != 0 {
|
||||
error make { msg: 'wl-copy failed to set the clipboard' }
|
||||
}
|
||||
|
||||
if $verify {
|
||||
let pasted = (with-env $target_env { ^wl-paste -n | complete })
|
||||
if $pasted.exit_code != 0 or ($pasted.stdout | str trim) != ($raw | str trim) {
|
||||
error make { msg: 'verification failed: clipboard content differs from the input' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def effective-user [user: string] {
|
||||
if $user != '' { return $user }
|
||||
$env.SUDO_USER? | default (whoami)
|
||||
}
|
||||
|
||||
def streaming-sunshine-pid [user: string] {
|
||||
let found = (^pgrep -o -u $user -x sunshine | complete)
|
||||
if $found.exit_code != 0 { return (-1) }
|
||||
($found.stdout | str trim | into int)
|
||||
}
|
||||
|
||||
def proc-env [pid: int, key: string, fallback: string] {
|
||||
let environ = (open --raw $"/proc/($pid)/environ")
|
||||
for kv in ($environ | split row "\u{0}") {
|
||||
if ($kv | str starts-with $"($key)=") {
|
||||
return ($kv | str replace $"($key)=" '')
|
||||
}
|
||||
}
|
||||
$fallback
|
||||
}
|
||||
|
||||
def single-wayland-display [runtime: string] {
|
||||
let sockets = (ls $runtime
|
||||
| where { |it| ($it.name | path basename) =~ '^wayland-[0-9]+$' }
|
||||
| get name)
|
||||
if ($sockets | length) == 1 {
|
||||
return ($sockets.0 | path basename)
|
||||
}
|
||||
error make {
|
||||
msg: $"cannot determine WAYLAND_DISPLAY under ($runtime)"
|
||||
help: $"candidates: (($sockets | path basename | str join ', ')) -- pass --display explicitly"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env nu
|
||||
|
||||
# Sunshine clipboard bridge (client side)
|
||||
#
|
||||
# Sends the local client clipboard to the Sunshine host and writes it into the
|
||||
# clipboard of the streamed session, so pasting inside the stream is a plain
|
||||
# Ctrl+V. Prefer this over Moonlight's Ctrl+Alt+Shift+V, which "types" the text
|
||||
# via unicode-hex key events and garbles it in non-GTK apps (e.g. browsers).
|
||||
#
|
||||
# Requires:
|
||||
# - wl-clipboard (wl-paste) on this machine, inside a Wayland session
|
||||
# - key-based SSH access to the host (the pipe needs a non-interactive session)
|
||||
# - the host-side `clipboard.nu` reachable at --remote (default assumes the
|
||||
# repo is cloned at ~/nix-config on the host)
|
||||
#
|
||||
# Usage:
|
||||
# nu send-clipboard.nu ryan@ai
|
||||
# nu send-clipboard.nu ryan@ai --remote /abs/path/to/clipboard.nu
|
||||
|
||||
def main [
|
||||
host: string = "" # SSH target of the Sunshine host, e.g. ryan@ai
|
||||
--remote: string = "~/nix-config/modules/nixos/desktop/networking/sunshine/clipboard.nu"
|
||||
] {
|
||||
if $host == '' {
|
||||
error make {
|
||||
msg: 'missing SSH target'
|
||||
help: 'usage: nu send-clipboard.nu <ssh-target>, e.g. `nu send-clipboard.nu ryan@ai`'
|
||||
}
|
||||
}
|
||||
|
||||
let picked = (^wl-paste -n | complete)
|
||||
if $picked.exit_code != 0 {
|
||||
error make { msg: $"wl-paste failed: ($picked.stderr | str trim)" }
|
||||
}
|
||||
if ($picked.stdout | str trim) == '' {
|
||||
error make { msg: 'client clipboard is empty: copy some text first' }
|
||||
}
|
||||
|
||||
$picked.stdout | ^ssh -o BatchMode=yes $host $"nu ($remote)"
|
||||
if $env.LAST_EXIT_CODE != 0 {
|
||||
error make {
|
||||
msg: $"could not write clipboard to ($host)"
|
||||
help: 'ensure key-based SSH works (`ssh -o BatchMode=yes <target> true`) and the remote script path is correct'
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user