fcdb4d147d0c9fef85aedd58d08e7e3109d394bc
107
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6edd7c3377 |
feat(client/windows): the tile menu grows real submenus
The flyout CAN nest at the current reactor rev (the "no submenus" comment predated the bump), so the per-profile families move into MenuFlyoutSubItems: one "Connect with" (bare profile names + Default settings as leaves) and one "Pin tiles" (verb-prefixed leaves). The top level stays a fixed handful whatever the catalog grows to. The backend wires submenu clicks recursively and reports LEAF text, so the connect-with leaves are matched as the fall-through arm against the catalog, and the pin leaves keep their prefix — that prefix is what tells the two families apart in the shared callback. UIA-verified through the nesting: Pin tiles -> Pin tile: Work persists and the pinned tile appears in the same gesture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
2409ec9a4b |
docs(client/windows): the harness passes where the real backend fails, and the note says so
Case 3 of the characterization suite (a sync use_state write from a backend-fired handler re-renders) is true of the ENGINE and was taken as license to de-hoist event-driven page state — and the live app immediately disproved it: with settings componentized and its scope local, a real ComboBox pick changed nothing on screen (UIA-verified, no repaint). The real WinUI backend's handler wiring still never pumps the pass the harness pumps. The de-hoist is reverted one commit back; this one makes the record honest. The module discipline note now separates the three tiers of evidence — engine rules (harness-pinned), real-backend behaviour (live-verified, hoisting stays), and the tween-keyed corollary — and case 3's comment warns it must not be read as de-hoist permission without a live UIA check. The upstream gap worth reporting grows to two: the AsyncSetState HostId registry miss, and the backend event wiring that bypasses the render pump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
27039e04ff |
Revert "refactor(client/windows): page-only state climbs back down from root, on the measured rules"
This reverts commit 427f99a06647fd99e5c72d18cb7a710d8a7ac885. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
35c61fee64 |
chore(client): the windows-rs pin moves a month forward, onto the SDK-metadata bindings
The July 2026 windows-rs brings a reconciler keyed-child-order fix (#4728), widget validation (#4727), a DPI collision fix (#4751), icon elements (#4736), multi-window support (#4730) and scroll virtualization (#4710) — the re-render fixes the Windows client has been working around at the architecture level. All three pinned deps (windows-reactor, windows, windows-reactor-setup) move together so windows-core stays unified across the swap-chain hand-off, and pf-client-core moves with them. The bulk of the diff is #4689: windows/windows-sys now generate straight from the Windows SDK, so the `Win32_*` namespace features became one feature per SDK header (winuser, dxgi, d3d11, …), the PascalCase namespace modules became header-named modules, struct-returning COM methods take explicit out-params and return HRESULT, Win32 functions return their raw BOOL/HANDLE instead of Result, and flag constants are plain integers. Both crates' Win32 code is rewritten to that shape; behaviour is unchanged on every path. Riding along, all already stale before the bump: the README and the three Windows workflows stop claiming windows-reactor's build.rs needs CARGO_WORKSPACE_DIR (that build.rs no longer exists — staging moved to windows-reactor-setup via OUT_DIR); the README layout section stops describing modules that moved into the session binary long ago and gains the manual smoke checklist; the notices generator learns the SPDX for crates that ship license files without a `license` field, which turns windows-reactor-setup's UNKNOWN into MIT OR Apache-2.0; and the crate records its real rust-version (1.96) instead of inheriting the workspace's 1.82. Verified: cargo check/clippy/fmt clean on punktfunk-client-windows, pf-client-core and punktfunk-client-session; both bins build; --discover finds the LAN hosts; the GUI shell comes up (WinAppSDK bootstrap intact under the new reactor-setup). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
944c03dd32 |
feat(client): the desktop clients wear the host's OS mark
The client half of the host's new `os=` advert, shared once in pf-client-core: `sanitize_os` (mDNS is unauthenticated input — lowercase `[a-z0-9._-]` tokens, capped) and `os_icon_tokens`, the most-specific-first walk with the brand aliases (`macos`→apple, `steamos`→steam) every platform resolves through. `DiscoveredHost` carries the chain, `KnownHost` persists it (`serde(default)`, elided when empty — older stores load unchanged and older clients read back exactly what they wrote), `upsert` moves it only when carried, and `learn_os` mirrors `learn_mac` — no-op, no disk write when unchanged — so the mark survives the host going to sleep. GTK shells: the card's status row leads with a recolorable symbolic glyph. That needed real embedded assets — the shells had none — so `data/` gains the ten `pf-os-*-symbolic` SVGs compiled into a gresource (new build.rs, glib-build-tools) and registered on the icon theme at startup; the Adwaita theme then tints them like every other status glyph. WinUI shell: reactor renders raster-from-URI only, so the embedded mid-gray PNGs (legible on both themes) materialize once into %LOCALAPPDATA%\punktfunk\os-icons\ — the library's poster-art pattern — and the tile's status row leads with a 16px image. The couch UI plumbs `HostRow.os` (live advert preferred, else the store) for a Skia glyph that is a declared follow-up; `--list-hosts` / `hosts --json` emit the stored chain so the Decky plugin can read it. A host that advertises no `os` renders everywhere exactly as it did before the field existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
5db93c1156 |
fix(packaging/windows): a release must not sign itself with a throwaway cert
Both pack scripts fell back to a fresh `New-SelfSignedCertificate` whenever MSIX_CERT_PFX_B64 was absent, and said nothing about it beyond one Write-Host. That fallback is right for canary and dev builds. On a tag it is not: an ephemeral cert is regenerated per build, so nobody can pin it, and a release signed with one is indistinguishable from a release signed by whoever else got to the artifact. A secret that got renamed, rotated away, or simply wasn't inherited by a new workflow would have downgraded a real release silently and shipped it. pack-msix.ps1 and pack-host-installer.ps1 now resolve -RequireSignedCert (default 'auto' = true iff GITHUB_REF is refs/tags/v*) and throw instead of falling back; pack-host-installer.ps1 also refuses -NoSign on the same condition. Reading GITHUB_REF inside the scripts rather than taking a workflow flag means a future packaging workflow inherits the guard instead of having to remember it. No effect on today's releases: MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD are both set as repo secrets, so the supplied-cert branch is the one tag builds take. This only closes the trapdoor underneath it. Verified on the windows-amd64 runner: both files parse, and the guard resolves true for refs/tags/v0.21.0, false for refs/heads/main and for an unset ref, with 'true'/'false' overriding as intended. Not touched: the pf-vdisplay / gamepad driver builds use the same pattern with DRIVER_CERT_PFX_B64, which has no secret behind it — those need attestation signing (a Partner Center action), and a guard there would just fail every tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9b00ad6658 |
fix(client/windows): prove the deep-link module the new deny caught
ci / docs-site (push) Successful in 1m18s
ci / web (push) Successful in 2m1s
apple / swift (push) Successful in 4m46s
android / android (push) Failing after 6m31s
deb / build-publish-host (push) Failing after 5m29s
ci / rust (push) Failing after 7m33s
arch / build-publish (push) Failing after 7m35s
ci / bench (push) Failing after 7m30s
deb / build-publish (push) Failing after 6m29s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
decky / build-publish (push) Successful in 31s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 19s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
ci / rust-arm64 (push) Successful in 10m5s
deb / build-publish-client-arm64 (push) Successful in 7m48s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7m5s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Failing after 8m30s
windows-host / package (push) Successful in 19m0s
windows-host / winget-source (push) Skipped
docker / build-push-arm64cross (push) Successful in 5m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 14m1s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 14m15s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 2m44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m49s
release / apple (push) Successful in 29m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m58s
apple / screenshots (push) Successful in 23m16s
`deeplink.rs` arrived with
|
||
|
|
8a5a5edc37 |
fix(small crates): the proof lint now covers every first-party crate but one
Six crates were still unguarded, and all six are OURS — none vendored: pf-console-ui, pf-gpu, punktfunk-tray, clients/windows, tools/display-disturb, wdk-probe. Two of them ship (the tray and the Windows client), so "small" was about item count, not exposure. Five are closed here. Only 17 of their 75 unsafe items actually lacked a proof — pf-gpu, punktfunk-tray and display-disturb were already fully documented and needed nothing but the deny, which is the good case: the convention was being followed, just not enforced. The 17 that were missing are the usual Win32/COM shapes, and two were worth stating properly. `clients/windows`'s `GetCurrentPackageFullName` is called with `len = 0` and no buffer — that is the documented identity PROBE, which writes nothing, and reading it as a normal query would be a mistake. `pf-console-ui`'s two `destroy_image_view` calls are the load-bearing ones: the comment above one already argued that in-flight sampling of that slot ended two presents ago (the ring alternates and the presenter waits its fence before each record), which is exactly the kind of reasoning a `// SAFETY:` should carry and it was sitting there unlabelled. Also fixes a real Windows-only clippy error this uncovered: `pf-gpu` had a `#[cfg(target_os = "windows")]` fn AFTER its `mod tests`, tripping `items_after_test_module`. It never fired on Linux (the item does not exist there) and no CI job clippies pf-gpu on Windows, so it sat unseen. Moved above the test module. Remaining: `wdk-probe` (26 items) alone, and only because it needs the WDK to build — .47 cannot, so nothing here can verify a deny on it. Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 the four Windows-relevant crates at `-D warnings` rc=0. |
||
|
|
bf98102714 |
feat(packaging): ship punktfunk everywhere, and deprecate the session's stray pairing flag
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m5s
android / android (push) Canceled after 3m17s
apple / swift (push) Canceled after 3m18s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 3m18s
ci / rust (push) Canceled after 3m19s
ci / rust-arm64 (push) Canceled after 3m20s
ci / bench (push) Canceled after 3m18s
deb / build-publish (push) Canceled after 2m23s
deb / build-publish-host (push) Canceled after 2m13s
deb / build-publish-client-arm64 (push) Canceled after 0s
decky / build-publish (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
flatpak / build-publish (push) Canceled after 2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 3m25s
windows-host / winget-source (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
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
The rest of C1. A CLI that only exists in a dev tree is not a door anyone can use, so it now builds and installs in deb, rpm, arch, nix, flatpak and MSIX alongside the client and the session. On Windows it gets its own `AppListEntry="none"` application with the alias `punktfunk.exe` — a command, not a Start-menu tile — which is what the Playnite importer will shell to for `punktfunk library <host> --json`. `punktfunk-session --pair` prints a deprecation notice and forwards. Pairing is a trust ceremony and belongs to the brain, fronted by `punktfunk pair` or a shell; a renderer owning one is precisely the mixing of concerns this split exists to undo. It keeps working for a release, because someone's provisioning script is calling it today. Decky's invocations are untouched and verified still working (`--list-hosts` JSON shape unchanged, `--reachable` exit codes unchanged): those flags are a frozen compat contract until Decky migrates to the verbs at its own pace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c27065c260 |
feat(client/windows): punktfunk:// opens the Windows client, and cards can write a shortcut
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 (43, bazzite, punktfunk-fedora-rpm) (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
WP4 / D2. Protocol activation of a full-trust packaged app delivers the URI as the command line, so a browser prompt, `start punktfunk://…` and a written `.lnk` all arrive the same way — as a positional argument, the same door the Linux shell uses. What Windows does not give us is single-instancing: unlike GApplication, a second activation is simply a second process. So the first instance claims the named mutex `unom.punktfunk.client` and any later one hands its URL to the winner over `WM_COPYDATA` and exits — one window, and the link opens where the user's hosts already are. The hand-off retries while the primary's window is still coming up (a shortcut double-clicked during startup is the ordinary case), and a hand-off that finds nobody falls through so this process becomes the shell that opens it. A link is never silently dropped, which is also why the inbox is a queue: two shortcuts in quick succession are two links. Routing is the same four lines of translation as on Linux, because the decisions belong to `plan_from_link`: a resolved link becomes the call a tile click makes, with the same wake, trust and error surfaces. Never preempting a live session is checked here, since only this layer knows one is running. The manifest declares the protocol and an app execution alias rather than writing registry keys, so uninstall leaves nothing behind — and Windows' own "allow this app to handle punktfunk links?" prompt is the origin friction the design wants, not duplicated in-app. (Manifest comments are free of double hyphens: XML forbids them and makepri rejects the whole file, which is how the console flag broke the v0.15.0 build. A test parses the substituted manifest and asserts both declarations, so that can't rot silently.) "Create shortcut…" writes a `.lnk` targeting the ALIAS with the URL as an argument, not a `.url` internet shortcut: both work while the scheme is registered, only this one still works if it isn't, and targeting the alias keeps it valid across updates when the package path changes. Verified on the CI VM (192.168.1.133): clippy -D warnings clean and its tests green. Not yet exercised on glass — `start punktfunk://…` cold and with the app running, and a written shortcut, still want a real desktop session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
3d25482469 |
feat(client/windows): pinned host+profile tiles render in the grid
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m40s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m56s
apple / swift (push) Successful in 5m12s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (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
android / android (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
The menu could pin since the last commit but nothing appeared, which is worse than not offering it. Each host's pins now render right after it, sharing its live status because they read the same record, with the profile named on an accent chip. A pin whose profile was deleted simply doesn't render. A pinned tile carries no menu of its own: it is a shortcut, not a second host, and pin/unpin already live on the primary tile's menu — the one place you decide it. Its hover key is `<fp>#<profile-id>` so two tiles for one host don't light up together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a088e13a4 |
feat(client/windows): bind a host to a profile, connect with one, pin one, copy its link
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m43s
apple / swift (push) Successful in 5m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
flatpak / build-publish (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 (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
apple / screenshots (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
android / android (push) Canceled after 2m34s
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
ci / rust (push) Canceled after 2m30s
arch / build-publish (push) Canceled after 2m39s
The WinUI half of §5.2: `Target` carries a one-off profile into the session's `--profile`,
the host editor gains the binding picker, and the tile names its bound profile on a chip so
what a plain click will do is visible without opening anything.
Shape notes, both forced by this toolkit rather than chosen:
- The flyout has no submenus, so "Connect with", "Pin as card" and "Unpin card" are flat
items, one per profile, matched by prefix. The prefixes end in ": " so a profile named
"Copy link" can't collide with a fixed entry.
- The binding picker commits on change rather than at Save. The rest of that sheet is text
boxes with draft refs, which need a Save; a ComboBox doesn't, and making it wait would be
the odd one out.
Semantics stay identical to the Linux client, because they are the design's, not the
platform's: "Connect with" never rebinds, "Default settings" is `Some("")` so it really
does override a bound host for one session, and a binding whose profile was deleted reads
as Default settings and resolves to it.
`pf_client_core::clipboard::set_text` is the small addition the crate needed for "Copy
link" — the OS clipboard plumbing was already there, just private to the streaming bridge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
5219107177 |
chore(unsafe): the workspace adopts the drivers' unsafe discipline
`packaging/windows/drivers/*` has run `deny(unsafe_op_in_unsafe_fn)` +
`deny(clippy::undocumented_unsafe_blocks)` for a while, with `forbid(unsafe_code)`
on the modules that need no unsafe at all. The main workspace had no lint config
whatsoever, so nothing stopped a clean crate from quietly growing an `unsafe`, and
nothing distinguished the handful of genuinely-unsafe lines inside a 600-line
`unsafe fn` from the safe ones surrounding them.
Three things, all mechanical:
* `#![forbid(unsafe_code)]` on the eight crates that already contain zero unsafe
(`pf-driver-proto`, `pf-host-config`, `pf-paths`, the three clean clients, both
tools). These were clean by accident, not by contract; now they are clean by
contract.
* `unsafe_op_in_unsafe_fn = "warn"` workspace-wide. `unsafe fn` states a contract
the CALLER must uphold — it was never meant to switch off checking for the whole
body. Measured fallout is 300 sites on Linux, and they are concentrated: six
files carry all of them, while `punktfunk-core`, `pf-frame`, `pf-clipboard` and
`pf-vdisplay` are already at zero. `warn` (not `deny`) so the build stays green
while those six are worked down; it flips to `deny` once they are. This is also
the Rust 2024 default, so it pays off the edition migration early.
* `proc::current_uid()` replaces eight `unsafe { libc::getuid() }` blocks. Each
site had copied out the same SAFETY note verbatim, which is the tell: `getuid()`
is parameterless, always succeeds and touches no memory, so there is no contract
for a caller to uphold and no reason for the unsafe to be visible eight times.
One `unsafe` behind a safe wrapper, none at the call sites.
Verified: `pf-vdisplay` builds clean on Linux (Nobara) at zero E0133; the
macOS-buildable crates build clean locally. No behaviour change.
|
||
|
|
37edb1cc1a |
refactor(clients): both shells wake through the shared state machine
audit / bun-audit (push) Successful in 21s
ci / web (push) Successful in 1m39s
ci / docs-site (push) Successful in 2m7s
audit / cargo-audit (push) Successful in 2m55s
apple / swift (push) Successful in 5m25s
ci / bench (push) Successful in 7m47s
ci / rust-arm64 (push) Successful in 10m28s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
windows-host / package (push) Failing after 11m22s
windows-host / winget-source (push) Skipped
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m17s
deb / build-publish-client-arm64 (push) Failing after 6m0s
deb / build-publish (push) Successful in 13m3s
arch / build-publish (push) Successful in 16m27s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m36s
flatpak / build-publish (push) Successful in 6m50s
android / android (push) Successful in 19m52s
deb / build-publish-host (push) Successful in 16m54s
ci / rust (push) Successful in 22m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m1s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 20m33s
release / apple (push) Successful in 30m41s
docker / build-push-arm64cross (push) Successful in 24s
docker / deploy-docs (push) Successful in 26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 28m24s
apple / screenshots (push) Successful in 22m37s
The last of C0's duplication: GTK's `wake_and_connect` and the WinUI shell's ran their own copies of the same 90 s / 6 s / 1 s loop, and the Windows one's comment said it "mirrors the Apple HostWaker" — now it literally does, because both drive `WakeWait`. What is left in each shell is the part that genuinely differs: its dialog or screen, its advert drain, its re-key, and its route back into the trust gate. Behavioural review, since a sleeping host to test against wasn't available: - GTK polled every 500 ms and resent on a wall-clock timer; it now ticks once a second like every other implementation. Up to half a second slower to notice a host coming back, and exactly the reference cadence in exchange. - GTK took the FIRST matching advert in a drain and returned mid-loop; it now drains fully and takes the last, i.e. the freshest address. Same connect, fresher re-key. - Windows sent one packet before the loop AND one on its first pass; the machine owns the packets now, so that duplicate is gone. Resends still land at 6 s, 12 s, … and the budget still expires at 90 s. - Both keep their own cancel path, their own re-key, and their own error surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a2be3f4de9 |
feat(client): settings profiles — the override catalog, the one resolver, and the session flag
A client setting is global today: pick 4K@120 HDR and the retro box gets it too.
The one escape hatch (per-host `clipboard_sync`) proves the shape works but covers a
single field. This is P0 of design/client-settings-profiles.md — the headless core the
shells, Apple, Android and the deep-link grammar all build against. No UI yet.
A profile is a NAMED BUNDLE OF OVERRIDES, not a snapshot: `SettingsOverlay` is sparse
`Option`s, so a field the user never touched keeps following the global value live, and
fixing a global once fixes it everywhere. `Some(x)` equal to today's global is still
meaningful — it pins x against a later global change — which is why the UI will write
`Some` on touch and `None` on an explicit reset, never by diffing.
The catalog is its own `client-profiles.json`, deliberately not part of the settings
file: that file has five whole-file load-modify-save writers with no merge, so a profile
written by one would be dropped by the next. Which host uses which profile is a field on
the host record instead of a map keyed by host — that is what dissolves the client's
long-standing "what identifies a host record" question for this feature.
Resolution has exactly one implementation, `trust::effective_settings()`:
effective = overlay(profile).apply(global)
profile = one-off pick ?? host binding ?? none
Both session construction sites go through it (`main.rs` AND `console.rs` — touching one
and not the other is a Windows-only build break), so the console, and therefore Decky,
honor host bindings with no work of their own. Nothing here can fail a connect: a
deleted binding resolves as "no profile", and a one-off that can't be honored falls back
to the DEFAULTS rather than to the host's own profile — "connect with Work" must never
quietly stream "Game". `--profile <id|name>` is the one-off door for the shells' coming
"Connect with ▸" and for `punktfunk://…&profile=`; `--profile ""` forces the defaults on
a bound host. The active profile's name closes the stats overlay's first line, so
"which profile am I on?" is answerable without leaving the stream.
Also here, because this effort touches every one of these anyway:
- `KnownHost` gains `profile_id`, `pinned_profiles` and a lazily minted stable `id`. The
id is groundwork (design §4.5) — no lookup is re-keyed, `fp_hex`/`addr:port` stay.
- `upsert` now preserves the state the USER set on a record — the profile binding, its
pins, the clipboard decision, the id — against refreshes that carry none of it.
`clipboard_sync` survived that only because the function happened not to mention it.
- `KnownHost::default()` exists so construction sites read `..Default::default()`; five
hand-written literals were exactly how a new field gets silently dropped on re-pair.
- All three client stores now write temp+rename. A torn settings file loads as `Default`
— i.e. silently resets every setting the user has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
94f0207cbd |
feat(windows/client): logs persist — %LOCALAPPDATA%\punktfunk\logs\client.log
apple / swift (push) Successful in 4m54s
ci / web (push) Successful in 58s
decky / build-publish (push) Successful in 34s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 23s
ci / bench (push) Successful in 7m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 22s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 40s
deb / build-publish-client-arm64 (push) Successful in 7m35s
deb / build-publish (push) Successful in 10m19s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m25s
ci / rust-arm64 (push) Successful in 13m57s
deb / build-publish-host (push) Successful in 14m30s
ci / docs-site (push) Successful in 1m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m41s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 14s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m32s
android / android (push) Successful in 15m23s
arch / build-publish (push) Successful in 14m1s
flatpak / build-publish (push) Failing after 8m16s
docker / deploy-docs (push) Successful in 11s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m25s
release / apple (push) Successful in 29m47s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m52s
ci / rust (push) Failing after 16m15s
docker / build-push-arm64cross (push) Failing after 4m25s
windows-host / package (push) Successful in 11m35s
windows-host / winget-source (push) Skipped
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 1m59s
apple / screenshots (push) Successful in 23m44s
The shell is a windows_subsystem binary and spawns punktfunk-session with CREATE_NO_WINDOW + inherited stderr: a normal GUI/MSIX launch has no console, so every log line — the shell's and the session's whole receive/decode/present forensic trail — evaporated exactly when a user hit something worth reporting. The 2026-07 PyroWave latency-sawtooth report had to be triaged from host logs alone; the deciding evidence (clock re-sync vs backlog-flush lines) lives client-side. - New logfile module: %LOCALAPPDATA%\punktfunk\logs\client.log, the host's rotation convention (10 MB cap -> .old at next start, one generation), best-effort (no dir -> plain stderr, never a startup failure). - The tracing subscriber tees to stderr AND the file (ANSI off — the file is what users send); startup logs the path. - The spawned session's stderr is piped through the same tee, line-buffered — dev-terminal runs keep their interleaved output, GUI runs finally keep anything at all. - The --console path and the punktfunk-console.exe MSIX shim forward the same way (a couch launch has no console either). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a01bc4463 |
feat(core+host+client): cursor channel — remote-desktop sweep M2a+M2b
The host cursor stops riding the video and becomes a real OS cursor on the client (the Parsec/RDP model): pointer feel no longer pays the capture→encode→network→decode→present round trip. Wire (M2a): - Hello grows a client_caps trailing byte (CLIENT_CAP_CURSOR) after the fixed display_hdr block — presence disambiguated by remaining length, which caps the post-HDR tail at 27 bytes (documented); Welcome answers HOST_CAP_CURSOR (capable-and-asked, the 444/clipboard precedent). - CursorShape (0x50, control stream): serial + dims + hotspot + straight RGBA, ≤120px/side so the u16 frame always fits (128² would overshoot); client caches by serial — re-showing a known shape costs 14 bytes, not a bitmap (RDP pointer-cache for free). - CursorState (0xD0 datagram): serial + visible/relative_hint flags + position, sent once per encode-loop tick — latest-wins, self-healing under loss, no refresh timer. relative_hint is reserved for M3. - Client core: two new planes (control-task + datagram-task arms) → next_cursor_shape/next_cursor_state; connect() grows client_caps (C ABI passes 0 until the v11 cursor poll fns exist). Host (M2b, Linux portal only): - handshake::cursor_forward is THE predicate (client asked ∧ Linux ∧ compositor ≠ gamescope) — Welcome bit and session wiring both read it. - SessionPlan.cursor_blend goes false for a forwarding session; the encode loop ticks a CursorForwarder every iteration: shape-serial diff → control-task bridge (mirrors probe_result), state datagram → conn. - CursorOverlay/capture CursorState carry the hotspot through (nearest-neighbor downscale backstop for XL cursors, unit-tested). Presenter: - CursorChannel drains both planes per loop iteration; shapes become SDL color cursors (from_surface + hotspot), applied while the desktop mouse model is engaged; visibility follows the host; capture/released hands back the system cursor. Sessions advertise the cap when they START in desktop mode. Verified on .21: fmt + clippy -D warnings (7 crates) + tests green (core 218 incl. new wire roundtrips, host 245 incl. e2e + forwarder downscale tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
134874b3c8 |
fix(client/windows): repair the v0.15.0 MSIX build — ARM64 feature leak + illegal XML comment
ci / docs-site (push) Successful in 52s
ci / web (push) Successful in 54s
decky / build-publish (push) Successful in 24s
apple / swift (push) Successful in 1m20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
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 6m21s
ci / bench (push) Successful in 6m57s
flatpak / build-publish (push) Successful in 6m21s
deb / build-publish (push) Successful in 9m54s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m58s
android / android (push) Successful in 13m35s
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
ci / rust (push) Failing after 20m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m56s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m26s
docker / deploy-docs (push) Successful in 25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m18s
Both MSIX matrix jobs failed at the v0.15.0 tag, for two unrelated reasons, both introduced by this release. 0.13.0 and 0.14.0 shipped both packages; 0.15.0 shipped neither. ARM64 failed to BUILD. The PyroWave Windows un-gating ( |
||
|
|
8fe90a8a4b |
fix(client): clipboard poll cadence was never applied (and CI clippy)
ci / web (push) Successful in 1m8s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m9s
apple / screenshots (push) Successful in 6m17s
ci / bench (push) Successful in 6m51s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
deb / build-publish (push) Successful in 9m11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
android / android (push) Successful in 13m32s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m1s
arch / build-publish (push) Successful in 17m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 5m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m43s
deb / build-publish-host (push) Successful in 11m42s
flatpak / build-publish (push) Failing after 8m23s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m14s
ci / rust (push) Successful in 25m38s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m32s
docker / deploy-docs (push) Successful in 27s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m8s
POLL was dead: the local clipboard was re-read once per inbound event wait (<=120 ms) instead of on its own 400 ms cadence, so the constant documenting the interval described something the code did not do. Give it a deadline of its own -- the event wait is short because it bounds teardown latency, which is no reason to hammer the Win32 clipboard eight times a second while the user is copying in another app. Build State in one expression while here, and note for next time: CI runs clippy as --workspace --all-targets -- -D warnings, so a scoped run without -D warnings (what I did) does not reproduce it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cac23b7a05 |
feat(client/windows): console UI in the header, one Edit dialog per host
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 57s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
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) Failing after 4m57s
ci / bench (push) Successful in 6m31s
ci / rust (push) Failing after 6m36s
apple / screenshots (push) Successful in 6m17s
deb / build-publish (push) Successful in 10m38s
flatpak / build-publish (push) Failing after 8m17s
docker / deploy-docs (push) Successful in 26s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m15s
deb / build-publish-host (push) Successful in 10m24s
arch / build-publish (push) Successful in 12m43s
android / android (push) Successful in 14m5s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 6m1s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m40s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 8m56s
Three related bits of the same cleanup: - The console UI moves from a card in the page body to a button in the header row, beside Add host / Shortcuts / Settings. It also opens the console's OWN host view now (bare --browse) rather than diving straight into one host's library, which is what the card did -- the couch counterpart of this page, not a shortcut past it. - spawn_browse / open_console take an Option target to express that. - The per-host overflow menu collapses: rename and the two clipboard labels become one Edit dialog carrying every per-host property, mirroring the Apple client's add/edit sheet (name, address, port, Wake-on-LAN MAC, share clipboard). A menu item per field read as clutter and buried the entries that matter. Cleared fields mean leave-as-is rather than erase, except the MAC, which is legitimately clearable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2508b72062 |
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>
|
||
|
|
aa45757a72 |
feat(client/windows): per-host clipboard toggle
The bridge landed always-on whenever the host permitted it; sharing a clipboard is a trust decision about a specific host, so it needs to be opted into. Mirrors the Apple client's per-host model (StoredHost.clipboardSync, "Share clipboard with this host") rather than a global switch: KnownHost::clipboard_sync, toggled from the host card's overflow menu, default off. The session binary resolves the stored flag itself in session_params, so a direct connect and the console's own launches honor the same decision without every caller having to remember to pass it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
1d587a259e |
fix(client/d3d11va): green bar at the bottom — clamp the blit to the frame
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m8s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m49s
ci / bench (push) Successful in 7m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
apple / screenshots (push) Successful in 6m12s
deb / build-publish (push) Successful in 9m49s
android / android (push) Successful in 13m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m53s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Successful in 6m32s
deb / build-publish-host (push) Successful in 13m26s
arch / build-publish (push) Successful in 17m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m2s
ci / rust (push) Successful in 28m51s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m0s
The decode surface is DXVA-aligned (height rounded up to the profile's
alignment, 128 for HEVC/AV1), so it is taller than the frame: a 2400-line
stream decodes into a 2432-line texture. VideoProcessorBlt was called with
no stream source rect, so it blitted the WHOLE surface — the uninitialized
padding rows (NV12 Y=0,U=V=0, which converts to vivid green) landed at the
bottom of the output and the picture was squashed to fit. Set the source
rect to the real frame.
Pre-existing on this backend, but
|
||
|
|
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> |