Cosimo

Architecture
Login

Architecture

Cosimo is split into a small reusable library and a wxdragon desktop application. The library owns the data model, deck parsing, review storage, and scheduling rules. The desktop application owns native controls, persistence policy, dialogue flow, localisation, and the current single-deck session state.

The guiding design is to keep user-authored learning content easy to inspect and recover, while keeping review history and scheduling state in a database. The GUI should remain screen-reader friendly by using native widgets and standard keyboard interaction wherever possible.

Runtime Data

Cosimo normally works with five runtime file groups:

Deck files use stable numeric card ids. Those ids are the long-term identity for scheduling, so editing a prompt or response does not disconnect the card from its review history. Older decks without numeric ids are migrated on open: Cosimo assigns unused ids, migrates any legacy review-store keys, backs up the deck according to the configured policy, and then rewrites the deck. Manual additions should use === rather than user-chosen numeric ids. That keeps new-card identity allocation in the same code path that can avoid both active deck ids and dormant review-storage ids. Deck metadata may include next_card_id, a high-water mark written by Cosimo before operations such as adding cards or scrubbing dormant database rows. This keeps card ID allocation monotonic without renumbering active cards or keeping otherwise-deleted database rows solely as ID reservations. Deck metadata may also include sync deck_uuid and visibility fields. These are shareable deck facts: the deck UUID follows the deck across renames or copies, while visibility is the author's requested sharing preference. The sync server still enforces owner authentication and visibility access rules.

Prompts are intended to be unique user-authored keys. Normal add and edit paths reject duplicate prompts; hand-edited decks with duplicate prompts can still open with a warning so the user can repair them. Duplicate responses are valid deck content. They are constrained only by generated reverse cards: from one duplicated-response group, at most one card may have an active generated reverse direction, otherwise the generated reverse prompts would collide.

The asset directory is named from the deck stem. For example, Indonesian.cosimo-deck uses Indonesian/ beside the deck for optional review audio. Audio files are keyed by stable card ID and side, such as 1.prompt.opus and 1.response.opus; legacy .wav files with the same naming scheme are still accepted as a fallback. Generated reverse cards reuse the parent card's prompt and response assets with the sides swapped, because the generated reverse direction is study state rather than separate deck-authored content. Ordinary review, exam question prompts, and quiz questions use the same resolver; the ready list can preview the selected card by playing prompt then response audio; exam grading/review and quiz answer review can play prompt and response audio using the same side resolution. The Windows voice helper also owns audio-manifest.tsv in the asset directory. That manifest records generated-file text hashes, selected voice filters, and selected voice engines so audio generation can skip unchanged files. The helper writes the selected Opus/WAV output format. Manifest-tracked audio is Cosimo-managed and may be regenerated or converted; existing untracked .opus or legacy .wav side audio is treated as user-owned and blocks generation for that side. Deck metadata may store prompt_voice, response_voice, prompt_voice_engine, and response_voice_engine defaults because preferred side voices and engines are shareable authored deck information. It may also store audio_generation=disabled to mark a deck as manual audio; Cosimo still plays existing audio, but the GUI generation command is disabled and the helper refuses to generate. The manifest remains generated-state metadata and records the actual text, voice, and engine used for each generated file. The GUI's Tools -> Generate Audio item is only a non-blocking launcher for the helper: it starts the sibling voice executable for the current deck, shows a progress dialogue from machine-readable helper output, and redirects helper output to logs/. Tools -> Remove Inoperative Audio from Deck scans only Cosimo-recognised sidecar audio filenames and deletes files that cannot be played because their card ID no longer exists, or because a preferred format exists for the same card side. It also prunes matching audio-manifest.tsv entries. Ordinary deck-content backups and review-database backups do not include sidecar audio; users must use full-deck bundle backups or back up manual recordings themselves because sidecar audio is outside those backup paths. Full deck bundle export is a separate explicit path that writes the deck file and same-stem sidecar directory into a zip archive. The study-history variant adds the review database by first asking SQLite to write a backup snapshot, then adding that snapshot to the zip, avoiding direct reads from the open database file. Optional automatic full-deck backups use the same bundle path and include sidecar audio and the review database. Optional automatic-review cue tones are generated temporary WAV files and are not deck data.

Library Modules

The public library surface is declared in src/lib.rs.

These modules do not depend on wxdragon. They are the preferred place for pure logic and data-loss-sensitive behaviour because they can be tested without the GUI toolkit.

Unsafe Code

Cosimo keeps unsafe code at platform and FFI boundaries. There is no unsafe code in the core deck, scheduler, or review-store library modules. Current unsafe uses are:

Each unsafe block should carry a nearby SAFETY: comment describing the invariant being relied on. Future unsafe additions should either fit one of the above wrappers or be documented here with their ownership and lifetime rules.

Desktop Application Modules

The binary is organised around src/main.rs, which starts the wx application and wires XRC controls to Rust event handlers. Supporting modules keep most nontrivial logic out of the event handlers:

Gettext .po files in po/ are the editable source for catalogue-backed translations. build.rs compiles them into a Rust lookup table during the Cargo build, so the portable Windows package does not need gettext runtime libraries or separate .mo files.

XRC files in ui/ define the native window, menus, and dialogues. The Rust code localises labels after loading the XRC resources.

Application State

AppState in src/app_state.rs is the central mutable state for the GUI. It owns:

Event handlers borrow AppState, perform one user action, and then call the rendering and synchronisation helpers in src/ready_screen.rs to update controls from state. The ready-list stores study-item indices, not visible row indices, so filtering and sorting can change without losing which forward or reverse item is selected when that item is still visible. Actions that edit the deck map the selected study item back to its parent deck card.

Do not make ready-list selection changes perform deck-wide menu recomputation. Keyboard arrow navigation can fire selection events for every row movement, so selection-local handlers should update only selection-local controls such as Edit, Remove, and Audio. The Cards menu should be synchronised when the menu is opened, or after state changes that actually affect menu state. A past regression wired full Cards-menu state synchronisation through the ready-list selection path; on large decks this turned ordinary arrow navigation into a noticeably slow deck-wide operation.

Because AppState is held in Rc<RefCell<_>>, UI handlers must finish mutable borrows before matching on results, rendering, opening dialogues, or setting focus. A review deferral regression exposed this risk: matching directly on a borrowed mutation result kept the borrow alive while the handler continued, so the UI could be rendered from stale state and the review prompt failed to advance. The safe pattern is to store mutation results in a scoped local first, for example let result = { state.borrow_mut().action() };, then match on the result after the borrow has been dropped. A source-shape regression test guards the most common risky forms.

Review Flow

At startup, Cosimo resolves the portable default paths, migrates old config files if present, loads settings, creates the default deck if needed, opens the deck and review database, runs the database integrity check, migrates deck card ids if needed, and renders the ready screen.

The ready screen shows the current deck summary, ready-list filter, list of prompts, and card management controls. Normal Start asks the scheduler for the due queue using the configured minimum size, excluding cards that are suspended or currently deferred. Force Start randomises every card that is not suspended or deferred and marks the pass as unscheduled for statistics text.

During an ordinary review pass the session phase is either prompt or response. Prompt ratings can be skipped by using reveal directly. Once the response is visible, recall feedback records an event in SQLite, updates the card schedule, and advances the session. Cards rated Again are handled by the scheduler's learning or relearning step rather than by an immediate same-pass repeat. The workflow commands live in study_actions.rs; review_input.rs maps keyboard shortcuts to the same command vocabulary. When the pass finishes or is ended early, the statistics dialogue shows the recorded pass summary and Continue returns to the ready screen.

Ready-card management is separate from ordinary study progression. The ready list displays study items, but mutation commands in ready_card_actions.rs map the selected study item back to its parent deck card when editing, removing, flagging, suspending, deferring, or changing generated-reverse state. The same module owns the current-card flag path during review because that path needs an explicit focus-restoration target.

Exam mode is an assessment flow rather than a scheduler-writing review flow. Starting an exam asks for a question count or percentage of active cards, then samples cards from the active deck in random order. Suspended and currently deferred cards are excluded. While the modal exam flow is running, AppState marks the session active so timed ready-screen refresh and notification code do not interrupt it. The modal question dialogue collects typed answers without recording review events or updating schedules. Exact answers are accepted automatically. In the default mode, nonblank inexact answers are adjudicated with a native Yes/No prompt after all submitted exam answers have been collected. Blank submitted answers are different: the review flow still shows the prompt, "no answer entered", and the expected response for learning value, but the answer is counted incorrect without offering a correctness choice. If the setup dialogue's Exact checkbox is selected, non-exact answers are counted incorrect without manual grading and are shown in a read-only answer review flow with Next and Summary controls. Finishing the exam resets the session and shows a report-only summary; it does not write review history, review-pass rows, or card schedules.

Timed exams add a single shared timer context to the modal question loop. The context stores the exam start time and a TimedExamProgress value that tracks 10% progress-tone boundaries and final timeout. Each question dialogue installs a one-second wx timer while it is open. The timer updates the main status bar with remaining time, plays the short progress tone at each boundary, and ends the current question with a timeout result when the limit expires. If the answer field contains text at that moment, that text is submitted before the timeout notice is shown. The timeout notice deliberately focuses a read-only message and suppresses Enter/Space on that field, requiring the user to tab to Next before grading, exact-answer review, or summary continues.

Quiz mode is another report-only alternative study flow. It samples active cards at random, excludes suspended and currently deferred cards, and avoids including both directions of the same generated-reverse family when the requested quiz size permits it. Each question presents the prompt and four distinct response choices. Distractors are selected from other active-card responses with a biased mix of close and farther strings, then shuffled so the choice structure does not become a reliable guessing cue. Feedback is withheld until all questions have been submitted or the quiz is ended early; the review phase then shows the prompt, selected response, and correct response. Finishing a quiz resets the temporary active session and shows a report-only summary. It does not write review events, review-pass rows, or card schedules.

Resident Tray Lifecycle

Minimise-to-tray is implemented in src/resident_tray.rs with a TaskBarIcon stored separately from ReviewUi. The normal Windows minimise action iconises the frame first; a size handler and a short polling timer then convert that state into resident tray mode by installing the tray icon and hiding the frame. Keeping this conversion outside the ordinary render path avoids changing keyboard focus and ready-screen behaviour when the option is disabled.

The tray polling timer and the ready-screen refresh timer must not share the same wx event owner. wxdragon binds Timer::on_tick as a timer event on the owner rather than filtering by a specific timer id, so two timers on the same frame can both invoke each other's handlers. Cosimo keeps the tray polling timer on the frame and owns the one-minute ready refresh timer from the root panel. This prevents the 250 ms tray poll from causing repeated ready-screen renders.

The same rule applies to transient menu work. Confirmation dialogues for destructive menu commands are deferred by posting command events to the frame, not by creating short-lived frame timers. A one-shot frame timer can leave a timer binding behind and be retriggered by the tray polling timer, which makes the confirmation dialogue appear to relaunch continuously.

Yes/No confirmations use the platform-native wxMessageDialog. The native dialogue exposes its message text better to screen readers than a custom dialog with a read-only text control. The important safety rule is not the dialogue implementation, but where it is opened: destructive menu confirmations must be opened only from the posted frame command path described above.

Menu commands that open modal dialogues follow the same event-queue pattern: the menu item consumes the native menu event and posts an internal command to the frame. src/menu_dispatch.rs maps those command ids to typed commands, including recent-deck slots and tray restore/quit requests. It also owns the frame menu-event hook: known internal command ids are consumed and dispatched, while unknown menu ids are skipped for normal wx processing. The frame consumes the internal command and only then opens the dialogue. This avoids relaunching a dialogue when Escape, Enter, or a button closes it while the original menu event is still unwinding. Direct state-change menu commands are also consumed before they mutate state.

The tray icon has a deliberately conservative lifetime. wxWidgets can assert if a window-like object is destroyed while one of its own event handlers, popup menu handlers, or pushed event handlers is still active. In practice this has affected the tray context-menu Quit path on Windows. For that reason Cosimo distinguishes hiding the tray icon from destroying the tray object:

Future tray changes should preserve that staging. In particular, do not call TaskBarIcon::destroy() from tray mouse callbacks, tray menu callbacks, or the ordinary shutdown path unless the underlying wxdragon/wxWidgets lifetime behaviour has been retested on Windows. Do not reintroduce manual tray popup_menu() handling unless the automatic set_popup_menu() path has been retested and found unsuitable.

For Windows tray regressions, Cosimo has an opt-in trace hook. If COSIMO_TRAY_TRACE is set to a file path, tray minimise, restore, quit, and frame-close lifecycle events are appended to that file. The tools/windows-tray-repro-auto.bat and tools/windows-tray-repro-manual.bat scripts use this hook to run automated minimise/close cycles, or to watch a manual tray repro attempt and report whether cosimo.exe remains running after the user has attempted to quit. The batch files are the preferred entry points on Windows; the PowerShell script contains the lower-level automation.

Debug builds also write a lightweight lifecycle log to cosimo-debug.log in the current working directory. Set COSIMO_DEBUG_LOG to a file path to choose a different location. This log is intentionally broader than COSIMO_TRAY_TRACE: it records startup, frame-close, tray lifecycle, and Rust panic-hook events. Release builds do not write this log. When investigating a long-running shutdown crash on Windows, run target\debug\cosimo.exe, reproduce the issue, and keep cosimo-debug.log together with any Windows assertion or crash message.

Persistence Policy

Deck modifications are written immediately because they are user-authored content. Before replacing an existing deck file, Cosimo applies the configured deck backup policy and then uses durable replacement.

Review events and schedules are written through SQLite. If enabled, a retained review-database backup is refreshed when a review pass starts and the database has opened successfully.

Optional automatic full-deck backups run after a deck has opened successfully and its review database has passed integrity checks. They use the same bundle writer as the study-history export, including a SQLite snapshot of the review database, but write to one retained backup path per deck and index it in backups/automatic-deck-bundle-index.txt. The 24-hour freshness check is per deck path and does not affect manual timestamped bundle backups.

The backup-directory wipe command is deliberately separate from backup pruning: it removes every direct file or folder entry under the portable backups/ directory after a confirmation, without checking whether each entry was generated or indexed by Cosimo. The directory itself is kept.

Full-deck restore is a staged overwrite operation. If the bundle replaces the currently open review database, the UI first swaps the active store to an in-memory store so Windows can release the database file handle before the restore replaces it; the restored deck is then reopened through the ordinary deck-load path.

Review-database scrub is a ready-screen maintenance action. It derives the set of active standard and generated reverse datum keys from the current deck and card_reversals, backs up the database first when review-database backups are enabled, then deletes dormant rows that are no longer reachable from those active keys. The store runs SQLite VACUUM after the scrub transaction so the database file can reclaim freed pages.

Settings changed in the options dialogue, including sound notifications and minimise-to-tray behaviour, are saved when the user accepts the dialogue. Opening a deck saves last_open_deck and recent decks when they change. Lower-value ready-list view state is persisted on normal application exit and only when it differs from the parsed cosimo.ini, avoiding unnecessary disk writes.

Testing

The test suite covers both library logic and GUI-adjacent pure behaviour. The library tests exercise deck parsing, typed ratings, review storage, and scheduling. Binary tests cover localisation, config migration, backup behaviour, ready-list filtering and sorting, card state markers including deferral, sort persistence, shortcut/mnemonic rules, review-session flow, and source-level guards around fragile wx event bindings.

Many binary tests still live in src/main.rs because that file historically owned much more application logic. New pure rules should normally be tested in the module that owns them, and tests should move opportunistically when logic is extracted. Do not weaken or delete broad integration-style tests merely to make main.rs shorter; the coverage is part of the safety net for rationalisation and release-candidate cleanup.

The Fossil pre-commit hook runs:

cargo test --lib --no-default-features
cargo test --bin cosimo

Use those commands before committing behaviour changes. For quick GUI compile verification, use:

cargo check --bin cosimo

Scheduler conformance work has one extra optional check:

cargo test --lib --no-default-features --features fsrs-reference fsrs_reference_tests

That feature compiles the upstream fsrs crate only for comparison tests and local source inspection. It is deliberately outside the default runtime build and ordinary commit hook. The published crate's own full test suite currently depends on fixture files that are absent from the crates.io archive, so Cosimo's targeted conformance tests are the practical local reference check unless the upstream repository is cloned separately.

Release preparation should additionally check the voice helper, release-tool binary, offline package verification, and Clippy:

cargo test --bin voice
cargo check --no-default-features --features release-tool --bin cosimo-release
cargo package --locked --offline --allow-dirty
cargo clippy --all-targets --no-deps