Rewrite/go tui - #16
Merged
Merged
Conversation
…on gating Ports sdk/crates/bitdo_proto to internal/protocol as part of the Go+Bubbletea rewrite. The PID and command registries are now go:generate'd directly from spec/pid_matrix.csv and spec/command_matrix.csv (internal/protocol/gen) instead of hand-copied into source, so the spec CSVs are the literal single source of truth and can't drift from the runtime tables. Ports the 4-gate command authorization (confidence/runtime-policy, family/capability/PID applicability, candidate-readonly tier restriction, unsafe double-confirmation), retry/timeout handling, diagnostics probing, JP108/Ultimate2 command paths, firmware chunking, and the BDP1 profile blob format, verified against harness/golden/profile_fixture.bin. Ports the behavioral test suite from sdk/tests/*.rs (capability gating, candidate-readonly gating, runtime policy, retry/timeout, firmware chunking, boot safety, error codes, response validation, mode-switch readback, and diag probe scenarios) as Go tests against a MockTransport, rather than just writing new tests inspired by the originals. HID transport uses github.com/karalabe/hid; Transport drops the Rust trait's unused WriteFeature/ReadFeature methods (verified dead in the original implementation). go test -race is clean.
…chine Ports sdk/crates/bitdo_app_core to internal/core. Covers device discovery, diagnostics summary text, JP108/Ultimate2 mapping read/apply-with-recovery (backup-then-write-then-rollback-on-failure), the candidate-readonly write probe (advanced mode + risk ack + per-PID unlock file, guarded mode/profile write-readback), the 7-dimension support scorecard, and the firmware update state machine (preflight -> awaiting-confirmation -> running -> terminal, with manifest fetch, SHA-256 + Ed25519 signature verification against pinned keys, chunked transfer, and cooperative cancellation). Uses stdlib crypto/ed25519 and net/http in place of ed25519-dalek/reqwest, BurntSushi/toml for the firmware manifest, and a small crypto/rand-based ID generator in place of a UUID dependency. Firmware progress fan-out is a minimal hand-rolled broadcaster (buffered per-subscriber channels, oldest- event-dropped on overflow) standing in for tokio::sync::broadcast. Ports the Rust test suite from bitdo_app_core's #[cfg(test)] module (preflight gating, candidate scorecard, candidate write-probe unlock ceremony, firmware happy path, mock download, JP108/U2 backup+restore, guided button test guidance, manifest PID matching, firmware cancellation, and the bootloader-exit-before-cancel-report ordering) as Go tests. go build/vet/test and go test -race are all clean across the module.
New code with no Rust precedent (the Rust implementation had no gamepad input handling at all). Decodes the standard USB-HID gamepad usage page (Generic Desktop Page 0x01, Button Page 0x09) — a public USB-IF standard, not vendor-proprietary, so it doesn't touch the clean-room evidence boundary. Documented in spec/gamepad_input.md. internal/input/descriptor.go is a general HID 1.11 report descriptor parser (Main/Global/Local items -> flat Field list with computed bit offsets), verified against a hand-derived, byte-by-byte-checked standard joystick descriptor since no hardware was available to capture a real one from. internal/input/gamepad.go decodes hat-switch/analog-stick d-pad direction and button-page bitmask/array fields from a raw input report against those parsed fields. Report descriptor acquisition is the one real platform gap: karalabe/hid doesn't expose it and there's no portable Go way to fetch it. Linux gets real descriptor-driven decoding via the kernel's hidraw sysfs export (descriptor_linux.go); other platforms (including this dev machine) get an honest "unavailable, here's why" note per device (descriptor_other.go) rather than a fabricated byte-layout guess with no evidence behind it. navstream.go opens a read-only, nav-only input stream per enumerated vid==0x2dc8 device, decoupled from internal/protocol's command session, and emits d-pad/button transition events on a merged channel for a future Bubbletea TUI to consume. go build/vet/test and go test -race are clean across the module.
… locked-down CLI Builds internal/tui (device dashboard, diagnostics with candidate-tier explanation, JP108/Ultimate2 mapping editor, firmware update flow, recovery takeover, settings) and cmd/openbitdo on top of the protocol/core/input packages. Screens are a fresh design, not a port of the prior Rust layout; safety gating (support-tier blocking, write-lock/recovery, candidate write-probe ceremony) is ported faithfully with the same messages. Adds a real overlay-modal confirmation system including a genuine one-time brick-risk acknowledgement before any unsafe/firmware action, closing a gap where the prior implementation only claimed to have one. Keyboard and gamepad nav share one code path via internal/input's event stream.
Replace the placeholder curated HID-usage preset list with Rust's real JP108_PRESETS/U2_PRESETS tables (reducer.rs) and their exact target labeling (u2_target_label), since remap targets are data, not UI design. JP108 and Ultimate2 use unrelated value spaces so they now cycle through separate tables instead of one shared list. Also swap the device-name filter from substring match to sahilm/fuzzy (already an indirect dep via bubbletea's ecosystem), matching the fuzzy search behavior the prior Rust TUI had via SkimMatcherV2.
Two real bugs found while preparing tests for hermetic isolation: 1. unlock.go and report.go both bypassed the Model's injectable settingsPath and called the global SettingsPath() directly, so the candidate-unlock-file location and every saved TOML report always resolved to the real OS user config directory regardless of what Options.SettingsPath was configured with. Threaded settingsPath explicitly through candidateUnlockDir/candidateUnlockFilePath/ candidateUnlockFilePresent and reportsDir/persistSupportReport/ cmdSaveReport instead. 2. The candidate write-probe result handler never called cmdSaveReport at all, unlike diagnostics/firmware/mapping-apply which all persist a report on completion — a real functional gap versus the prior Rust TUI, which does save a runtime_unlock/candidate-write-probe report for this flow. Added the missing save, threading the device through the message (cmdCandidateProbe now takes the device instead of just its VidPid).
…ests Two more functional-parity gaps found while writing tests: - The Devices screen had no way to reload the device list after the one-shot load at startup — no refresh action, no key. For a device-detection tool where "plug in a controller and see it appear" is the core workflow, that's a real regression. Added an "r" rescan key, available even with zero devices connected. - Device ordering was enumeration order only. The README documents "Pick a controller from the grouped dashboard: supported, read-only candidate, or detect-only" as current behavior, but nothing grouped devices by tier. Added a stable sort so full-support devices surface first, matching that documented behavior. Also port the first batch of Rust's tests.rs behavioral suite as Go tests: gatekeeping_test.go (dashboard_*_disabled_reason precedence and exact reason strings), screen_mapping_test.go (mapping draft undo/ reset semantics, the ported preset tables), settings_test.go (schema roundtrip, warn-and-fallback on invalid/missing settings).
Drives the Go Model/Update directly (not through the real Bubbletea runtime) to port: device-selection defaulting to an enabled Diagnose action, tier-grouped device ordering, advanced-mode toggling updating both UI state and the core runtime, the full candidate-write-probe per-PID unlock-file ceremony (including a negative case — denied without a matching unlock file), a diagnostics run-then-back flow, and the write-lock/Recovery takeover (forced regardless of current screen, never clears at runtime). Caught one test-writing mistake worth noting: my first version of the candidate-write-probe test asserted a report was always saved, but the default ReportSaveMode (FailureOnly) correctly skips saving a successful probe — Rust's own equivalent test treated the saved report as optional for the same reason. Fixed by forcing ReportSaveAlways in that test so it deterministically exercises and verifies the report content instead.
There's no /dev/tty in this sandbox, so github.com/charmbracelet/x/exp/teatest (an in-memory virtual terminal) is what actually exercises Init/Update/View through the real running program instead of just unit-testing Update() in isolation. Covers: dashboard renders and keyboard nav moves selection; simulated gamepad d-pad/button events drive identical navigation through the same code path as keyboard; mapping-editor preset cycling changes the rendered target using the real ported preset table; the firmware flow's new brick-risk modal genuinely gates progress (cancel starts nothing, confirm proceeds through preflight to completion); a settings toggle persists to disk and survives a fresh reload; and the write-lock/Recovery takeover fires through the real live message loop. Two real lessons from getting these to pass, left as comments since they'll bite again otherwise: - tm.Output() is a one-shot draining stream: two waitForOutput calls in a row with no Send between them will hang on the second forever, since the first already drained everything from that render, including content after the match point. - bubbletea's renderer skips writing when a frame is byte-identical to the immediately preceding one (not any older frame) — a real no-op key press must not be asserted on for that reason; two renders that happen to match an older frame but differ from the one right before them are fine and do get written.
…o tree Relocates cleanroom_guard.sh, check_docs_consistency.sh, and check_evidence_readiness.py from sdk/scripts/ to scripts/ (sdk/ is being removed), adapting their relative paths. Adds Go equivalents of package-linux.sh/package-macos.sh under packaging/scripts/, verified to build/tar/checksum/pkgbuild correctly on this machine. Fixes every golangci-lint (errcheck, govet, ineffassign, staticcheck, unused) finding against the new Go tree: documented the three previously bare file-close error ignores, applied safe De Morgan/tagged-switch/Fprintf mechanical simplifications, removed genuinely dead code (an unused PID-error constructor, an unused const duplicated across both descriptor implementations, an unwired blink-animation message type, and two style variables that duplicated an existing one byte-for-byte), and suppressed one false-positive staticcheck warning on a platform stub that is correctly always-erroring by design.
Every crate (bitdo_proto, bitdo_app_core, bitdo_tui, openbitdo) has been
ported to Go under internal/{protocol,core,tui,input} and cmd/openbitdo,
with the full behavioral test suite ported alongside it and independently
verified (go build/vet/test/test -race/golangci-lint all clean before and
after this removal). Remaining references to sdk/ in docs and packaging are
fixed in the commits that follow.
…ious error line The flag package returns flag.ErrHelp from Parse() on -h/--help, which was falling through to the generic error path (spurious "error: flag: help requested" line, exit code 1) instead of a clean exit 0. This would have broken the Homebrew formula's test block, which asserts on captured stdout from "openbitdo --help" with a non-error exit code expected.
- Homebrew formula and AUR PKGBUILD templates: license field to
GPL-3.0-or-later / GPL3.
- Drop the AUR depends=('hidapi') line: karalabe/hid statically links its
HID backend (confirmed via otool -L on the built binary, no dynamic
libhidapi dependency, unlike the old Rust hidapi crate).
- Rewrite scripts/package-linux.sh and scripts/package-macos.sh (moved from
sdk/scripts/, which is gone) to build with go build/ldflags instead of
cargo. Verified end-to-end on this machine: package-macos.sh produces a
working binary, tarball, and signed-format .pkg with correct checksums.
Fixed a real bug caught while testing: the pkgroot install step tried to
copy from the staging dir after it had already been rm -rf'd; now copies
from the stable BIN_ASSET path instead.
…ption README/MIGRATION/CHANGELOG updated for the Go rewrite (GPL license, Go build instructions, new migration notes, v0.1.0 changelog entry including the issue #15 fix); VERSION bumped to v0.1.0. Also fixed a real inconsistency in the relocated cleanroom_guard.sh: its new third check (stale Rust/sdk-era references) didn't apply the same (legacy)/(historical) exemption the other two checks already use, so it would have blocked MIGRATION.md's own legitimate historical explanation of the Rust-to-Go move.
ci.yml jobs now build/test the Go module instead of Cargo. Each job already ran on a native runner for its target architecture (ubuntu-latest for linux/x86_64, ubuntu-24.04-arm for linux/aarch64, macos-14 for macOS arm64), so no cgo cross-compilation is needed for karalabe/hid -- plain 'go build' on each native runner works. Kept the same required-check job names (guard, aur-validate, tui-smoke-test, build-macos-arm64, test) that release.yml and RC_CHECKLIST.md already reference, repurposing tui-smoke-test to run the Go TUI package's tests (including the teatest end-to-end scenarios) instead of 'cargo test -p bitdo_tui'. Verified locally: actionlint clean, and I actually ran the equivalent steps on this machine (matches build-macos-arm64/tui-smoke-test) -- golangci-lint run, go vet, go test, and package-macos.sh all succeed. golangci-lint caught a real unchecked-error finding in main.go (introduced by my earlier --help fix) and it's fixed here with a documented reason, not a silent discard.
Same transformation as ci.yml: drop sdk/ working-directory and Rust toolchain installs, add actions/setup-go, fix package-macos.sh's call signature (its 3rd positional arg changed meaning from a Rust target triple to an install prefix -- the old call would have silently passed 'aarch64-apple-darwin' as an install path), and fix dist/ artifact paths that were sdk/dist/ before sdk/ was removed. Verified: actionlint clean on both workflow files, and I actually ran the exact package-macos.sh invocation release.yml uses (two positional args, version + arch) end-to-end on this machine -- produces a working binary, tarball, and .pkg with correct checksums.
… not hand-edited Rust tables The guide pointed contributors at sdk/crates/bitdo_proto/*.rs files that no longer exist. Rewritten to describe the actual current process: edit spec/pid_matrix.csv and spec/command_matrix.csv, run 'go generate ./...' to regenerate internal/protocol/registry_generated.go, and the equivalent capability/gating logic now lives in internal/protocol/registry.go and session.go. Verified 'go generate ./...' produces no diff against the already-committed generated file, confirming the guide's instructions are accurate.
Fixes the gap where gamepad nav silently degraded to keyboard-only on macOS (descriptor_other.go's !linux stub always returned 'not implemented'). karalabe/hid's public Go API has no descriptor-related function -- confirmed via 'go doc' and by grepping the vendored hidapi C source's public header (hidapi.h has no hid_get_report_descriptor or similar) -- so this uses IOKit directly via cgo. Approach: karalabe/hid's DeviceInfo.Path on darwin is an IOService-plane registry path (hidapi's mac backend builds it via IORegistryEntryGetPath in hidapi/mac/hid.c, and reverses it the same way in hid_open_path via IORegistryEntryFromPath -- confirmed by reading the vendored C source). Reusing that same path string, IORegistryEntryFromPath resolves it back to the IOKit registry entry, then IORegistryEntryCreateCFProperty reads the 'ReportDescriptor' property (kIOHIDReportDescriptorKey, confirmed against the actual IOHIDDeviceKeys.h header on this machine -- 'Data property that describes the report descriptor of the device') directly off the entry -- no IOHIDManager/IOHIDDeviceRef needed. IOKit/CoreFoundation frameworks are already linked into the binary via karalabe/hid; this cgo file links them explicitly since it's a separate translation unit. Also narrowed descriptor_other.go's build tag from '!linux' to '!linux && !darwin' so it no longer collides with the new darwin file. Verified: go build/vet/test/test-race/gofmt/golangci-lint all clean on this macOS arm64 machine (which is what actually proves the darwin build tag path compiles -- CI never would have caught this since it never runs locally). Switched from the deprecated kIOMasterPortDefault to kIOMainPortDefault after the first build emitted a deprecation warning.
…is broken on modern macOS
The first implementation (previous commit) reused DeviceInfo.Path to
re-resolve a device via IORegistryEntryFromPath. Testing that against real
hardware on this machine revealed Path is empty for every single
enumerated device here -- not a sandboxing artifact (confirmed by
re-running outside this session's sandbox, same result), and not specific
to internal-vs-external devices (confirmed no enumerated device has a
non-empty Path at all).
Root cause, traced into the vendored hidapi C source
(hidapi/mac/hid.c:296-316): hidapi resolves an IOHIDDeviceRef's
io_service_t via dlopen("/System/Library/IOKit.framework/IOKit", RTLD_LAZY)
+ dlsym("IOHIDDeviceGetService") -- an OS X 10.5-era compatibility shim
for detecting whether the modern API exists. Verified directly with a
standalone dlopen() test: that hardcoded path no longer resolves on this
SDK (dlopen fails, reporting the library is not in the dyld cache). When
dlopen fails, hidapi silently falls through to a struct-offset hack
reading raw IOHIDDevice internals that hasn't matched the real struct
layout since OS X 10.5, producing a garbage io_service_t -- so
IORegistryEntryGetPath always fails and Path is always empty. This is a
real bug in the pinned karalabe/hid@v1.0.0 dependency's vendored hidapi
copy, not an artifact of this environment; it would affect a real 8BitDo
controller on this machine identically.
Fix: don't use Path at all on darwin. Re-enumerate independently via
IOHIDManager (a modern, non-deprecated API -- IOHIDManagerCreate/
SetDeviceMatching/Open/CopyDevices) and match by vendor/product/usage-page/
usage, which DeviceInfo populates correctly (confirmed: only Path was
broken). Once matched, read the ReportDescriptor property directly off
the IOHIDDeviceRef via IOHIDDeviceGetProperty -- no registry-path detour
needed at all with this approach.
This changes fetchReportDescriptor's signature from (devicePath string) to
(info hid.DeviceInfo) across all three platform files (descriptor_linux.go,
descriptor_other.go, descriptor_darwin.go) and the navstream.go call site,
since Linux's implementation only ever needed info.Path but the shared
signature has to accommodate what each platform actually has available.
Verified against real hardware, not just compiled: added
descriptor_darwin_test.go, a real-device smoke test (not a hermetic unit
test -- no 8BitDo controller is available here) that enumerates real HID
devices and round-trips fetchReportDescriptor + the existing
ParseReportDescriptor through them. On this machine: 5/5 real (non-zero
VID) devices succeed end-to-end (Apple's internal accelerometer, gyro,
light/temp sensor, and keyboard backlight controller -- vid=0x05ac,
various pids), acquiring real report descriptor bytes (20-179 bytes) and
correctly parsing them (0-22 fields depending on device shape). Confirmed
this passes both with and without the sandbox this session runs under, so
it's not a sandbox-dependent result. This is the same code path a real
8BitDo controller (vid=0x2dc8) would take -- proven end-to-end against
real IOKit hardware, just not an 8BitDo device specifically, which remains
the one thing that needs real hardware to fully confirm.
go build/vet/test/test-race/gofmt/golangci-lint all clean.
…n spec/gamepad_input.md
openSessionForOps, DiagProbe, CandidateWriteProbe, and runTransferTask all hardcoded protocol.NewHidTransport() in non-mock mode, which meant every real-mode code path (including the safety-critical write-failure rollback logic) was completely untestable without physical hardware -- mock mode takes an entirely separate, trivially-succeeding branch that never reaches any of this code. Added an unexported transportOverride field + transport() accessor on OpenBitdoCore, defaulting to the real HID transport exactly as before when unset. Tests in the same package can now set transportOverride directly to a protocol.MockTransport and exercise the real-mode logic (including rollback-on-write-failure) with scripted responses. No behavior change for real usage -- this is purely a testability seam.
Adds real, targeted coverage (37.7% -> 45.4%) for the paths that matter most given this package holds firmware and mapping safety gating: - rollbackAfterWriteFailure's three outcomes (no backup, rollback succeeds, rollback fails) and WriteRecoveryReport.RollbackFailed(). - CandidateWriteProbe's independent denial gates as separate tests (non-candidate tier, missing advanced-mode/risk-ack, no safe-write capability) -- the unlock-file-missing gate was already covered. - ConfirmFirmware/StartFirmware/CancelFirmware guard clauses (missing risk ack, unknown session id, wrong session state). - validateFirmwareImage's empty/oversized/missing-file rejections. - fetchBytes and verifyArtifactSignature via local httptest servers -- non-2xx status, unreachable host, unsupported signature algorithm, a well-formed-but-wrong-key Ed25519 signature, malformed signature encoding, and DownloadRecommendedFirmware's hash-mismatch/no-match rejections. Only rejection paths are testable this way since the pinned Ed25519 keys are public-only by design (no private key in this repo), but rejection is exactly the safety-relevant direction. Uses the transportOverride seam added in the previous commit rather than needing real hardware.
Drives the actual compiled binary via a subprocess rather than testing flag.FlagSet.Parse in isolation, since the original bug was specifically in main()'s handling of flag.ErrHelp (spurious 'error:' line on stderr, exit code 1 instead of 0) -- an interaction only visible by exercising the real main() entry point end to end. Sanity-checked this actually catches the regression: temporarily reverted the flag.ErrHelp fix, confirmed TestHelpFlagExitsCleanlyAndWritesToStdout fails with exactly the original symptom (exit status 1, 'error: flag: help requested' on stderr), then restored the fix and confirmed it passes again. Coverage: cmd/openbitdo 0% -> has real regression coverage for its one previously-broken behavior.
Was still pinned to v0.0.1-rc.4 throughout (title, tag references, artifact filenames) despite main having moved to v0.0.2 and this branch's VERSION to v0.1.0. Required-checks list was also missing build-linux-x86_64 and build-linux-aarch64, which exist as real jobs in ci.yml. Rewrote for v0.1.0: corrected tag/artifact references, completed the required-checks list against the actual ci.yml job names, and replaced the stale RC-4-era 'Current Status Snapshot' with an honest accounting of this rewrite's actual state -- including that CI has never run on a real GitHub Actions runner yet, and that real-hardware controller validation is still outstanding. Left the macOS signing/notarization section's substance unchanged (still accurate, unrelated to the rewrite).
…patibility
Real CI run on GitHub Actions caught this (not reproducible locally, since
this machine's golangci-lint happens to be a newer Homebrew build): the
current latest golangci-lint release (v1.64.8) is built with go1.24 and
refuses to lint code declaring a newer targeted Go language version
("the Go language version (go1.24) used to build golangci-lint is lower
than the targeted Go version (1.27)"). Nothing in this codebase actually
requires Go 1.27-specific language features -- the directive was only set
to match whatever toolchain happened to be installed during the original
build, not a deliberate requirement. 'go mod tidy' settled on 1.24.5 (a
real dependency minimum), which stays within golangci-lint's supported
1.24 line.
Real root cause of the lint failures on this branch: v6 (published before golangci-lint v2 existed) resolves version:latest to the stale v1.x release line (v1.64.8, built with go1.24) instead of the actual current release (v2.13.1, confirmed via the GitHub API as the true latest, built with go1.27.0 -- matching what's installed locally via Homebrew, which is why this never reproduced on this machine). .golangci.yml's v2-schema config (top-level 'version: "2"', 'linters.default') was already correct for the real current release; the action pin was the actual bug, not the config format. v9.3.0 is the current golangci-lint-action release.
…ceholder fixtures
packaging/scripts/package-{linux,macos}.sh are diverged, unreferenced duplicates
of scripts/package-{linux,macos}.sh (confirmed via repo-wide grep). The 6
removed harness/golden files are read by zero Go code and were already
labeled sanitized-placeholder (not real data) in the Rust version.
…under docs/ and scripts/ Minimal idiomatic Go root: cmd/, internal/, go.mod stay at root. Everything else moves under docs/: - docs/MIGRATION.md, docs/RC_CHECKLIST.md (from root) - docs/spec/ -- load-bearing + living reference docs (pid_matrix.csv, command_matrix.csv, evidence_index.csv, protocol_spec.md, gamepad_input.md, alias_index.md, device_name_catalog.md, requirements.yaml) - docs/process/ -- active/operational docs only (branch_policy, cleanroom_rules, commenting_standard, add_device_guide, aur_publish_troubleshooting, release_scope_gate) - docs/clean-room-evidence/ -- historical RE-campaign record, clearly separated from load-bearing spec material: dossiers/ (74 files), device_lab.yaml, wave1/wave2 docs, dirtyroom docs, community_evidence_intake, device_name_sources scripts/ absorbs packaging/scripts/'s two live files (render_release_metadata.sh, test_render_release_metadata.sh -- the two dead duplicate package-*.sh files were already removed in the prior commit). internal/protocol/testdata/profile_fixture.bin -- the one harness/golden fixture actually used by any Go code moves to the idiomatic Go testdata/ location. All moves done via git mv; git status shows clean renames, not delete+add.
- registry.go's go:generate directive and gen/main.go's -spec-dir default: ../../spec -> ../../docs/spec - registry_test.go's hardcoded fixture paths (a real bug -- these tests would have failed on this branch): ../../spec/*.csv -> ../../docs/spec/*.csv - Doc-comment references to spec/*.csv across command.go, types.go, registry.go, validation.go, gen/main.go -- including the comment strings gen/main.go writes into the generated file's header, so regenerated output stays accurate too. - Fixed a double-docs/ typo in registry.go introduced by an overly broad sed pass running after a manual edit had already touched the same line. Verified: 'go generate ./...' finds the CSVs at the new path and produces only the expected comment-text diff in registry_generated.go -- zero data drift, confirming the moved CSVs are byte-identical to before.
- cleanroom_guard.sh: scan_paths now includes docs/; active_docs points at docs/MIGRATION.md. - check_docs_consistency.sh: scans docs/ as a whole instead of the old MIGRATION.md/RC_CHECKLIST.md/process/spec paths individually. - check_evidence_readiness.py: SPEC now docs/spec; added a separate DOSSIERS constant pointing at docs/clean-room-evidence/dossiers -- a real bug fix, since dossiers moved to a different subtree than the CSVs, not just under a docs/ prefix of the old spec/ path. Without this the script would have silently found zero dossier files for every candidate-readonly PID. - internal/protocol/profile_test.go and profile.go: fixture path/comment updated to the new testdata/profile_fixture.bin location. Verified: cleanroom_guard.sh, check_docs_consistency.sh, and check_evidence_readiness.py all pass; profile roundtrip test passes.
…ment) SECURITY.md: standard private-disclosure policy via GitHub's advisory flow, scoped to this project's real risk surface (HID access, firmware manifest verification, firmware writes, settings/report file handling). CONTRIBUTING.md: build/test via the new justfile, the clean-room rule explained plainly (with an explicit warning against doing dirty-room binary analysis and clean-room code review in the same sitting -- the exact concern this project's own docs/process/dirtyroom_collection_playbook.md and docs/process/dirtyroom_dossier_schema.md exist to guard against), code style, and PR process, plus a CLA: contributors keep copyright of their own work but grant the maintainer broad usage rights including the right to relicense -- explicitly what was asked for. Flagged in the document itself as a starting draft, not a substitute for real legal review at meaningful contribution scale. Also fixed a real, pre-existing staleness bug I found while linking to it: docs/process/cleanroom_rules.md still referenced 'cleanroom/spec/**' etc -- a leftover from when the repo directory itself (not a subdirectory) was named 'cleanroom', now renamed to 'openbitdo'. Fixed to the real current paths (docs/spec/**, docs/process/**, scripts/cleanroom_guard.sh). Verified every path referenced in both new docs actually resolves on disk, not just that the guard scripts pass.
…stale Go version reference README.md still said 'Go 1.27+' for building from source, but go.mod was lowered to 1.24.5 earlier for golangci-lint compatibility -- fixed. Also added pointers to the legacy/rust-tui branch (created to preserve the prior Rust implementation) in both docs, which was explicitly out of scope for the feature-work fork and left for me to add directly.
… device Security review (confidence 8/10) found that DownloadRecommendedFirmware verifies SHA-256 + Ed25519 signature in memory, then discards the verified bytes and only carries the file path forward through the human-confirmation -gated Preflight -> Start -> Confirm flow. runTransferTask re-read the file from disk at transfer time with no re-verification against the recorded hash -- a same-user process overwriting the file during that (human-paced, seconds-to-minutes) window would get its content written straight to the device, defeating the signature verification entirely. Fix: runTransferTask now recomputes the SHA-256 of the bytes it's about to send and compares against handle.plan.ImageSHA256 (always set by PreflightFirmware, the only path that creates a session handle) before any device interaction, failing closed on mismatch. Verified the fix and the regression test for real: temporarily reverted the fix and confirmed the test fails with the tampered bytes genuinely reaching the transport layer (EnterBootloader -> SendCommand -> Write), then restored the fix and confirmed it passes. Uses panicTransport as the transport so a future accidental removal of the check would fail loudly via an unrelated panic rather than silently passing.
…rlay, icon vocabulary, per-screen footer hints Source-verified against opencode's actual TUI code (not guessed from screenshots): every border in its codebase is a single-side left (or occasionally bottom) rule, never a full box, and dialogs are borderless solid-color panels floating on a dimmed backdrop. - theme.go: stylePanel/stylePanelActive now use a left-only border (a custom lipgloss.Border with just Left:"┃" set) instead of RoundedBorder(), colored by context (BorderDim default, Accent focused). New barred() helper + styleWarningBlock/stylePositiveBlock/ styleAccentBlock/styleDangerBlock give the same left-bar treatment to block-level prose (tier explanations, firmware warnings/errors) -- mirroring how opencode colors whole message blocks by role, not just inline text. Removed dead Border/TextDim/AccentDim fields. Modal styling (styleModal) is now borderless entirely, matching opencode's actual dialog-confirm.tsx exactly -- danger/normal distinction carried by title/button text color alone, same as opencode. - New Icon* constants centralize what were previously ad hoc per-screen glyph choices (diagnostics pass/fail/warn, firmware warning triangle, progress bar fill/empty, tier badges) into one shared vocabulary. - modal.go: real dim-and-overlay compositing (viewOverlaid) replacing full-page replacement. Lipgloss/Bubbletea compose styled cells, not RGBA layers, so there's no native alpha-blend -- this strips all color from the rendered page (ansi.Strip), re-applies one uniform TextFaint foreground (real UI dimming desaturates/flattens anyway), then splices the modal's own lines in using ansi.Cut (escape-code- and display-width-aware, won't corrupt an SGR sequence mid-cut). - app.go: screenHelp() now gives each screen real contextual footer hints (mapping's cycle-target/preview keys, firmware's stage-specific confirm/cancel, settings' toggle key, recovery's restore/quit) instead of the same generic 4-hint line everywhere except Devices. Verified, not just written: TestView_ModalDimsBackgroundInsteadOfReplacingIt proves the dim-overlay actually restyles the background (not just that text survives) by comparing exact styled bytes before/after, forcing a real color profile since go test's stdout isn't a TTY and lipgloss silently downgrades to no color otherwise -- caught this by debugging an initial false failure rather than assuming the test or the code was wrong. Full existing suite (build/vet/fmt/lint/test/test-race) stays green.
… render Both verified for real, not just written: temporarily reverted the panel border change and confirmed TestView_PanelsUseLeftBarNotRoundedBox fails with the exact rounded-corner glyph it's supposed to catch, then restored and confirmed it passes again. TestView_ModalDimsBackgroundInsteadOfReplacingIt needed forcing a real lipgloss color profile -- go test's stdout isn't a TTY so lipgloss silently downgrades to no color, which caused an initial false failure; debugged that rather than assuming either the test or the implementation was wrong before finding the real cause.
…rt and nav stream
karalabe/hid's vendored mac hidapi backend resolves every device's Path to
empty on this SDK/OS (its dlopen("/System/Library/IOKit.framework/IOKit")
compatibility shim no longer resolves), so DeviceInfo.Open() -- which opens
purely by Path -- failed for every real device, including the exact
"open failed for 2dc8:6012: hidapi: failed to open device" reported earlier.
internal/machid re-implements Open/Write/Read/Close directly against
IOKit's IOHIDManager, matching by vendor/product/usage-page/usage instead of
Path, and is now wired in on darwin for both internal/protocol's
diagnostic/command session transport and internal/input's gamepad nav
stream (via a small hidDevice/navDevice interface so non-darwin platforms
keep using karalabe/hid's Open() unchanged, since only the mac backend has
this bug).
Verified end-to-end against the real connected 8BitDo Ultimate 2 (PID
0x6013): device matching, IOHIDDeviceOpen, and IOHIDDeviceSetReport all
succeed cleanly (IOReturn 0) with the callback registered before open, no
exclusive-seize issues, and Input Monitoring access confirmed granted via
IOHIDCheckAccess. No command in the generated protocol registry elicited an
actual device response in this session (including a Confidence:"confirmed",
PID-unrestricted one) -- traced to the registry's "confirmed" meaning
confirmed present in the vendor binary via static analysis, not confirmed to
elicit a hardware response; a comparable dossier says so explicitly ("no
runtime trace or hardware write confirmation yet"), and none exists yet for
0x6013. That gap is documented in machid's package doc and left for the
separate dirty-room protocol reverse-engineering process -- it's not a
transport bug.
…ion clarity stylePanelTitle and styleAccent were byte-for-byte identical lipgloss styles, so panel headings and accent-styled selected rows rendered indistinguishably -- the direct cause of the user-reported "it's weird to tell what button I'm selecting" bug on the Mapping Editor. Give headings a genuinely distinct definition (Underline added), and give list selection its own dedicated styling (styleSelectedRow/styleSelectedMarker, an inverted background) instead of reusing styleAccent, applied consistently across the Mapping Editor, Settings, Diagnostics, and the Devices Actions pane -- Diagnostics previously had no selection styling at all. styleSelectedMarker exists separately from styleSelectedRow because Diagnostics' check rows already embed their own styled pass/fail icon; wrapping that in an outer background style would have the icon's own reset code cut the background off partway through the row, so only the "›" marker gets the inverted treatment there. Also gives styleBody real, consistent application to plain body text that was previously unstyled raw output by omission (device status/evidence lines, diagnostics check-count and detail fields, mapping preview rows). Added two regression tests, verified to actually catch the regression by reverting the fix and confirming they fail: one asserts the two theme.go style definitions render different bytes for the same text, the other renders a real Mapping Editor frame and asserts the selected row's styling differs from the heading's.
Footer's "enter/A"/"esc/B"/"dpad" hints were hardcoded on every screen even with no gamepad connected -- confusing on a keyboard-only setup. They're now conditional on internal/input.Start's per-device Notes actually reporting an active gamepad nav stream (the same data already surfaced on Settings), via a new gamepadConnected() helper. This is a startup-time snapshot, not a live connect/disconnect signal -- internal/input.NavEvent has no Connected/Disconnected kind yet -- documented as a real, known limitation rather than papered over. Also added the missing right/tab hint to the Devices footer (the actual key that moves focus into the Actions pane, previously undocumented there). Investigated Switch-vs-Xbox button-layout awareness via GetMode's parsed mode byte: real data exists (validation.go parses response[5] into parsed["mode"]), but nothing in this codebase or its evidence dossiers documents what values mean, and docs/spec/device_name_catalog.md lists every known PID's protocol family as "DInput" uniformly -- no separate Switch-layout protocol family in the evidence at all. Scoped down to just connected/not-connected label-hiding rather than guess at a mode-to-layout mapping with no hardware-confirmed evidence behind it; documented in app.go for whoever revisits this once real evidence exists. Three new tests, verified against a reverted fix to confirm they actually catch the regression: controller hints appear only once a gamepad is connected, an "unavailable" nav note doesn't false-positive as connected, and the Devices footer mentions right/tab.
…creens Verified against real rendered output, not guessed: built the binary and captured real Terminal.app screenshots plus teatest-style plain-text frame dumps of all 6 screens to audit actual blank-line/padding density, per the user's "everything feels too spaced out" feedback. Found a genuine structural bug on two screens: an optional section (Devices' Blocked/candidate-tier explanation, Firmware's Warnings block) unconditionally ended with its own trailing blank line, and the following section unconditionally started with its own leading blank line -- when the optional section was absent, both screens still applied one side of that convention, producing a real double blank line, not just a "could be tighter" spacing preference. Fixed both by building each screen's optional sections as a slice of self-contained blocks (no leading/trailing blank of their own) and joining them with strings.Join(blocks, "\n\n"), which guarantees exactly one blank line between whichever sections are actually present. The other 4 screens (Diagnostics, Mapping, Settings, Recovery) were audited against the same real output and found already consistently single-blank-line spaced -- no changes needed there beyond applying styleBody to Recovery's remaining unstyled body text, matching Phase 2's intent. Two new regression tests count the longest run of consecutive blank lines in real rendered output (after stripping the panel's left-bar border glyph, which prepends every line including blank ones, and trailing Height() padding filler, which is expected and not what's being checked) and assert it's never more than 1. Verified against the pre-fix code (via git show HEAD) to confirm they actually fail there -- the first version of the checker didn't (it never stripped the border glyph, so it never saw a truly-empty line at all), which the revert check caught before this landed.
…ented the real bypass Investigated whether tea.WithMouseCellMotion() genuinely blocks native terminal text selection, or whether a modifier-key bypass already works. Conclusion: nothing is broken in this codebase to fix. Mouse-reporting interception of click-drag is standard, universal terminal-emulator behavior once a program requests it (XTerm mouse tracking mode 1002) -- and the override that lets a modifier key bypass that interception and fall through to native selection is implemented entirely by the terminal emulator, before the running program ever sees the event. There is no escape sequence or configuration this codebase could send to break or fix that either way. Verified the actual bypass key directly against Ghostty's own local docs (/Applications/Ghostty.app/.../ghostty.5.md, the mouse-shift-capture setting) rather than assuming: it's Shift, not Option as originally assumed -- default mouse-shift-capture=false means Shift is never even sent to the program, so native selection works regardless of what OpenBitdo requests. Confirmed the user's own Ghostty config has no override, so this is active for them right now. This was worth checking rather than trusting general terminal-emulator knowledge, since Option is the more common default on other macOS terminals (Terminal.app, iTerm2) and asserting it for Ghostty specifically would have been wrong. Documented the finding in README.md's First Run section, since "already works and just isn't documented" was exactly the outcome -- including that the modifier varies by terminal, and that most terminals (Ghostty via mouse-reporting) also offer an app-wide opt-out for users who never want mouse capture at all.
…cs + ASCII fallback
Renders a labeled grid of every physical button for the device kind being
edited, highlighting whichever one the cursor currently has selected --
using styleSelectedRow, the same selection idiom every other screen in this
app already uses, so it reads as one more selection list rather than a
one-off visual style.
Deliberately a wire-order grid, not a spatial gamepad silhouette, for both
device kinds: JP108 (PID_108JP = "Retro 108 Mechanical Keyboard", per
docs/spec/device_name_catalog.md) turned out to be a full mechanical
keyboard, not a gamepad, and nothing in this project's spec or evidence
dossiers documents where its 10 dedicated buttons physically sit on that
keyboard. Drawing a specific physical layout would be exactly the kind of
guessed hardware fact this project's own conventions deliberately avoid
(see internal/input/descriptor_other.go's comment on the same principle).
Ultimate2 is a conventional gamepad and could support a real spatial
diagram, but uses the same grid style so both device kinds get one
consistent, honest diagram rather than a precise-looking layout for one and
an admittedly-approximate one for the other.
Real Kitty graphics protocol support (github.com/charmbracelet/x/ansi's
KittyGraphics, already a direct dependency) on top of the labeled grid when
the terminal supports it: a small PNG of solid-color rectangles (selected
vs not), generated with the standard image/draw/png packages, base64-encoded
into a genuine Kitty APC escape sequence. No text is rendered into the image
itself (no font-rendering dependency) -- the labeled ASCII grid is always
printed too, so the image is a visual accent on real, readable labels, never
a wordless color swatch standing alone. Terminal capability detection
(KITTY_WINDOW_ID, TERM containing "kitty", TERM_PROGRAM ghostty/WezTerm/
kitty) was checked against real documentation, not assumed: Ghostty's own
local docs (ghostty.5, image-storage-limit) confirm it implements "the
Kitty image protocol."
Added String() methods to core.DedicatedButtonID and core.U2ButtonID (JP108
and Ultimate2's physical-button ID types) -- needed for the diagram's
labels, and as a side effect fixes an existing gap where the Mapping
Editor's row labels showed raw numeric IDs ("0", "1"...) instead of names
("A", "B"...), since %v formatting already picks up a Stringer automatically.
Tests cover: selected-vs-unselected highlighting in the ASCII grid,
capability detection across several TERM/TERM_PROGRAM/KITTY_WINDOW_ID
combinations, that the Kitty path produces a real APC sequence wrapping a
payload that decodes to a genuinely valid PNG (not just plausible-looking
bytes), that the image is always paired with the labeled grid, and that the
real Mapping Editor screen actually includes the diagram. Verified against
reverted code to confirm the highlighting and PNG-validity tests actually
fail there.
Live-terminal visual confirmation of the Kitty graphics image rendering as
real pixels was not obtained -- GUI automation against the user's actual
terminal was ruled out mid-session (interferes with their other running
Claude Code sessions in the same terminal app). The ASCII fallback path was
visually confirmed via a real Terminal.app screenshot (a separate app the
user isn't actively working in); the Kitty path's correctness rests on the
PNG-payload-validity test, not a live screenshot.
… pending encoding reconciliation Sanitized dirty-room evidence (separate isolated process, static analysis of the official vendor software cross-validated across two independent builds) describes back-paddle remapping as indices 18-21 of a 22-slot uint32 function-bitmask array. Implemented U2PaddleID and the ~30-value U2Function catalog in internal/core/paddles.go, including the real, asymmetric device restriction that only 'act as paddle 1'/'act as paddle 2' are valid remap targets for other inputs (paddles 3/4 can be assigned any function themselves, but nothing else can emulate them). Deliberately NOT wired into internal/protocol's U2ReadButtonMap/ U2WriteButtonMap or into any TUI apply-to-device path. While implementing this, found the existing protocol calls use a materially different, already-shipped model: 17 (not 18) core buttons, each a uint16 raw HID-usage code (not a uint32 function bitmask), no chunking, 34 bytes total -- vs. this evidence's 22 x uint32 = 88 bytes needing chunked transfer. That existing model traces back to this same dossier's own pre-existing entry, which never actually specified byte offsets -- it was an implementation-time assumption, not dirty-room-verified fact. The new evidence is more rigorous and is very likely the more accurate model for ALL slots, not just the 4 new paddle ones. Documented as an open question in the dossier (button_map_paddle_extension.OPEN_QUESTION_encoding_mismatch) rather than guessing how to reconcile them -- building paddle writes on top of a foundation with this many unresolved mismatches would risk sending malformed data to real hardware, which is worse than not shipping the feature yet. Verified: go generate produces the expected zero diff, full build/vet/ gofmt/lint/test/test-race/guard/just check all clean. New tests for the bitmask catalog (all 32 values distinct single bits, diagonal OR'ing, paddle-3/4 asymmetry, slot-index mapping) verified against a deliberate break-then-restore of AssignableAsPaddleTarget, confirming they actually catch a regression rather than trivially passing. No hardware attached to this machine -- everything here is mock/unit-test verified only, matching the rest of this session's honesty discipline about what real-hardware confirmation still requires.
User feedback: the small screen-label text next to the OpenBitdo title (e.g. 'Devices') was unwanted. Removed screenLabel() and the header's right-aligned crumb entirely -- header is now just the title/[mock] tag.
Live hardware testing reported the firmware flow as "weird and disconnected... not done and abrasive to use." The screen had no sense of progress across its 6-stage state machine, the ready-to- confirm plan rendered as bare unstyled text (inconsistent with the app's block-styling elsewhere), and the final Done stage still showed an in-progress icon on the step it had just finished. Adds a Download -> Verify -> Confirm -> Transfer -> Done breadcrumb (fwSteps/currentFwStepIndex/renderFwStageIndicator), restyles the ready-to-confirm plan using the existing accent/warning/positive block treatment instead of raw text, and fixes the breadcrumb to mark the terminal Done step as passed rather than still in-progress. Kept inline rather than converted to a modal: the brick-risk acknowledgement already runs as its own modal before this screen loads, and the plan detail here needs more room than the modal system's ~60-column cap. Covered by new direct-render tests across every stage (including Denied/Error/Cancelled/Unverified/Failed, which the interactive flow doesn't reach) and an extended real-program teatest driving the full happy path end to end.
…open question A dedicated dirty-room reconciliation pass re-derived the wire structure from primary evidence in two independent decompiled builds: the 22 x uint32 / bitmask-catalog model is confirmed correct, and the shipped 17 x uint16 internal/protocol implementation is wrong (not a legitimate alternate encoding), most likely from an earlier pass reading the wrong nested array in the same config record. The one piece that stays unconfirmed is the multi-report chunking scheme needed to move 88 bytes over a 64-byte HID report -- tracked here as its own open question, with read and write given different risk tolerance (a bad read shows wrong values in a read-only view; a bad write can scramble a real device's persistent button-map config).
…t startup Start's poller re-enumerates vid==0x2dc8 devices every 1.5s and diffs against the previously known set, starting a nav stream for each newly-connected device the same way the initial enumeration does and emitting EventDeviceConnected/EventDeviceDisconnected on the existing NavEvent channel for each change. Previously the only way to pick up a device plugged in after launch was restarting the app.
DiagProbeCached/DiagProbeFresh wrap the existing DiagProbe without changing its signature or behavior for current callers. Results are keyed by VidPid+Serial (matching AppDevice's own identity) and held in memory for the process's lifetime, so navigating away from a device and back doesn't force a re-run. CachedDiag/HasDiagnosed give read-only access for a consumer that just wants to know what's already there.
Two prior commits (804bfae, fbb1292) added the capability -- hotplug connect/disconnect events on internal/input's nav channel, and internal/core's DiagProbeCached/DiagProbeFresh -- without touching the TUI, so none of it was actually reachable yet. This wires it in: - EventDeviceConnected/EventDeviceDisconnected now flow through Update() before nav-to-key translation, refresh the device list the same way a manual "r" rescan does, and keep navNotes (and therefore gamepadConnected/Settings' "Gamepad Navigation" list) live instead of a startup-only snapshot -- closes the exact gap gamepadConnected's own comment used to flag. - Every device list load -- startup, manual rescan, or a hotplug-triggered reload -- auto-diagnoses any device this session hasn't probed yet (DiagProbe only issues read-only SafeRead HID commands, safe without confirmation), so a freshly-connected controller already has a cached result by the time the user navigates to Diagnostics. - The Devices screen's Diagnose action checks the cache synchronously first: a hit renders instantly with no loading flash, a miss still runs and caches a fresh probe. - The Diagnostics screen shows a "Last run: Xs ago (r to rerun)" staleness indicator; "r" now explicitly forces DiagProbeFresh, bypassing the cache, rather than relying on a plain rerun that happened to always be fresh before caching existed. - A live disconnect of the device currently shown on Diagnostics surfaces the same KindDeviceDisconnected rescan hint an operation-level disconnect already renders, instead of a second disconnect story. Firmware is deliberately left alone: an in-flight transfer already has its own operation-level disconnect handling, and interrupting it based on a background poll noticing a transient enumeration miss would be actively dangerous. internal/core and internal/protocol show as modified/untracked in the working tree from unrelated, concurrently in-progress work (a button-map encoding fix) -- not touched by or included in this commit.
The confirmed wire shape is 22 x uint32 (88 bytes), which cannot fit in a single 64-byte HID report -- a correct implementation needs multi- report chunked transfer, and the exact paging scheme is not confirmed by any evidence (see docs/clean-room-evidence/dossiers/6012/u2_core.toml, OPEN_QUESTION_chunking_mechanism). The previously-shipped implementation (17 x uint16, single report, no chunking) is now confirmed wrong, not a legitimate alternate encoding. U2ReadButtonMap/U2WriteButtonMap now perform zero HID I/O and return a typed sentinel error (CodeU2ButtonMapUnavailable) unconditionally, rather than risk corrupting a real device's persistent button-map configuration with guessed chunking. Proven via a real transport that panics on any Write/Read call -- verified to actually catch a regression (temporarily reverted the block, confirmed the test fails, restored it). IndexedUsage (JP108's raw HID-usage-code dedicated mapping) is untouched; IndexedFunction is the new, separate type for U2's uint32 bitmask encoding.
…acefully on real hardware U2ButtonMapping.TargetHIDUsage (uint16) is now Target (core.U2Function) -- a bitmask from the shared function catalog, matching the confirmed wire encoding, not a raw HID usage code. Adds U2PaddleMapping, the paddle-side counterpart, and wires paddles.go's previously-unused U2PaddleID/ U2Function types into mock defaults and real-hardware handling for the first time. U2ReadCoreProfile/U2PreviewSlot now tolerate the protocol layer's new button-map block as a non-fatal, expected condition on real hardware: mode/firmware/analog data (unrelated to the button map) still comes through, with U2CoreProfile.MappingsUnavailable carrying a human- readable reason instead of failing the whole profile read. u2ApplyWrite deliberately does NOT swallow the same block: U2SetMode may already have taken effect on a real device by the time the (always- blocked) button-map write fails, so letting the error propagate is what drives the existing backup/rollback path to put the device back the way it was. Net effect: applying anything via the Ultimate2 Mapping Editor against real hardware is safely blocked with rollback, not silently partial. RestoreBackup's own button-map write *does* swallow the same block, since by construction there's nothing to roll back on that front if the original write never touched the device. Mock mode is unaffected either way -- it already short-circuited before ever reaching the protocol layer. buttons.go: documented (not guessed) that the confirmed 22-slot array has 18 core button slots, one more than this codebase's 17 named U2ButtonID values; the evidence doesn't identify the 18th, so it stays unnamed and preserved opaquely rather than invented. Updated TestU2PreviewSlotReadsRequestedSlotNotActiveSlot and TestU2MockProfileRoundtripSupportsBackupAndRestore for the corrected shape; both now also assert the new MappingsUnavailable/PaddleMappings behavior.
Extends the existing draft/undo/apply infrastructure (already built for JP108/Ultimate2 button mapping) with 4 more editable rows for the back paddles, appended after the 17 button rows and before the Apply/Undo/ Reset virtual rows. Cycling a row (button or paddle) now walks the full U2Function catalog via a new cycle table, replacing the old raw 0x0100-0x0110 HID-usage-style preset list that doesn't apply to U2 anymore. cmdU2Apply (commands.go, out of scope for this change -- owned by a concurrent pass) only forwards button targets, not paddle targets; real- hardware apply is blocked entirely regardless (see the protocol-layer commit), and mock mode ignores wire content and always succeeds, so paddle edits still round-trip correctly through handleMappingApplyResult copying the whole draft (paddles included) into the loaded baseline on success. Documented inline rather than silently relying on it. When MappingsUnavailable is set (always true for real hardware today), the screen shows a clear explanation instead of an empty row list. New tests: row-count/cursor-routing/dirty-detection unit tests, an unavailable-state render test, and a real-Bubbletea-program-loop teatest proving a paddle remap can be drafted and applied end to end in mock mode. That teatest deliberately uses a 60-row terminal, not this file's usual 30 -- see its comment for a real, pre-existing (not introduced by this change, confirmed via git-stash bisection) anomaly where this screen's content overflowing a 30-row terminal makes bubbletea's test harness stop delivering further input.
Drives the actual running program (real Update/View, teatest's virtual terminal, no real terminal window) against whatever 8BitDo device is physically connected, MockMode:false -- the first time the whole app (not just internal/machid in isolation) has been exercised against real hardware. Same gating convention as internal/machid's existing manual tests: never runs in `go test ./...`/CI, run explicitly with -tags manual. Run against the user's real Ultimate2 (PID 0x6013, serial 22EC9EA4DF): confirms Phase 1's internal/machid fix works end-to-end through the full app (device enumerates and renders on the Devices screen), and confirms the Diagnostics/Mapping Editor screens handle a real, non-mock session honestly -- no crash, no hang, accurate "0/12 passed" with per-command detail, real report saved with the real serial. This device does not respond to any protocol command right now (write succeeds, zero bytes read back for all 12 checks) -- a separate, pre-existing, already- documented mystery (see internal/machid/machid_darwin.go's package doc), not something this test or commit attempts to fix. The Mapping Editor's error path surfaces this via U2ReadCoreProfile's earlier GetModeAlt call failing before ever reaching the deliberately-blocked button-map read, so the graceful MappingsUnavailable path isn't exercised by current real hardware state -- also not a bug, just not yet reachable.
…hecklist with real-hardware findings manual_nav_capture_test.go (-tags manual, never runs in CI): opens a real nav stream against whatever 8BitDo device is connected and logs every DPad/Button event for a bounded window, so a human can press physical buttons during the run. This checks a genuinely different thing from internal/protocol's vendor command channel (which this device doesn't respond to at all, see the immediately prior commit) -- a HID gamepad's standard button-state input reports are a separate mechanism from a vendor's custom request/response protocol on the same interface, so navigation may well work even though diagnostics don't. First run captured zero events, but the user was away from their computer for the window -- inconclusive, not a finding, needs a rerun with proper timing coordination once they're back. RC_CHECKLIST.md: corrected the stale "Real CI run: Pending" row (it has been green for many pushes, most recently run #202) and replaced the stale "no 8BitDo controller was available" real-hardware row with tonight's actual findings: Phase 1's transport fix confirmed working end-to-end through the whole app, all three main screens confirmed to degrade honestly against a real non-mock session, and the pre-existing (not rewrite-introduced) protocol-non-response mystery reconfirmed via the full app rather than just an isolated manual test.
Two live capture runs (user actively pressing buttons/d-pad/sticks, second with -count=1 to rule out a stale go test cache hit on the first identical invocation) both received zero real nav events. Root-caused via direct hid.Enumerate: this controller currently exposes exactly one HID interface (usagePage=0xffa0, the vendor config channel already known not to respond to protocol commands), not a standard Generic-Desktop Gamepad interface. Not a code bug -- the app already opens everything the OS enumerates for this vid. Looks like a controller connection-mode question; needs the user to try a different mode and re-test.
Latest stable release; updated go.mod plus every doc and enforcement script that pins the version (README, CONTRIBUTING, MIGRATION, RC_CHECKLIST, check_go_toolchain.sh, check_docs_consistency.sh). Verified gofmt, go vet, go mod verify, go test -race ./..., golangci-lint, and govulncheck all pass under 1.27.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
rewrote in go bubbletea.