Files
lightcraft/docs/background-tasks.md
T
Nathaniel LedfordandClaude Opus 5.5 d2523e8544 Preview cache: cached thumbnails are no longer taken for the person's photos
The thumbnail cache wrote .jpg files into the library's thumbs folder, which is usually inside
Pictures: photo galleries listed every thumbnail as a photo (issues #610, #1066), and every read
brought the file's date forward, so each launch made them all look new.

- Cache files are named <32 hex>.lcthumb (JPEG data as before). A .jpg of before is taken over
  under the new name when it is asked for; the rest when the cache is tidied, which the engine does
  once per cache folder, on a thread of its own with a row in the activity stack that shows its
  progress and can stop it. Nothing is rendered again.
- Reading writes nothing: what was read is remembered for the session (a full cache lets it go
  last), and only a file that went unused for a week has its date brought forward.
- A folder that holds nothing but the cache gets a CACHEDIR.TAG, when the cache makes it and when
  it is tidied; one that also holds somebody's files is not tagged (issue #98).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:26:17 -04:00

4.5 KiB

Long-running tasks and the activity stack

Everything that takes a while runs off the UI thread and shows up in one place: the activity stack in the top-left corner under the top bar (issue #345). Each task has a row with its name, a progress bar (a sweep while the amount of work isn't known), the count and what it's working on, and ✕ if it can stop. Quitting asks first while a task that can stop is running. Agents see the same list through activity.list / activity.cancel, and ui.inspect reports it as activity.

The registry is lightcraft_engine::activity (crates/engine/src/activity.rs, Session::activity). The stack is crates/ui-egui/src/panels/activity.rs. Every job that can last more than a moment uses it; don't add a progress window, panel or progress toast of your own. A dialog may still show progress inline (the Synchronize Folder dialog counts files while it scans), and a toast at the end saying what happened is fine. A view saying its pixels aren't final isn't progress either: the loupe's "Rendering…" pill (loupe:rendering) marks a stand-in or an out-of-date render on the photo itself, and the render behind it holds no row (a slider drag replaces it many times a second).

Adding a task

use lightcraft_engine::activity::{Cancel, Unit};

// when the job starts, with the cancel flag the worker already checks between files
let guard = app.session.activity.start("export", "Exporting", Cancel::Flag(cancel.clone()));
guard.progress(0, total as u64);

// every frame (or from the worker, through `guard.handle()`)
guard.progress(done as u64, total as u64);
guard.detail(&file_name);
  • Keep the guard for exactly as long as the job runs. Put it in the struct the UI polls (ExportTask, ScanTask, SyncRun…), or move it into the worker thread. Dropping it removes the row, also when the worker panics, so a dead job never leaves a stuck bar. A worker that only reports progress takes a TaskHandle (guard.handle(), Clone + Send).
  • kind is a stable camelCase id that agents filter by (export, import, scan, sync, previews, download, faces, cache…). label is a short English name in sentence case ("Building previews"). The stack shows it translated, so add it to every catalog in crates/ui-egui/locales/.
  • Progress: progress(done, total). A total of 0 means the amount of work isn't known yet, and the bar sweeps. set_unit(Unit::Bytes) makes the count read "12 of 340 MB"; Unit::Percent reads "40 %". The default Unit::Count reads "3 of 25".
  • Detail: what the job is on (a file name, a model's name). Detail set by engine code is a name, never a sentence. UI code may set translated text.
  • Cancel:
    • Cancel::Flag(Arc<AtomicBool>) adopts the flag the job already checks. The row's ✕, activity.cancel and the job's own Cancel then all set the same atomic.
    • Cancel::Yes: the job polls guard.is_cancelled() itself.
    • Cancel::No: the job can't stop; it gets no ✕ and doesn't make Quit ask.
    • set_cancellable(false) for a phase that has to finish once started (a Lightroom import adding its photos).
  • Quiet periodic work shows no row: the auto-import folder listing runs every few seconds through tasks::spawn(.., None, ..).

Examples in the code

Job Where Shows how to
Export, contact sheet crates/ui-egui/src/export_task.rs adopt the job's flag; update the row each frame
Build Previews crates/engine/src/cmd/previews.rs move the guard into the worker thread
Model downloads crates/engine/src/face_download.rs report progress through a TaskHandle in shared state
Lightroom catalog import crates/ui-egui/src/lightroom_import.rs stop being cancellable for a phase that must finish
Face scan, AI Denoise crates/engine/src/cmd/face_recognize.rs, crates/engine/src/denoise.rs a continuous job with no ✕, kept in session state
Find Missing Photos crates/ui-egui/src/tasks.rs a generic background task with a row (kind: Some(..))

Testing

Assert the row (session.activity.list(): kind, label, total, cancellable), that activity.cancel(id) stops the job, and that the row is gone once the job ends. crates/ui-egui/src/tests_activity.rs has the helpers, and the job-specific tests show the pattern (tests_sync.rs, export_task.rs, tests_denoise.rs…). A row only appears on screen once its task is 0.5 s old, so tests that click its ✕ wait for activity:cancel:<id> first.