Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
11 changes: 10 additions & 1 deletion .github/workflows/version-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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`

Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/windows-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ Win32/
# --- vcpkg ---
vcpkg/
vcpkg_installed/
vcpkg-configuration.json

# --- Windows ---
Thumbs.db
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/desktop-ui/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions app/desktop-ui/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
2 changes: 1 addition & 1 deletion app/desktop-ui/src-tauri/src/collections.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
8 changes: 4 additions & 4 deletions app/desktop-ui/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String> {
let hotkeys = config.get("hotkeys")?.as_object()?;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/desktop-ui/src-tauri/src/exe_icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/src-tauri/src/game_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]

Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/src-tauri/src/settings_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<(Option<SystemTime>, u64, paths::OutputDirs)>> = Mutex::new(None);

Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- CSP is injected by Tauri from tauri.conf.json's app.security.csp. A
static meta tag here would intersect with it (most-restrictive-wins
per directive) and silently re-block asset:/ipc: even though the
config allows them — see docs/DECISIONS.md ADR-0015. -->
config allows them - see docs/DECISIONS.md ADR-0015. -->
<title>Monolith</title>
<link rel="stylesheet" href="/styles.css" />
</head>
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions app/desktop-ui/ui/src/home/clip-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/ui/src/home/collection-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function CollectionPicker({ clip, onClose, onChanged }: {
{loading
? <span class="loading-dots"><i /><i /><i /></span>
: collections.length === 0
? <p class="modal-msg">No collections yet — create one below.</p>
? <p class="modal-msg">No collections yet - create one below.</p>
: collections.map((c) => {
const inCol = membership.has(c.id);
return (
Expand Down
10 changes: 5 additions & 5 deletions app/desktop-ui/ui/src/home/detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -143,7 +143,7 @@ function BookmarkMarkers(
<button
class="bm-marker"
key={bm.seq}
title={`${bm.label} — ${formatDuration(bm.time_seconds)}`}
title={`${bm.label} - ${formatDuration(bm.time_seconds)}`}
style={{ left: `${pct}%`, background: bm.color }}
onClick={() => onSeek(bm.time_seconds)}
/>
Expand Down Expand Up @@ -283,7 +283,7 @@ export function DetailView(
return () => { cancelled = true; };
}, [clip?.id, clip?.source]);

// Fullscreen reuses this same <video> element in place (see render below) —
// Fullscreen reuses this same <video> element in place (see render below) -
// only the surrounding chrome/layout changes, so playback never restarts.
useEffect(() => {
if (!fsMode) return;
Expand Down Expand Up @@ -591,7 +591,7 @@ export function DetailView(
? (
<span class="trim-readout">
<span class="trim-range">
{formatDuration(trimStart)} – {formatDuration(trimEnd)}
{formatDuration(trimStart)} - {formatDuration(trimEnd)}
</span>
<button class="btn btn-primary" disabled={trimBusy} onClick={applyTrim}>
Apply
Expand Down Expand Up @@ -740,7 +740,7 @@ export function DetailView(
{bookmarks.length === 0
? (
<p class="bm-empty">
No bookmarks — press Ctrl+Shift+F12 while recording to add one.
No bookmarks - press Ctrl+Shift+F12 while recording to add one.
</p>
)
: (
Expand Down
4 changes: 2 additions & 2 deletions app/desktop-ui/ui/src/home/fullscreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function fmtTime(seconds: number): string {

// Custom YouTube-style fullscreen chrome (not the native <video controls>),
// driven by an externally-owned <video> element rather than one it renders
// itself — this lets a caller that already has a playing element (e.g. the
// itself - this lets a caller that already has a playing element (e.g. the
// detail view) reuse it in place instead of restarting playback on a new one.
export function FullscreenChrome(
{ videoEl, multitrack, onClose }: {
Expand Down Expand Up @@ -125,7 +125,7 @@ export function FullscreenChrome(
}, [onClose, videoEl, muted]);

// Document-level (not a wrapping div's onMouseMove) so the autohide timer
// resets no matter which sibling element the pointer is over — the chrome
// resets no matter which sibling element the pointer is over - the chrome
// here only renders fixed-position overlays, not a full-viewport container.
useEffect(() => {
document.addEventListener("mousemove", wake);
Expand Down
2 changes: 1 addition & 1 deletion app/desktop-ui/ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export interface EngineStatus {
replay_enabled?: boolean;
recording_enabled?: boolean;
clip_generation?: number;
// Engine component version (the interface version is separate — the two
// Engine component version (the interface version is separate - the two
// are versioned independently by the component updater).
version?: string;
connected?: boolean;
Expand Down
6 changes: 3 additions & 3 deletions app/desktop-ui/ui/src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Display formatting helpers (deno.md §2.1).

export function formatSize(bytes: number): string {
if (!bytes || bytes <= 0) return "—";
if (!bytes || bytes <= 0) return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let v = bytes;
let i = 0;
Expand Down Expand Up @@ -43,7 +43,7 @@ export function appLabel(displayName?: string | null, processName?: string | nul

// Same as appLabel, but for clips specifically: when no game was detected,
// the label depends on how the clip was captured rather than falling back to
// the generic "Desktop" — a manual recording with no game is a screen
// the generic "Desktop" - a manual recording with no game is a screen
// recording, while a replay-buffer save with no game genuinely has none.
export function clipSourceLabel(
clip: { source: "replay" | "manual"; game_display_name?: string | null; game_process_name?: string | null },
Expand Down Expand Up @@ -83,7 +83,7 @@ export function formatDate(iso: string): string {
return `${month} ${d.getDate()}, ${d.getFullYear()} ${time}`;
}

// Windows device paths ("\\.\DISPLAY2") are never user-facing copy — derive
// Windows device paths ("\\.\DISPLAY2") are never user-facing copy - derive
// a friendly "Display N" label from the trailing device number instead.
export function monitorDisplayName(
mon: { device?: string | null },
Expand Down
Loading
Loading