Commit Graph
46 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 a7143a6510 Merge origin/main into audio/mic-latency-echo
Main moved through the same surfaces while the audio work was in flight, so
three files needed hand-resolution:

- clients/windows/src/app/settings.rs — main gave Windows its speaker and
  microphone endpoint pickers, the gap this branch could only report. Both
  keep their rows: the pickers, then Echo cancellation, and the microphone
  description keeps the sentence naming the mute chord.
- crates/pf-console-ui/src/screens/settings.rs — both sides grew the couch
  row list. Main's seven new rows and Echo cancellation are all reachable
  in Gaming Mode; the count is 22 and the rationale comment names echo
  cancellation among the fields that would otherwise be unreachable there.
- crates/pf-presenter/src/run.rs — both sides added a stats test at the same
  line. Both are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 00:04:34 +02:00
enricobuehlerandClaude Fable 5 7ec3107b4a feat(client): the mic has an off switch, and echo cancellation has a switch too
Ctrl+Alt+Shift+V mutes and unmutes the microphone mid-stream — V for
voice, since M and S were taken. The uplink keeps running while muted:
`MicStreamer::spawn` takes a shared AtomicBool the capture callback reads
every quantum, and a muted callback drains whole frames and sends
nothing. Stopping the stream instead would have re-primed the device
buffers and, on Linux, re-run source selection on every unmute — a
second of glitch for a key people press mid-sentence. The sequence
counter deliberately does NOT advance while muted, so the host sees one
continuous sequence with a pause rather than a gap the size of the mute,
which its de-jitter would try to conceal frame by frame (its 600 ms
stale-flush covers the rest).

The mute lives on SessionHandle as a MicControl with two flags, not one:
`live` is raised by the pump only once the uplink is actually running, so
a session with the mic off in Settings — or whose capture device wouldn't
open — reports "nothing to mute", the chord says so in the log, and no
indicator appears. Per session, never persisted.

Muted state draws as a persistent "Microphone muted" badge in the stream's
top-right corner, off `FrameCtx::mic_muted` rather than the stats text: it
has to be there with the stats overlay Off, which is where most people
leave it. The Detailed mic line still reads throughput, so it simply falls
to zero — the badge is what answers "am I muted".

Echo cancellation stops being an env-only lever. `Settings::echo_cancel`
(default on, `#[serde(default)]` so every stored file loads with it on)
now gates the same hooks PUNKTFUNK_NO_AEC gated: the echo-cancelled
PipeWire source preference and WASAPI's Communications stream category.
The env var still wins, one-way — it can only turn AEC off, never back on
— and both `aec_enabled` helpers say so. The row ships in the GTK, WinUI
and console settings, under the microphone toggle and greyed out while it
is off, matching what Apple and Android shipped in wave 1.

SettingsOverlay grows `echo_cancel` as a first-class field — apply,
absorb, clear, is_empty — instead of riding the `extra` passthrough, where
`clear_override("echo_cancel")` answered false. The JSON key is the one
Apple and Android already write, so one catalog round-trips through all
three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 23:07:30 +02:00
enricobuehlerandClaude Fable 5 fd75e66041 feat(windows): the shell spawns from the shared brain, and links, tests and pairing stop losing things
Windows-shell parity gaps from the 2026-07-31 sweep, closed:

* The spawner goes through the shared brain: `ConnectPlan::for_target` →
  `session_args()` + a written `--resolved-spec`, replacing the
  hand-assembled argv that meant Windows sessions ALWAYS took the compat
  path (re-resolving every setting from the stores — the drift
  orchestrate.rs documents as a trap) and that any field added to the
  spec was silently Windows-dead. Fullscreen now comes from the plan's
  effective settings (profile-aware) instead of a caller argument, the
  spec temp file is cleaned up at exit, and the reader finally parses
  the `{"window":…}` line so the SPAWNER persists the match-window size
  (§5) instead of the renderer's load-modify-save fallback.
* A deep link to an unpaired host runs the trust ceremony instead of
  refusing: the Pair screen opens seeded with what the link CLAIMED —
  name shown as claimed, fingerprint carried, and the launch/profile
  surviving the detour (`Target` grew a `launch` field for exactly
  this). GTK parity; a shared link was a dead end here before.
* The speed test learns the Ask tier: a bound host whose profile
  INHERITS bitrate now offers both "Set as default" and "Set in
  profile" instead of silently creating a profile override. The global
  write (and the forwarded-controller handler) also rebase on the file
  before their whole-struct saves — two more stale-snapshot writers.
* The Controllers card shows the detected-pad inventory (read-only,
  with the Steam-Input-virtual note) — the fastest answer to "is my
  controller even detected?", present on GTK all along.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:05:05 +02:00
enricobuehlerandClaude Fable 5 698925a036 fix(clients): settings saves stop reverting each other, Windows gets audio pickers, and a game link launches its game
The rest of the client-gaps handoff, plus what a parity sweep surfaced:

* Every whole-file settings save rebases on the file first — the GTK
  dialog close and its two speed-test apply arms, the Windows
  per-control commit, the console screen. The file has five writers and
  no merge (profiles.rs documents the debt); saving a shell-lifetime
  snapshot silently reverted whatever another writer stored meanwhile,
  most visibly the spawner-persisted match-window size (item 4).
* Windows honors the Speaker/Microphone picks: `audio_wasapi` grows
  endpoint enumeration (on its own MTA thread — UI threads are STA) and
  resolves PUNKTFUNK_AUDIO_SINK/SOURCE as endpoint ids, falling back to
  the default when the picked device is gone; the settings page gets
  the two rows (defaults scope, "(not detected)" like the GPU row).
  `speaker_device`/`mic_device` stop being Linux-only fields (item 5).
* A punktfunk:// link with `launch=` opened a plain desktop session on
  Windows — the id was parsed, validated, planned, and dropped at the
  last hop. It now rides `initiate_launch` (plus a waking variant for
  the dial-first path). Linux always forwarded it.
* The console UI offered "VAAPI" on Windows — a dead option there that
  also hid d3d11va, the actual Windows hardware path. The decoder list
  is per-OS now.
* The Windows gamepad picker learns "Steam Deck" (the GTK picker had
  it; the host-side pad has always existed).
* Doc fixes: gpu.rs named a nonexistent env var (PUNKTFUNK_ADAPTER →
  PUNKTFUNK_VK_ADAPTER), and session/Cargo.toml claimed video decode is
  Linux-only while explaining what the ARM64 leg drops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:21:26 +02:00
enricobuehlerandClaude Fable 5 a9d3ca2fb1 feat(client/windows): the sheets learn to be dismissed, and the profile sheet says Save
Escape closes every overlay — the profile sheet, the host editor, the add-host modal —
and so does a tap on the scrim. The scrim-tap needed one trick: WinUI bubbles `Tapped`
out of the card into the scrim (reactor cannot mark it handled), so the card raises a
shared flag first and the scrim's handler swallows exactly that tap; only a genuine
outside tap dismisses. The profile sheet's dismiss button reads Save (with the Save
glyph) instead of Close — every field commits as you type, so Save is the promise the
button already kept, and a sheet full of edits wants a verb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 9614e1a92e fix(client/windows): the switcher becomes what it pretended to be — one native control
The fused combo-plus-pencil was three controls squeezed into a wrapper, and hover gave the
trick away (each kept its own radius and hover fill; the pencil clipped). The honest
verdict: WinUI can build a real input group — CornerRadius is per-corner on every control —
but windows-reactor exposes only a uniform radius on Border, so the fake was never going
to hold. The native answer was next to it all along: the switcher is now ONE
DropDownButton — its label is the scope in play, its menu holds Default settings, the
profiles, "New profile…" and "Edit …" — with one hover state and no seams, and the pencil
folded into the menu. Retired with the ComboBox: the items/selected_index remount hazard
(a button label is one plain prop) and the scope-sentinel id.

UIA-verified: menu -> Work applies the scope (the section repaints, markers included),
menu -> Edit opens the sheet, Close closes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 6703be377f feat(client/windows): pinning moves to the tile menu, and the chrome quiets down
Pin/unpin lives in the tile's "…" menu now (review decision, reversing the editor-owns-it
rule): one flat prefix-matched entry per profile — "Pin tile: X" / "Unpin tile: X" —
beside Copy link and Create shortcut, the other tile-shaped actions. The write is paired
with a new `hosts_rev` bump (the hosts-page mirror of `settings_rev`), so the pinned tile
appears — or vanishes — in the same gesture instead of on the next discovery tick.
UIA-verified: menu -> Pin tile: Work -> the pin is in the store and the second tile is on
the grid immediately.

The rest is the review list:
* Both sheets (host editor, profile) put their content in a scroll_view — a window shorter
  than the card scrolls it instead of clipping the bottom controls.
* The home header keeps ONE labelled action: Add host, in accent. Console/Shortcuts/
  Settings drop to icons with tooltips — four written-out buttons read as four competing
  calls to action, and icon-only removes the compact-width special case too.
* The profile switcher becomes one combined element: combo + pencil (icon only, the label
  is gone) inside a shared control-look wrapper — stock 4-epx radius, CardStroke outline,
  the pencil a borderless segment. In the defaults scope the bare combo stands alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 8783f11e90 fix(client/windows): pinning works where you can see it, and the rows learn one skeleton
"Can't pin hosts with a certain profile" — live diagnosis found the pin switches WORKED
but sat below the fold: the host editor rendered as an in-grid tile, its grid cell parked
mid-page, and on an ordinary window the pin section's visible rect was a 9-px sliver a
mouse can't hit (UIA's pattern-toggle, which bypasses hit-testing, persisted fine — that
is what isolated it). The editor is now a centred sheet — scrim + card, titled with the
host, Save/Cancel right-aligned — in a stable overlay slot beside the add modal and the
forget dialog. Verified end-to-end with a real mouse click: toggle flips, the pin lands in
client-known-hosts.json, and the pinned tile is on the grid after Save. Pin toggles also
log now — this hunt was blind until they did.

The settings rows learn one skeleton, label / (marker) / input / caption, per review:
* The Overridden capsule ("Overridden │ Reset", one tinted pill, whole-pill tap target)
  sits BETWEEN the row's label and its input, left-aligned — not trailing controls of
  wildly different widths, not mixed into the caption.
* Labels move out of the widgets (`.header` is gone from the row builders) into the row
  itself — a widget-embedded header allows nothing between label and box, and uniform
  6-epx row spacing needs the row to own all four lines.
* The scope bar's combo and Edit button share an explicit 36-epx height, and the bar shows
  the active profile's colour as a 12-epx chip beside the combo (dropdown items are plain
  strings in this toolkit, so the chip cannot ride inside the list).
* The sheet's colour swatches size the disc itself, not its child — they rendered as
  squashed ovals under the sheet's layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:10 +02:00
enricobuehlerandClaude Fable 5 ba7667ee2b fix(client/windows): dialogs stop panicking the shell, and the chrome stops shouting
Deleting a profile reliably killed the app with E_BOUNDS ("Daten außerhalb des gültigen
Bereichs"): a ContentDialog is a PHANTOM child in the reactor backend — tracked logically,
never attached to the panel — and the reconciler unmounts a child before removing it, so
by the time `remove_child` runs the dialog's handle is gone, the backend no longer
recognises it as phantom, and it RemoveAt()s a visual child that never existed. (The third
upstream windows-reactor bug this client documents.) Both confirmation dialogs — delete
profile, forget host — are now ALWAYS MOUNTED with `is_open` doing the arming, and both
live in STABLE overlay slots (settings: [nav, sheet slot, dialog]; hosts: [page, add-modal
slot, dialog], each closed slot a same-kind background-less Border) so no pass ever
removes or repositions a dialog. UIA-verified: create profile -> delete -> confirm, the
shell survives.

The rest is the review feedback:
* A group with no fields renders NOTHING — Decoding and Library showed a heading over an
  empty card in profile scope (device facts, never per profile).
* The override marker leaves the caption: a small "Overridden" chip and the Reset button
  sit beside the control, bottom-aligned; the description below stays a description,
  identical in both states.
* Host tiles stop wearing three badges: Paired is the resting state and earns no chip
  (a chip is for a decision — Trusted/PIN/Open), and the bound profile becomes a small
  dot in its accent colour plus the name in caption text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:10 +02:00
enricobuehlerandClaude Fable 5 7bbee0eeb6 feat(client/windows): the profile selector settles above the settings surface
Out of the NavigationView for good. The pane footer clipped in the compact rail, the
clip-aware monogram variant read as broken, and reactor offers no pane-opened event to
adapt on — so the switcher is now a slim bar riding an Auto grid row above the nav: an
"Editing" label, the scope combo, and Edit profile… (with its glyph), each vertically
centred against the combo and sharing the content column's 24/28 page margins. Visible
from every section at every width, in no one's way, and the nav below keeps its Star row
so the surface still fills the window. The sheet drops its embedded scope combo — the bar
owns the choice now — and opens only with a profile in scope.

UIA-verified live: scope switch from the bar repaints in place (marker 5 -> 6), the sheet
opens from the bar and closes, Default scope restores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 69743e3a09 feat(client/windows): the profile surfaces get their icons
Reset carries Undo, Edit profile carries Edit, Duplicate/Delete/Close carry Copy, Delete
and Accept — the icon support the bumped reactor added (#4736), applied where a glyph
disambiguates faster than a word. The rest of the shell already had its icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 e8eb803a3f feat(client/windows): 4:4:4 reaches Windows, and the Overridden marker stops moving the page
The GTK client grew its "Full chroma (4:4:4)" switch in the same commit that fixed its
override marker; Windows got neither until now. The row lands in the Quality group under
HDR with the GTK wording verbatim (same setting, same constraints), a full overridable
profile row — the core overlay and the session binary already spoke `enable_444`, so the
shell was the only gap. OverrideFlags grows the matching flag, and a unit test now pins
the whole overlay -> row-flag mapping (tri-state resolution included): a field that
records without marking its row is the original Overridden-row bug wearing a new face.

And the marker obeys the GTK rule learned the hard way there: nothing that appears on an
edit may move what was edited. Both states of a profile-scope row reserve the same
caption-line height, so the marker + Reset materialise in place instead of shoving every
row below them down by a button's height.

Scope-blind audit (the bitrate-box failure class): clean. Decoder and GPU are hidden in
profile scope as device facts (GTK parity), the forwarded controller stays gated, and
every visible profile row commits through `commit`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 09d1691af4 fix(client/windows): the switcher footer survives the pane clip instead of guessing at it
Binding the footer's form to the WINDOW width was wrong twice over: the pane's open state
belongs to the user (the hamburger), so a wide window with a closed pane still clipped the
combo, and a narrow window with the pane opened still showed the compact disc. Reactor
exposes no pane-opened/closed event — so the footer now needs no pane state at all: its
LEADING 48 epx is the scope's monogram disc (profile accent when set) and the combo + Edit
button sit after it, so the compact rail's clip line falls exactly between them. Closed
pane → a clean disc (which opens the profile sheet); open pane → disc + combo. The same
trick NavigationViewItems use, at every width and in every mode — the display mode goes
back to Auto, the width threshold and the root window-size read are gone.

The sheet is now self-sufficient (title "Profiles"): it always carries the scope combo —
whether the pane's own combo is visible depends on state this page cannot observe — plus
the name/colour/duplicate/delete rows whenever a profile is in scope, so the disc is a
complete profile entry point from the bare rail.

UIA-verified: scope pick from the pane combo, Overridden marker 5 -> 6 in place, the
sheet opens from Edit profile… and closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:49 +02:00
enricobuehlerandClaude Fable 5 4b5bca93a0 fix(client/windows): the scope switcher stays in the nav at every width
The width-adaptive fallback moved the switcher OUT of the pane on narrow windows, and in
the pane it could still clip — both read as broken. Now the nav is the switcher's only
home: `Left` mode (always-expanded 280 pane, combo sized to fit) while the content column
gets a workable width, and below that a `LeftCompact` rail whose footer swaps to a fitting
compact form — a 40-square monogram disc for the scope (profile accent when set) instead
of a clipped ComboBox. The disc opens the profile sheet, which in compact mode carries the
scope combo too (keyed per scope against the items/selected_index diff hazard), so
switching, creating, and editing profiles all stay reachable from the rail. The inline
content-column fallback is gone.

UIA-verified on the expanded path: scope pick from the pane, Overridden marker 5 -> 6 in
place, sheet opens and closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:49 +02:00
enricobuehler 27039e04ff Revert "refactor(client/windows): page-only state climbs back down from root, on the measured rules"
This reverts commit 427f99a06647fd99e5c72d18cb7a710d8a7ac885.
2026-07-29 18:31:49 +02:00
enricobuehlerandClaude Fable 5 69c13b5aae fix(client/windows): the settings pane stops auto-collapsing under the default window
WinUI Auto mode collapses the NavigationView pane below 1008 epx — narrower than the
app's own 1000-wide default window, so the scope switcher kept getting booted out of the
nav into its inline fallback. The page now forces the mode: Left (always expanded, at a
tighter 250 pane length) down to 720 epx, LeftMinimal below that — the switcher lives in
the pane wherever the content column still gets a workable width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00
enricobuehlerandClaude Fable 5 6ab2ee4819 refactor(client/windows): page-only state climbs back down from root, on the measured rules
The root component had absorbed every page's UI state because of re-render
folklore; the characterization suite (tests/reactor_semantics.rs) replaced the
folklore with measured rules at the current windows-reactor pin: a child's sync
use_state re-renders its owner reliably, including from backend-wired event
handlers (MenuFlyout clicks, pointer enter/exit, ComboBox selection) and under
element-equal wrappers. Only a background thread's AsyncSetState write is still
dropped. So user-event-driven page state moves into the pages that own it:

* hosts_page: forget, rename and hover become local sync use_state; the
  HostsProps fields, their PartialEq arms and the root plumbing are gone.
* settings_page: converted from a hook-free 14-parameter fn into a real
  component(...) with a SettingsProps (data-only PartialEq, the HostsProps
  pattern). Scope, the delete confirmation, the edit-modal flag and the repaint
  revision are its own sync use_state; commit() and the row helpers take the
  sync setter now.

What deliberately stays rooted, each with its reason in place: show_add and the
settings section drive root tween workers (a thread writing root async state
can only be started by the owner of the trigger), the tween progress values are
those workers' output, and everything thread-fed (discovery, HUD, probes,
pads, deep links, speed, library) keeps the async-drop rule. window size stays
a root read so resize re-renders the tree.

One deliberate behaviour shift: page-local state now resets when its page
unmounts, so re-entering Settings lands on the Default settings scope (the
section still persists, being root state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00
enricobuehlerandClaude Fable 5 9907ce6229 test(client/windows): the re-render rules become measured, and the switcher survives a collapsed pane
Adopts windows-reactor's own headless harness (the `test` feature + the upstream repo's
`test_reactor`/RecordingBackend crate, same pinned rev) and pins the three semantics this
client's state placement depends on, in tests/reactor_semantics.rs:

* A child's sync `use_state` re-renders it through element-equal wrappers — the pre-bump
  reconciler pruned at the wrapper; fixed upstream (`nested_state_rerender.rs`).
* A sync write from a backend-fired event handler re-renders — the "MenuFlyout handlers
  bypass the flush" claim does not hold at the engine level on this rev.
* A child-owned `AsyncSetState` write still does NOT re-render its owner — newly diagnosed:
  every component RenderCx draws a fresh HostId but rerender callbacks are registered only
  for the root, so the child's request is a registry miss, silently dropped; the value
  surfaces on the next unrelated pass. The test asserts the broken behaviour on purpose, so
  the bump that fixes it upstream turns the test red and the discipline can be relaxed.
  (Upstream's own async test installs the guard for the very cx it tests and cannot see
  this; worth reporting with this repro.)

The stale discipline note in app/mod.rs — written against the June rev — is replaced by the
measured rules, each citing its test. Net: per-screen EVENT-driven UI state may live in the
screen's own component; THREAD-driven state stays hoisted at root, now for a proven reason.

Riding along (field report from the live review): the scope switcher no longer clips when
the NavigationView collapses. WinUI's Auto mode shrinks the pane below 1008 epx and CLIPS
pane-footer content to the rail; reactor exposes no pane-opened event, so root now reads
the window size and the switcher follows the same threshold — expanded pane → pane footer,
collapsed → a slim row leading the content column (an Auto/Star grid row, not a vstack,
so the scroll viewport survives).

Test builds use their own CARGO_TARGET_DIR by convention: windows-reactor-setup stages
resources.pri next to the test binary and a RUNNING dev client holds that file mapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00
enricobuehlerandClaude Fable 5 1c1b81133c fix(client/windows): the settings surface fills the window again
The redesign left the page rooted in a vstack, and a StackPanel hands its child the
DESIRED height — the NavigationView was clipped in a short window and floated in a tall
one. The nav, the Edit-profile scrim and the delete ContentDialog now share one grid, so
every layer stretches with the window (the dialog is its own WinUI layer and only needs
to be in the tree).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00
enricobuehlerandClaude Fable 5 6b956a2bf4 fix(client/windows): the Overridden marker appears on edit, and profile chrome leaves the page
Committing a profile-scope edit wrote the catalog and nothing else — no reactor-observable
state changed, so no render pass ran, OverrideFlags was never recomputed, and the
"Overridden here" row only surfaced after some unrelated navigation. The mirror of the
Linux fix ("the override marker appears on touch"): `commit` now ends every save with a
`settings_rev` bump, threaded through `setting_combo`/`setting_toggle`, so the marker lands
in the same pass that recorded the override. The bitrate box goes through `commit` too —
it wrote the GLOBAL settings regardless of scope, which corrupted the defaults from inside
a profile and could never mark its row.

The profile chrome moves out of the content column while its wiring is open. The scope
switcher sits in the NavigationView's pane footer (`pane_footer`, new in the bumped
reactor) where it reads as navigation chrome — which layer am I editing — instead of a bar
of controls above every section. Rename, colour, duplicate and delete hide behind an
"Edit profile…" modal (the Add-host scrim-and-card pattern; ContentDialog still fits no
text box), and "New profile…" lands straight in that modal to be named. The footer combo
is keyed by scope + name list so a rename or delete remounts it past the documented
items/selected_index diff hazard; renames repaint the dropdown on modal Close, not per
keystroke.

The absorb-level semantics (record the touched field against the effective snapshot; a
value equal to the global is still a pin) were already covered by pf-client-core's
`absorb_records_the_touched_field_only`; render-level tests wait for the reactor test
harness. Verified live via UIA on the running shell: with the "Game" profile in scope,
changing Render scale grew the marker count 5 -> 6 in place with no navigation, the
override landed in client-profiles.json, the global settings file stayed byte-identical,
and the Edit modal opens and closes from the pane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:47 +02:00
enricobuehler dff61ecea2 fix(client/windows): the scope switcher leaves the page content, and the polish from the GTK round comes across
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 36s
ci / bench (push) Failing after 6m40s
deb / build-publish-client-arm64 (push) Failing after 5m55s
ci / rust-arm64 (push) Failing after 7m3s
arch / build-publish (push) Failing after 7m26s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 36s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 31s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 32s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m0s
apple / swift (push) Successful in 5m9s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m53s
deb / build-publish-host (push) Successful in 10m36s
docker / build-push-arm64cross (push) Successful in 13s
android / android (push) Successful in 12m36s
docker / deploy-docs (push) Successful in 25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m21s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m0s
deb / build-publish (push) Failing after 6m54s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 1m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m17s
ci / rust (push) Failing after 8m37s
flatpak / build-publish (push) Failing after 8m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m54s
apple / screenshots (push) Successful in 20m39s
Four things the Linux review found, applied here without waiting to re-find them:

The profile switcher was rendered as the first group INSIDE each section's
content, so on About — a page with barely any rows — it read as an About
setting, and on every other page it read as one more row. It is chrome: which
layer am I editing. It now sits in its own card above the NavigationView,
visible from every section and never mistakable for a setting.

Profiles get their colour here too, from the same eight-swatch palette as GTK,
and the chip on a host tile is tinted with it — a profile that is red on Linux
is red on Windows. An unparsable accent falls back to the neutral chip rather
than being handed to the renderer.

The tile flyout had become a list of everything, with connect/library/speed
buried under list management. Two rules thin it: anything that CONFIGURES the
host moves into the editor — the pinned tiles join the default profile and the
clipboard toggle there, since a pin is a property of the record — and what
remains is grouped by separators, so a glance lands on the right third.

Compiled on .221; the last one-line clippy fix went in after the box dropped.
2026-07-29 12:38:53 +02:00
enricobuehlerandClaude Opus 5 f5de661c9e feat(client/windows): overridden rows are marked and resettable, and the speed test targets the right layer
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
decky / build-publish (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
android / android (push) Canceled after 0s
The two remaining pieces of P1 on Windows, both closing the same kind of gap the Linux
client closed earlier.

Overridden rows in profile scope now say so — an accent-marked caption — and carry the
only way back to inheriting. Without it a profile is a one-way door here too, because
overrides are recorded on touch and never inferred from comparing values. Reset calls the
shared `SettingsOverlay::clear`, so the list of what can be cleared stays in one place
across both shells. It also bumps a revision counter: the catalog changes behind the
controls and nothing the page reads as state does, and root state compares before
re-rendering, so without the bump the row would keep showing the value it just dropped.

The speed test writes to the layer the TESTED host reads bitrate from, not the global —
measuring one host used to re-tune every other one. Resolved exactly the way a connect
resolves it: the one-off the test was started with (a pinned tile carries one), else the
host's binding, else the global; a dangling binding falls back. The button names its
target, so it says where it will write before you press it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:21:13 +02:00
enricobuehlerandClaude Opus 5 b220daf537 feat(client/windows): the settings screen edits profiles too
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m9s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m30s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m53s
apple / swift (push) Successful in 5m13s
ci / docs-site (push) Successful in 1m4s
ci / web (push) Successful in 3m3s
ci / bench (push) Successful in 5m43s
deb / build-publish-host (push) Failing after 6m17s
arch / build-publish (push) Failing after 9m36s
ci / rust-arm64 (push) Failing after 9m41s
ci / rust (push) Failing after 9m44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 32s
decky / build-publish (push) Successful in 43s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 24s
deb / build-publish (push) Successful in 8m52s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
android / android (push) Canceled after 13m2s
deb / build-publish-client-arm64 (push) Canceled after 6m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 2m47s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m42s
P1 on the WinUI shell. The same surface now edits either layer, switched by a combo above
the section title — one settings UI, not a second editor that drifts from the first.

The interesting difference from GTK is where the override gets recorded. This shell
commits PER CONTROL, so it cannot hand over a list of touched rows the way a
commit-on-close dialog can. Instead each control hands `SettingsOverlay::absorb` the
effective settings before and after it fired, and absorb records the field that moved.
The comparison is against what the control was SHOWING, not against the globals, so
setting a value to whatever the global happens to be still records an override — the pin
the design asks for — and nothing is ever removed by inference.

⚠️ The content key becomes `(scope, section)`. Keyed on section alone, switching scope
diffs one scope's controls into the other's, which re-sets each reused ComboBox's items
(clearing WinUI's selection) while skipping `selected_index` wherever the two scopes'
values compare equal — the combos then render blank until touched. That is the same trap
the section key already existed for, one axis wider.

Only profileable rows render in profile scope: auto-wake, the library toggle, the decoder
and GPU pickers and the forwarded-pad picker are facts about this device or this network,
not about "Game vs Work".

Two shapes follow from this toolkit rather than from the design. Creating a profile takes
an auto-numbered name and is renamed from the row below, because ContentDialog here is
text-only and cannot hold a text field — the same constraint that put the host editor in a
tile. And Delete arms a root state that renders the confirmation declaratively, since
dialogs here are elements with `is_open`, not calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:12:43 +02:00
enricobuehlerandClaude Fable 5 df04880273 feat(client/input): desktop (absolute) mouse mode — remote-desktop sweep M1
New physical-mouse model beside capture: Desktop leaves the pointer
uncaptured and sends absolute positions through the letterbox
(MouseMoveAbs — every host injector already consumes it). Capture
stays the default and the game model.

- pf-client-core: MouseMode (capture|desktop) persisted in Settings,
  default capture so existing stores are unchanged.
- pf-presenter: Capture grows a desktop model — latest-wins pending
  abs position coalesced per loop iteration (same 1000 Hz discipline
  as relative), flushed before clicks/keys/wheel so they land where
  the cursor is; Ctrl+Alt+Shift+M flips the model live; local cursor
  stays hidden over the window (the host's composited cursor is the
  one you see until the M2 cursor channel); Windows keyboard grab
  only engages for capture (a desktop stream is something you
  Alt-Tab away from).
- gamescope gating: its EIS is relative-only, so desktop mode is
  pinned off there (resolved_compositor), with a log note.
- Settings surfaces: GTK row (dynamic caption), WinUI combo,
  console-UI row + step test, capture-hint line.

Verified: fmt + clippy -D warnings + tests (33/40/19) on Linux .21;
clippy -D warnings for all five crates incl. punktfunk-client-windows
native on .173.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:37:32 +02:00
enricobuehlerandClaude Fable 5 a042dc2850 fix(client/windows): settings combos render blank until touched
The section-switch key was moved onto the scroll_view's direct child during
the parity restructure, which silently disabled it: ScrollView::children()
is Children::PositionalSingle, reconciled positionally with keys ignored.
The column was therefore reused across section switches, diffing one
section's controls into another's -- which re-sets a reused ComboBox's items
(clearing WinUI's selection) but skips selected_index whenever the two
sections' values compare equal, so every combo rendered empty until changed.

Put the keyed column back inside a panel's child list, where the keyed diff
path actually runs and the switch remounts. This is the same bug the
original code carried a comment about; the comment now names the
PositionalSingle trap that makes the key placement load-bearing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 67d2964db9 fix(client/windows): render the settings category title in the content column
NavigationView.Header carries WinUI's own left inset -- the reactor exposes
it as a string prop, so the template's padding is not ours to move, and the
title sat visibly right of the cards beneath it. Render the title as the
first element of the content column instead, where it shares the cards'
left edge by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 344618e1f0 fix(client/windows): flush the settings headings with the cards below
The shared section() helper carries a 2px left inset -- right for the
hosts/licenses lists it was written for, but in Settings it left every
sub-section heading hanging one nudge right of its card's edge. Use a
settings-local heading pinned flush left, and drop the same inset from the
footer notes, so heading, card and note all share one left edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 51f7ff1b3f feat(client/windows): invert scroll direction
The Apple client's toggle, wired through the shared session presenter:
Settings::invert_scroll -> SessionOpts -> Capture, applied at the single
seam where wheel deltas enter (before accumulation, so the fractional
remainders stay consistent with what was actually sent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 7a1809547b fix(client/windows): left-align setting captions, drop the width cap, add auto-wake
Three follow-ups to the parity sweep:

- Captions read centred: a TextBlock defaults to Stretch alignment, so the
  MaxWidth cap left it floating in the middle of the leftover width instead
  of sitting under its control. Pin them (and the footers) Left.
- The settings column kept the shared page() 640px cap, which -- next to the
  NavigationView pane spending the left third -- squeezed the cards into a
  narrow ribbon. Drop the cap here.
- Auto-wake on connect (the Apple client's toggle) is now a setting rather
  than unconditional behavior. Default on, which is what it did before.
  Gating the flow's ENTRY, not its packet sends: with it off a failed dial
  just fails, instead of showing a "Waking..." wait that would never send a
  packet to end itself. The explicit "Wake" host-card menu action is
  deliberately NOT gated -- that is the user asking directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 72583b272e feat(client/windows): settings parity with the Apple client's revamp
Adopt the Apple client's 2026-07 settings structure so the two desktop
clients read the same way. Category map is now theirs verbatim -- General
(session/app behavior), Display (everything about the picture), Input,
Audio, Controllers, About -- which moves four things: statistics and the
game-library toggle out of Video/About into General, controllers out of
Input into their own category, and the codec/bitrate/HDR/decoder controls
out of the old Windows-only "Video" tab into Display, where Apple keeps
them. Sub-section headers (Resolution / Quality / Decoding / Host output)
mirror their Sections, as do the two "applies from the next session"
footers.

Every field now carries its explanation directly under it instead of only
in a hover tooltip -- the same move Apple made, for the same reason. The
old comment here argued against prose in the card because a paragraph
ABOVE a control reads as its label; that holds, so the caption goes below,
which is what Apple does and what Windows Settings itself does. Wording is
shared verbatim wherever a setting means the same thing on both platforms.
Where behavior differs the text stays Windows-specific -- notably the
forwarded-controller picker, where Apple forwards one pad as player 1 and
this client forwards every controller as its own player.

Two fixes fell out of the sweep: the decoder picker never offered D3D11VA
even though the chain accepts it (and 40030e90 made it Intel's default), and
settings opened on Display rather than the first sidebar item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 1ef0229bd9 feat(client/windows): PyroWave decode + surface it in the GUI codec picker
The decoder was gated to Linux because, when it landed, the Windows client
still had its own in-process WinUI/D3D11 presenter and the PyroWave present
path there was an open question. That client has since been retired: Windows
now spawns the SAME Vulkan session presenter as Linux, and the decoder is
plain Vulkan compute on the presenter's device (no fds, no dmabuf, no D3D11
interop), so the question that gated it answered itself. pyrowave-sys already
builds on Windows too -- the Windows HOST encoder ships on it.

So this is a port by un-gating: every cfg(all(target_os = "linux", feature =
"pyrowave")) becomes any(linux, windows) -- decoder module, backend variant,
Decoder::new_pyrowave, the CODEC_PYROWAVE advertisement, the session pump's
opt-in/build/label arms, and the presenter's planar CSC pass. No new code.

Then offer it in the Windows GUI, which is what prompted this. It stays
preference-only (resolve_codec never auto-picks it) and a host or device that
can't do PyroWave just falls back down the ladder to HEVC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 871ebb31ce feat(clients): render-scale setting on every client — shared punktfunk_core::render_scale
Client-side supersampling/downscaling: the client asks the host to render
and encode at chosen-resolution × scale (the host does no scaling) and the
presenter rescales the decoded frame to the display. >1 supersamples for
sharpness; <1 lightens the host GPU and the link. Default 1.0 = Native, the
prior behavior.

The geometry lives once in punktfunk_core::render_scale (multiply, preserve
aspect ratio, floor to even, clamp to the codec's per-axis ceiling — 4096
for H.264, 8192 otherwise), the Rust twin of the Apple client's
RenderScale.swift, consumed by the native session client, the presenter's
match-window path, the Windows/Linux settings UIs, Decky, and Android
(settings + host connect + unit test).

Implemented and platform-verified by the Apple-client-features session
(Linux+Android+Apple green there); the punktfunk-core wiring
(pub mod render_scale) is restored here after being lost in a working-tree
reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:15 +02:00
enricobuehlerandClaude Opus 4.8 f88d0ae4dc feat(touch): cross-client touch-input modes on Linux + Windows
ci / web (push) Successful in 43s
ci / rust (push) Failing after 53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m53s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 52s
apple / swift (push) Successful in 4m41s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 54s
ci / bench (push) Successful in 6m30s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
Bring the SDL presenter (Linux/Deck + Windows) to parity with the Android and
Apple clients: a persisted TouchMode selects how a touchscreen drives the host —

  * Trackpad (default): relative cursor with pointer ballistics + the shared
    gesture vocabulary (tap = left click, two-finger tap = right click,
    two-finger drag = scroll, tap-then-drag = held left drag, three-finger tap =
    cycle the stats overlay).
  * Direct pointer: the cursor jumps to and follows the finger (absolute).
  * Touch passthrough: every finger is a real host touchscreen contact.

Previously the presenter had no finger handling, so SDL synthesized mouse events
from touch and — under the stream's relative-mouse lock — walked the host cursor
into the corner (the reported Deck bug). SDL touch->mouse synthesis is now off;
DIRECT touchscreens route through a new incremental gesture engine (a port of
Android TouchInput.kt / Apple TouchMouse.swift), while INDIRECT trackpads keep
driving the mouse. Fingers map through the aspect-fit letterbox onto the content
rect.

TouchMode lives in the shared trust::Settings (default trackpad, so passthrough
is opt-in like the other clients); the GTK and WinUI settings screens both gained
a "Touch input" picker. Gesture engine, letterbox mapping, and settings
back-compat are unit-tested (28 tests green); clippy -D warnings clean; full
Linux client + session build verified on-host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:51:29 +02:00
enricobuehlerandClaude Opus 4.8 ef5808254a refactor(windows): remove the legacy in-process builtin stream path
The real Windows client is the spawned punktfunk-session Vulkan binary
(pf-client-core); the in-process builtin GUI stream — reachable only via
PUNKTFUNK_BUILTIN_STREAM=1 — was dead weight kept alive by nothing and a
recurring source of wasted effort. Remove it: delete present/render/input/
audio.rs and the builtin remainder of session/video.rs, rip all the builtin
wiring (app/mod, connect, stream), and make connect always spawn.

Preserve the two shipped keepers that happened to live in those files by
relocating them to a new probe.rs: run_speed_probe (the per-host network speed
test used by the Settings speed page and --headless --speed-test) and
decodable_codecs (the codec-capability advert on the probe connect). Trim gpu.rs
to just the Settings adapter picker (adapter_names + helpers). --headless now
supports only --speed-test — the in-process decode/frame-counter went with the
pump.

Drops the now-orphaned deps opus, wasapi, crossbeam-channel, anyhow; keeps
ffmpeg-next (probe::decodable_codecs still needs it). Net 4432 deletions.
Statically verified (module wiring, imports, orphaned symbols/deps all clean);
the type-level compile runs on the windows-amd64 CI runner, which has the
toolchain this non-Windows host lacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:22:09 +02:00
enricobuehlerandClaude Opus 4.8 9514a8c0e2 fix(client): correct Linux/Windows "Forwarded controller" copy for multi-controller
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 56s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
decky / build-publish (push) Successful in 52s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 4m39s
ci / bench (push) Successful in 6m27s
windows-host / package (push) Successful in 8m34s
flatpak / build-publish (push) Successful in 7m15s
docker / deploy-docs (push) Successful in 24s
android / android (push) Successful in 12m49s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m16s
deb / build-publish (push) Successful in 12m50s
arch / build-publish (push) Successful in 16m32s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m53s
ci / rust (push) Successful in 25m18s
release / apple (push) Successful in 22m3s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m57s
apple / screenshots (push) Successful in 18m56s
The relm4 (Linux) and native (Windows) shells spawn punktfunk-session, which
since the native-plane rework forwards ALL controllers by default — but the
"Forwarded controller" settings dropdowns still described the pre-rework world
("Automatic (most recent)", "Exactly one controller is forwarded to the host").

The dropdown already lists every detected pad and wires set_pinned(None)=all /
set_pinned(key)=single-player; this fixes only the misleading labels, subtitle,
tooltip, and stale leading comments to match: Automatic forwards every real
controller (each its own player); pick one to force single-player.

cargo check -p punktfunk-client-linux green; Windows is windows-gated (pure
string edits, CI windows.yml).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:53:07 +02:00
enricobuehlerandClaude Opus 4.8 a95b518ef3 fix(windows): show app version on About screen + capitalize Punktfunk on licenses
ci / docs-site (push) Successful in 51s
ci / web (push) Successful in 57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
decky / build-publish (push) Successful in 19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m1s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m3s
apple / swift (push) Successful in 5m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m25s
ci / bench (push) Successful in 8m10s
docker / deploy-docs (push) Successful in 20s
arch / build-publish (push) Successful in 11m11s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m47s
android / android (push) Successful in 16m10s
deb / build-publish (push) Successful in 16m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m47s
ci / rust (push) Successful in 23m26s
apple / screenshots (push) Successful in 19m57s
The About settings card had no version at all — add an identity block (app name +
"Version <CARGO_PKG_VERSION>", the workspace version) at the top, the WinUI
Settings convention and matching the Apple client's "Version X" wording. Also
capitalize the brand name on the licenses screen (was lowercase "punktfunk").

Verified against the pinned windows-reactor source + cargo fmt --check; full
Windows link left to CI (Windows-only crate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 02:46:45 +02:00
enricobuehlerandClaude Fable 5 d0d9bd5bfb feat(resize): mid-stream resolution resize — host hardening (H1–H5) + session-binary Match window (C1)
design/midstream-resolution-resize.md Phase 0 + Phase 1.

Host (Phase 0):
- H1/H5: per-backend Reconfigure acceptance gate — reject for gamescope
  (all sub-modes; a resize must never relaunch the title) and under the
  per-client-mode identity policy (a resize would resolve a different
  display slot). Synthetic stays reconfigurable on purpose (the protocol
  test source; the C-ABI roundtrip test rides it). Plus a 500 ms host-side
  min-interval backstop against Reconfigure spam.
- H2: rollback/corrective ack — the data plane reports the mode actually
  live after a failed rebuild (or a refresh the backend capped) through a
  reconfig_result channel; the control task forwards it as a second
  accepted Reconfigured so the client's mode slot self-corrects.
- H3: live stats mode — SendStats reads a packed AtomicU64 (w|h|hz)
  updated on every switch instead of latching the session-start mode.
- H4: registry::retire(gen) — a mode-switch rebuild force-releases the
  superseded Linux display, so linger/forever keep-alive policies don't
  accumulate kept monitors at stale modes. VirtualOutput carries pool_gen
  (fresh AND reused) and the Pipeline tuple threads it to the switch arm.

Client (Phase 1, default off):
- Settings: match_window policy + persisted last window size; exposed as
  the Resolution tri-state (Native / Match window / explicit) in the Skia
  console, GTK and WinUI settings pages.
- pf-presenter: window opens at the persisted size; Hello mode follows the
  window's pixel size; D2 trigger discipline (400 ms debounce to
  resize-end, ≥1 s spacing, even-floor + ≥320×200 clamp, each distinct
  size requested at most once — covers rejects and host rollbacks) as a
  pure, unit-tested decision; HUD line + title refresh on a switch.
- Session binary wires both --connect and --browse paths; the WinUI shell
  is session-always, so this covers Windows too.

Verified: workspace tests + clippy green; synthetic --remode end-to-end;
live session-binary run (window at persisted 1000×600 → Hello 1000×600@60).
On-glass per-backend matrix (Mutter/KWin/gamescope-reject, keep-alive
accumulation) still pending before any default flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:53:47 +02:00
enricobuehlerandClaude Fable 5 38b9f310e2 feat(clients): tiered stats overlay everywhere — Compact/Normal/Detailed on every platform
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m10s
ci / web (push) Successful in 1m12s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m46s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m57s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 53s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m10s
ci / bench (push) Successful in 5m43s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
release / apple (push) Successful in 8m10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 11m59s
android / android (push) Successful in 13m3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m39s
docker / deploy-docs (push) Successful in 18s
apple / screenshots (push) Successful in 5m32s
deb / build-publish (push) Successful in 14m54s
ci / rust (push) Successful in 23m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m53s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m44s
flatpak / build-publish (push) Failing after 35s
Ship the Android client's 3-tier stats-overlay semantics in every other client
(design/stats-unification.md vocabulary): Off → Compact (one line: fps · e2e ms ·
Mb/s + loss flag) → Normal (mode + e2e p50/p95 + loss counters) → Detailed
(decoder path, HDR tag, per-stage latency equation).

Apple: new StatsVerbosity in PunktfunkKit persisted under punktfunk.statsVerbosity
(migrates the legacy hudEnabled bool: explicit off → Off, else Normal). The
existing three-finger tap (TouchMouse, trackpad/pointer modes only — touch
passthrough untouched) now cycles the tiers instead of toggling, matching
Android; ⌃⌥⇧S (menu + captured-state monitor) cycles the same ladder. Tiered
StreamHUDView (compact glass pill / headline HUD / full equation HUD); the iOS
corner disconnect also shows in Compact (the pill carries no button). Tier
pickers on iOS, macOS, tvOS and the gamepad settings UI.

Session stack (Linux + Windows + Deck share punktfunk-session): shared
pf_client_core::trust::StatsVerbosity; Settings grows stats_verbosity with a
show_stats fallback, and writes keep the legacy bool in sync so pre-tier
binaries reading the same JSON agree on off vs on. Ctrl+Alt+Shift+S cycles the
tier and re-renders the OSD immediately from the last stats window; the stdout
stats: line always carries the full Detailed text so the shell status card and
scripts keep a stable shape; --stats bumps Off → Normal without demoting a
richer tier. Tier pickers in the GTK dialog, the WinUI settings page and the
console-UI settings row; shortcut copy updated (GTK shortcuts window, Windows
help, session README). The Windows legacy builtin path keeps its bool HUD.

Tests: tier migration/round-trip in trust.rs, tiered stats_text output in
pf-presenter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:42:46 +02:00
enricobuehlerandClaude Fable 5 f4850625bd feat(clients/windows): game library page + session window placement + help polish
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 4m56s
arch / build-publish (push) Successful in 5m56s
audit / bun-audit (push) Successful in 13s
audit / cargo-audit (push) Successful in 1m34s
ci / web (push) Successful in 1m11s
windows-host / package (push) Successful in 7m57s
ci / docs-site (push) Successful in 1m27s
release / apple (push) Successful in 8m39s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m26s
ci / bench (push) Successful in 5m0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m16s
apple / screenshots (push) Successful in 5m35s
ci / rust (push) Successful in 10m32s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m47s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m36s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 6m38s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m39s
flatpak / build-publish (push) Successful in 5m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m33s
docker / deploy-docs (push) Successful in 18s
UX polish batch 2 (plan workstreams D/E5/E6):

- D: a mouse/keyboard game library page over pf-client-core::library (the GTK
  ui_library.rs counterpart): responsive 2-6 column poster grid (2:3 portraits,
  store badge, monogram placeholder while art streams in), loading / error+retry
  / empty / grid states, tap-to-play via a normal spawn carrying --launch id.
  Poster bytes land in a disk cache (%LOCALAPPDATA%\punktfunk\art-cache) so the
  Image widget loads file:/// URIs (reactor has no from-bytes source) and
  revisits render instantly. Reached from a paired host's "Browse library..."
  menu item behind the new "Show game library (experimental)" Settings toggle
  (the shared library_enabled field, Apple/GTK parity). A Shared::library_gen
  generation guard keeps a superseded fetch from publishing (speed-test pattern).

- E5: the session window opens at the shell's own top-left (--window-pos, new
  SessionOpts::window_pos) instead of centered on the primary display - streams
  land on the monitor the shell is on, and the hide/restore handoff reads as one
  window changing content. Fullscreen follows that display.

- E6: the Help shortcuts add Alt+Enter and the controller escape chord
  (LB+RB+Start+Back, hold to disconnect).

Also: rustfmt normalization of the merged probe helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:43:46 +02:00
enricobuehlerandClaude Fable 5 573b2af334 feat(clients/windows): single-window handoff, shared settings store, console-UI surfacing
UX polish batch 1 (clients/windows/ui-polish-plan.md, workstreams W0/A1/B/C/E):

- W0: the shell's duplicated trust/settings structs are gone — src/trust.rs
  re-exports pf-client-core's, so the shell and the spawned session binary share
  ONE Settings shape over client-windows-settings.json. Fixes real bugs: the
  shell's saves no longer drop session-side fields; the stats-overlay toggle
  (show_hud -> show_stats, serde alias migrates old files) and the forwarded-
  controller pick now actually reach the spawned session. The "Streaming engine"
  Settings combo is gone (PUNKTFUNK_BUILTIN_STREAM=1 stays as the dev A/B knob).

- Forwarded-controller pinning by stable key (vid:pid:name, pf-client-core's
  format): persisted as forward_pad, applied by the shell's own service at
  startup AND by the session binary in session_params (both OSes — the session
  never applied the pin before).

- A1 single window: the shell hides itself when the spawned session window
  presents its first frame ({"ready":true}) and restores + foregrounds on the
  child's exit — exactly one visible Punktfunk window at any time. Restore runs
  before the request-access cancel gate so a Ready/Cancel race can't strand the
  shell hidden.

- C console UI: punktfunk-session --browse gains --json-status (ready when the
  library window presents; error line on a failed start), and the shell
  surfaces it — "Open console UI" on paired hosts' menus, plus a controller-
  detected hint card targeting the most recent paired host (x64 only; aarch64
  ships no skia ui feature). Browse spawns hide/restore the shell like streams.

- B responsive: minimum window size (420x360); the hosts header collapses to
  icon-only buttons below 700 px; the session status card shrinks instead of
  clipping (max_width); busy pages, help actions, licenses and long labels wrap.

- E polish: session exe gets the app icon (winresource + WM_SETICON via the new
  pf-presenter win32.rs); both exes share an explicit AppUserModelID on
  unpackaged runs (packaged identity wins) so the windows group as one taskbar
  app across the visibility handoff; "Start streams fullscreen" toggle passes
  --fullscreen (GTK parity); connects stamp KnownHost::last_used; the connect
  target is stashed for every route so "Connecting to X" always names the host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:25:02 +02:00
enricobuehlerandClaude Fable 5 d6647b9183 feat(clients/windows): port the Vulkan session client to Windows — session-always
The punktfunk-session Vulkan client (clients/linux-session, now clients/session)
builds and runs on Windows; the WinUI shell spawns it for every stream. Verified
live: 10-bit HEVC via Vulkan Video on both AMD (iGPU) and NVIDIA, 5120x1440 at
130 fps / 8 ms end-to-end on the RTX 4090.

- pf-ffvk: Windows bindgen branch (FFMPEG_DIR + PF_FFVK_VULKAN_INCLUDE, no
  pkg-config); provisioning fetches Vulkan-Headers (pinned v1.4.309).
- pf-client-core: builds on Windows — WASAPI audio (audio_wasapi.rs, cfg-swapped
  via #[path], same surface as the PipeWire twin), VAAPI/dmabuf gated inline
  (chain = vulkan -> software), trust reads the WinUI shell's %APPDATA% stores
  (parity tests pin both serialized shapes), Settings gains adapter/hdr_enabled
  (serde-defaulted; Linux stores unaffected).
- pf-presenter: builds on Windows — dmabuf module Linux-gated; SDL keyboard grab
  while captured (Alt+Tab/Win reach the host); pick_device ranks discrete over
  integrated (device 0 was the iGPU on hybrid boxes — the silent footgun) and
  honors PUNKTFUNK_VK_ADAPTER (the Settings GPU pick, exported by the session).
- run loop: block in one SDL wait woken by input AND decoded frames (a per-
  session forwarder pushes a FrameWake user event) instead of a 1 ms poll —
  measured 111%% -> 5%% of a core (NVIDIA), 86%% -> 3.5%% (AMD), stats unchanged.
  The pump's decode-fence wait became once-per-window sampling (no per-frame
  pipeline stall; the stat now shows true backlog).
- pf-console-ui: builds on Windows (skia-safe msvc prebuilts); font lookup falls
  through fontconfig aliases to concrete DirectWrite families (Consolas/Segoe UI)
  — browse/coverflow works, verified against a live host.
- WinUI shell: session-always via new src/spawn.rs (GTK spawn.rs port —
  CREATE_NO_WINDOW, stdout contract, kill handle); the Stream screen is a status
  card (chips + stage lines from the child's stats). The legacy in-process
  D3D11VA path stays behind Settings "Streaming engine" / PUNKTFUNK_BUILTIN_
  STREAM=1 as the A/B baseline until Phase 8 deletes it. SessionParams.video_caps
  makes the HDR toggle real.
- clients/linux-session renamed to clients/session (builds for both OSes).
- CI/MSIX: both workflows build/test both bins with widened path filters; the
  MSIX ships punktfunk-session.exe. ARM64 session builds --no-default-features
  (rust-skia has no aarch64-pc-windows-msvc prebuilts; flip when it does).

A/B on this box (5120x1440 HEVC vs home-worker-5): NVIDIA Vulkan 130 fps / 8 ms
e2e / 1.6 ms decode — clearly better than the built-in path. The AMD iGPU VCN
saturates at ~52 fps where its own D3D11VA does ~70 — Adrenalin Vulkan decode is
slower on APU silicon; discrete RDNA validation gates Phase 8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:21:36 +02:00
enricobuehlerandClaude Opus 4.8 8a4b0c7bae feat(windows): surface shortcuts on a Help screen; exit fullscreen when the stream stops
apple / swift (push) Successful in 1m10s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 44s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 41s
android / android (push) Successful in 4m11s
ci / rust (push) Failing after 1m21s
ci / web (push) Successful in 55s
apple / screenshots (push) Successful in 5m52s
ci / docs-site (push) Successful in 1m2s
arch / build-publish (push) Has been cancelled
ci / bench (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
The in-stream keyboard shortcuts were only reachable deep in Settings > Input.
Move them to a dedicated Help screen reached from a Help button on the host
list (mirrors the Licenses screen — a hook-free page routed from root), and drop
the reference card out of Settings so there's one home for it.

Fullscreen (F11) was a window-style toggle that stream stop never undid, so
disconnecting while fullscreen left the GUI borderless-fullscreen. Split
toggle_fullscreen into is_fullscreen/enter_fullscreen/exit_fullscreen sharing a
module-level SAVED_PLACEMENT, and have input::uninstall() call exit_fullscreen()
on teardown. exit_fullscreen is a no-op when nothing was saved, so it's safe on
every stream stop and idempotent with F11 — fullscreen is now streaming-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:17:19 +02:00
enricobuehlerandClaude Opus 4.8 bf07700c74 feat(clients): windows shortcut parity (Ctrl+Alt+Shift+S, F11) + surface stream shortcuts
apple / swift (push) Successful in 1m7s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m39s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m19s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 54s
android / android (push) Successful in 4m45s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 57s
arch / build-publish (push) Successful in 5m31s
ci / rust (push) Failing after 1m11s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m0s
apple / screenshots (push) Successful in 5m56s
ci / bench (push) Successful in 5m17s
deb / build-publish (push) Successful in 4m53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
decky / build-publish (push) Successful in 26s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m35s
flatpak / build-publish (push) Successful in 4m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 8m48s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m54s
Windows was missing two of the four stream shortcuts the GTK client has:
Ctrl+Alt+Shift+S (toggle the stats overlay live) and F11 (toggle fullscreen).
Add both to the low-level keyboard hook — S flips a HUD_VISIBLE atomic seeded
from Settings::show_hud at install (Settings is the default, the key overrides
it for the session, matching GTK), F11 drives a borderless-fullscreen toggle on
the window HWND and re-locks the pointer geometry for the new client rect. Both
are consumed locally, never sent on the wire.

Surface the full key set in two places, on both clients:
- in the UI: a read-only "In-stream keyboard shortcuts" reference card in the
  Windows Settings > Input section (the counterpart of the GTK Shortcuts
  window), plus the expanded HUD hint; the Linux keyboard hint gains F11.
- at stream start: a bottom-centre banner listing the shortcuts for the first
  few seconds of every session, independent of the HUD setting. Linux gets the
  matching start-flash of its capture hint (capture engages on map and hid it,
  so the keys were never shown until the first release).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:55:45 +02:00
enricobuehlerandClaude Fable 5 84fcbd3345 feat(clients/windows): WinUI UX batch - tile hover, Settings NavigationView, modal slide-up
Bump windows-reactor + windows to a4f7b2cb (from b4129fcc) for the new
PointerEntered/PointerExited events; migration is mechanical renames only
(SymbolGlyph->Symbol, placeholder->placeholder_text, on_changed->
on_text_changed/on_toggled, on_menu_item_clicked->on_item_clicked,
on_ready->on_mounted). New runtime model: reactor lost its build.rs, so the
client build.rs stages the WinAppSDK bootstrap via
windows-reactor-setup::as_framework_dependent() and main calls
windows_reactor::bootstrap() (missing either = 0x80040154 at launch);
staged filenames unchanged, so pack-msix and the MSIX manifest are untouched.

- Host tiles: WinUI pointer-over fill (ControlFillSecondary) via the new
  pointer enter/exit events, hover id in root state (backend-wired handlers
  bypass the reconciler flush, like the flyout clicks).
- Settings: stock NavigationView sidebar (Windows-Settings pattern) with
  Display/Video/Input/Audio/About panes, built-in back arrow, wide content
  column, and a per-section content slide-up tween. The section card is
  KEYED by section: an in-place diff across sections re-sets a reused
  ComboBox's items (clearing WinUI's selection) but skips selected_index
  when the values compare equal, rendering a blank selection - the key
  forces a remount. Card titles/descriptions dropped; per-control guidance
  moved to hover tooltips (ToolTipService).
- New "Show the stats overlay (HUD)" setting (show_hud, default on),
  honored mid-stream via the 400 ms HUD re-render.
- Add-host modal: entrance fade + slide-up tween (scrim fades with it).
- Self-initiated disconnect (Ctrl+Alt+Shift+D -> Ended(None)) returns to
  the host list silently instead of raising the error banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:23:25 +02:00
enricobuehlerandClaude Fable 5 9d6798b8e0 feat(clients/windows): GPU picker, disconnect shortcut, richer stream HUD
- Settings gains a GPU selector (shown only on multi-GPU boxes): the picked
  DXGI adapter drives decode + present, persisted as Settings.adapter and
  applied at the next stream - gpu.rs now caches the shared device keyed by
  the resolved preference (env PUNKTFUNK_ADAPTER > Settings > the window's
  monitor's adapter) so a change needs no app restart.
- Ctrl+Alt+Shift+D disconnects the session (consumed locally, captured or
  released): the hook releases capture and trips the session stop flag,
  plumbed through the stream-page handoff; the pump winds down and the UI
  navigates back to the host list.
- Stream HUD extended: codec chip (HEVC/H.264/AV1), display-side line from
  the render thread (presents/s + capture-to-decoded vs capture-to-on-glass
  p50), session line (host name, duration, network-lost frames, skipped
  backlog frames), and both shortcut hints incl. the new disconnect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:41:16 +02:00
enricobuehlerandClaude Fable 5 670f5a40a6 feat(clients/windows): screen-module restructure + parity features (speed test, native mode, capture UX)
Structure: split the 1400-line app.rs into per-screen app/ modules (mod=root/
router, hosts, connect, pair, speed, settings, licenses, stream, style) with
shared card/header/busy-page builders and setting_combo/toggle helpers; the
re-render rule (thread-driven state lives in root use_async_state, flows down
as props) is now documented at the module root.

Parity features the other clients already had:
- "Native display" resolves the real monitor mode at connect
  (MonitorFromWindow -> EnumDisplaySettingsW; was a hardcoded 1080p60)
- per-host network speed test: saved-host card button + a results screen
  (probe burst -> goodput/loss -> ~70% recommended bitrate applied in one
  tap; stale runs invalidated by generation) and `--headless --speed-test`;
  the bitrate setting becomes a free-form NumberBox so the recommendation
  round-trips
- forget host (ContentDialog confirm -> KnownHosts::remove_by_fp)
- settings: forwarded-controller picker (pads/pinned/set_pinned now wired),
  gamepad type, host compositor, capture-system-shortcuts; the previously
  dead Settings.compositor / inhibit_shortcuts are honored (shortcuts off =
  Alt+Tab/Alt+Esc/Ctrl+Esc/Win act locally)
- click-to-recapture after a Ctrl+Alt+Shift+Q release; the HUD hint tracks
  the live capture state

Perf: the input hook caches lock geometry (clip rect + contain-fit scale) at
engage instead of GetClientRect per WM_MOUSEMOVE; the audio jitter ring trims
via drain() and reuses the render scratch buffer.

Validated on the bare-metal box: --discover, synthetic-host loopback E2E
(TOFU -> clock skew -> HEVC negotiate -> D3D11VA init -> session end),
speed-test E2E, and the WinUI shell rendering in the console session via
PsExec (SSH/session-0 cannot create windows, pre-existing 0x80070005).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:16:19 +02:00