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:
- A
.cosimo-deckfile stores editable prompts and responses. - A neighbouring
.reviews.sqlite3file stores review events and card schedules. - A same-stem deck asset directory may hold optional Opus or WAV review audio.
cosimo.inistores application settings, the last opened deck, recent decks, ready-list view state, and sync state such as the configured sync server URL, owner login token, optional user-given device name, and per-deck remembered remote revisions.- A profile lock beside
cosimo.iniprevents two GUI processes from using the same portable profile at the same time. The lock is OS-backed and is held for the process lifetime; it is not used by the sync-server binaries. backups/stores automatic deck-content backups, optional retained review-database backups, manual full-deck bundle backups, and optional retained automatic full-deck bundle backups.
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.
- src/model.rs defines cards, stable card keys, prompt guesses, recall feedback, and completed reviews. Rating constructors validate stored integer values before they become typed ratings.
- src/deck_format.rs parses and formats the plain-text deck format. It accepts legacy terminators and emits the current numbered-id format.
- src/sync.rs contains sync-adjacent value types that do not depend on networking: canonical UUID validation and generation, deck visibility values, local device-identity helpers, local per-deck remote state, SHA-256-verified bundle payloads, a local in-memory sync server for protocol tests, and the SQLite-backed durable sync server used by the development standalone and CGI entry points. The in-memory and SQLite servers model the intended access boundary: public/shared metadata is a separate type from owner metadata, so public listings cannot expose study-history hashes or owner-only device diagnostics by accident.
- src/sync_workflow.rs bridges sync protocol types and
real Cosimo files. It builds content-only
bundles for sharing-only upload or owner study-history bundles for owner
sync, uploads them through the HTTP client, restores downloaded
bundles through the existing staged restore path, and records per-deck local
client sync state in
cosimo.ini. The GUI surface is deliberately small: deck metadata editing sets visibility, Options sets the sync server URL and local device name, and the Deck > Sync submenu handles OAuth login/logout, status, remote lists, and owner study-history upload/download. Upload conflicts are intentionally non-destructive: a client with no remembered remote revision must download first before it can upload over an existing remote deck. - src/sync_http.rs contains the sync HTTP request routing shared by the standalone server and CGI entry point. Its upload format keeps bundles opaque: headers describe optional content and study-history lengths/SHA-256 hashes, and the body stores any supplied byte ranges sequentially. The routing layer validates declared hashes before constructing sync payloads.
- src/sync_http_client.rs contains the first sync
HTTP client transport. It uses a std-only socket path for loopback
development
http://URLs andreqwest/rustls for deployedhttps://sync URLs. Non-loopback plain HTTP is rejected. The client reuses the typed sync model for uploads, downloads, listings, revision conflicts, and privacy-bound metadata. - src/bin/cosimo-sync-server.rs is the
standalone TCP server for local testing and reverse-proxy deployments. By
default it uses in-memory storage for protocol tests and temporary local
experiments; with
--storage-rootit uses the durable SQLite/file storage in src/sync.rs. - src/bin/cosimo-sync-cgi.rs is the CGI entry
point over the same sync HTTP and durable storage code. It reads CGI
environment variables, maps
HTTP_AUTHORIZATIONto the sync bearer-token header, and writes CGI-style responses without relying on long-lived process memory. The durable sync server stores bearer-token verifier hashes in SQLite rather than the client bearer tokens themselves. - src/review_store.rs wraps SQLite review storage. It performs integrity checks, schema setup, event insertion, schedule loading, key migration, review-pass recording, reversal persistence, sparse card-state persistence for suspension, flagging, and deferral, reverse-batch undo metadata, first-seen deck-card tracking for dashboard analytics, legacy non-reversible-marker migration, higher-level deck consistency checks, and one-copy database backups.
- src/scheduler.rs contains FSRS-6 card scheduling and review-queue selection. Review database migration replays stored review events to rewrite derived schedule rows with the current scheduler, and ordinary review writes also replay the card's stored history before applying the new answer. The legacy scheduler marker can still be read from old storage, but the old scheduler is no longer executable calculation code. The scheduler calculation is isolated behind explicit engine and settings objects, so the GUI and review store do not depend on the scheduling formula. The FSRS-6 engine has deterministic reference learning/relearning step behaviour and deterministic interval fuzzing for review-state intervals, seeded from stable card/review data so replays and tests are reproducible. Incoming stored FSRS-6 stability and difficulty are clamped to reference bounds before each update, which keeps old or manually edited schedule rows from feeding invalid memory values into the calculation. Review-database migration replays stored review events to rewrite derived schedule rows with FSRS-6, and new review-database writes use the same replay path for the selected card before applying the new answer. Due reviewed cards are prioritised by due time, equal priorities are randomised, new cards come before minimum-fill cards, and fill cards are randomised. Estimated-recall diagnostics are also centralised here; short learning/relearning steps are capped for diagnostics, but fuzzed review intervals do not cap review-state recall estimates.
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:
- src/file_ops.rs calls Windows
MoveFileExWfor durable replacement and wxdragon's browser-launch FFI for local documentation. Call sites build NUL-terminated path or URL buffers, keep them alive for the call, and check the returned success value. - src/app_config.rs calls
GetUserDefaultLocaleNameon Windows. The buffer length passed to the API matches the writable UTF-16 buffer, and the returned length is validated before conversion. - src/sync.rs calls Windows
BCryptGenRandomwhen generating UUIDs on Windows. The buffer length passed to the API matches the writable byte buffer, and the return status is checked before the bytes are used. - src/review_audio.rs calls Windows MCI through
mciSendStringWfor cancellable WAV fallback playback, and uses Rodio plus Symphonia/libopus for Opus playback. MCI commands and optional status buffers are NUL-terminated UTF-16 or writable UTF-16 storage that live for the call. The worker starts playback asynchronously, polls MCI status for real completion on the WAV path, and stops the active MCI alias or Rodio sink when playback is cancelled. - src/bin/voice.rs calls libopus through
opusic-syswhen encoding generated speech to Ogg Opus. The raw encoder pointer is confined to a small RAII wrapper, every libopus result code is checked, encoded packets are copied into owned Rust buffers, and the Ogg container writer is pure Rust. - src/bin/voice/sapi.rs owns the largest unsafe
surface: a small COM/SAPI
IDispatchwrapper used byvoice.exe. The wrapper confines raw COM pointers toComDispatch, pairsCoInitializeExwithCoUninitialize, checks HRESULTs, wraps ownedVARIANTs in RAII types, type-checks VARIANT tags before reading union members, balancesAddRef/Release, and frees BSTRs withVariantClearor explicit EXCEPINFO cleanup. The parentvoicebinary calls this module through safeSapiSynthesizerand voice-listing methods only.
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:
- src/app_config.rs reads, writes, and migrates
cosimo.ini; detects the system language; and stores recent decks and ready-list view preferences. It also parses and preserves the sync sections containing the configured sync server URL, local owner/token state, optional device name, and per-deck remote revision/hash state. - src/app_state.rs owns the loaded deck, review database, active review session, ready-list view state, and state mutations behind deck editing, scheduling, card states, reversal, database maintenance, and learning-dashboard data.
- src/deck_data.rs resolves default deck paths, derives review-database paths, and handles card-id enumeration.
- src/deck_bundle.rs writes compressed full-deck bundles
containing the deck file and same-stem sidecar directory while excluding the
review database by default. The explicit study-history export adds the review
database through a SQLite snapshot. Automatic full-deck bundle backups are
indexed separately from numbered text-only deck backups. Restore validates
the zip layout, extracts into a temporary staging directory, checks the deck
and optional review database, writes a compressed backup of an existing target
deck, then durably replaces the staged files into
decks/. If restore would overwrite sidecar files but no target deck exists to back up, it fails before replacing anything. Deck sync also uses this module as the privacy boundary for deriving a content-only bundle from a study-history bundle, omitting the review database through the same validated zip-entry model. - src/file_ops.rs handles durable file replacement, portable paths, deck backups, review-database backups, and opening local HTML docs.
- src/review_session.rs models the active review pass, including prompt/response phase and per-pass statistics.
- src/study_actions.rs owns the GUI-facing ordinary
study workflow: scheduled, forced, and flagged study entry; reveal; rating
activation; feedback; current-card deferral; pass ending; pending-warning
display; and summary continuation. Keeping these commands outside
main.rsmakes the wx event layer mostly dispatch into named workflows. - src/ready_card_actions.rs owns ready-card mutation workflows, selected-card details, add and batch-add, selected-card edit/remove, revealed-card edit with response-phase focus restoration, current-card flagging, duplicate prompt resolution, and bulk shown-card flag, suspension, and reversal actions.
- src/deck_properties_actions.rs owns deck metadata display and editing, including sync UUID replacement cleanup, render/focus refresh after save, and dynamic menu refresh.
- src/exam_mode.rs models exam setup, typed prompt/answer collection, exact-match checking, end-of-exam grading, and report-only exam summaries.
- src/quiz_mode.rs models quiz sizing, active-card sampling, family-aware question selection, multiple-choice distractor generation, and report-only quiz summaries.
- src/study_item.rs expands deck cards into study directions. Forward cards use the deck prompt and response; generated reverse cards swap them and use a child datum key so their scheduling and review history stay separate.
- src/ready_list.rs computes ready-list filtering, duplicate prompt/response detection, sorting, labels, counts, selection normalisation, and minimal list updates.
- src/ready_screen.rs renders and synchronises the main ready/review screen: schedule previews, status-bar text, ready-list labels, filter and sort controls, dynamic card-menu state, focus restoration after ready-screen changes, timed and F5 refresh handling, and due-notification baseline updates. It now uses targeted render models for phase-dependent status text, card-panel content, ready summary, phase chrome, review action controls, and ready controls. The remaining direct wx work is intentional: list replacement, focus movement, and audio-player sync are tied to native widget behaviour and should be changed only with manual accessibility review.
- src/review_dialogs.rs, src/exam_dialog.rs, src/quiz_dialog.rs, src/options_dialog.rs, and src/dialog_support.rs isolate dialogue construction and modal behaviour.
- src/menu_model.rs centralises menu labels, mnemonics, shortcuts, and recent-deck menu layout.
- src/menu_ui.rs applies dynamic wx menu updates such as current labels and recent-deck menu insertion.
- src/menu_dispatch.rs owns the internal command ids
used to defer modal menu actions and tray menu requests back to the frame
event queue, plus the frame
on_menuregistration that turns command ids into typed dispatch decisions. - src/sync_runtime.rs, src/sync_menu.rs,
src/sync_actions.rs,
src/sync_status_model.rs, and
src/sync_remote_decks_model.rs keep sync
policy, dynamic menu state, GUI workflows, and user-facing status/list text
out of
main.rs. The remainingmain.rssync wrappers exist only where they adapt a sync action to the local deck-loading callback. - src/help_actions.rs handles Help menu actions that launch local documentation or project web pages and report browser-launch failures.
- src/keyboard.rs contains key predicates used by handlers.
- src/review_input.rs owns review-pass keyboard binding and the key-to-command dispatch policy for prompt and response phases.
- src/review_audio.rs resolves sidecar audio files for the
current study side, preferring Opus and falling back to WAV, and owns audio
play/stop state. Queued ready-list previews
use a worker thread that plays each file synchronously, so prompt-to-response
sequencing depends on playback completion rather than estimated durations. On
Windows this is implemented with MCI status polling so
Ctrl+Pcan stop the current file as well as the remaining queue, without involving wx media controls or the GUI thread; other platforms usewxSound. Rendering calls the player after each ordinary review-screen update so automatic audio follows prompt/response changes. If guiding tones are enabled, the player queues generated prompt, response, or summary cue WAV files before the relevant audio or summary. These cue WAVs are cached under versioned filenames in the operating-system temporary directory and are recreated if missing; they are not deck assets and do not need backup. Review, exam-ending, and quiz-ending paths stop audio before leaving the current prompt/response context. Exam grading/review and quiz review dialogues use short-lived player instances, so their automatic orCtrl+Pplayback is stopped when the modal dialogue closes. - src/audio_generation.rs plans and launches the
sibling
voicehelper from the GUI. It redirects stdout and stderr to a timestamped report file and reads machine-readable progress lines from stdout to drive the progress dialogue and final generated/skipped count summary. - src/audio_cleanup.rs plans and executes the GUI
sidecar cleanup command. It treats
.opusas preferred over.wav, ignores non-Cosimo filenames, deletes obsolete or shadowed files only after user confirmation, and removes matching manifest rows. - src/bin/voice.rs is a standalone Windows helper that uses
OneCore and SAPI voices to generate Opus or WAV files in the same sidecar
layout.
Bare deck names use the same portable-root rule as the main application:
decks/beside the helper executable, not the shell's current directory. It maintains the sidecaraudio-manifest.tsv, preserves existing untracked.opusand.wavside audio, encodes speech output to Ogg Opus when Opus is selected, and isolates the Windows SAPI COM boundary in src/bin/voice/sapi.rs. - src/resident_tray.rs owns the minimise-to-system-tray icon, tray popup menu, tray polling timer, restore/quit command ids, and tray trace hook.
- src/card_details_model.rs formats the per-card details report.
- src/learning_dashboard_model.rs computes deck analytics, including separate standard, reverse, and total summaries when generated reverse cards exist.
- src/time_format.rs formats timestamps and speech-friendly durations.
- src/runtime.rs provides current time and deterministic shuffle helpers.
- src/localization.rs contains locale parsing, language
selection, placeholder formatting, and the
LocalizerAPI used by the rest of the application. Labels, message templates, and plural nouns are loaded through a generated gettext-style catalogue.
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:
- the loaded deck path and review database path
- the loaded
Datumlist - the open
SqliteReviewStore - the current
ReviewSession - selected ready-list study item and ready-list filter/sort settings
- effective settings and localised status text
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:
- Restoring from the tray removes the visible icon and shows the frame again,
but keeps the
TaskBarIconobject available for later cleanup. - The tray context menu is installed with
TaskBarIcon::set_popup_menu()and the menu object is retained inResidentTray. This follows wxdragon's automatic taskbar-menu path instead of manually callingTaskBarIcon::popup_menu()from a right-click callback. wxdragon copies the retained menu when wxWidgets requests the tray menu, so the retained menu acts as the template for the current language. - Tray context-menu Restore and Quit do not run directly from the menu event. Cosimo posts the requested command to the frame, so the frame handles it on the next main-loop turn after the popup has unwound.
- Quitting from the tray context menu then removes the visible icon and asks the frame to close.
- Ordinary frame close stops and drops the ready-refresh and tray-minimise
timers and removes the visible tray icon if one has existed. It deliberately
does not call
TaskBarIcon::destroy()on the retained tray object; Windows testing showed tray destruction to be the fragile path. If a tray object had existed, Cosimo exits the process directly after saving view state and removing the visible icon, leaving the retained tray object to process teardown.
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