diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 226b94b..adc5a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,3 +59,24 @@ jobs: - name: Build app run: npm run build + + # =========================================================================== + # egui prototype (input_viewer_egui). macOS rather than Linux: the camera and + # windowing crates need system packages on Linux, and macOS is where the + # GPU path is developed. + # =========================================================================== + egui: + name: egui test & build + runs-on: macos-latest + defaults: + run: + working-directory: input_viewer_egui + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + + - name: Unit tests + run: cargo test --locked + + - name: Build + run: cargo build --release --locked + diff --git a/.github/workflows/release-egui.yml b/.github/workflows/release-egui.yml new file mode 100644 index 0000000..8a82ef0 --- /dev/null +++ b/.github/workflows/release-egui.yml @@ -0,0 +1,337 @@ +name: Release (egui) + +# Installers and auto-update packages for input_viewer_egui, built with +# Velopack. Same shape as release.yml: manual dispatch, version derived from +# Conventional Commits, tests gate the publish. +# +# It shares GitHub Releases with the Electron app, and two rules keep the two +# from interfering: +# +# 1. Tags are `egui-v*`. release.yml finds its last tag with +# `git describe --match 'v[0-9]*'`, so these never become its baseline. +# 2. Releases are published with `make_latest: false`. electron-updater +# resolves /releases/latest; if an egui release were "latest", every +# Electron install would look there for a latest.yml that is not there. +# +# Unlike release.yml there is no version-bump commit: the tag is the source of +# truth, and the version is written into Cargo.toml and Info.plist at build +# time only. +# +# `publish: false` (the default) builds and uploads the installers as workflow +# artifacts and stops. Pull requests touching the egui app get the same build +# automatically (version 0.0.0-pr.N, never published), which is also how this +# workflow gets exercised before it reaches main: GitHub only offers +# workflow_dispatch for workflows already on the default branch. + +on: + pull_request: + branches: [main] + paths: + - 'input_viewer_egui/**' + - '.github/workflows/release-egui.yml' + workflow_dispatch: + inputs: + publish: + description: 'Publish a GitHub release (main only). Off = build artifacts only.' + required: false + type: boolean + default: false + bump: + description: 'Version bump (auto = derive from commits touching input_viewer_egui/)' + required: false + type: choice + default: auto + options: [auto, patch, minor, major] + +env: + VPK_VERSION: '1.2.161' # keep equal to the velopack crate in Cargo.lock + PACK_ID: InputViewerEgui + PACK_TITLE: Input Viewer (egui) + +jobs: + analyze: + name: Analyze commits + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + last_tag: ${{ steps.version.outputs.last_tag }} + publish: ${{ steps.version.outputs.publish }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + with: + fetch-depth: 0 + + - name: Compute version + id: version + run: | + set -euo pipefail + LAST_TAG=$(git tag -l 'egui-v*' --sort=-v:refname | head -n1) + CURRENT=${LAST_TAG#egui-v} + CURRENT=${CURRENT:-0.0.0} + IFS=. read -r MA MI PA <<< "$CURRENT" + + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + # A pre-release of the next minor. Not 0.0.0-pr.N: vpk requires + # >= 0.0.1, and a pre-release sorts below its release, so + # 0.0.1-pr.N fails too. + VERSION="$MA.$((MI + 1)).0-pr.${{ github.event.pull_request.number }}" + echo "Pull request: packaging check only, as $VERSION" + { + echo "version=$VERSION" + echo "last_tag=" + echo "publish=false" + } >> "$GITHUB_OUTPUT" + exit 0 + fi + + BUMP="${{ inputs.bump }}" + if [ "$BUMP" = "auto" ]; then + # Only commits that touch the egui app count; an Electron fix must + # not bump the egui version. + RANGE=${LAST_TAG:+$LAST_TAG..HEAD} + COMMITS=$(git log "${RANGE:-HEAD}" --pretty=format:%s --no-merges -- input_viewer_egui) + BUMP=none + if echo "$COMMITS" | grep -qiE '^.+!:|BREAKING CHANGE'; then BUMP="major" + elif echo "$COMMITS" | grep -qiE '^feat(\(.+\))?:'; then BUMP="minor" + elif echo "$COMMITS" | grep -qiE '^(fix|perf|refactor|build|ci)(\(.+\))?:'; then BUMP="patch" + fi + fi + if [ "$BUMP" = "none" ]; then + echo "::error::No releasable commits touch input_viewer_egui/ since ${LAST_TAG:-the start}. Pick a bump explicitly to release anyway." + exit 1 + fi + + case $BUMP in + major) VERSION="$((MA + 1)).0.0" ;; + minor) VERSION="$MA.$((MI + 1)).0" ;; + patch) VERSION="$MA.$MI.$((PA + 1))" ;; + esac + + PUBLISH=false + if [ "${{ inputs.publish }}" = "true" ]; then + if [ "$GITHUB_REF" != "refs/heads/main" ]; then + echo "::error::Publishing is only allowed from main (this run is on $GITHUB_REF)." + exit 1 + fi + PUBLISH=true + fi + + echo "${LAST_TAG:-none} -> $VERSION ($BUMP), publish=$PUBLISH" + { + echo "version=$VERSION" + echo "last_tag=$LAST_TAG" + echo "publish=$PUBLISH" + } >> "$GITHUB_OUTPUT" + + build-windows: + name: Build Windows + needs: analyze + runs-on: windows-latest + defaults: + run: + shell: bash + working-directory: input_viewer_egui + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '8.0.x' + + # Cargo.toml and Cargo.lock both, or --locked refuses to build; CRLF-safe + # for the Windows checkout. See the script. + - name: Set version + run: packaging/set-version.sh "${{ needs.analyze.outputs.version }}" + + - name: Test + run: cargo test --release --locked + + - name: Build + run: cargo build --release --locked + + - name: Install vpk + run: | + dotnet tool install -g vpk --version "$VPK_VERSION" + echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" + + # The previous release's full package lets vpk generate a delta, so an + # update downloads only what changed. + - name: Fetch previous release for deltas + if: needs.analyze.outputs.last_tag != '' + continue-on-error: true + run: | + vpk download http --channel windows --outputDir releases \ + --url "https://github.com/${{ github.repository }}/releases/download/${{ needs.analyze.outputs.last_tag }}/" + + - name: Pack + run: | + mkdir -p publish + cp target/release/input-viewer.exe publish/ + vpk pack \ + --packId "$PACK_ID" \ + --packVersion "${{ needs.analyze.outputs.version }}" \ + --packTitle "$PACK_TITLE" \ + --packAuthors "Schuberg Philis / Lab271" \ + --packDir publish \ + --mainExe input-viewer.exe \ + --icon ../input_viewer_electron/build/icon.ico \ + --channel windows \ + --outputDir releases + ls -la releases + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: egui-windows + path: input_viewer_egui/releases/* + if-no-files-found: error + + build-mac: + name: Build macOS + needs: analyze + runs-on: macos-latest + defaults: + run: + working-directory: input_viewer_egui + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '8.0.x' + + # Cargo.toml and Cargo.lock both, or --locked refuses to build; CRLF-safe + # for the Windows checkout. See the script. + - name: Set version + run: packaging/set-version.sh "${{ needs.analyze.outputs.version }}" + + - name: Test + run: cargo test --release --locked + + # Universal, like the Electron DMG: one download for Intel and Apple silicon. + - name: Build universal binary + run: | + rustup target add aarch64-apple-darwin x86_64-apple-darwin + cargo build --release --locked --target aarch64-apple-darwin + cargo build --release --locked --target x86_64-apple-darwin + lipo -create -output input-viewer \ + target/aarch64-apple-darwin/release/input-viewer \ + target/x86_64-apple-darwin/release/input-viewer + lipo -info input-viewer + + # Built by hand rather than left to vpk, so Info.plist carries + # NSCameraUsageDescription. Without it macOS kills the app on first + # camera access. + - name: Assemble app bundle + run: | + APP="publish/Input Viewer (egui).app" + mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + cp input-viewer "$APP/Contents/MacOS/" + cp ../input_viewer_electron/build/icon.icns "$APP/Contents/Resources/icon.icns" + sed 's/@VERSION@/${{ needs.analyze.outputs.version }}/g' packaging/macos/Info.plist > "$APP/Contents/Info.plist" + plutil -lint "$APP/Contents/Info.plist" + # Ad-hoc signature. Unsigned like the Electron build (no Developer ID + # in this repo), but Apple silicon will not run an arm64 binary + # without at least this. + codesign --force --deep --sign - "$APP" + + - name: Install vpk + run: | + dotnet tool install -g vpk --version "$VPK_VERSION" + echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" + + - name: Fetch previous release for deltas + if: needs.analyze.outputs.last_tag != '' + continue-on-error: true + run: | + vpk download http --channel macos --outputDir releases \ + --url "https://github.com/${{ github.repository }}/releases/download/${{ needs.analyze.outputs.last_tag }}/" + + - name: Pack + run: | + vpk pack \ + --packId "$PACK_ID" \ + --packVersion "${{ needs.analyze.outputs.version }}" \ + --packTitle "$PACK_TITLE" \ + --packAuthors "Schuberg Philis / Lab271" \ + --packDir "publish/Input Viewer (egui).app" \ + --mainExe input-viewer \ + --channel macos \ + --outputDir releases + ls -la releases + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: egui-macos + path: input_viewer_egui/releases/* + if-no-files-found: error + + publish: + name: Publish release + needs: [analyze, build-windows, build-mac] + if: needs.analyze.outputs.publish == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: egui-windows + path: artifacts/windows + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: egui-macos + path: artifacts/macos + + # Both platforms upload into one release, where asset names must be + # unique. The custom channels (windows/macos rather than the default + # win/osx) are what make vpk put the channel in every file name; this + # fails the run if that ever stops being true, rather than letting one + # platform's package silently replace the other's. + # + # The previous release's packages fetched for delta generation are + # dropped: they already live on that release. + - name: Collect assets + run: | + set -euo pipefail + VERSION="${{ needs.analyze.outputs.version }}" + mkdir upload + for f in artifacts/*/*; do + name=$(basename "$f") + case "$name" in + *.nupkg) [[ "$name" == *"-$VERSION-"* ]] || { echo "skip $name (previous release)"; continue; } ;; + esac + if [ -e "upload/$name" ]; then + echo "::error::Both platforms produced $name" + exit 1 + fi + cp "$f" upload/ + done + ls -la upload + + - name: Create release + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 + with: + tag_name: egui-v${{ needs.analyze.outputs.version }} + target_commitish: ${{ github.sha }} + name: Input Viewer (egui) ${{ needs.analyze.outputs.version }} + # Never "latest": that slot belongs to the Electron app's updater. + make_latest: 'false' + body: | + Prototype of Input Viewer built on egui. Installs alongside the Electron app. + + - **Windows:** the `-Setup.exe`. Installs per user, no admin rights needed. + - **macOS:** the `-Setup.pkg`. Unsigned, like the Electron build: right-click → Open the first time. + + Installed copies update themselves from releases tagged `egui-v*`. + files: upload/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Summary + run: | + { + echo "### Input Viewer (egui) ${{ needs.analyze.outputs.version }}" + echo "" + echo "Tag \`egui-v${{ needs.analyze.outputs.version }}\`, not marked latest." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c31d178..bf54739 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,10 @@ jobs: exit 0 fi - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + # --match: release-egui.yml tags egui-v* in this same repo, and an + # unfiltered describe would take the newest of those as the baseline, + # analysing only the commits since the last *egui* release. + LAST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo "") if [ -z "$LAST_TAG" ]; then COMMITS=$(git log --pretty=format:"%s" --no-merges) diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 0000000..b525860 --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,33 @@ +# NNNN. Title in the imperative + +| | | +|---|---| +| **Status** | Proposed · Accepted · Superseded by NNNN · Open | +| **Date** | YYYY-MM-DD (when the decision was made, from the PR or issue) | +| **Area** | Product · Capture · Signal · Screensavers · UI · Platform · Delivery | +| **Evidence** | #issue, PR, commit, `path:line` | +| **egui verdict** | Keep · Adapt · Drop · Decide | +| **egui status** | Done · Partial · Not started · N/A | + +## Context + +The problem, the forces, and the incident or measurement that settled it. +Numbers where they exist. "Rationale not recorded" where it does not -- +never a reconstructed reason presented as the original one. + +## Decision + +What was decided, concretely enough to reimplement: thresholds, timings, +sizes, precedence. + +## Consequences + +What it enables, what it costs, what it constrains. + +## Porting to egui + +- **Verdict** -- *Keep*: same behaviour, port as is. *Adapt*: same goal, + different mechanism. *Drop*: an Electron or browser artefact with nothing + to carry over. *Decide*: a product question to answer before porting. +- **What carries over**, **what changes**, and where the prototype + (`input_viewer_egui/`) stands. diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md new file mode 100644 index 0000000..fc766dc --- /dev/null +++ b/docs/adr/0001-record-architecture-decisions.md @@ -0,0 +1,51 @@ +# 0001. Record architecture decisions, and port by them + +| | | +|---|---| +| **Status** | Accepted | +| **Date** | 2026-10-12 | +| **Area** | Product | +| **Evidence** | This directory | +| **egui verdict** | Keep | +| **egui status** | N/A | + +## Context + +Input Viewer's design lives in four places: `.claude/CLAUDE.md`, code +comments, 117 issues and 208 merged PRs. The "why" behind a threshold is +usually in a PR body or an issue comment, and some of it has drifted: the +same fact is stated differently in the docs and the code in at least a dozen +places (see [open-questions.md](open-questions.md)). + +A rewrite on Rust and egui ([0002](0002-port-to-rust-and-egui.md)) has to +decide, decision by decision, what to keep. Reading the Electron app to find +out would repeat the research every time, and would miss what is only in the +issue tracker. + +## Decision + +- One ADR per decision a rewrite must consciously keep, change or drop. The + Electron app's decisions are recorded as **Accepted**, dated from the PR or + issue that made them. +- Every ADR ends with **Porting to egui**: a verdict (Keep / Adapt / Drop / + Decide) and where the prototype stands. The index in [README.md](README.md) + collects those into the port's status board. +- Evidence is cited, not paraphrased from memory: issue and PR numbers, + commits, `path:line`. Where the rationale was never written down the ADR + says so. +- Defects and doc drift found while writing these go in + [open-questions.md](open-questions.md), not into the ADRs as if they were + decisions. +- An ADR is changed by superseding it, not by editing history: a new ADR, and + the old one's status set to "Superseded by NNNN". + +## Consequences + +- The port has a checklist with a reason attached to every item. +- These were reconstructed in 2026-10 from code and history, not written at + the time. Where a reason is inferred, the ADR says "inferred". +- Line numbers are from `main` at 787375b (v3.4.1) and will drift. + +## Porting to egui + +Keep. New egui-only decisions get their own ADRs, numbered after these. diff --git a/docs/adr/0002-port-to-rust-and-egui.md b/docs/adr/0002-port-to-rust-and-egui.md new file mode 100644 index 0000000..e410592 --- /dev/null +++ b/docs/adr/0002-port-to-rust-and-egui.md @@ -0,0 +1,67 @@ +# 0002. Port Input Viewer to Rust and egui + +| | | +|---|---| +| **Status** | Proposed | +| **Date** | 2026-10-12 | +| **Area** | Product | +| **Evidence** | #249 (Tauri, "recorded, not recommended"), #225, #62, `input_viewer_egui/` | +| **egui verdict** | Decide | +| **egui status** | Prototype | + +## Context + +The Electron app works and is in production on the wall. The case for moving +is about what Electron costs on a single-purpose wall PC: + +- **Size and update surface.** A 95 MB installer and a 211 MB DMG (#249), + carrying a Chromium whose security fixes reach the wall only by + redeploying (labs-videowall-config `docs/software.md`), while Dependabot's + Electron bumps never trigger a release on their own + ([0045](0045-release-by-hand-bump-from-commits.md)). +- **Workarounds that exist only because the UI is a web page.** CORS + proxies through the main process ([0043](0043-keep-privileged-work-in-one-place.md)), + WebRTC constraint negotiation that once locked a card at 640x480 (#34), + one capture session shared between two `