summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 22:03:59 +0900
committerAdam Malczewski <[email protected]>2026-06-27 22:03:59 +0900
commit831747ddb5886364707b696ac38cce8da333f15d (patch)
tree1b3c983c2b9964d55adec16e2e3a78190fbedbbe
parentb0534c0b53fc116521245c14b05d0120a1cded89 (diff)
downloadstudy-player-V1.tar.gz
study-player-V1.zip
V1: preserve pre-refactor state (single-file main.c + harness docs)V1
-rw-r--r--.dispatch/build-agent.md48
-rw-r--r--.dispatch/package-agent.md53
-rw-r--r--.dispatch/rules/contracts-are-h.md17
-rw-r--r--.dispatch/rules/one-owner.md8
-rw-r--r--.dispatch/rules/zero-warnings.md11
-rw-r--r--AGENTS.md83
-rw-r--r--GLOSSARY.md49
-rw-r--r--ORCHESTRATOR.md403
-rwxr-xr-xbin/serve18
-rw-r--r--notes/restructure-plan.md344
-rw-r--r--src/layout_editor.c4
-rw-r--r--src/main.c8
-rw-r--r--study-player.cfg16
-rw-r--r--tasks.md43
-rw-r--r--web/shell.html110
15 files changed, 1209 insertions, 6 deletions
diff --git a/.dispatch/build-agent.md b/.dispatch/build-agent.md
new file mode 100644
index 0000000..cbf21dd
--- /dev/null
+++ b/.dispatch/build-agent.md
@@ -0,0 +1,48 @@
+# Build system agent brief
+
+You are the **build system owner-agent** for this raylib project. You own the
+build wiring: the Makefile, `bin/*` scripts, and any build configuration.
+
+## Your scope
+You MAY read ANY file in the project — `.h` headers, `.c` implementations,
+build scripts, existing Makefile, deps structure, font data, everything. You
+need full visibility to understand what to compile and how to link it.
+
+You MAY write ONLY:
+- `Makefile`
+- `bin/build`, `bin/build-web`, `bin/clean`, `bin/serve`
+
+You MUST NOT write to `src/*` or any other source files.
+
+## Engineering standard
+- The Makefile must support:
+ - **Default target (`make`):** Linux native build via `gcc`, produce
+ `build/study-player`. Defines: `-DPLATFORM_DESKTOP -DPLATFORM_LINUX
+ -D_GLFW_X11`. Link: `-lm -lrt -ldl -lpthread -lX11`.
+ - **Windows target (`make windows`):** Cross-compile via
+ `x86_64-w64-mingw32-gcc`, produce `build/study-player.exe`. Defines:
+ `-DPLATFORM_DESKTOP -D_GLFW_WIN32`. Link: `-lgdi32 -lwinmm -lcomdlg32
+ -lole32`.
+ - **Clean target (`make clean`):** remove `build/`.
+ - **Individual `.o` compilation:** each `src/*.c` → `build/<name>.o` with
+ `-std=c99 -Wall -Wextra`.
+- Raylib is built as a static library from `deps/raylib/src/*.c`. Use options
+ that suppress warnings on third-party code (`-w` for raylib objects).
+- Font header generation: `build/font_data.h` is a prerequisite built by
+ running `xxd -i` on the font file in `resources/`. If no font file exists,
+ the build should still work (font_data.h just won't define `FONT_EMBEDDED`).
+- The `bin/build-web` script builds for Web/WASM via `emcc` + `emar`. Keep it
+ functional.
+- All SRCS should be `$(wildcard src/*.c)` so new modules auto-compile.
+- Use `-j$(nproc)` in any make invocation inside scripts for parallelism.
+
+## Verification
+1. `make clean && make -j$(nproc)` — exits 0, zero warnings from project code
+2. `make windows -j$(nproc)` — exits 0, zero warnings from project code
+3. `bin/build-web` still works (even if run separately)
+
+## Report
+Write `reports/build-system.md`:
+1. **Files touched**
+2. **What you changed** (bullet list)
+3. **Build result** for both Linux and Windows targets
diff --git a/.dispatch/package-agent.md b/.dispatch/package-agent.md
new file mode 100644
index 0000000..fecd7b8
--- /dev/null
+++ b/.dispatch/package-agent.md
@@ -0,0 +1,53 @@
+# Package owner-agent brief (C/Raylib)
+
+You are the **exclusive owner-agent** for a C module in this raylib project.
+
+## Your scope
+You own the module's `.h` + `.c` pair. You may read and edit ONLY those two
+files. No other agent may touch them. This project follows a **single-writer
+rule**.
+
+## Visibility: contracts vs implementation
+- You MAY read the `.h` header files of OTHER modules (their contracts).
+- You MUST NOT read the `.c` implementation files of ANY other module.
+- If you think you need a change in another module's `.h` contract, REPORT it
+ in your final report — do NOT edit it yourself.
+- If you think you need to read another module's `.c` to understand its
+ behavior, STOP — the `.h` contract is underspecified. REPORT this.
+
+## Engineering standard
+This is a C99 project. Your code must:
+- Compile with `-Wall -Wextra` producing ZERO warnings.
+- Use `#pragma once` as the include guard in every `.h` file.
+- Put NO global mutable variables — all shared state through `PlayerState*`.
+- Prefix public functions with your module name (`player_`, `study_`, `ui_`).
+- Use `static` for module-internal helper functions.
+- Mark pointer parameters `const` when the function does not mutate them.
+- Use `raylib.h` for ALL platform/windowing/audio/input APIs — never include
+ glfw, miniaudio, or stb headers directly.
+- Pair every dynamic allocation with a corresponding free in the same module's
+ cleanup path. No leaks.
+
+## Build
+Run `make` from the repo root to build the full project. Your module's `.c`
+file will be compiled to `.o` and linked into the final executable.
+
+## Verification
+Before writing your report, you MUST:
+1. Run `make` from the repo root. It must exit 0 with ZERO warnings.
+2. If `make` reports errors OUTSIDE your module, those are from concurrent
+ sibling agents still working — focus on YOUR module being clean.
+
+## Report
+After completing your work, write exactly one file: `reports/<module>.md`:
+1. **Files touched** (list paths)
+2. **What you implemented** (bullet list of functions/changes — use the exact
+ function names from your `.h` contract)
+3. **Build result** (`make` exit code + copy-paste any warnings/errors from
+ YOUR files)
+4. **Contract gaps or issues** discovered (e.g. missing declarations in
+ another module's `.h`)
+5. **Changes needed in other modules** (e.g. "`types.h` needs `MAX_FOO`")
+
+The orchestrator will read your report — not your `.c` file. Be precise about
+what you built and what still needs attention.
diff --git a/.dispatch/rules/contracts-are-h.md b/.dispatch/rules/contracts-are-h.md
new file mode 100644
index 0000000..29f9ce3
--- /dev/null
+++ b/.dispatch/rules/contracts-are-h.md
@@ -0,0 +1,17 @@
+# Contracts are header files
+
+- The `.h` file IS the contract between modules. Other agents read ONLY your
+ `.h` — never your `.c`.
+- Every `.h` must be **self-contained**: it includes all types it references.
+ A consumer should be able to `#include "your_module.h"` and nothing else.
+- Prefer **forward declarations** over full includes when only a pointer is
+ needed. Example: `typedef struct PlayerState PlayerState;` avoids including
+ `types.h`.
+- A `.h` file must NOT include any `.c` file. Ever.
+- If you expose a function, its full signature (return type, name, parameter
+ types and names) must be in the `.h`. The documentation of what it DOES
+ (preconditions, postconditions, side effects) goes in a comment in the `.h`
+ — that is the contract's behavioral specification, not just its type
+ signature.
+- If an agent NEEDS to read your `.c` to understand what your module does,
+ your `.h` contract is UNDESPECIFIED. Report this as a contract gap.
diff --git a/.dispatch/rules/one-owner.md b/.dispatch/rules/one-owner.md
new file mode 100644
index 0000000..15ef1a8
--- /dev/null
+++ b/.dispatch/rules/one-owner.md
@@ -0,0 +1,8 @@
+# One-owner
+
+- You are the EXCLUSIVE writer for the files assigned in the TASK block.
+- No other agent may write those files — ever, under any circumstances.
+- If another module needs a change in your file, the orchestrator summons YOU
+ to make it.
+- Check your work: `git status` should show changes ONLY in your assigned files.
+ If you accidentally touch something else, revert it.
diff --git a/.dispatch/rules/zero-warnings.md b/.dispatch/rules/zero-warnings.md
new file mode 100644
index 0000000..a6239d9
--- /dev/null
+++ b/.dispatch/rules/zero-warnings.md
@@ -0,0 +1,11 @@
+# Zero warnings
+
+- Your code must compile with `-Wall -Wextra` producing EXACTLY ZERO warnings.
+- No `-w` suppression. No `(void)` casts to silence legitimate warnings unless
+ you have a real reason (e.g. an unused parameter that must exist for a
+ callback signature).
+- The orchestrator will re-run `make` after you and will REJECT any warning —
+ even ones from other files your code includes. If `raylib.h` or a system
+ header triggers a warning, isolate it with platform guards.
+- Run `make` yourself before reporting. The exit code must be 0.
+- The build output is your trust signal. A clean build = a clean module.
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9e9c600
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,83 @@
+# AGENTS.md — subagent constitution for C/Raylib project
+
+> **This is loaded by every agent.** It contains ONLY project-specific,
+> non-obvious rules. Never restate what a frontier model already knows about C
+> or raylib.
+
+---
+
+## 1. C dialect & build rules
+
+- **C99** (`-std=c99`). No C11/C17 features.
+- **Compile with `-Wall -Wextra`** — zero warnings. If you cannot silence a
+ warning without reducing correctness, flag it in your report.
+- **Raylib is the ONLY external library.** Include `raylib.h` for all platform
+ APIs (windowing, audio, input, font loading, drawing). Never include glfw,
+ miniaudio, or stb headers directly. For web: `emscripten.h` is allowed behind
+ `#ifdef PLATFORM_WEB`.
+- **`build/font_data.h`** (generated by `xxd` from `resources/`) is included
+ behind `#include "font_data.h"` guard its use with `#if FONT_EMBEDDED`.
+- **No dynamic allocation** unless paired with an explicit free in the same
+ module's cleanup. No leaks.
+- **No VLAs** (variable-length arrays on the stack). Use fixed-size buffers or
+ guard with `MAX_*` constants.
+
+## 2. Module boundaries (THE KEY RULE)
+
+- **Your `.h` file IS your contract.** It declares every public type, constant,
+ and function signature that other modules consume. Keep it minimal —
+ consumers should not see implementation details.
+- **Your `.c` file IS your implementation.** It is PRIVATE. Static functions
+ are module-internal. No other module includes your `.c` file — ever.
+- **Include guards:** Every `.h` file starts with `#pragma once`.
+- **Self-contained headers:** A `.h` file must `#include` all types it
+ references (directly or via forward declaration). Consuming agents should
+ need to include ONLY your `.h`, not hunt for transitive includes.
+- **Forward-declare when possible:** `typedef struct PlayerState PlayerState;`
+ in a header avoids pulling in the full `types.h` when only a pointer is
+ needed.
+
+## 3. State management
+
+- **No global mutable variables.** All shared state lives in `PlayerState` and
+ is passed by pointer.
+- **File-scope statics** are allowed ONLY for module-private data (e.g. cached
+ fonts, colors, layout constants in the UI module).
+- **Const correctness:** Mark pointers `const` when the function does not
+ mutate the data. Example: `const char *path` in functions that only read.
+
+## 4. Function naming
+
+- **Prefix public functions** with the module name or a clear namespace:
+ - `player_*` for player module
+ - `study_*` for study module
+ - `ui_*` for UI module
+- **Static helpers** (module-internal) may omit the prefix.
+- **Verb-first naming:** `player_load`, `player_seek`, `study_detect_silence`.
+
+## 5. Testing & verification
+
+- **Build is the primary test.** If `make` exits 0 with zero warnings, your
+ module compiles and links correctly.
+- **If you add a new `.c` file**, note it in your report — the orchestrator
+ updates the Makefile's `SRCS` list (this is orchestrator-owned build wiring,
+ NOT your responsibility).
+
+## 6. What you may read from other modules
+
+- **YES:** The `.h` header files of other modules (their contracts).
+- **NO:** The `.c` implementation files of ANY other module. If you think you
+ need to read a sibling's `.c` to understand its behavior, STOP — that means
+ the `.h` contract is underspecified. Report this to the orchestrator.
+
+## 7. Report format
+
+After completing your work, write `reports/<module>.md` with:
+1. **Files touched** (list paths)
+2. **What you implemented** (bullet list of functions/changes)
+3. **Build result** (`make` output — exit code + any warnings)
+4. **Issues or contract gaps** discovered
+5. **Any changes needed in other modules** (e.g. "study.h needs a new function
+ declaration")
+
+Keep the report concise — the orchestrator reads many of these per wave.
diff --git a/GLOSSARY.md b/GLOSSARY.md
new file mode 100644
index 0000000..4d05b8b
--- /dev/null
+++ b/GLOSSARY.md
@@ -0,0 +1,49 @@
+# GLOSSARY.md — canonical vocabulary
+
+> **One term per concept. Never coin a synonym silently.** If you think a new
+> term is needed, propose it and wait for approval before using it in code.
+
+## Core concepts
+
+| Term | Definition | Avoid calling it... |
+|------|-----------|-------------------|
+| **PlayerState** | The single struct holding all mutable runtime state: loaded audio, playback status, silence regions, study mode flag | "app state", "context", "global state" |
+| **speaking portion** | A contiguous segment of audio between two silence regions (i.e. the "meaningful content"). 0-based index. | "section", "segment", "clip", "part" |
+| **silence region** | A detected gap in audio where amplitude stays below threshold for ≥ minDuration. Normalized 0–1. | "pause", "gap", "quiet zone" |
+| **study mode** | Boolean toggle (`PlayerState.studyMode`). When ON, auto-pauses at silence boundaries. | "auto-pause mode", "learning mode" |
+| **seek** | Jump playback position to a specific time (seconds). `player_seek(PlayerState*, float)`. | "scrub", "jump", "skip" |
+| **auto-pause** | The study-mode mechanism that pauses playback when entering a silence region. | "auto-stop", "silence break" |
+| **padding zone** | The 0.25s "breathing room" added around speaking portions (silence shrunk by 0.25s on each side) to avoid auto-pausing during natural speech pauses. | "grace period", "buffer zone" |
+| **portion navigation** | Jumping between speaking portions via keys (V/B) or section buttons. | "chapter skip", "section nav" |
+| **embedded font** | A `.otf`/`.ttf` font file converted to `build/font_data.h` via `xxd`, compiled into the binary. Guarded by `#if FONT_EMBEDDED`. | "baked font", "built-in font" |
+
+## Module names
+
+| Module | Files | Owns |
+|--------|-------|------|
+| **types** | `src/types.h` | `PlayerState`, `SilenceRegion`, all enums, defines, constants |
+| **player** | `src/player.h`, `src/player.c` | Audio loading, playback control, seek, time formatting |
+| **study** | `src/study.h`, `src/study.c` | Silence detection, study mode logic, portion navigation |
+| **ui** | `src/ui.h`, `src/ui.c` | All rendering, font/color initialization, input handling |
+| **main** | `src/main.c` | Entry point, main loop, platform glue |
+
+## Build terms
+
+| Term | Meaning |
+|------|---------|
+| **desktop build** | Host-native build via `gcc`, produces `build/study-player` (Linux) |
+| **Windows build** | Cross-compile via `x86_64-w64-mingw32-gcc`, produces `build/study-player.exe` |
+| **web build** | WASM via `emcc` + Emscripten, produces `build-web/index.{html,js,wasm}` |
+| **font header** | `build/font_data.h` generated by `xxd -i` from a font in `resources/` |
+
+## Platform defines
+
+| Define | When set |
+|--------|----------|
+| `PLATFORM_DESKTOP` | Building for desktop — both Linux native and Windows cross-compile. Set in Makefile. |
+| `PLATFORM_LINUX` | Defined by `-DPLATFORM_LINUX` when building for Linux. |
+| `PLATFORM_WEB` | Building for web (Emscripten). Set in web build script. |
+| `_GLFW_X11` | GLFW X11 backend (Linux). Set in Makefile for Linux builds. |
+| `_GLFW_WIN32` | GLFW Windows backend. Set in Makefile for Windows cross-compile. |
+| `GRAPHICS_API_OPENGL_ES2` | WebGL 2 backend. Set in web build script. |
+| `FONT_EMBEDDED` | Defined in `font_data.h` by `xxd`. Guards `#if FONT_EMBEDDED` blocks. |
diff --git a/ORCHESTRATOR.md b/ORCHESTRATOR.md
new file mode 100644
index 0000000..b5d5319
--- /dev/null
+++ b/ORCHESTRATOR.md
@@ -0,0 +1,403 @@
+# ORCHESTRATOR.md — how to drive this project
+
+> **You are the orchestrator.** You do NOT write feature code yourself. You plan,
+> summon owner-agents (one per module), verify their work, resolve errors, and keep
+> the build green. This file is your complete operating manual. Read it fully
+> before acting. Also read: `AGENTS.md` (the subagent constitution — you enforce
+> it), `GLOSSARY.md`, `.dispatch/rules/`, `tasks.md` (live progress), and
+> `notes/restructure-plan.md` (the full module design + rationale).
+
+---
+
+## 0. Mental model (why this project is built this way)
+
+This is a **C/Raylib desktop application** built from composable modules. Each
+module is a `.h` (contract) + `.c` (implementation) pair. The team structure is
+**isomorphic to the module structure**: one owner-agent per module, and agents
+communicate only through **header-file contracts** — exactly as the code does.
+If an agent needs to read another module's `.c` file to understand its behavior,
+the `.h` contract is underspecified — that is a bug, not normal.
+
+### The harness layers
+
+- **Constitution** (`AGENTS.md`) — loaded by every agent. C99 rules, raylib
+ conventions, zero-warning policy.
+- **Safety reflexes** (`.dispatch/rules/*.md`) — tiny, crystallized scar tissue.
+- **Glossary** (`GLOSSARY.md`) — one canonical name per concept. Prevents
+ synonym drift across modules.
+- **This file** — the orchestrator's workflow (plan → summon → verify → commit).
+- **Contracts** — `.h` files are the ONLY interface between modules. They
+ declare types, constants, and function signatures. `.c` files are private
+ implementation — never read by anyone but the owning agent.
+
+### C/Raylib-specific principles
+
+1. **Contracts are headers** — a `.h` file IS the boundary. It must be
+ self-contained (all types it uses are included within it). Prefer forward
+ declarations over pulling in heavy headers.
+2. **No cross-module `.c` includes** — ever. If module A needs module B, A
+ includes `B.h`, never `B.c`.
+3. **All shared mutable state goes through `PlayerState*`** — no global
+ variables. File-scope statics are only for module-private state (e.g. fonts
+ in the UI module).
+4. **One `.o` per module** — each `.c` compiles independently. The linker
+ resolves dependencies. This is what makes parallel-agent waves possible.
+5. **Zero warnings on `-Wall -Wextra`** — the build is the trust signal. If
+ `make` barks, the wave is not green.
+6. **Raylib is the only external dependency** — no pulling in new libraries
+ without a design decision.
+
+---
+
+## 1. The golden workflow (build/modify a feature)
+
+1. **Plan.** Decide the module(s); split into dependency-topological **waves** of
+ disjoint modules, and WIDEN each wave where you can (§2a).
+2. **Overlap check FIRST.** Before creating anything new, check `GLOSSARY.md` +
+ existing `*.h` files. If the request *describes* an existing concept under a
+ new name, steer to the canonical term. New term? Propose the
+ standard/training-baked name and **ask the user** before adding it to the
+ glossary.
+3. **Boundary decision is the USER's.** "New module vs. extend an existing one?"
+ — surface it; never decide granularity silently.
+4. **Write the prompt** to `prompts/<module>.md` (gitignored). See §3 for the
+ prompt recipe.
+5. **Summon the wave** via `opencode run` (see §2); disjoint modules run in
+ PARALLEL (§2a). RE-READ `.dispatch/rules/` + the §3 scoping map before each
+ wave — assemble from the files, not from memory.
+6. **Verify** the reports + independently re-run checks (see §4). Trust nothing
+ until you've re-run `make` yourself and it exits 0 with zero warnings.
+7. **Resolve** any contract gaps / errors (see §5).
+8. **Commit** the milestone with a clear message. Update `tasks.md`.
+
+---
+
+## 2. Summoning agents via `opencode run` (the harness)
+
+OpenCode CLI is the summon mechanism. The orchestrator assembles each agent's
+prompt by concatenating standardized briefs + scoped rules + the TASK block.
+
+**Working dir:** always the repo root, `/home/tradam/projects/study-player`.
+
+**Two agent types:**
+
+| Agent type | Brief | Reads | Writes |
+|---|---|---|---|
+| **Module agent** | `.dispatch/package-agent.md` | Only other `.h` files | Own `.h` + `.c` pair |
+| **Build system agent** | `.dispatch/build-agent.md` | ANY file | `Makefile`, `bin/*` only |
+
+**Module agent canonical invocation** — the invariant guardrails live ONCE in
+the brief, so `prompts/<module>.md` is JUST the TASK block (§3). Do NOT use
+`-f` (see gotcha); ALWAYS redirect output to a file.
+
+```bash
+cd /home/tradam/projects/study-player && \
+opencode run --dir /home/tradam/projects/study-player \
+ "$(cat .dispatch/package-agent.md)
+$(cat .dispatch/rules/one-owner.md .dispatch/rules/zero-warnings.md .dispatch/rules/contracts-are-h.md)
+
+## TASK
+$(cat prompts/<module>.md)" \
+ > reports/<module>.run.log 2>&1
+```
+
+**Build system agent canonical invocation:**
+
+```bash
+cd /home/tradam/projects/study-player && \
+opencode run --dir /home/tradam/projects/study-player \
+ "$(cat .dispatch/build-agent.md)
+$(cat .dispatch/rules/one-owner.md .dispatch/rules/zero-warnings.md)
+
+## TASK
+$(cat prompts/build-system.md)" \
+ > reports/build-system.run.log 2>&1
+```
+
+**Assembly order is fixed: agent brief → scoped rules → TASK.**
+
+**Scoping map** — include ONLY the rules matching the agent type:
+- **Every module agent:** `one-owner.md`, `zero-warnings.md`, `contracts-are-h.md`.
+- **Build system agent:** `one-owner.md`, `zero-warnings.md` (it reads any file
+ so `contracts-are-h.md` doesn't apply).
+
+`AGENTS.md` is auto-loaded by opencode — never `cat` it.
+
+**MANDATORY — capture output to a file, never display it.** The agent's streamed
+output is enormous and will overwhelm context if it lands in your terminal.
+ALWAYS redirect the summon's stdout+stderr to a log file (e.g.
+`> reports/<module>.run.log 2>&1`) and do NOT echo/`cat` that log back. Read
+the agent's `reports/<module>.md` report (and, if necessary, `grep`/`tail` the
+log for a specific error). Dumping a full run log into context is a hard
+failure.
+
+**Run discipline:**
+- **Do NOT background it. Use a large timeout** (e.g. 1800000 ms = 30 min).
+- One summon per tool call. For PARALLEL agents on disjoint files, launch
+ multiple summons as concurrent tool calls — but ONLY when their file sets do
+ not overlap (single-writer rule).
+- Log parallel runs in `tasks.md`.
+
+**GOTCHAS:**
+- `-f/--file` is an ARRAY flag and greedily eats your trailing message as
+ another filename → "File not found". **Inline with `"$(cat prompts/X.md)"`
+ instead.**
+- A quick smoke test: `opencode run "Reply with exactly SMOKE_OK"` should print
+ `SMOKE_OK`.
+- `opencode agent list` lists agent profiles; `opencode run --help` for flags.
+
+---
+
+## 2a. Parallel execution — WAVES
+
+Throughput comes from running disjoint modules at once. Organise it as waves:
+- **A wave = modules that (a) touch DISJOINT files and (b) have no dependency
+ on each other's `.c` files** (each includes only already-authored `.h`
+ contracts). Launch a wave by emitting one summon per module as CONCURRENT tool
+ calls. The composition root (`main.c`) is almost always the LAST wave.
+- **Pre-author the seam to widen the wave.** Because the orchestrator OWNS
+ contracts (§6), write ALL `.h` contracts FIRST (WAVE 0), then summon the
+ implementors in the SAME wave against those fixed contracts — no module needs
+ another's implementation. Authoring the contracts up front turns a sequential
+ chain into one parallel wave.
+- **One writer per file, always** — even across waves. If two units would edit
+ the same file, they are NOT separable; merge them into one module or sequence
+ them.
+- **After a wave:** read every report, run `make` ONCE for the whole wave,
+ commit the milestone (update `tasks.md`), then start the next wave. Don't open
+ a new wave before the prior one is green.
+
+---
+
+## 3. The per-summon `prompts/<module>.md` is JUST the TASK block
+
+The invariant guardrails — single-writer ownership, visibility, zero warnings,
+contract discipline, and the report format — live ONCE in the standardized
+briefs the summon concatenates (§2). `prompts/<module>.md` contains ONLY:
+
+1. **Your module files:** e.g. `src/player.h` and `src/player.c` — name the
+ FILES the agent may edit (it owns them exclusively).
+2. **The job + algorithm**, naming specific functions and their signatures from
+ the pre-authored `.h` contract.
+3. **The specific `.h` contract file(s)** to read (e.g. `src/types.h`,
+ `src/study.h`) — the agent reads ONLY these headers, never `.c` files.
+4. **Any build instructions** (e.g. "run `make` from repo root").
+
+Keep it scoped: state only the project-specific, non-inferable task — the briefs
+carry the rest.
+
+**Make agents IMPLEMENT, not deliberate.** A summoned owner must edit files +
+run `make` + write its report in one run. If a summon returns only a plan,
+re-summon (§5a).
+
+---
+
+## 4. Verification (the orchestrator's trust protocol)
+
+The orchestrator confirms work from **contracts (.h files) + build output** —
+that is the designed trust mechanism. The header files ARE how you trust a
+module without depending on its internals.
+
+**Stay out of implementation files (§6 Visibility).** Your trust signals are the
+agent's report, the `.h` contract/surface it exposes, and the `make` output you
+re-run yourself — NOT its `.c` implementation. Do NOT open a module's `.c` file
+— not even to "skim", double-check, or diagnose a bug. You diagnose from the
+`make` output + the `.h` contract + the agent's report, then **summon the owning
+agent** (or a temporary multi-knowledge agent, §5) to read its own code and fix
+it.
+
+After every agent, independently:
+```bash
+cd /home/tradam/projects/study-player
+make clean && make -j$(nproc) 2>&1 # must exit 0 with zero warnings
+git status --short # confirm agent stayed in its lane
+```
+
+- **Read ONLY the `.h` files** the unit exposes (its contract), not its `.c`
+ file. The contract plus a green build is enough to trust a module; subtle
+ mistakes show up as link errors or undefined symbols, which `make` catches.
+- Confirm the agent touched ONLY its assigned files (one-owner rule).
+
+**Concurrency caveat (parallel waves):** `make` is whole-project, so an agent's
+OWN mid-wave check can transiently see a sibling's half-written `.c` file. Don't
+act on a report's out-of-module compile errors; YOUR post-wave `make` run is
+authoritative.
+
+---
+
+## 5. Resolving errors & contract changes
+
+- **A module needs something from another module's contract:** that's a CONTRACT
+ CHANGE. The owner of the `.h` makes it. To find every consumer, grep for
+ `#include "<header.h>"` across `src/`. Then summon the affected module owners
+ to update. The orchestrator dispatches this fan-out; agents don't reach
+ across.
+- **Link error or undefined symbol (X and Y each compile but don't link):** no
+ single file owns it. Summon a **temporary multi-knowledge agent** with
+ read/write to the 2–3 relevant files (it MAY see `.c` files — exception to
+ the visibility rule), as their temporary exclusive owner.
+- **CR (change-request) in a report:** if it's **build/config** (`Makefile`,
+ `.gitignore`, `deps/` reference) the orchestrator edits it directly, then
+ re-verifies with `make`. If it's **implementation** (a `.c` file), the
+ orchestrator **summons the owning agent** — it does NOT edit `.c` files
+ itself.
+- **Makefile changes:** the Makefile is orchestrator-owned (it's build wiring,
+ §6). If a module addition requires updating `SRCS`, the orchestrator does it.
+
+---
+
+## 5a. Agent-failure recovery patterns
+
+- **Plan-only / "shall I proceed?" agent.** A summon sometimes returns a PLAN
+ and STOPS without editing (no diff, no `reports/<module>.md`). Detect via
+ `git status` + the missing report. Re-summon the SAME TASK prefixed:
+ "IMPLEMENT THIS NOW — make all edits, run `make`, write the report; do not
+ stop to plan or ask."
+- **Agent strayed out of its lane.** `git status --short` after every wave; if
+ an agent touched a file outside its assigned set, keep it ONLY if it's
+ legitimately the orchestrator's lane (contracts / Makefile / harness / docs,
+ §6) — otherwise revert + re-summon with a tighter scope.
+- **Flaky green.** A module that compiles once but relies on stale `.o` files
+ might pass for the wrong reason; always `make clean && make` before
+ committing.
+
+---
+
+## 6. Restrictions & invariants (NEVER violate)
+
+- **Single-writer:** never let two agents edit the same file concurrently.
+- **Visibility rule:** agents see only other modules' `.h` contracts, NEVER
+ their `.c` implementation. An agent *needing* to read another module's `.c`
+ code is a signal that the `.h` contract is underspecified — fix the contract,
+ don't grant code access. (Exception: the temporary multi-knowledge integration
+ agent, §5.)
+- **The orchestrator NEVER reads or edits `.c` implementation files.** You read
+ ONLY `.h` files (contracts) + `make` output + agent reports. Do NOT open
+ `.c` files — not even during a bug. Clean context = level-headed decisions;
+ the subagents do the implementation.
+- **What the orchestrator MAY edit directly:**
+ (a) **Contracts** — any `.h` header file, especially `types.h` (pure shared
+ types with no .c file) and other `.h` files when pre-authoring contracts
+ or resolving gaps.
+ (b) **Build wiring + config** — `Makefile`, `.gitignore`, `deps/`
+ structure. (Note: the build system agent also owns `Makefile` and
+ `bin/*` — coordinate, don't conflict.)
+ (c) **Harness/docs** — `ORCHESTRATOR.md`, `AGENTS.md`, `GLOSSARY.md`,
+ `.dispatch/`, `notes/`, `tasks.md`, `prompts/`, `reports/`.
+ Everything else — all `.c` implementation files — changes ONLY by summoning
+ the owning agent.
+- **Roadblock → surface to the user.** If a needed change doesn't fit the above
+ (ambiguous ownership, a design question, a stuck agent), stop and ask rather
+ than reaching into implementation.
+- **Subagents inherit this restriction.** Every prompt you write must instruct
+ the agent to read ONLY the `.h` files of OTHER modules, with the sole
+ exception that it MAY read the `.c` files of the module it is assigned to.
+- **Linux native + Windows cross-compile** — `make` builds for Linux; `make
+ windows` cross-compiles for Windows via MinGW. Both platforms must work.
+ Use `#ifdef PLATFORM_LINUX` / `_GLFW_X11` vs `_GLFW_WIN32` guards where
+ platform differences exist.
+- **No global mutable state.** All shared state passes through `PlayerState*`.
+ File-scope statics are for module-private data only (e.g. cached fonts in the
+ UI module).
+- **C99 only.** No C11/C17 features the compiler doesn't support. No C++
+ in `.c` files.
+- **Raylib is the only external library.** No SDL, no GLFW standalone, no
+ third-party UI — everything goes through raylib's API.
+
+---
+
+## 7. Repo geography
+
+```
+/home/tradam/projects/study-player
+
+ AGENTS.md the subagent constitution (auto-loaded by opencode; you enforce it)
+ ORCHESTRATOR.md the orchestrator's operating manual (this file)
+ GLOSSARY.md canonical vocabulary + aliases-to-avoid
+ tasks.md live progress checklist / milestone log
+ Makefile build — orchestrator-owned, never touched by agents
+ README.md project overview, build instructions, usage guide
+
+ .dispatch/
+ package-agent.md base owner-agent brief (module agents)
+ build-agent.md build system agent brief (Makefile, bin/*)
+ rules/ safety reflexes — tiny crystallized scar tissue
+ one-owner.md
+ zero-warnings.md
+ contracts-are-h.md
+
+ .rules/ original design plans (reference only)
+ plan/
+ plan.md
+ phase1.md
+ phase2.md
+ phase3.md
+ ideas/
+
+ notes/
+ restructure-plan.md the full module split design + rationale + wave plan
+
+ prompts/ (gitignored — orchestrator→agent TASK blocks)
+ reports/ (gitignored — agent→orchestrator reports)
+
+ src/
+ types.h CONTRACT — shared types, enums, constants (PlayerState, etc.)
+ player.h CONTRACT — audio playback: load, seek, play, pause, format_time
+ player.c IMPL
+ study.h CONTRACT — study mode: detect_silence, portion navigation
+ study.c IMPL
+ ui.h CONTRACT — rendering: init, render_frame, destroy
+ ui.c IMPL
+ main.c COMPOSITION ROOT — entry point + main loop
+
+ deps/
+ raylib/ raylib library (built as static lib)
+ raygui/ raygui library (reserved, not yet used)
+
+ bin/
+ build build script for desktop (Windows cross-compile)
+ build-web build script for WASM/web
+ clean clean build artifacts
+ serve serve web build locally
+
+ resources/ font files, assets (gitignored)
+ build/ desktop build artifacts (gitignored)
+ build-web/ web build artifacts (gitignored)
+ web/ shell.html for emscripten
+```
+
+---
+
+## 8. Current status & how to run
+
+See `tasks.md` for the live checklist. The project is a working single-file
+`src/main.c` (778 lines) that needs to be split into modules as described in
+`notes/restructure-plan.md`.
+
+**Desktop build:**
+```bash
+cd /home/tradam/projects/study-player
+make -j$(nproc) # native Linux build → build/study-player
+# or for cross-compile:
+make windows -j$(nproc) # Windows cross-compile → build/study-player.exe
+```
+
+**Web build:**
+```bash
+bin/build-web # emscripten → build-web/index.html
+bin/serve # serve on port 8080
+```
+
+**Manual make:**
+```bash
+make clean && make -j$(nproc)
+```
+
+**Clean:**
+```bash
+bin/clean # removes build/ and build-web/
+```
+
+When the module restructure is complete, `make` builds for the host platform.
+`make windows` cross-compiles for Windows via MinGW. The font header generation
+is a make prerequisite.
diff --git a/bin/serve b/bin/serve
new file mode 100755
index 0000000..931e2db
--- /dev/null
+++ b/bin/serve
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$SCRIPT_DIR"
+
+BUILD_DIR="build-web"
+
+if [ ! -f "$BUILD_DIR/index.html" ]; then
+ echo "Error: Web build not found. Run 'bin/build-web' first."
+ exit 1
+fi
+
+PORT="${1:-8080}"
+
+echo "Serving study-player at http://localhost:$PORT"
+echo "Press Ctrl+C to stop."
+python3 -m http.server "$PORT" -d "$BUILD_DIR"
diff --git a/notes/restructure-plan.md b/notes/restructure-plan.md
new file mode 100644
index 0000000..90126b7
--- /dev/null
+++ b/notes/restructure-plan.md
@@ -0,0 +1,344 @@
+# Restructure plan — single-file → modular C/Raylib project
+
+> **Status:** Pending. This document describes the target architecture and the
+> step-by-step plan for splitting `src/main.c` (778 lines) into composable
+> modules. The orchestrator will execute this plan.
+
+---
+
+## §1 Design principles
+
+1. **`.h` = contract, `.c` = implementation.** Every module exposes a single
+ self-contained header. No other module ever includes a `.c` file.
+2. **All shared mutable state through `PlayerState*`.** Defined in `types.h`.
+ File-scope statics only for module-private data (fonts, colors in UI).
+3. **One `.o` per module.** Each `.c` compiles independently. The linker
+ resolves dependencies. This makes parallel agent waves possible.
+4. **Zero-copy from current codebase.** Move functions as-is, then improve.
+ The first pass preserves all behavior exactly.
+5. **Linux native + Windows cross-compile.** Both platforms supported. The
+ Makefile handles both targets (platform conditionals in the build, not in
+ the source unless needed).
+6. **Minimal module count.** Four modules + types header. Don't over-split.
+
+---
+
+## §2 Target module structure
+
+```
+src/
+ types.h CONTRACT — shared types, enums, constants
+ player.h CONTRACT — audio playback
+ player.c IMPL
+ study.h CONTRACT — silence detection + study mode
+ study.c IMPL
+ ui.h CONTRACT — rendering + input
+ ui.c IMPL
+ main.c COMPOSITION ROOT — entry point + main loop
+```
+
+### §2.1 `src/types.h` — shared types and constants
+
+No `.c` file. Pure definitions.
+
+```c
+#pragma once
+
+// ── Platform detection ──
+#ifdef PLATFORM_LINUX
+ #define _GLFW_X11
+#endif
+
+// ── Constants ──
+#define SCREEN_W 1920
+#define SCREEN_H 1080
+#define MAX_SILENCE_REGIONS 4096
+
+// ── Types ──
+typedef struct {
+ float start; /* normalized 0..1 */
+ float end; /* normalized 0..1 */
+} SilenceRegion;
+
+typedef struct {
+ Music music;
+ bool loaded;
+ bool playing;
+ float duration;
+ float currentTime;
+ char filename[256];
+ SilenceRegion silence[MAX_SILENCE_REGIONS];
+ int silenceCount;
+ bool studyMode;
+ bool wasInSilence;
+ int lastSilenceIdx;
+ int skipAutoUpdate;
+ double lastVPress;
+} PlayerState;
+```
+
+### §2.2 `src/player.h` — audio playback contract
+
+```c
+#pragma once
+#include "types.h"
+
+void player_load(PlayerState *s, const char *path);
+void player_unload(PlayerState *s);
+void player_seek(PlayerState *s, float seconds);
+void player_play(PlayerState *s);
+void player_pause(PlayerState *s);
+void player_update(PlayerState *s); /* call once per frame */
+void format_time(float seconds, char *buf, int bufsize);
+const char *basename_from_path(const char *path);
+```
+
+**`src/player.c`** implements:
+- `player_load()` — load MP3, set up Music stream, start playback, update filename + window title, calls `study_detect_silence()` to populate silence regions
+- `player_unload()` — stop + unload music stream
+- `player_seek()` — seek music stream to target seconds
+- `player_play()` / `player_pause()` — Resume/Pause + update state
+- `player_update()` — `UpdateMusicStream()` + `GetMusicTimePlayed()` with skip logic
+- `format_time()` — seconds → "MM:SS" or "H:MM:SS" string
+- `basename_from_path()` — extract filename from path
+- `strcasecmp_ext()` — helper for extension checking (static)
+
+### §2.3 `src/study.h` — study mode contract
+
+```c
+#pragma once
+#include "types.h"
+
+void study_detect_silence(const char *path, PlayerState *s, float threshold, float minDuration);
+int study_find_silence_at(const PlayerState *s, float pos);
+float study_speaking_portion_start(const PlayerState *s, int portion);
+int study_current_portion(const PlayerState *s, float pos);
+int study_total_portions(const PlayerState *s);
+float study_segment_seek_target(const PlayerState *s, int portion);
+bool study_in_padding_zone(const PlayerState *s, float pos, int portion);
+void study_update(PlayerState *s); /* auto-pause logic for one frame */
+```
+
+**`src/study.c`** implements:
+- `study_detect_silence()` — Wave analysis, silence region detection with padding (move from current `detect_silence`)
+- `study_find_silence_at()` — find silence region at normalized position
+- `study_speaking_portion_start()` — get start of speaking portion N
+- `study_current_portion()` — get current speaking portion index
+- `study_total_portions()` — total speaking portions
+- `study_segment_seek_target()` — seek target for a portion (with 2-frame offset)
+- `study_in_padding_zone()` — check if position is in padding zone
+- `study_update()` — the auto-pause state machine: detect silence entry/exit, auto-pause → seek to next portion
+
+### §2.4 `src/ui.h` — rendering + input contract
+
+```c
+#pragma once
+#include "types.h"
+
+void ui_init(void);
+void ui_destroy(void);
+void ui_render_frame(PlayerState *s);
+```
+
+**`src/ui.c`** implements:
+- **File-scope statics:** fonts (`fontSmall`, `font`, `fontMed`, `fontLarge`, `fontHelp`), sizes, colors, layout constants, button positions
+- `ui_init()` — load fonts, set colors, compute layout
+- `ui_destroy()` — unload embedded fonts
+- `ui_render_frame()` — one complete frame:
+ - Handle drag-drop file loading (desktop) → calls `player_load()`
+ - Handle keyboard input (C, N, Space, V, B, Arrows, 0–9, Up/Down) → calls `player_seek()`, `player_play()`, `player_pause()`
+ - Handle mouse input (click-to-seek, play/pause button, section nav buttons, study mode checkbox)
+ - Call `player_update()` for music stream update
+ - Call `study_update()` for study mode auto-pause logic
+ - Call `BeginDrawing()` / `EndDrawing()` with all rendering (title, progress bar, time labels, percentage, buttons, checkboxes, help text)
+- `draw_text_centered()` — static helper
+- `draw_play_icon()`, `draw_pause_icon()`, `draw_seek_back_icon()`, `draw_seek_fwd_icon()` — static helpers
+- `button_hit()` — static helper
+
+### §2.5 `src/main.c` — composition root
+
+No `.h` file. Entry point only.
+
+```c
+#include "raylib.h"
+#include "player.h"
+#include "study.h"
+#include "ui.h"
+#include "font_data.h"
+#ifdef PLATFORM_WEB
+#include <emscripten/emscripten.h>
+#endif
+
+/* File-scope PlayerState (needed for emscripten main loop callback) */
+static PlayerState state = { 0 };
+
+#ifdef PLATFORM_WEB
+EMSCRIPTEN_KEEPALIVE
+void load_file_web(const char *path) {
+ player_load(&state, path);
+}
+#endif
+
+static void update_frame(void) {
+ ui_render_frame(&state);
+}
+
+int main(void) {
+ InitWindow(SCREEN_W, SCREEN_H, "Study Player");
+ InitAudioDevice();
+ SetTargetFPS(60);
+ ui_init();
+
+ memset(&state, 0, sizeof(state));
+ state.studyMode = true;
+ state.lastSilenceIdx = -1;
+
+#ifdef PLATFORM_WEB
+ emscripten_set_main_loop(update_frame, 0, 1);
+#else
+ while (!WindowShouldClose()) {
+ update_frame();
+ }
+#endif
+
+ player_unload(&state);
+ ui_destroy();
+ CloseAudioDevice();
+ CloseWindow();
+ return 0;
+}
+```
+
+---
+
+## §3 Dependency graph
+
+```
+types.h ← player.h ← ui.h
+ ← study.h ← ui.h
+ ← player.c (player depends on study for silence detection)
+ ← main.c
+
+player.h ← player.c (includes: types.h)
+study.h ← study.c (includes: types.h)
+ui.h ← ui.c (includes: types.h, player.h, study.h)
+```
+
+- `types.h` — no dependencies (pure definitions)
+- `player.h` — depends on `types.h` (PlayerState, Music type via raylib)
+- `study.h` — depends on `types.h` (PlayerState, SilenceRegion)
+- `ui.h` — depends on `types.h` (PlayerState)
+- `player.c` — depends on `types.h`, `player.h` (its own contract), `study.h` (calls `study_detect_silence` in `player_load`), `raylib.h`
+- `study.c` — depends on `types.h`, `study.h` (its own contract), `raylib.h`
+- `ui.c` — depends on `types.h`, `player.h`, `study.h`, `ui.h`, `raylib.h`, `font_data.h`
+- `main.c` — depends on all `.h` files, `raylib.h`, `font_data.h`, `emscripten.h` (web only)
+
+All modules compile to `.o` independently — zero `.c` includes another `.c`.
+
+---
+
+## §4 Wave plan
+
+### WAVE 0 — Orchestrator + build system agent (sequentially)
+
+**Orchestrator (direct work):**
+1. Write `src/types.h` with all shared types and constants
+2. Pre-author `src/player.h`, `src/study.h`, `src/ui.h` — define every public
+ function signature so module agents have fixed contracts to implement
+ against
+3. Write TASK prompts to `prompts/build-system.md`, `prompts/player.md`,
+ `prompts/study.md`, `prompts/ui.md`, `prompts/main.md`
+
+**Build system agent:** (reads ANY file, writes only Makefile + bin/*)
+1. Update `Makefile`:
+ - Linux native target (default): `gcc -o build/study-player src/*.c ...`
+ - Windows target (`make windows`): cross-compile via MinGW
+ - Font header generation as a make prerequisite
+ - `SRCS = $(wildcard src/*.c)`, `OBJS = $(SRCS:.c=.o)`
+ - Raylib `.o` compilation with `-w` (third-party warnings suppressed)
+2. Update `.gitignore` (add `prompts/`, `reports/`)
+
+**Verification:** Module `.h` files compile cleanly (no syntax errors).
+`make` will fail on missing `.c` implementations — that's expected, WAVE 1
+resolves it.
+
+### WAVE 1 — All `.c` implementations in parallel (disjoint files)
+
+Four module agents, launched as concurrent tool calls. Each owns its `.h` +
+`.c` pair, reads only other `.h` files, writes only its own files:
+
+| Agent | Files it owns | .h files it reads |
+|---|---|---|
+| Agent A: player | `src/player.h`, `src/player.c` | `src/types.h` |
+| Agent B: study | `src/study.h`, `src/study.c` | `src/types.h` |
+| Agent C: ui | `src/ui.h`, `src/ui.c` | `src/types.h`, `src/player.h`, `src/study.h` |
+| Agent D: main | `src/main.c` (no .h) | all `.h` files |
+
+File sets are DISJOINT. No compile-time dependency between `.c` files — each
+compiles to `.o` independently. All `.h` contracts were fixed in WAVE 0.
+
+**Verification:** `make clean && make -j$(nproc)` — exit 0, zero warnings
+(Linux). Then `make windows -j$(nproc)` — exit 0, zero warnings (Windows).
+
+### WAVE 2 (if needed) — Integration fixes
+
+Any link errors, behavioral regressions, or contract gaps discovered during
+WAVE 1 verification. Summon affected agents to fix.
+
+---
+
+## §5 Function migration map
+
+Every function in the current `src/main.c` moves to exactly one target file:
+
+| Current function | → Target file | New name |
+|---|---|---|
+| `SilenceRegion` struct | `types.h` | (unchanged) |
+| `PlayerState` struct | `types.h` | (unchanged) |
+| `#define` constants | `types.h` | (unchanged) |
+| `strcasecmp_ext()` | `player.c` | static (no prefix) |
+| `detect_silence()` | `study.c` | `study_detect_silence()` |
+| `basename_from_path()` | `player.c` | (unchanged, public) |
+| `seek_to()` | `player.c` | `player_seek()` |
+| `format_time()` | `player.c` | (unchanged, public) |
+| `find_silence_at()` | `study.c` | `study_find_silence_at()` |
+| `speaking_portion_start()` | `study.c` | `study_speaking_portion_start()` |
+| `current_speaking_portion()` | `study.c` | `study_current_portion()` |
+| `total_speaking_portions()` | `study.c` | `study_total_portions()` |
+| `segment_seek_target()` | `study.c` | `study_segment_seek_target()` |
+| `in_padding_zone()` | `study.c` | `study_in_padding_zone()` |
+| `draw_text_centered()` | `ui.c` | static (no prefix) |
+| `draw_play_icon()` | `ui.c` | static (no prefix) |
+| `draw_pause_icon()` | `ui.c` | static (no prefix) |
+| `draw_seek_back_icon()` | `ui.c` | static (no prefix) |
+| `draw_seek_fwd_icon()` | `ui.c` | static (no prefix) |
+| `button_hit()` | `ui.c` | static (no prefix) |
+| `load_audio_file()` | `player.c` | `player_load()` |
+| `load_file_web()` (emscripten) | `main.c` | (unchanged) |
+| `update_frame()` | `main.c` | (simplified — just calls `ui_render_frame()`) |
+| `main()` | `main.c` | (unchanged, simplified) |
+| File-scope statics (state, fonts, colors, layout) | `main.c` (`state`), `ui.c` (rest) | — |
+
+**Auto-pause logic** currently inlined in `update_frame()` (lines 533–562)
+moves into `study_update()` in `study.c`. The UI module calls
+`study_update(&state)` after `player_update(&state)`.
+
+---
+
+## §6 Current code as-is invariants (must preserve)
+
+During the split, preserve every existing behavior:
+1. Drag-and-drop MP3 loading (desktop)
+2. All keyboard shortcuts: C, N, Space, V, B, Arrows, Up, Down, 0–9
+3. Click-to-seek on progress bar
+4. Study mode auto-pause at silence boundaries
+5. Study mode checkbox toggle
+6. Play/pause button and section navigation buttons
+7. Progress bar rendering with elapsed/remaining time labels
+8. Percentage display above progress bar
+9. "PLAYING"/"PAUSED" status text
+10. Help text at bottom
+11. Dark theme colors
+12. Embeddable font support (`FONT_EMBEDDED`)
+13. Web platform support (`PLATFORM_WEB`, emscripten main loop, file upload)
+14. Linux native build + Windows cross-compile both work
diff --git a/src/layout_editor.c b/src/layout_editor.c
index e2d7aee..0a40c59 100644
--- a/src/layout_editor.c
+++ b/src/layout_editor.c
@@ -71,7 +71,7 @@ void layout_editor_draw(const char *exePath, UILayout *layout)
}
}
- /* 5: Smart play button (HOLD) */
+ /* 5: Smart play button (Play) */
{
Rectangle r = { layout->smartPlayX,
layout->smartPlayY, 200.0f, 80.0f };
@@ -250,7 +250,7 @@ void layout_editor_draw(const char *exePath, UILayout *layout)
Color f = (dragIndex == 5) ? highlightColor : fillColor;
Rectangle r = { layout->smartPlayX,
layout->smartPlayY, 200.0f, 80.0f };
- draw_label("HOLD", r, f, borderColor);
+ draw_label("Play", r, f, borderColor);
}
/* --- 6: Section nav buttons --- */
diff --git a/src/main.c b/src/main.c
index c545442..49d1ab2 100644
--- a/src/main.c
+++ b/src/main.c
@@ -667,14 +667,14 @@ static void update_frame(void)
if (portion > total) portion = total;
char portionBuf[32];
snprintf(portionBuf, sizeof(portionBuf), "%d/%d", portion, total);
- float portionY = layout.secNavY - szSmall / 2.0f;
+ float secBtnRadius = 35.0f;
+ float portionY = layout.secNavY - szSmall - secBtnRadius - 10.0f;
float portionSpacing = szSmall * 0.03f;
Vector2 portionSize = MeasureTextEx(fontSmall, portionBuf, szSmall, portionSpacing);
float portionX = layout.secNavX - portionSize.x / 2.0f;
DrawTextEx(fontSmall, portionBuf, (Vector2){ portionX, portionY }, szSmall, portionSpacing, mutedColor);
/* Section nav buttons */
- float secBtnRadius = 35.0f;
float secPrevX = layout.secNavX - 65.0f;
float secNextX = layout.secNavX + 65.0f;
float secBtnY_draw = layout.secNavY;
@@ -701,10 +701,10 @@ static void update_frame(void)
DrawRectangleRounded(smartBtn, 0.3f, 8, btnFill);
DrawRectangleRoundedLines(smartBtn, 0.3f, 8, btnBorder);
float btnSpacing = szSmall * 0.03f;
- Vector2 btnSize = MeasureTextEx(fontSmall, "HOLD", szSmall, btnSpacing);
+ Vector2 btnSize = MeasureTextEx(fontSmall, "Play", szSmall, btnSpacing);
float tx = smartBtn.x + (smartBtn.width - btnSize.x) / 2.0f;
float ty = smartBtn.y + (smartBtn.height - szSmall) / 2.0f;
- DrawTextEx(fontSmall, "HOLD", (Vector2){ tx, ty }, szSmall, btnSpacing, btnTextColor);
+ DrawTextEx(fontSmall, "Play", (Vector2){ tx, ty }, szSmall, btnSpacing, btnTextColor);
}
}
else
diff --git a/study-player.cfg b/study-player.cfg
new file mode 100644
index 0000000..3d221cc
--- /dev/null
+++ b/study-player.cfg
@@ -0,0 +1,16 @@
+# Study Player layout config
+title_y=60.00
+title_x=960.00
+bar_y=460.00
+bar_height=50.00
+bar_width=1248.00
+btn_radius=55.00
+help_y=1000.00
+help_x=40.00
+status_x=960.00
+btn_y=715.00
+btn_center_x=960.00
+smart_play_y=868.00
+smart_play_x=1611.00
+sec_nav_y=745.00
+sec_nav_x=1713.00
diff --git a/tasks.md b/tasks.md
new file mode 100644
index 0000000..14c6e80
--- /dev/null
+++ b/tasks.md
@@ -0,0 +1,43 @@
+# tasks.md — live progress checklist
+
+> Updated by the orchestrator after each milestone. One line per completed wave.
+
+---
+
+## WAVE 0 — Orchestrator + build system agent
+
+- [ ] Orchestrator: write `src/types.h` (shared types, constants)
+- [ ] Orchestrator: pre-author `src/player.h` (audio playback contract)
+- [ ] Orchestrator: pre-author `src/study.h` (study mode contract)
+- [ ] Orchestrator: pre-author `src/ui.h` (rendering + input contract)
+- [ ] Orchestrator: write TASK prompts in `prompts/`
+- [ ] Build agent: update `Makefile` (Linux native + Windows cross-compile targets)
+- [ ] Build agent: update `.gitignore` (add `prompts/`, `reports/`)
+
+## WAVE 1 — All `.c` implementations (parallel)
+
+- [ ] Agent A: implement `src/player.c` from `player.h` contract
+- [ ] Agent B: implement `src/study.c` from `study.h` contract
+- [ ] Agent C: implement `src/ui.c` from `ui.h` contract
+- [ ] Agent D: implement `src/main.c` (composition root)
+
+## WAVE 2 — Integration fixes (if needed)
+
+- [ ] Fix any link errors or behavioral regressions
+
+## Post-milestone
+
+- [ ] `make clean && make -j$(nproc)` exits 0, zero warnings (Linux)
+- [ ] `make windows -j$(nproc)` exits 0, zero warnings (Windows cross-compile)
+- [ ] Functional test: play an MP3, test all keyboard shortcuts, study mode, UI
+- [ ] Commit: `refactor: split src/main.c into modular .h/.c files`
+
+---
+
+## Open items (future)
+
+- [ ] `bin/build` and `bin/build-web` scripts can be retired or simplified
+- [ ] Test coverage (unit tests for study module?)
+- [ ] Volume control
+- [ ] Playlist support
+- [ ] Config file / persistence
diff --git a/web/shell.html b/web/shell.html
new file mode 100644
index 0000000..199756b
--- /dev/null
+++ b/web/shell.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Study Player</title>
+<style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body {
+ background: #0d0d1a;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ color: #eaeaea;
+ }
+ #controls {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 12px;
+ align-items: center;
+ }
+ #controls button, #controls label {
+ background: #1a1a2e;
+ color: #eaeaea;
+ border: 2px solid #e94560;
+ border-radius: 6px;
+ padding: 8px 18px;
+ font-size: 15px;
+ cursor: pointer;
+ transition: background 0.15s;
+ }
+ #controls button:hover, #controls label:hover {
+ background: #e94560;
+ }
+ #controls input[type="file"] {
+ display: none;
+ }
+ #canvas-container {
+ position: relative;
+ display: inline-block;
+ border: 2px solid #333;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+ canvas.emscripten {
+ display: block;
+ }
+ #status {
+ margin-top: 10px;
+ font-size: 13px;
+ color: #8c8ca0;
+ }
+</style>
+</head>
+<body>
+ <div id="controls">
+ <label for="file-input">📂 Load MP3</label>
+ <input type="file" id="file-input" accept=".mp3">
+ <button id="fullscreen-btn">⛶ Fullscreen</button>
+ </div>
+ <div id="canvas-container">
+ <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
+ </div>
+ <div id="status"></div>
+
+ <script>
+ var Module = {
+ canvas: document.getElementById('canvas'),
+ onRuntimeInitialized: function() {
+ console.log('Emscripten runtime initialized');
+ document.getElementById('status').textContent = 'Ready \u2014 load an MP3 file';
+ }
+ };
+
+ document.getElementById('fullscreen-btn').addEventListener('click', function() {
+ var container = document.getElementById('canvas-container');
+ if (container.requestFullscreen) container.requestFullscreen();
+ else if (container.webkitRequestFullscreen) container.webkitRequestFullscreen();
+ else if (container.msRequestFullscreen) container.msRequestFullscreen();
+ });
+
+ document.getElementById('file-input').addEventListener('change', function(e) {
+ var file = e.target.files[0];
+ if (!file) return;
+ console.log('File selected:', file.name, 'size:', file.size);
+ document.getElementById('status').textContent = 'Loading ' + file.name + '...';
+ var reader = new FileReader();
+ reader.onload = function(ev) {
+ var data = new Uint8Array(ev.target.result);
+ var filename = '/tmp/' + file.name;
+ console.log('Writing to virtual FS:', filename, 'bytes:', data.length);
+ try { FS.unlink(filename); } catch(ex) {}
+ try { FS.mkdir('/tmp'); } catch(ex) {}
+ FS.writeFile(filename, data);
+ console.log('Calling load_file_web...');
+ Module.ccall('load_file_web', null, ['string'], [filename]);
+ console.log('load_file_web returned');
+ document.getElementById('status').textContent = file.name;
+ };
+ reader.readAsArrayBuffer(file);
+ });
+ </script>
+
+ {{{ SCRIPT }}}
+
+</body>
+</html>