Core and desktop comments describe the code to a stranger (#6224)
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 1m3s
CI & Build / integration (push) Successful in 1m37s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m14s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m3s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m42s
Android / Build the server image (push) Successful in 1s

Milestone 546 "Bring the repo to the comment standard (rule 32) and
compose defaults (rule 198)", step #6224 "Comment standard sweep: Rust
core and desktop client".

Doc comments and comments across inkwell-core, the Tauri app and
desktop/packaging now say what the code does and the non-obvious why in
the present tense. Gone: milestone tags (M10.x, M11.3, M13, M304, M315,
M314, milestone 325), issue and Scribe-note numbers, family-idea and
family-rule citations, the run numbers and commit hashes behind past
bugs, and the narration of what used to be (the stable channel before it
held its own bundles, CARGO_PKG_VERSION as the shown version, the
MANIFEST_TAG split, the store's missing tests, the old PKGBUILD). The
schema's migration notes keep what each step does and why, without
"until now". The forge is called the forge rather than Forgejo.

Hazards stay as facts: the dev-rolling tag that must not shadow the
branch, the manifest matched on this build's version, the APK's fixed
name surviving the prune, the LIKE guard in v9, -wal/-shm moving with
the renamed store, panic = abort ending the app.

34 files, comments only: a comment-stripping diff of every changed file
against HEAD is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:30:37 -04:00
co-authored by Claude Opus 5.5
parent 0a631219ee
commit 01b53118b9
34 changed files with 300 additions and 384 deletions
+3 -7
View File
@@ -1,10 +1,6 @@
//! The desktop syncs on its own (#5167).
//!
//! Until this existed, the only thing that ever called the sync engine was the
//! "Sync now" button on the Sync screen. A linked desktop sat on its edits until
//! someone went looking for that button, and never heard about a note written on
//! the phone. Android has done better from the start (a periodic WorkManager job,
//! plus a flush when the app leaves the foreground); this is the desktop's version.
//! The desktop syncs on its own, so a linked desktop sends its edits and hears about
//! notes written elsewhere without anyone pressing "Sync now". Android does the same
//! with a periodic WorkManager job and a flush when the app leaves the foreground.
//!
//! ## When a cycle runs
//!
+2 -2
View File
@@ -1,7 +1,7 @@
//! Tauri command surface for the local store — the offline mirror of the REST API
//! the frontend's repository seam calls. Each command locks the shared connection and
//! delegates to `store`; errors are stringified for the JS boundary. adapters/local.ts
//! (M10.5) invokes these.
//! invokes these.
use serde_json::Value;
use tauri::State;
@@ -109,7 +109,7 @@ pub fn notes_update_item(
store::update_item(&conn, &id, &item_id, &changes).map_err(|e| e.to_string())
}
/// Attach a file to a note, online or not (#5168).
/// Attach a file to a note, online or not.
///
/// The file's bytes are the raw IPC body — a JSON number array would be several
/// times the file's size — and its note, name and type ride in headers. Header values
+4 -4
View File
@@ -1,7 +1,7 @@
//! Tauri commands for pairing with a server (M10.7a).
//! Tauri commands for pairing with a server.
//!
//! Linking is opt-in and reversible; the app is fully usable having never touched
//! any of this. The Settings UI (M10.7e) drives these.
//! any of this. The Sync screen drives these.
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, State};
@@ -167,7 +167,7 @@ pub fn sync_has_pending(db: State<'_, Db>) -> Result<bool, String> {
push::has_pending(&conn).map_err(|e| e.to_string())
}
// --- sharing (#5175) -----------------------------------------------------------
// --- sharing -------------------------------------------------------------------
//
// The Share dialog's calls, straight to the linked server. Unlinked, each answers
// `sharing::NEEDS_SERVER`, which the dialog shows as it is.
@@ -184,7 +184,7 @@ pub async fn shares_list(note_id: String, db: State<'_, Db>) -> Result<Vec<NoteS
sharing::list(&db, &url, &token, &note_id).await
}
/// Share with a person (`user_id`) or a group (`group_id`, #5177): exactly one.
/// Share with a person (`user_id`) or a group (`group_id`): exactly one.
#[tauri::command]
pub async fn shares_share(
note_id: String,
+3 -3
View File
@@ -1,6 +1,6 @@
//! One-time move of a ThoughtSync install's data into Inkwell's.
//!
//! The app was renamed ThoughtSync → Inkwell (Scribe note 5071), and the Tauri
//! The app was renamed ThoughtSync → Inkwell, and the Tauri
//! identifier with it. The app-data dir is named after the identifier, so the
//! renamed app looks in `com.fabledsword.inkwell` and would start EMPTY beside the
//! old `com.fabledsword.thoughtsync`, which still holds every note. Local-first means
@@ -12,7 +12,7 @@
//! hold committed writes not yet checkpointed into the main file, and a database
//! renamed without them silently loses them.
//!
//! A removable shim, deliberately (family rule 22): delete this module and its one
//! A removable shim, deliberately: delete this module and its one
//! call in `lib.rs` once no install is still on the old identifier.
use std::fs;
@@ -42,7 +42,7 @@ pub enum Outcome {
///
/// Entry by entry rather than renaming the directory, because `new_dir` can already
/// exist without a store: the installer writes its channel marker there before the
/// app first runs (update.rs, issue 2183), and the webview may create its own files
/// app first runs (update.rs), and the webview may create its own files
/// before setup. Anything already in `new_dir` wins; the old copy of it is left in
/// place, and so is `old_dir` itself unless it ends up empty.
pub fn adopt_legacy_data(old_dir: &Path, new_dir: &Path) -> io::Result<Outcome> {
+1 -1
View File
@@ -94,7 +94,7 @@ pub fn integrate_desktop() -> Result<IntegrationStatus, String> {
// StartupWMClass is the exception: it must be the BINARY name, because GTK
// derives the window's WM_CLASS from the executable and the desktop matches the
// two to group the taskbar icon. Not the product name, and not this AppImage's
// filename — AppRun execs `usr/bin/inkwell` inside it (issue 2075).
// filename — AppRun execs `usr/bin/inkwell` inside it.
let entry = desktop_entry_path().ok_or("Cannot resolve the applications dir.")?;
if let Some(parent) = entry.parent() {
fs::create_dir_all(parent).map_err(|e| format!("create applications dir: {e}"))?;
+19 -23
View File
@@ -1,7 +1,7 @@
//! Inkwell desktop (Tauri v2).
//!
//! The window loads the shared Vue 3 frontend (`../../frontend`), which reaches the
//! store and sync engine through the `frontend/src/adapters/` seam (M10.3) over Tauri
//! store and sync engine through the `frontend/src/adapters/` seam over Tauri
//! `invoke`.
//!
//! This crate is the DESKTOP WRAPPER, not the core. The on-device SQLite store and
@@ -20,26 +20,23 @@ mod update;
/// The build a PERSON reads, baked in by the desktop lane at compile time.
///
/// Lives at the crate root because it has two readers — `config_get`, which puts it
/// in the UI, and `log_environment`, which puts it in the log — and this repo has
/// spent several issues on one fact held in two places (2181, 2182, 2183).
/// in the UI, and `log_environment`, which puts it in the log — and one fact belongs
/// in one place.
///
/// `option_env!`, not `env!`: a local `cargo tauri build` sets nothing, and this has
/// to keep compiling. `None` becomes "unknown" at each call site rather than a
/// plausible-looking default — note 3127 §5 makes this string the only answer to
/// "which build is this?" now that there are no version tags, so there is nothing
/// left to contradict it if it lies. An honest "I cannot say" is the only safe wrong
/// answer.
/// plausible-looking default: with no version tags, this string is the only answer to
/// "which build is this?", so an honest "I cannot say" is the only safe wrong answer.
///
/// NOT `CARGO_PKG_VERSION`, which both readers used to use, and which was wrong on
/// every build ever shipped: `cargo tauri build --config '{"version": ...}'`
/// overrides `tauri.conf.json`, not Cargo's own metadata, so the literal `0.2.0` in
/// Cargo.toml is what reached the UI and the log regardless of what was built.
/// NOT `CARGO_PKG_VERSION`: `cargo tauri build --config '{"version": ...}'` overrides
/// `tauri.conf.json`, not Cargo's own metadata, so Cargo.toml's literal would reach
/// the UI and the log regardless of what was built.
///
/// NOT the ordering key either. That value — `1.0.<minutes>`, which the override
/// above does set — is the opaque value Tauri's updater compares; it lands in bundle
/// filenames and `latest.json` and must never be shown to a person (#3144). Two
/// values, two audiences. `update.rs` deliberately still reads the key, through
/// `app.package_info().version`, because a comparator is exactly what it is.
/// filenames and `latest.json` and must never be shown to a person. Two values, two
/// audiences. `update.rs` reads the key, through `app.package_info().version`,
/// because a comparator is exactly what it is.
const DISPLAY_VERSION: Option<&str> = option_env!("INKWELL_DISPLAY_VERSION");
/// The baked build, or the honest "I cannot say". The only way in — the const is
@@ -49,9 +46,8 @@ pub(crate) fn display_version() -> &'static str {
}
// The store and the sync engine live in the shared `inkwell-core` crate, which
// the Android client binds through uniffi (Scribe note 2730). Aliased to their old
// names so every call site below reads exactly as it did when they were modules of
// this crate — the extraction changed where they live, not what they are.
// the Android client binds through uniffi. Aliased to short names so the call
// sites below read as if they were modules of this crate.
use inkwell_core::{local, sync};
/// The app's own window, as `tauri.conf.json` names it. The capture window is the
@@ -91,7 +87,7 @@ pub fn run() {
])
.build(),
)
// In-app updates (M10.9). Registering the plugin is inert on its own — it
// In-app updates. Registering the plugin is inert on its own — it
// reads its config only when `update_check`/`update_install` ask it to, so a
// build without a signing key still starts normally and simply reports that
// updates aren't configured.
@@ -102,8 +98,8 @@ pub fn run() {
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
// System notifications for due reminders, raised by the `reminders` worker.
.plugin(tauri_plugin_notification::init())
// Attachment bytes are served to the webview from the local blob store
// (M10.7f). Registered on the BUILDER because a scheme has to exist before
// Attachment bytes are served to the webview from the local blob store.
// Registered on the BUILDER because a scheme has to exist before
// the webview is created; the directory it reads from arrives later, in
// `setup`, via `blobs::publish_root`.
.register_uri_scheme_protocol(sync::blobs::BLOB_SCHEME, |_ctx, request| {
@@ -143,9 +139,9 @@ pub fn run() {
let db = local::open(&db_path)?;
log::info!("local store ready — {}", local::summary(&db));
// Before anything can ask what channel we're on: the installer left a note
// in this directory saying which one the user picked (issue 2183).
// in this directory saying which one the user picked.
update::adopt_installer_channel(&db, &dir);
// And which server, when it installed from one (milestone 325 step 6).
// And which server, when it installed from one.
update::adopt_installer_server(&db, &dir);
sweep_local_trash(&db);
// Before the store is handed to the app: `restore` needs to read the
@@ -153,7 +149,7 @@ pub fn run() {
capture::restore(app.handle(), &db);
app.manage(db);
// Attachment bytes live beside the database, filed by content hash, so a
// synced image is readable with no network (M10.7d).
// synced image is readable with no network.
let blobs = sync::blobs::BlobStore::new(dir.join(local::BLOBS_DIR))?;
log::info!("attachment store ready: {}", blobs.root().display());
// Hand the directory to the URI-scheme handler registered below, which
+5 -5
View File
@@ -1,10 +1,10 @@
//! Reminders reach you when the window isn't in front (#5171).
//! Reminders reach you when the window isn't in front.
//!
//! The shared frontend's reminder check is a 45-second timer in the page, which
//! raises a toast and, if allowed, a page `Notification`. Inside this app that was
//! not enough. The page's timer only does anything while the webview is running
//! it, and WebKitGTK's page notifications go nowhere. So a reminder that came due
//! while Inkwell sat minimised or behind another window was never announced.
//! raises a toast and, if allowed, a page `Notification`. Inside this app the page's
//! timer runs only while the webview is running it, and WebKitGTK's page
//! notifications go nowhere, so on its own it misses a reminder that comes due while
//! Inkwell sits minimised or behind another window.
//!
//! This worker owns announcing on the desktop instead. Every [`TICK`] it asks the
//! local store which reminders have come due, and announces each occurrence once:
+24 -31
View File
@@ -1,22 +1,17 @@
//! In-app updates (M10.9).
//! In-app updates.
//!
//! Two channels, because two audiences: `stable` follows every merge to `main`,
//! `dev` follows every green push to `dev`. Each reads a `latest.json` published as
//! an asset on a release whose TAG NEVER CHANGES — verified necessary, because
//! Forgejo has no `/releases/latest/download/<asset>` route (it 404s), so "newest"
//! cannot be named in a URL. A fixed tag can.
//!
//! `stable` followed tagged `v*` releases until M314 step 3, and its manifest pointed
//! at bundles living on a different release. It holds its own bundles now, exactly as
//! `dev` always has — so a build reaches stable users with no tag cut anywhere, which
//! is the whole point of the change. NOTHING HERE MOVED: this code only ever read
//! `<channel>/latest.json`, and that is still where the manifest lands.
//! the forge has no `/releases/latest/download/<asset>` route (it 404s), so "newest"
//! cannot be named in a URL. A fixed tag can. Each channel's release holds its own
//! bundles, so a build reaches stable users with no tag cut anywhere.
//!
//! The feed is Fabled-Git by default, deliberately: this app is usable having never
//! linked a server, and an install that can't reach its own updates because it isn't
//! paired with anything would contradict the whole local-first premise.
//!
//! An Inkwell server can be the feed instead (milestone 325 step 6) — the one a
//! An Inkwell server can be the feed instead — the one a
//! self-hoster installed from, whose forge they may have no access to. That reads
//! alarming until you notice what does NOT move: the server hands out the same
//! signed AppImage, and the updater checks it against the public key baked into
@@ -24,8 +19,8 @@
//! can pass on official builds; it cannot substitute its own.
//!
//! Updates are signed. The public half is baked into `tauri.conf.json`; the private
//! half exists only as a CI secret, and is generated by the operator — a release
//! signing key that has passed through anyone else's hands is not a signing key.
//! half exists only as a CI secret — a release signing key that has passed through
//! other hands is not a signing key.
use std::path::Path;
@@ -107,10 +102,10 @@ impl Channel {
/// The release tag holding this channel's builds — not the channel's name.
///
/// `dev` used to be both, and a tag named `dev` shadowed the branch of the same
/// name, so `git push origin dev` failed as ambiguous (Scribe #2184). The channel
/// is still `dev` everywhere a person sees it and in the stored pref; only the tag
/// moved. Mirrors `packaging/channel-tag.sh`, which is what CI publishes with.
/// A tag named `dev` would shadow the branch of the same name and make
/// `git push origin dev` ambiguous, so the dev channel publishes on `dev-rolling`.
/// The channel is `dev` everywhere a person sees it and in the stored pref.
/// Mirrors `packaging/channel-tag.sh`, which is what CI publishes with.
fn release_tag(self) -> &'static str {
match self {
Channel::Stable => "stable",
@@ -127,8 +122,8 @@ impl Channel {
///
/// `read_source` is THE ONE READER. Three things hold an opinion about this — the
/// installer's marker files, this app's prefs, and `plugins.updater.endpoints` in
/// `tauri.conf.json` — and issues 2181–2183 were each one decision with several
/// holders and only one of them set. So: the markers only ever feed the prefs
/// `tauri.conf.json` — and one decision with several holders goes wrong as soon as
/// only one of them is set. So: the markers only ever feed the prefs
/// (`adopt_installer_*`), the prefs are what is read, and the config's endpoint is
/// never consulted, because every check below names its own.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -200,7 +195,7 @@ const NOT_AN_ADDRESS: &str =
"That isn't a server address. It should look like https://notes.example.com.";
/// A server that answered with no AppImage. Never "up to date": the app would sit
/// on its build forever believing it current, which is #2183's shape again.
/// on its build forever believing it current.
fn no_server_build(base: &str) -> String {
format!(
"{base} has no desktop build to update from. Ask whoever runs it, or switch \
@@ -256,16 +251,15 @@ fn self_update_blocker() -> Option<String> {
/// adopted. Called once at startup, before the frontend can ask what the channel is.
///
/// The installer knows which channel the user asked for; the app is what acts on it.
/// Until this existed, `install.sh --channel dev` left the pref at its `stable`
/// default, so a dev build checked the stable feed — which advertises an OLDER
/// version — and reported "up to date" forever (issue 2183). Two places held the same
/// decision and only one of them was set.
/// Without this, `install.sh --channel dev` would leave the pref at its `stable`
/// default, and a dev build would check the stable feed, which advertises an OLDER
/// version, and report "up to date" forever.
///
/// Comparing against the last-adopted value, rather than only seeding when the pref is
/// unset, is what makes both directions work: choosing a channel in the app sticks
/// across launches (the marker hasn't changed since we adopted it), while re-running
/// the installer on a DIFFERENT channel is honoured (it has). Seeding-when-unset would
/// have fixed only the very first install and left the second one silently wrong.
/// the installer on a DIFFERENT channel is honoured (it has). Seeding only when unset
/// would honour the first install and silently ignore the second.
///
/// Every failure is logged and stepped over. No marker at all is the ordinary state of
/// a build installed some other way — from source, from a downloaded AppImage, or by a
@@ -552,9 +546,8 @@ mod tests {
}
/// A scratch path nothing else will collide with. Process id keeps concurrent
/// `cargo test` runs apart, the counter keeps tests within one run apart — which
/// is all uuid was doing here, without the dependency (the core owns uuid now,
/// and pulling it into the desktop crate for two test filenames would be silly).
/// `cargo test` runs apart, the counter keeps tests within one run apart — without
/// a uuid dependency for two test filenames.
fn scratch_path(tag: &str) -> std::path::PathBuf {
static SEQ: AtomicU32 = AtomicU32::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
@@ -613,7 +606,7 @@ mod tests {
#[test]
fn reinstalling_on_a_different_channel_moves_the_app() {
// The half a seed-only-when-unset fix would have missed: install stable, then
// The case seeding only when unset would miss: install stable, then
// install dev. The pref is already set, but the user just asked for dev.
let db = db();
adopt_installer_channel(&db, &data_dir_with_marker("stable"));
@@ -649,7 +642,7 @@ mod tests {
#[test]
fn no_channel_feed_is_named_like_a_branch() {
// A release tag spelled like a branch makes `git push origin <branch>`
// ambiguous once the tag is fetched (Scribe #2184). `dev` and `main` are the
// ambiguous once the tag is fetched. `dev` and `main` are the
// branches this repo pushes.
for channel in [Channel::Stable, Channel::Dev] {
assert!(
@@ -718,7 +711,7 @@ mod tests {
#[test]
fn with_no_server_recorded_updates_come_from_the_forge() {
// The operator's own installs: nothing here may change for them.
// An install that never came from a server keeps the forge feed it always had.
let db = db();
assert_eq!(read_source(&db), Ok(Source::Forge(Channel::Stable)));
set_channel(&db, Channel::Dev);