From 2508b7206271c065e5c70bb7f4398f8455ce1679 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 19 Jul 2026 18:45:14 +0200 Subject: [PATCH] feat(client/windows): make the console UI findable, and launchable on its own Two problems, one feature. The gamepad/couch UI existed but you had to already know it was there: - On the hosts page its card only rendered when a controller was CONNECTED, so with no pad plugged in there was no visible entry point at all -- the only other door being a per-host overflow menu behind a "..." nobody opens. The card now always shows, with copy that adapts to whether a pad is present, so the feature is findable before you own the hardware. - There was no way to start it directly. An HTPC or TV box wants the couch interface as its first screen, not the desktop shell. "punktfunk-client --console" now hands straight off to it (fullscreen unless --windowed), which covers shortcuts, Steam entries, autostart and Task Scheduler. The Start-menu tile needs its own executable: an MSIX cannot pass arguments to a full-trust exe, so "punktfunk-client.exe --console" is not expressible there. punktfunk-console.exe is a ~20-line hand-off to the session binary's browse mode, staged into the package and given its own tile ("Punktfunk Console"). Nothing new is implemented behind either door: a bare "--browse" was already a complete standalone client (host list, discovery, PIN pairing, settings, Wake-on-LAN, library). It just had no front door. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/windows-msix.yml | 5 ++- clients/windows/Cargo.toml | 6 +++ clients/windows/packaging/AppxManifest.xml | 17 +++++++++ clients/windows/packaging/pack-msix.ps1 | 2 +- clients/windows/src/app/hosts.rs | 40 +++++++++++++++----- clients/windows/src/bin/punktfunk-console.rs | 38 +++++++++++++++++++ clients/windows/src/main.rs | 21 ++++++++++ 7 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 clients/windows/src/bin/punktfunk-console.rs diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml index f36f3f5a..6e70e5da 100644 --- a/.gitea/workflows/windows-msix.yml +++ b/.gitea/workflows/windows-msix.yml @@ -104,8 +104,9 @@ jobs: "MSIX_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Output "MSIX version $v arch ${{ matrix.arch }} target ${{ matrix.target }}" - # Both client binaries — the shell spawns punktfunk-session.exe (a package sibling) - # for every stream. --no-default-features on ARM64 is a no-op for the shell. + # All three client binaries — the shell spawns punktfunk-session.exe (a package + # sibling) for every stream, and punktfunk-console.exe is the couch Start-menu tile's + # hand-off shim. --no-default-features on ARM64 is a no-op for the shell. - name: Build (release) shell: pwsh run: cargo build --release -p punktfunk-client-windows -p punktfunk-client-session ${{ matrix.session_flags }} --target ${{ matrix.target }} diff --git a/clients/windows/Cargo.toml b/clients/windows/Cargo.toml index 87f1e626..af6d3960 100644 --- a/clients/windows/Cargo.toml +++ b/clients/windows/Cargo.toml @@ -12,6 +12,12 @@ repository.workspace = true name = "punktfunk-client" path = "src/main.rs" +# The couch/HTPC Start-menu entry. Its own executable because an MSIX cannot +# pass arguments to a full-trust exe — see the binary's own docs. +[[bin]] +name = "punktfunk-console" +path = "src/bin/punktfunk-console.rs" + # Everything is Windows-gated so `cargo build --workspace` stays green on Linux/macOS (the # other native clients live in clients/linux and clients/apple); on other # platforms this builds as a stub binary. Mirrors the Linux client's cfg(target_os="linux") diff --git a/clients/windows/packaging/AppxManifest.xml b/clients/windows/packaging/AppxManifest.xml index 62429448..dc1c9de7 100644 --- a/clients/windows/packaging/AppxManifest.xml +++ b/clients/windows/packaging/AppxManifest.xml @@ -57,6 +57,23 @@ + + + + + + diff --git a/clients/windows/packaging/pack-msix.ps1 b/clients/windows/packaging/pack-msix.ps1 index 83112f66..9aac70fa 100644 --- a/clients/windows/packaging/pack-msix.ps1 +++ b/clients/windows/packaging/pack-msix.ps1 @@ -72,7 +72,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $layout 'Assets') | Out-Nul # session client the shell spawns for every stream (sibling resolution — see clients/windows/ # src/spawn.rs); Skia links statically and vulkan-1.dll is a GPU-driver component, so the session # adds no DLLs of its own. -$required = @('punktfunk-client.exe', 'punktfunk-session.exe', 'Microsoft.WindowsAppRuntime.Bootstrap.dll', 'SDL3.dll', 'resources.pri') +$required = @('punktfunk-client.exe', 'punktfunk-session.exe', 'punktfunk-console.exe', 'Microsoft.WindowsAppRuntime.Bootstrap.dll', 'SDL3.dll', 'resources.pri') foreach ($f in $required) { $src = Join-Path $TargetDir $f if (-not (Test-Path $src)) { throw "missing build artifact '$f' in $TargetDir (did 'cargo build --release' run?)" } diff --git a/clients/windows/src/app/hosts.rs b/clients/windows/src/app/hosts.rs index 890ccdaa..8b658df5 100644 --- a/clients/windows/src/app/hosts.rs +++ b/clients/windows/src/app/hosts.rs @@ -352,10 +352,15 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element { ); } - // A controller is connected and a paired host is REACHABLE (advertising or probed — - // an offline host would just open the console onto an error scene): offer the couch - // experience — the console (gamepad) UI on the most recently used such host. - if CONSOLE_UI_AVAILABLE && props.pads > 0 { + // The couch entry point, for the most recently used paired + REACHABLE host (an offline + // one would just open the console onto an error scene). + // + // Deliberately NOT gated on a controller being connected any more: it used to be, which + // meant that with no pad plugged in the gamepad UI had no visible entry point on this + // page at all — its only other door is the per-host overflow menu, behind a "…" nobody + // opens. The card now always shows (the copy below adapts), so the feature is findable + // before you own the hardware for it. + if CONSOLE_UI_AVAILABLE { let reachable = |k: &&crate::trust::KnownHost| { hosts .iter() @@ -382,12 +387,27 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element { card( grid(( vstack(( - text_block("Controller detected").font_size(14.0).semibold(), - text_block(format!( - "Browse {}\u{2019}s game library with the gamepad \u{2014} \ - launches stream in the same window.", - k.name - )) + text_block(if props.pads > 0 { + "Controller detected" + } else { + "Console UI" + }) + .font_size(14.0) + .semibold(), + text_block(if props.pads > 0 { + format!( + "Browse {}\u{2019}s game library with the gamepad \u{2014} \ + launches stream in the same window.", + k.name + ) + } else { + format!( + "The couch interface for {} \u{2014} a controller- and \ + remote-friendly library that launches streams in the same \ + window. Works with keyboard arrows too.", + k.name + ) + }) .font_size(12.0) .wrap() .foreground(ThemeRef::SecondaryText), diff --git a/clients/windows/src/bin/punktfunk-console.rs b/clients/windows/src/bin/punktfunk-console.rs new file mode 100644 index 00000000..06538880 --- /dev/null +++ b/clients/windows/src/bin/punktfunk-console.rs @@ -0,0 +1,38 @@ +//! `punktfunk-console.exe` — the couch/HTPC entry point. +//! +//! Exists because an MSIX `` cannot pass ARGUMENTS to a full-trust executable: +//! a second Start-menu tile therefore cannot simply be "punktfunk-client.exe --console", it +//! needs its own executable. This is that executable, and it is deliberately nothing but a +//! hand-off — it starts the session binary's `--browse` mode (the complete controller-driven +//! client: host list, discovery, PIN pairing, settings, Wake-on-LAN, library) fullscreen and +//! mirrors its exit code, so whatever supervises this process sees the real result. +//! +//! `--windowed` keeps it in a window; everything else is the session binary's own business. + +// No console window: this is launched from a Start-menu tile / shortcut, and a flashing +// console behind the couch UI looks like a crash. +#![cfg_attr(windows, windows_subsystem = "windows")] + +#[cfg(windows)] +fn main() { + // The session binary ships beside us in the package; fall back to PATH for a dev run. + let session = std::env::current_exe() + .ok() + .map(|e| e.with_file_name("punktfunk-session.exe")) + .filter(|p| p.exists()) + .unwrap_or_else(|| "punktfunk-session".into()); + + let mut cmd = std::process::Command::new(session); + cmd.arg("--browse"); + if !std::env::args().any(|a| a == "--windowed") { + cmd.arg("--fullscreen"); + } + match cmd.status() { + Ok(st) => std::process::exit(st.code().unwrap_or(0)), + Err(_) => std::process::exit(1), + } +} + +/// The workspace builds on Linux/macOS too; there is nothing to launch there. +#[cfg(not(windows))] +fn main() {} diff --git a/clients/windows/src/main.rs b/clients/windows/src/main.rs index c6340683..aa256880 100644 --- a/clients/windows/src/main.rs +++ b/clients/windows/src/main.rs @@ -76,6 +76,27 @@ fn main() { return; } + // `--console`: go straight to the gamepad/couch UI, skipping the WinUI shell entirely — + // the HTPC entry point (a Start-menu tile, a Steam shortcut, a startup item). The session + // binary's bare `--browse` IS a complete standalone client: host list, discovery, PIN + // pairing, settings and Wake-on-LAN, all controller-driven. We just exec it and mirror + // its exit code, so anything supervising this process sees the real result. + if flag("--console") { + let mut cmd = std::process::Command::new(spawn::session_binary()); + cmd.arg("--browse"); + // A couch UI is fullscreen unless explicitly told otherwise. + if !flag("--windowed") { + cmd.arg("--fullscreen"); + } + match cmd.status() { + Ok(st) => std::process::exit(st.code().unwrap_or(0)), + Err(e) => { + eprintln!("could not start the console UI: {e}"); + std::process::exit(1); + } + } + } + // Windowed (default): the WinUI 3 app owns host selection, settings, and pairing. // Framework-dependent deployment: initialize the Windows App SDK runtime before any WinUI // call (build.rs stages the bootstrap DLL via windows-reactor-setup).