rename: lumen → punktfunk, everywhere
ci / rust (push) Has been cancelled

Full project rename, decided 2026-06-10:
- Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs.
- C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h,
  PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl.
  PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants).
- Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1.
  WIRE BREAK: clients must be rebuilt from this revision.
- Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / ….
- Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the
  persistent identity is unchanged, pinned fingerprints stay valid).
- Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection
  (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated.
- scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated.

Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of
"desktop but no apps/settings" over the stream: plasmashell launched without
XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and
rendered an empty menu. The script sets the complete KDE session env (menu prefix,
KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell.

Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS,
zero lumen references left outside .git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:11:59 +00:00
parent b8b23c8fb2
commit bfd64ce871
119 changed files with 1245 additions and 1185 deletions
+128
View File
@@ -0,0 +1,128 @@
//! Virtual display orchestration (plan §6) — the project's differentiator.
//!
//! A [`VirtualDisplay`] creates a *client-sized* output on demand, rendered natively and
//! headless (no scaling), to be captured and streamed, then torn down on disconnect. There is
//! no cross-compositor Wayland protocol for this, so each compositor has its own backend behind
//! this trait:
//!
//! * **KWin** — privileged `zkde_screencast_unstable_v1::stream_virtual_output` ([`kwin`]).
//! * **wlroots/Sway** — `swaymsg create_output` + `output mode --custom` (TODO).
//! * **Mutter/GNOME** — D-Bus `RemoteDesktop` + `ScreenCast.RecordVirtual` (TODO).
//!
//! [`VirtualDisplay::create`] returns a [`VirtualOutput`]: the PipeWire node to capture plus an
//! owned keepalive whose `Drop` releases the output (RAII — no explicit `destroy`). Capture
//! consumes the node via [`crate::capture::capture_virtual_output`].
use anyhow::Result;
pub use punktfunk_core::Mode;
use std::os::fd::OwnedFd;
/// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop
/// tears the output down (releases the compositor-side resource).
///
/// Allowed dead on non-Linux: the backends that construct it are all `cfg(target_os = "linux")`.
#[allow(dead_code)]
pub struct VirtualOutput {
/// PipeWire node id of the output's screencast stream.
pub node_id: u32,
/// Portal/remote PipeWire fd when the node lives on a sandboxed remote (e.g. Mutter's
/// RemoteDesktop+ScreenCast). `None` means the node is on the user's default PipeWire daemon
/// (KWin `zkde_screencast`), captured by connecting to that daemon directly.
pub remote_fd: Option<OwnedFd>,
/// `(width, height, refresh_hz)` to prefer in the PipeWire format negotiation. KWin and
/// gamescope outputs are created at the exact size, so this just confirms it; **Mutter sizes
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
pub preferred_mode: Option<(u32, u32, u32)>,
/// Keeps the output — and whatever connection/thread backs it — alive; dropped on teardown.
pub keepalive: Box<dyn Send>,
}
/// Pluggable virtual-output creation, per compositor.
pub trait VirtualDisplay: Send {
/// Human-readable backend name (e.g. `"kwin"`, `"wlroots"`, `"mutter"`).
fn name(&self) -> &'static str;
/// Create a virtual output of the given mode. Teardown is RAII: drop the returned
/// [`VirtualOutput`]'s `keepalive`.
fn create(&mut self, mode: Mode) -> Result<VirtualOutput>;
}
/// Compositors punktfunk knows how to drive (plan §6).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Compositor {
/// KWin / Plasma 6 — `zkde_screencast` virtual output.
Kwin,
/// wlroots (Sway/Hyprland) — headless `create_output`.
Wlroots,
/// Mutter / GNOME — headless backend + Mutter DBus `RecordVirtual`.
Mutter,
/// gamescope — spawned headless at the client's size/refresh; capture its PipeWire node.
Gamescope,
}
/// Detect the compositor to drive: `PUNKTFUNK_COMPOSITOR` override, else `XDG_CURRENT_DESKTOP`.
pub fn detect() -> Result<Compositor> {
if let Ok(v) = std::env::var("PUNKTFUNK_COMPOSITOR") {
return match v.trim().to_ascii_lowercase().as_str() {
"kwin" | "kde" | "plasma" => Ok(Compositor::Kwin),
"wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots),
"mutter" | "gnome" => Ok(Compositor::Mutter),
"gamescope" => Ok(Compositor::Gamescope),
other => {
anyhow::bail!(
"unknown PUNKTFUNK_COMPOSITOR '{other}' (kwin|wlroots|mutter|gamescope)"
)
}
};
}
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_ascii_uppercase();
if desktop.contains("KDE") {
Ok(Compositor::Kwin)
} else if desktop.contains("GNOME") {
Ok(Compositor::Mutter)
} else if desktop.contains("SWAY")
|| desktop.contains("WLROOTS")
|| desktop.contains("HYPRLAND")
{
Ok(Compositor::Wlroots)
} else {
anyhow::bail!(
"could not detect compositor from XDG_CURRENT_DESKTOP='{desktop}'; set PUNKTFUNK_COMPOSITOR"
)
}
}
/// Open the virtual-display driver for `compositor`.
pub fn open(compositor: Compositor) -> Result<Box<dyn VirtualDisplay>> {
#[cfg(target_os = "linux")]
{
match compositor {
Compositor::Kwin => Ok(Box::new(kwin::KwinDisplay::new()?)),
Compositor::Gamescope => Ok(Box::new(gamescope::GamescopeDisplay::new()?)),
Compositor::Mutter => Ok(Box::new(mutter::MutterDisplay::new()?)),
Compositor::Wlroots => {
anyhow::bail!("wlroots virtual-output backend not yet implemented")
}
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = compositor;
anyhow::bail!("virtual displays require Linux (Wayland compositor)")
}
}
/// Path of the file where the gamescope backend relays the nested session's `LIBEI_SOCKET`
/// (gamescope's EIS server) for the input injector.
#[cfg(target_os = "linux")]
pub fn gamescope_ei_socket_file() -> &'static str {
gamescope::EI_SOCKET_FILE
}
#[cfg(target_os = "linux")]
mod gamescope;
#[cfg(target_os = "linux")]
mod kwin;
#[cfg(target_os = "linux")]
mod mutter;