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 <Application> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -104,8 +104,9 @@ jobs:
|
|||||||
"MSIX_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
"MSIX_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
Write-Output "MSIX version $v arch ${{ matrix.arch }} target ${{ matrix.target }}"
|
Write-Output "MSIX version $v arch ${{ matrix.arch }} target ${{ matrix.target }}"
|
||||||
|
|
||||||
# Both client binaries — the shell spawns punktfunk-session.exe (a package sibling)
|
# All three client binaries — the shell spawns punktfunk-session.exe (a package
|
||||||
# for every stream. --no-default-features on ARM64 is a no-op for the shell.
|
# 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)
|
- name: Build (release)
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: cargo build --release -p punktfunk-client-windows -p punktfunk-client-session ${{ matrix.session_flags }} --target ${{ matrix.target }}
|
run: cargo build --release -p punktfunk-client-windows -p punktfunk-client-session ${{ matrix.session_flags }} --target ${{ matrix.target }}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ repository.workspace = true
|
|||||||
name = "punktfunk-client"
|
name = "punktfunk-client"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
# The couch/HTPC Start-menu entry. Its own executable because an MSIX <Application> 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
|
# 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
|
# 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")
|
# platforms this builds as a stub binary. Mirrors the Linux client's cfg(target_os="linux")
|
||||||
|
|||||||
@@ -57,6 +57,23 @@
|
|||||||
<uap:DefaultTile Square71x71Logo="Assets\Square71x71Logo.png" />
|
<uap:DefaultTile Square71x71Logo="Assets\Square71x71Logo.png" />
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
|
<!--
|
||||||
|
Second entry point: the couch/console UI, for an HTPC or a TV-attached box where the
|
||||||
|
desktop shell is the wrong first screen. Same full-trust executable, launched with
|
||||||
|
`--console`, which hands straight off to the session binary's controller-driven
|
||||||
|
browse mode (host list, pairing, settings, library) fullscreen.
|
||||||
|
-->
|
||||||
|
<Application Id="PunktfunkConsole" Executable="punktfunk-console.exe"
|
||||||
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="Punktfunk Console"
|
||||||
|
Description="Controller-driven couch interface for TVs and HTPCs"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Square71x71Logo="Assets\Square71x71Logo.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
</Applications>
|
</Applications>
|
||||||
|
|
||||||
<Capabilities>
|
<Capabilities>
|
||||||
|
|||||||
@@ -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/
|
# 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
|
# src/spawn.rs); Skia links statically and vulkan-1.dll is a GPU-driver component, so the session
|
||||||
# adds no DLLs of its own.
|
# 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) {
|
foreach ($f in $required) {
|
||||||
$src = Join-Path $TargetDir $f
|
$src = Join-Path $TargetDir $f
|
||||||
if (-not (Test-Path $src)) { throw "missing build artifact '$f' in $TargetDir (did 'cargo build --release' run?)" }
|
if (-not (Test-Path $src)) { throw "missing build artifact '$f' in $TargetDir (did 'cargo build --release' run?)" }
|
||||||
|
|||||||
@@ -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 —
|
// The couch entry point, for the most recently used paired + REACHABLE host (an offline
|
||||||
// an offline host would just open the console onto an error scene): offer the couch
|
// one would just open the console onto an error scene).
|
||||||
// experience — the console (gamepad) UI on the most recently used such host.
|
//
|
||||||
if CONSOLE_UI_AVAILABLE && props.pads > 0 {
|
// 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| {
|
let reachable = |k: &&crate::trust::KnownHost| {
|
||||||
hosts
|
hosts
|
||||||
.iter()
|
.iter()
|
||||||
@@ -382,12 +387,27 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
card(
|
card(
|
||||||
grid((
|
grid((
|
||||||
vstack((
|
vstack((
|
||||||
text_block("Controller detected").font_size(14.0).semibold(),
|
text_block(if props.pads > 0 {
|
||||||
text_block(format!(
|
"Controller detected"
|
||||||
"Browse {}\u{2019}s game library with the gamepad \u{2014} \
|
} else {
|
||||||
launches stream in the same window.",
|
"Console UI"
|
||||||
k.name
|
})
|
||||||
))
|
.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)
|
.font_size(12.0)
|
||||||
.wrap()
|
.wrap()
|
||||||
.foreground(ThemeRef::SecondaryText),
|
.foreground(ThemeRef::SecondaryText),
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
//! `punktfunk-console.exe` — the couch/HTPC entry point.
|
||||||
|
//!
|
||||||
|
//! Exists because an MSIX `<Application>` 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() {}
|
||||||
@@ -76,6 +76,27 @@ fn main() {
|
|||||||
return;
|
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.
|
// Windowed (default): the WinUI 3 app owns host selection, settings, and pairing.
|
||||||
// Framework-dependent deployment: initialize the Windows App SDK runtime before any WinUI
|
// Framework-dependent deployment: initialize the Windows App SDK runtime before any WinUI
|
||||||
// call (build.rs stages the bootstrap DLL via windows-reactor-setup).
|
// call (build.rs stages the bootstrap DLL via windows-reactor-setup).
|
||||||
|
|||||||
Reference in New Issue
Block a user