mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-09-11 12:31:46 +02:00
feat(sunshine): add clipboard bridge for streamed sessions
This commit is contained in:
+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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user