Migrate to Tauri v2 (full Rust rewrite) with self-update#31
Merged
Conversation
Begin the Tauri/Rust migration with the pure, OS-independent core. This adds a Cargo workspace and the `hyperdeck-core` crate, porting the Go `internal/core/domain` package 1:1: - Chord parser (parse_chord) with case-insensitive modifiers - Profile / Match / Keymap / ControlMode / InjectionMode and the uses_controller, has_action, matches_window methods - TransportState (+ HyperDeck status mapping), Clip/ClipList, Window/LockState All Go domain unit tests are ported as Rust #[test]s (7 tests, green). A `rust` CI job (fmt + clippy -D warnings + test) runs alongside the existing Go job so both stay green during the migration. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Port the driving adapter's pure logic from
internal/adapter/driving/hyperdeck and the inbound port interfaces:
- error::DeckError (port of app.ErrNotLocked + injector/other variants)
- port::{Transport, Query} inbound traits
- protocol::parse_command (single- and multi-line forms; never panics)
- protocol::Responder over a deck implementing both inbound ports,
including relative/absolute goto and the \r\n response formatting
Go parser + responder tests ported as Rust #[test]s, with a property-
style "never panics" smoke test mirroring the rapid check. 18 tests green
(fmt + clippy -D warnings clean).
Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv
Co-authored-by: Claude <noreply@anthropic.com>
Port internal/core/app session + virtual deck plus the driven (outbound)
port traits they need:
- port::{KeyInjector, WindowEnumerator, PlayerController, ClipSource,
StateProbe, StatusPresenter}
- app::Session: mutex-guarded shared state; clip source / probe held as
Arc so the reconciler can call them without holding the lock
- app::VirtualDeck: Transport + Query impls with the play/stop toggle vs
discrete-stop logic, cue-on-navigate, relative/clamped goto, rehome,
and out-of-band controller dispatch
- testsupport::MockInjector (port of injector.Mock) shared by unit tests
All Go session + virtualdeck + hardening transport tests ported as Rust
#[test]s. 37 tests green (fmt + clippy -D warnings clean).
Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv
Co-authored-by: Claude <noreply@anthropic.com>
Port the remaining application services from internal/core/app: - app::LockManager: poll/lock/unlock by profile matching, the pinned- profile filter (set_active / Auto), and presenter notifications - app::Reconciler: per-tick clip-list refresh + probe-driven state correction, no-op when unlocked - testsupport gains FakePresenter, FakeClipSource, NoProbe, PlayingProbe The clock-driven Run loops are intentionally deferred to the async runtime wiring; this ports the pure poll()/tick() logic. All Go lockmanager + reconciler + relock tests ported. 44 tests green. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Port the remaining pure driven adapters into hyperdeck-core: - config::Store (serde_norway YAML loader + full validation), config:: SelectionStore (serde_json pinned-profile persistence), config:: ensure_default (embeds examples/profiles.yaml as the first-run seed) - clipsource: Positional, Mitti (positional fallback), Playlist (.m3u + .xspf via quick-xml serde), and the type-driven factory - stateprobe: TitleRegex, NoneProbe, and Uia behind a pure ElementNamer trait (Windows namer supplied later by the OS crate), plus the factory - port::ProfileStore; domain::KeyName::parse (unmodeled actions like Mitti's "pause" are validated then dropped, matching Go's open KeyName) New deps: serde, serde_json, serde_norway (maintained serde_yaml fork), quick-xml. All Go config/clipsource/stateprobe tests ported, including loading testdata fixtures and the embedded default seed. 70 tests green. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Split rustfmt into its own job and run the Rust clippy + test job across ubuntu, macOS, and Windows. This proves the pure core compiles and passes on every target platform and gates the OS-specific adapters (injection, window enumeration, UIA) that land next behind cfg(target_os). Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Introduce the hyperdeck-os crate that will host the OS- and player-specific driven adapters (injection, window enumeration, UIA) which need external or platform crates and so live outside the pure core. First adapter: vlchttp::VlcController (port of internal/adapter/driven/ vlchttp), a PlayerController for api.type "vlc_http" that drives VLC's HTTP requests interface (pl_play/pl_stop/pl_next/pl_previous) with empty-user Basic auth and a 4s timeout. Uses ureq (default-features off: plain HTTP, no TLS/system deps) so it compiles cleanly on the Linux/macOS/Windows CI matrix. Ported all Go vlchttp tests against an in-process TCP stub server (mapped commands + Basic auth, unmapped-key no-op, 401/500 errors, empty base url). 5 tests green; workspace 75 total. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Port injector_windows.go + keymap_windows.go into hyperdeck-os behind
cfg(windows), with a no-op fallback elsewhere:
- injector::keymap_windows: pure VK-code / modifier / extended-key mapping
(unit-tested)
- injector::windows_impl::WinInput (windows crate): Focus (SetForegroundWindow
with the AttachThreadInput + foreground-lock-timeout dance), SendKeys
(SendInput when the target is foreground, PostMessage WM_KEYDOWN/UP when
background), and OpenWindows (EnumWindows + Toolhelp pid->exe names)
- injector::{injector, enumerator, request_accessibility} composition entry
points; non-Windows returns a no-op injector/enumerator (macOS impl follows)
Adds the windows crate as a cfg(windows)-only dependency. Verified locally
with `cargo check/clippy --target x86_64-pc-windows-gnu` (real FFI) and the
apple-darwin + linux targets (no-op path); workspace fmt/clippy/tests green.
Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv
Co-authored-by: Claude <noreply@anthropic.com>
Port injector_darwin.go + keymap_darwin.go + cinject_darwin.m into hyperdeck-os behind cfg(target_os = "macos"): - injector::keymap_darwin: pure kVK key codes + CGEventFlags modifier masks (unit-tested) - injector::macos_impl::MacInjector: Focus (NSRunningApplication activate by pid), SendKeys (CGEvent keyboard events posted via CGEventPostToPid for background, focus-free injection), OpenWindows (CGWindowList info dictionaries -> pid/owner/title) - request_accessibility() via AXIsProcessTrustedWithOptions (prompting), wired into the injector module's macOS dispatch Uses core-graphics (elcapitan feature for post_to_pid), core-foundation, and objc2-app-kit (libc feature). Verified locally with `cargo check/clippy --target aarch64-apple-darwin` plus the windows-gnu and linux targets; all three clippy-clean, workspace tests green. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Port internal/adapter/driven/uia into hyperdeck-os behind cfg(windows), using the windows crate's typed COM interfaces instead of raw vtable dispatch: - uia::Engine owns an MTA COM apartment + IUIAutomation on a dedicated worker thread, serving requests serially over a channel (COM thread affinity), so it is Send + Sync for use as a shared adapter - implements PlayerController (Invoke the AutomationId-mapped control for control: uia profiles) and ElementNamer (read an element's Name for the UIA state probe); missing controls are an acked no-op / not-detected - ElementFromHandle + CreatePropertyCondition(AutomationId) + FindFirst (descendants) + GetCurrentPatternAs(Invoke) / CurrentName Adds windows features Win32_UI_Accessibility, Win32_System_Com, Win32_System_Variant. Verified with cargo check/clippy --target x86_64-pc-windows-gnu (real COM) + apple-darwin/linux; all clippy-clean. With this, every Go driven adapter is ported to Rust. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
* feat(core): port the HyperDeck TCP server Port internal/adapter/driving/hyperdeck/server.go: protocol::Server accepts controller connections on a TcpListener, greets with the 500 connection-info banner, reads single- and block-form command blocks, dispatches them through the Responder, and closes on quit. Each connection is handled on its own thread over a shared Arc<Deck>. Adds blanket Transport/Query impls for Arc<T> so the shared deck satisfies the inbound ports with cheap per-connection clones. Ported the Go end-to-end server test (TcpListener on 127.0.0.1:0 -> greeting -> "play" -> "200 ok" + recorded keystroke via the mock injector). 71 core tests green; this completes the protocol runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv * style: apply rustfmt 1.9 (stable) formatting to server test CI's newer stable rustfmt expands struct literals in calls; align the committed formatting with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(tauri): scaffold tray-only Tauri shell with self-update Add the src-tauri Tauri v2 application (its own workspace, excluded from the library workspace so the Linux CI never needs webkit): - tauri.conf.json: tray-only (no windows), bundle targets dmg/app/nsis/msi, createUpdaterArtifacts, and the updater plugin pointed at the GitHub releases latest.json endpoint. pubkey is intentionally empty (TODO: the maintainer fills in their minisign public key per issue #12). - minimal main.rs: registers the updater/process/dialog plugins, sets the macOS Accessory activation policy (menu-bar agent, no Dock icon), and builds a tray menu with "Check for Updates…" (download+install+relaunch) and Quit. Core wiring (server/lock/reconcile/profile menu) lands next. - generated desktop icon set, a minimal frontend stub (ui/), capabilities, and a committed Cargo.lock. - CI: new `tauri` job builds + clippies + fmt-checks the app on macOS and Windows (where the webview prerequisites ship with the runner). The Tauri app cannot be built in the Linux dev sandbox (no webkit); it is verified by the macOS/Windows CI job and, at release time, tauri-action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv * fix(tauri): add serde_json dep for generate_context!, drop unused import tauri::generate_context! expands to code referencing serde_json in the app crate root, so it must be a direct dependency. Also remove the unused tauri::Manager import (clippy -D warnings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv --------- Co-authored-by: Claude <noreply@anthropic.com>
…25) Rewrite release.yml to build and publish the Tauri app via tauri-apps/tauri-action@v0.6.2 on tag v* / manual dispatch, matching the cuesheet pattern: - matrix: macOS universal-apple-darwin + Windows (no Linux, like cuesheet) - projectPath src-tauri; tag v__VERSION__, draft release "HyperDeck Adapter v__VERSION__" - TAURI_SIGNING_PRIVATE_KEY(+_PASSWORD) at job env; updater artifacts are auto-disabled when the signing key is absent (so unsigned builds still succeed) - Apple Developer ID cert import + "Configure Apple signing" guard (avoids the empty-identity failure); Windows Authenticode cert import -> thumbprint passed via --config bundle.windows.certificateThumbprint tauri-action generates latest.json + signed updater bundles once the signing key (issue #12) is configured. Triggers only on tags, so this is inert until a release is cut. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
* feat(tauri): wire the HyperDeck core into the app Connect the tray shell to the ported core (analog of cmd/hyperdeck-adapter/ main.go): - backend.rs composition root: loads profiles.yaml (seeding the default on first run) + selection.json, builds the injector/enumerator, a control router (api -> VLC HTTP, uia -> UI Automation on Windows), Session, VirtualDeck, LockManager, Reconciler; binds the HyperDeck TCP server on 0.0.0.0:9993 and spawns server / poll / reconcile threads; applies the persisted profile selection on startup - main.rs: tray menu gains Re-home (VirtualDeck::rehome) alongside Check for Updates + Quit; a TrayPresenter reflects lock status in the tray tooltip; --check-accessibility and --no-tray/--headless flags (parity with the Go CLI) with a LogPresenter for headless runs The adapter is now functional in Auto mode; the tray profile submenu for pinning a profile lands next. Adds the dirs dependency for the OS config path. Verified via cargo fmt; compiled by the macOS/Windows tauri CI job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv * fix(tauri): drop unused tauri::Manager import tray_by_id / handle / default_window_icon are inherent on App/AppHandle in Tauri 2; the Manager import is unused under clippy -D warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv --------- Co-authored-by: Claude <noreply@anthropic.com>
The full Rust rewrite (hyperdeck-core + hyperdeck-os + src-tauri) reaches parity with the Go daemon, so retire the Go tree and its packaging: - Delete cmd/, internal/, go.mod, go.sum, and build/ (NSIS/Info.plist/ signing scripts/nfpm/gen_appicon) — superseded by the Tauri bundler and tauri-action. - ci.yml: drop the Go test job; keep rustfmt, the rust matrix, and the tauri job. - justfile: rewrite recipes for cargo/tauri (test/fmt/build/run/serve/demo/ trust). - README.md: rewrite for the Rust/Tauri architecture, install, dev, config, and releasing (with the maintainer key-setup note). - .gitignore: drop Go-only entries. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Replicate the Go tray's Profile menu in the Tauri tray: - A "Profile" submenu with "Auto (match any)" plus one checkbox per profile id; the checkmark starts on the persisted selection (Auto when none or the pinned id no longer names a loaded profile). - Selecting an entry persists the id via SelectionStore, re-points the LockManager (which re-polls immediately so the lock reflects the choice), and moves the checkmark to the chosen entry. - Port the Go `checkedProfile` helper verbatim, with its unit tests. backend.rs now consumes lock_manager/selection/profile_ids/active, so drop the placeholder #[allow(dead_code)] on Backend. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
Complete tray parity with the Go app's status display:
- Add a disabled status line at the top of the tray menu ("Locked: <id>
(<title>)" / "Disconnected — no player"); the presenter updates its text
from the poll thread via a shared handle populated when the tray is built.
- The TrayPresenter now also sets a menu-bar lock indicator (HD● / HD○,
rendered on macOS where tray titles show) alongside the existing tooltip.
Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv
Co-authored-by: Claude <noreply@anthropic.com>
Add a small Rust diagnostic binary (port of the Go cmd/injcheck) for verifying the real OS injector against a running player during on-device macOS/Windows testing: - `injcheck trust` — check / prompt for the input permission (macOS). - `injcheck list [filter]` — list on-screen windows. - `injcheck focus <pid>` — bring an app to the foreground. - `injcheck keys|bgkeys <pid> <chord...>` — send chords with / without stealing focus. It drives the existing hyperdeck-os injector/enumerator entry points, so it also gives those per-OS adapters compile coverage in the rust CI matrix. Add a `just injcheck` recipe and document it in the README. Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv Co-authored-by: Claude <noreply@anthropic.com>
statik
marked this pull request as ready for review
June 21, 2026 16:10
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.
Ports
hyperdeck-adapterfrom the Go tray daemon to a Tauri v2 desktop app over a full Rust rewrite, adding in-app self-update — modeled onkindlyops/cuesheet. This is the integration PR that lands the whole migration (steps #13–#30) onmain, retiring the Go app there.What changes
crates/hyperdeck-core— OS-independent domain, ports (traits), application services (Session / VirtualDeck / LockManager / Reconciler), the HyperDeck protocol parser + responder + TCP server, profile/selection config, clip sources, and state probes. The Go_test.gosuites are ported as Rust unit tests (71 tests).crates/hyperdeck-os— per-OS driven adapters behind the core traits: keystroke injection + window enumeration (macOS CoreGraphics/AppKit, Windows WinAPI), Windows UI Automation, the VLC HTTP controller, and theinjcheckon-device diagnostic.src-tauri— the tray-only Tauri app: wires the core to the OS adapters, owns the tray menu (Status · Profile · Re-home · Check for Updates… · Quit), runs as a macOS menu-bar agent, and provides self-update viatauri-plugin-updater.ci.ymlruns rustfmt + a clippy/test matrix (ubuntu/macos/windows) + a Tauri build job (macos/windows);release.ymlmirrors cuesheet'stauri-actionpipeline (macOS universal + Windows, updater-artifact + Apple/Windows signing guards, draft release).cmd/,internal/,go.mod/go.sum, and the Go-erabuild/packaging; rewritesjustfileandREADME.mdfor the Rust/Tauri toolchain.The TCP protocol (
:9993) andscripts/hyperdeck-demo.pyare unchanged, so existing Companion/ATEM controllers keep working. Thesite/landing page and its release-asset resolution are unaffected.Self-update / signing — maintainer action required
Per the security constraint for this work, no keys or certificates were generated.
plugins.updater.pubkeyis committed empty and the release workflow auto-disables updater artifacts when no signing key is present, so builds succeed unsigned. The in-app updater will not verify until the maintainer adds the keys/secrets — full instructions are in the maintainer key-setup issue (#12); on-device test plans are in #10 (macOS) and #11 (Windows).Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(71 core + 5 os), and the src-tauri clippy/test/fmt all pass.🤖 Generated with Claude Code
Generated by Claude Code