diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ac2b23f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/version-tag.yml b/.github/workflows/version-tag.yml index 96f32a9..372014b 100644 --- a/.github/workflows/version-tag.yml +++ b/.github/workflows/version-tag.yml @@ -24,6 +24,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Check repository text + run: python scripts/check-text.py + - name: Extract version from tag id: version shell: pwsh @@ -260,12 +263,18 @@ jobs: $ui = "$pkg/ui" New-Item -ItemType Directory -Force -Path $ui | Out-Null Copy-Item "$rel/ui/*" $ui -Recurse + ./scripts/verify-runtime-dlls.ps1 ` + -RuntimeDir $ui ` + -VcpkgBinDir "build/vcpkg_installed/x64-windows/bin" Compress-Archive -Path "$ui/*" -DestinationPath "monolith-ui-${{ steps.compvars.outputs.ui }}.zip" # updater: Updater.exe at the app root. $upd = "$pkg/updater" New-Item -ItemType Directory -Force -Path $upd | Out-Null Copy-Item "$rel/Updater.exe" $upd + ./scripts/verify-runtime-dlls.ps1 ` + -RuntimeDir $upd ` + -VcpkgBinDir "build/vcpkg_installed/x64-windows/bin" Compress-Archive -Path "$upd/*" -DestinationPath "monolith-updater-${{ steps.compvars.outputs.updater }}.zip" Get-ChildItem monolith-*.zip | ForEach-Object { Write-Host "$($_.Name) $([math]::Round($_.Length / 1MB, 1)) MB" } @@ -317,7 +326,7 @@ jobs: **Install:** download and run `MonolithSetup-${{ steps.version.outputs.version }}.exe` (per-user, no admin needed, no prerequisites). Existing installs update automatically via the in-app updater. - **Components:** engine ${{ steps.compvars.outputs.engine }} · ui ${{ steps.compvars.outputs.ui }} · updater ${{ steps.compvars.outputs.updater }} — clients download only what changed. + **Components:** engine ${{ steps.compvars.outputs.engine }} · ui ${{ steps.compvars.outputs.ui }} · updater ${{ steps.compvars.outputs.updater }} - clients download only what changed. **Stream Deck:** `Monolith-StreamDeck-${{ steps.version.outputs.version }}.streamDeckPlugin` diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index b6bda24..9edd8f6 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] permissions: contents: read @@ -25,6 +24,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Check repository text + run: python scripts/check-text.py + + - name: Test runtime dependency verifier + shell: pwsh + run: ./tests/packaging/verify-runtime-dlls.tests.ps1 + # Pinned to the builtin-baseline commit in vcpkg.json so dependency # versions (FFmpeg DLL ABIs etc.) are reproducible across CI runs. - name: Clone vcpkg (pinned) diff --git a/.gitignore b/.gitignore index ff90312..585ebee 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ Win32/ # --- vcpkg --- vcpkg/ vcpkg_installed/ -vcpkg-configuration.json # --- Windows --- Thumbs.db diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..91c5222 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Repository instructions + +Follow `CLAUDE.md` and `docs/DEVELOPMENT_RULES.md` for build, architecture and verification rules. + +## Text and comments + +- Never use U+2013 or U+2014 in any context: code, comments, documentation, commit messages, pull requests or reports. Use an ASCII hyphen, comma, colon or parentheses as appropriate. +- Remove or replace these characters whenever encountered. Keep all tracked UTF-8 text free of them. +- Keep comments short and essential. Explain only non-obvious intent, constraints or logic. Do not claim that code works, is safe or is correct in a comment. +- Keep names short, clear and specific to the function or type. + +## Contributions + +- Work on separate branches and open pull requests in English. +- Report exact checks performed and distinguish static inspection from Windows runtime verification. +- Update `docs/handover/ACTIVE_HANDOVER.md` after substantial work. diff --git a/CLAUDE.md b/CLAUDE.md index 54e937a..6a837f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ Dis file = work guide for agents and maintainers in repo. Monolith = Windows-only, recorder-first screen capture + replay-buffer app. Target Windows 11 x64. Built on native C++23 recording engine, Win32 tray/message-loop host, Tauri v2/WebView2 desktop UI sidecar, Elgato Stream Deck controller plugin. -Component versions (independent — bump only what changed): engine = root +Component versions (independent - bump only what changed): engine = root `CMakeLists.txt` `project(VERSION)`, ui = `app/desktop-ui/src-tauri/tauri.conf.json`, updater = `app/updater/src-tauri/tauri.conf.json`. The git tag only names a release and versions the full installer (see docs/RELEASING.md, ADR-0018). diff --git a/CMakeLists.txt b/CMakeLists.txt index a9b5498..1ab55b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ endif() project(monolith VERSION 1.6.2 LANGUAGES CXX) -# Engine version — the engine component of the component updater (the ui and +# Engine version - the engine component of the component updater (the ui and # updater components carry their own versions in their tauri.conf.json). # Bump it when the engine changes; local builds fall back to project(VERSION). set(MONOLITH_VERSION "${PROJECT_VERSION}" CACHE STRING "Monolith engine version (X.Y.Z)") diff --git a/app/desktop-ui/CMakeLists.txt b/app/desktop-ui/CMakeLists.txt index bbed1b3..a256566 100644 --- a/app/desktop-ui/CMakeLists.txt +++ b/app/desktop-ui/CMakeLists.txt @@ -21,7 +21,7 @@ if(NPM_EXECUTABLE AND CARGO_EXECUTABLE) COMMAND "${NPM_EXECUTABLE}" install COMMAND "${NPM_EXECUTABLE}" run build # 2. Compile the Tauri host. Plain `cargo build --release` is used (not - # `cargo tauri build`) so the tauri-cli is not required — bundling is + # `cargo tauri build`) so the tauri-cli is not required - bundling is # disabled (tauri.conf.json bundle.active=false) and we ship the bare exe. # --features custom-protocol is REQUIRED: without it tauri's build # script sets cfg(dev)=true and the shipped exe loads build.devUrl diff --git a/app/desktop-ui/PRODUCT.md b/app/desktop-ui/PRODUCT.md index 09f04aa..133f6ac 100644 --- a/app/desktop-ui/PRODUCT.md +++ b/app/desktop-ui/PRODUCT.md @@ -24,7 +24,7 @@ instantly in the library, and the app never gets in the way of the game. Quiet, precise, native. The app should feel like part of Windows: dark chrome, monochrome restraint, red reserved for recording/destructive meaning. A single desaturated lavender-ice accent marks interactive/active/selected state -(active nav item, active tab, focused input, primary button) — it never +(active nav item, active tab, focused input, primary button) - it never appears on recording or favorite affordances, and it's deliberately muted rather than saturated so it reads as UI state, not brand color. No gamer-RGB styling, no streamer-brand loudness. @@ -43,7 +43,7 @@ styling, no streamer-brand loudness. 2. Resolve, don't expose: every process, device, or file is shown by its human name and icon; identifiers stay internal. 3. One accent, one meaning: red means recording or destruction, gold means - favorited, and the lavender-ice accent means interactive/active/selected — + favorited, and the lavender-ice accent means interactive/active/selected - each color carries exactly one signal, never mixed or reused. 4. Native fidelity: Windows-resident fonts, standard affordances, offline and CSP-clean assets. diff --git a/app/desktop-ui/README.md b/app/desktop-ui/README.md index 873c972..a40cbb6 100644 --- a/app/desktop-ui/README.md +++ b/app/desktop-ui/README.md @@ -44,7 +44,7 @@ Rust modules: (favorite/title/hashtags/rename/delete). - `engine_rpc.rs`: JSON-RPC client to `Monolith.exe` (recorder control, `clip_regen_thumb`, `reload_settings`, status). Unchanged by the IPC - migration — also used by `plugins/stream-deck`. + migration - also used by `plugins/stream-deck`. - `game_catalog.rs`: game metadata lookup. - `exe_icon.rs`: native exe icon extraction. - `paths.rs`: Monolith runtime paths. diff --git a/app/desktop-ui/src-tauri/capabilities/default.json b/app/desktop-ui/src-tauri/capabilities/default.json index 06be53b..f5a83e2 100644 --- a/app/desktop-ui/src-tauri/capabilities/default.json +++ b/app/desktop-ui/src-tauri/capabilities/default.json @@ -2,13 +2,17 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capabilities for the main Monolith UI window.", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", "core:window:allow-minimize", "core:window:allow-toggle-maximize", "core:window:allow-close", "core:window:allow-start-dragging", - "core:event:default" + "core:event:default", + "core:window:allow-maximize", + "core:window:allow-unmaximize" ] } diff --git a/app/desktop-ui/src-tauri/src/collections.rs b/app/desktop-ui/src-tauri/src/collections.rs index 871a8aa..8a48c60 100644 --- a/app/desktop-ui/src-tauri/src/collections.rs +++ b/app/desktop-ui/src-tauri/src/collections.rs @@ -1,6 +1,6 @@ // Collections ("albums"): user-curated groupings of clips that span both // sources (replay + manual). Stored in a single global collections.db under -// %LocalAppData%\Monolith — deliberately not per-catalog, because a collection +// %LocalAppData%\Monolith - deliberately not per-catalog, because a collection // mixes clips from clips.db and recs.db. Rows referencing clips that have // since been deleted are pruned on read. diff --git a/app/desktop-ui/src-tauri/src/commands.rs b/app/desktop-ui/src-tauri/src/commands.rs index b26acd3..5eaf0fc 100644 --- a/app/desktop-ui/src-tauri/src/commands.rs +++ b/app/desktop-ui/src-tauri/src/commands.rs @@ -8,7 +8,7 @@ // // IMPORTANT: every command here is `async fn`. Tauri v2 runs plain // (non-async) commands inline on the main thread, which is also the WebView2 -// message-pump thread — a synchronous SQLite query or engine_rpc TCP call +// message-pump thread - a synchronous SQLite query or engine_rpc TCP call // there freezes the whole window. `async fn` commands are dispatched via // `async_runtime::spawn`, and the blocking bodies below run on // `spawn_blocking` so the UI thread never waits on disk/network I/O. @@ -202,7 +202,7 @@ pub async fn reveal_in_explorer(path: String) -> Result<(), String> { .await } -// Spawns the component updater (Updater.exe) with its window visible — the +// Spawns the component updater (Updater.exe) with its window visible - the // Settings "Check for updates" action. Resolution mirrors the engine's // resolve_tauri_ui() (settings_window.cpp): installed exe next to the app // root, then the CMake build output, then a dev cargo tree. The updater has @@ -400,7 +400,7 @@ pub async fn get_settings() -> Value { // Rejects a config document that assigns the same normalized hotkey chord to // more than one action. Defense-in-depth mirror of the check the engine's -// `settings::save()` also runs before persisting — this one catches it before +// `settings::save()` also runs before persisting - this one catches it before // the write even happens. "NONE" (hotkey disabled) never collides with itself. fn find_hotkey_collision(config: &Value) -> Option { let hotkeys = config.get("hotkeys")?.as_object()?; @@ -447,7 +447,7 @@ pub async fn save_settings(app: tauri::AppHandle, config: Value) -> Result<(), S blocking_result(move || settings_store::write_config(&config).map_err(|err| err.to_string())).await?; if let Err(err) = blocking(engine_rpc::reload_settings).await { // Settings are saved; the engine just couldn't reload them right now - // (it may not be running — it reads the new values at next start). + // (it may not be running - it reads the new values at next start). eprintln!("engine settings reload failed: {err}"); } // Output folders may have changed: re-scope the asset protocol so diff --git a/app/desktop-ui/src-tauri/src/exe_icon.rs b/app/desktop-ui/src-tauri/src/exe_icon.rs index 8dc0d3d..8a5a57e 100644 --- a/app/desktop-ui/src-tauri/src/exe_icon.rs +++ b/app/desktop-ui/src-tauri/src/exe_icon.rs @@ -3,8 +3,8 @@ // bar capture status pattern) instead of remote artwork or a raw process name. // Windows-only by definition (SHDefExtractIconW). // -// Cached in game_catalog.db keyed by process name (not install path) — see -// game_catalog::cached_exe_icon/store_exe_icon — so the same game keeps its +// Cached in game_catalog.db keyed by process name (not install path) - see +// game_catalog::cached_exe_icon/store_exe_icon - so the same game keeps its // icon across reinstalls, relocations, or different machines instead of // re-extracting (and never sharing a cache entry) whenever the exe's on-disk // path differs. diff --git a/app/desktop-ui/src-tauri/src/game_catalog.rs b/app/desktop-ui/src-tauri/src/game_catalog.rs index eac2c03..d614ccc 100644 --- a/app/desktop-ui/src-tauri/src/game_catalog.rs +++ b/app/desktop-ui/src-tauri/src/game_catalog.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::time::{Duration, SystemTime, UNIX_EPOCH}; // Discord is only ever used to enrich the display (name/icon/cover) of a game -// the C++ recorder's local heuristic already detected — never to decide which +// the C++ recorder's local heuristic already detected - never to decide which // process is active. All reads used by the clip grid go through // resolve_artwork_cached (below), which never makes a network call; artwork // is populated/refreshed only by the scheduled background job in main.rs diff --git a/app/desktop-ui/src-tauri/src/main.rs b/app/desktop-ui/src-tauri/src/main.rs index b9aebfe..b90b94a 100644 --- a/app/desktop-ui/src-tauri/src/main.rs +++ b/app/desktop-ui/src-tauri/src/main.rs @@ -3,7 +3,7 @@ // protocol) instead of navigating to a local loopback HTTP server. The engine // (Monolith.exe) is a separate process, reached only over JSON-RPC on // 127.0.0.1:45991 for clip mutations / settings reload / recorder control -// (engine_rpc.rs) — that transport is unchanged and also used by the Stream +// (engine_rpc.rs) - that transport is unchanged and also used by the Stream // Deck plugin. See docs/DECISIONS.md (ADR superseding ADR-0011/0012). #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] diff --git a/app/desktop-ui/src-tauri/src/settings_store.rs b/app/desktop-ui/src-tauri/src/settings_store.rs index f1a8af9..32f0753 100644 --- a/app/desktop-ui/src-tauri/src/settings_store.rs +++ b/app/desktop-ui/src-tauri/src/settings_store.rs @@ -75,7 +75,7 @@ pub fn read_runtime_status() -> Value { // output_dirs() is on the hot path of every catalog command (db_path + // media_folder each call it). Reading settings.db + parsing the full JSON // each time showed up with large libraries, so cache the result keyed by the -// settings.db mtime+size — a stat() instead of a DB round-trip, and still +// settings.db mtime+size - a stat() instead of a DB round-trip, and still // correct when the engine (or anything else) rewrites the file. static DIRS_CACHE: Mutex, u64, paths::OutputDirs)>> = Mutex::new(None); diff --git a/app/desktop-ui/ui/index.html b/app/desktop-ui/ui/index.html index 14b103d..d5b86d8 100644 --- a/app/desktop-ui/ui/index.html +++ b/app/desktop-ui/ui/index.html @@ -6,7 +6,7 @@ + config allows them - see docs/DECISIONS.md ADR-0015. --> Monolith diff --git a/app/desktop-ui/ui/src/app.tsx b/app/desktop-ui/ui/src/app.tsx index d86b4a9..4e45411 100644 --- a/app/desktop-ui/ui/src/app.tsx +++ b/app/desktop-ui/ui/src/app.tsx @@ -112,7 +112,7 @@ export function App() { }, [clips]); // Card-level mutations (favorite, thumbnail capture, duration fix) are all - // patched optimistically via updateClip — a full catalog reload per event + // patched optimistically via updateClip - a full catalog reload per event // caused a reload storm when many cards were missing thumbnails. const handleCardChanged = useCallback((next: Clip) => { updateClip(next); diff --git a/app/desktop-ui/ui/src/home/clip-card.tsx b/app/desktop-ui/ui/src/home/clip-card.tsx index c89cb5d..02af0c2 100644 --- a/app/desktop-ui/ui/src/home/clip-card.tsx +++ b/app/desktop-ui/ui/src/home/clip-card.tsx @@ -61,7 +61,7 @@ export const ClipCard = memo(function ClipCard( // Icon is always resolved locally: prefer the icon embedded in the game's // own executable (no network, no cache dependency), then fall back to // whatever list_clips already resolved from the local Discord artwork - // cache (game_icon_url) — never fetched here, so opening the library never + // cache (game_icon_url) - never fetched here, so opening the library never // triggers a network call. A game newly added to the cache picks up its // icon on the next scheduled refresh (see game_catalog::refresh_stale). useEffect(() => { @@ -138,7 +138,7 @@ export const ClipCard = memo(function ClipCard( }; video.onerror = () => { - // Container/codec unsupported by WebView2 (e.g. .mkv) — hand off to the + // Container/codec unsupported by WebView2 (e.g. .mkv) - hand off to the // engine regenerator instead of silently giving up. if (needsThumb) { void fallbackToEngine().finally(finish); diff --git a/app/desktop-ui/ui/src/home/collection-picker.tsx b/app/desktop-ui/ui/src/home/collection-picker.tsx index c69c507..39945fd 100644 --- a/app/desktop-ui/ui/src/home/collection-picker.tsx +++ b/app/desktop-ui/ui/src/home/collection-picker.tsx @@ -101,7 +101,7 @@ export function CollectionPicker({ clip, onClose, onChanged }: { {loading ? : collections.length === 0 - ? + ? : collections.map((c) => { const inCol = membership.has(c.id); return ( diff --git a/app/desktop-ui/ui/src/home/detail-view.tsx b/app/desktop-ui/ui/src/home/detail-view.tsx index 95511ac..d2b5ccb 100644 --- a/app/desktop-ui/ui/src/home/detail-view.tsx +++ b/app/desktop-ui/ui/src/home/detail-view.tsx @@ -16,7 +16,7 @@ interface Props { onClipUpdate: (clip: Clip) => void; onDelete: (clip: Clip) => void; /** Opens the collection picker for this clip (grid path wires it; collection - * detail leaves it unset — clips there are already in a collection). */ + * detail leaves it unset - clips there are already in a collection). */ onAddToCollection?: (clip: Clip) => void; } @@ -143,7 +143,7 @@ function BookmarkMarkers(