Video windows are ordinary, closable cards
Headless-QEMU-injected-input evidence only; no physical touch, panel, or
mpv/decode proof. See runtime/video’s existing board evidence for actual
decode/presentation, and task 4.1 of
openspec/changes/video-windows-become-ordinary-cards/tasks.md for the
outstanding board re-check.
Root cause
k230-video-software/k230-video-mvx (mpv, launched by
nix/video-session.py) were excluded from card_shell ordinary alongside
real transient/popup views (nix/card-shell/adapter.c’s “video and
transient views stay unmarked” check). A playing video therefore stayed a
small 480x270/568x320 floating window: no card, not in the overview, not
reachable by the bottom-edge switch gesture, and with no other close
control in this touch-only shell. Separately, xdg_toplevel close alone
cannot tell video-session.py’s controller “the user closed this” from
“the decoder died”, so the MVX fallback path could relaunch a fresh
software player right after a close.
Fix
nix/card-shell/adapter.c:video_app_id()replaces the video-app_id exclusion incmd_card_shell’sordinaryhandler; only genuine transient/popup views (wants_floating) stay unmarked.nix/shell.nix:k230-video-(software|mvx)gets the samecard_shell ordinary, floating enable, resize set 100 ppt 100 ppt, move position 0 0treatment as every other coherent-shell app. The old 480x270/568x320 floating rule is kept only for the plain (non-coherent- shell) touch launcher, which has no deck to close a card from.nix/card-shell/route.c/route.h:card_shell_video_stop()spawns$SWAY_K230_CARD_VIDEO_STOP stop(wired to${videoSession}/bin/k230-video-session) alongside the existingview_close()whenever a closed card’svideo_viewflag is set.video-session.py’s existingstopsubcommand and process-group teardown are unchanged.nix/card-shell/adapter.c:struct card.video_viewandcard_shown_large()freeze a video card’s deck-sized mirror on its most recently captured frame once it is not shown at full panel size, so a playing video does not pay continuous decode-to-thumbnail rescale cost for an unselected small card.
QEMU regression
tests/test_card_shell_video_card.py maps a real k230.card.one fixture
and the video app_id k230-video-software (both via
card-composition-probe-client, extended to accept the two video app_ids
alongside its existing k230.card.one|two|three fixture set) under real
cross-built Sway/qemu-riscv64-static with injected touch
(SWAY_K230_CARD_TEST_INPUT=1), and proves:
- Both reach full
568x1232ordinary-maximized geometry. - A real touch-first vertical entry swipe (
down/motion/motion/upviacard_shell test-touch) starting from the focused video card is accepted and settles into the overview (mode==CS_DECK), exactly as it would for any other source. - The video card is a genuine, switchable deck member:
card_shell previous/next(the persistent-button route a lateral release resolves to) reaches the other card and back. The video app_id was mapped second in this run (deck index 1), sopreviousmoves towardk230.card.oneandnextreturns to the video card — see the compositor log excerpt below. card_shell closeon the selected video card sends the ordinaryxdg_toplevelclose (the probe client exits with code 0, matching its default close-accept behavior) and invokes the video-stop helper exactly once with the argumentstop.- The other ordinary card is unaffected (still running) and the deck still shows one card afterward.
Verify with (from tests/):
python3 -m unittest test_card_shell_video_card
7 consecutive passes were observed locally (~8-11s each). Two intermediate,
now-fixed bugs surfaced while developing this test, kept here because they
are real properties of the compositor, not artifacts of the test:
- A real frame must render between the entry gesture’s
downand its firstmotionso the adapter’s per-frame geometry reconciliation (cs_entry_set_geometry, called fromcard_shell_prepare) sets a positiveentry_travelbeforecs_entry_motionruns; otherwise it seesentry_travel<=0and immediately fails/reverts the gesture (cs_entry_motion,nix/card-shell-policy/card-shell-policy.c). Other suites (tests/card_shell_runtime.py) get this for free from agrimcapture betweendownandmotion; this test has no capture, so it sleeps for one frame instead. card_shell previous/nextare a deliberate no-op whilemode==CS_ENTERING(cs_step, same file), so a test must wait for the entry animation to actually settle toCS_DECK(debug-scene’smode==1) before stepping —selected_app_idalone is not a sufficient gate, sinceshell.policy.selectedis already correct and unchanged throughout the settle.
A new debug-scene field, selected_app_id, was added
(nix/card-shell/adapter.c’s debug_scene_text) so a test can read the
deck’s own current selection directly; Sway’s real seat focus does not move
while merely browsing the deck (restore() only reassigns it at
CS_RESTORE, i.e. when the overview is actually left), so polling the IPC
tree’s "focused" node cannot observe previous/next stepping.
Log excerpt (representative run)
K230_CARD_SHELL_DEBUG_SCENE active=1 ... mode=1 entry_progress=1.0000 ... selected_app_id=k230-video-software # after entry: video selected
K230_CARD_SHELL_DEBUG_SCENE active=1 ... mode=1 entry_progress=1.0000 ... selected_app_id=k230-video-software # after `next` (already last index: no-op)
K230_CARD_SHELL_DEBUG_SCENE active=1 ... mode=1 entry_progress=1.0000 ... selected_app_id=k230.card.one # after `previous`
K230_CARD_SHELL close-request id=5
K230_CARD_SHELL mirror-release id=5 (x2)
K230_CARD_SHELL source-gone id=5
K230_CARD_SHELL unmap id=5
K230_CARD_SHELL_DEBUG_SCENE active=1 ... mode=1 ... ordinary_maximized_cards=1 ... selected_app_id=k230.card.one # after close: video gone, one card remains
video.poll() = 0 # the probe client exited cleanly on the ordinary xdg_toplevel close
(Full unittest run’s assertions additionally confirm the video-stop helper
log contains exactly one line, stop, and that the other card’s process
is still running (poll() is None) throughout.)
Narrow proof commands
nix build .#card-shell --max-jobs 1 --cores 6nix build .#handheld-shell-rust --max-jobs 1 --cores 6nix eval .#nixosConfigurations.k230.config.system.build.toplevel.drvPathpython3 -m unittest test_card_shell_video_card(fromtests/)python3 -m unittest test_card_shell_state(fromtests/, unaffected: 29/29 pass)openspec validate video-windows-become-ordinary-cards --strict
Limits
- No board evidence. QEMU cannot prove mpv decode, MVX hardware fallback
behavior, actual panel presentation, or physical close-gesture feel —
those stay with
runtime/video’s existing board evidence and the outstanding board re-check (task 4.1 in this change’stasks.md). tests/test_card_shell_home_bleed.py(a pre-existing, unrelated two-axis suite) was observed failing both with and without this change’s diff applied on this host (confirmed viagit stash), so it is a pre-existing environment-sensitive flake, not a regression introduced here; it was not otherwise investigated or fixed as part of this change.