diff --git a/.forgejo/workflows/android.yml b/.forgejo/workflows/android.yml index 3665d9e..c66f999 100644 --- a/.forgejo/workflows/android.yml +++ b/.forgejo/workflows/android.yml @@ -4,7 +4,7 @@ name: Android # # Replaces the Tauri-mobile lane deleted in step 2. What changed is what this # builds, not that Android has a lane: the UI is Compose, and the store and sync -# engine are `thoughtsync-core` cross-compiled by cargo-ndk and loaded through +# engine are `inkwell-core` cross-compiled by cargo-ndk and loaded through # uniffi. # # CI can only prove this BUILDS. A Linux runner cannot execute an APK, so anything diff --git a/.forgejo/workflows/desktop.yml b/.forgejo/workflows/desktop.yml index 7492fc6..3076a4b 100644 --- a/.forgejo/workflows/desktop.yml +++ b/.forgejo/workflows/desktop.yml @@ -167,9 +167,9 @@ jobs: # quotes in a bug report, the key above is the one only a comparator # sees. Exported rather than passed as a flag because the macro that # reads it is in Rust source, not in Tauri's config. - THOUGHTSYNC_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)" - export THOUGHTSYNC_DISPLAY_VERSION - echo "Baking display version $THOUGHTSYNC_DISPLAY_VERSION" + INKWELL_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)" + export INKWELL_DISPLAY_VERSION + echo "Baking display version $INKWELL_DISPLAY_VERSION" cargo tauri build \ --config '{"build":{"beforeBuildCommand":""}}' \ --config "{\"version\":\"$version\"}" \ @@ -240,7 +240,7 @@ jobs: - name: Upload bundles uses: actions/upload-artifact@v7 with: - name: thoughtsync-linux + name: inkwell-linux path: | target/release/bundle/appimage/*.AppImage target/release/bundle/deb/*.deb @@ -362,9 +362,9 @@ jobs: # quotes in a bug report, the key above is the one only a comparator # sees. Exported rather than passed as a flag because the macro that # reads it is in Rust source, not in Tauri's config. - THOUGHTSYNC_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)" - export THOUGHTSYNC_DISPLAY_VERSION - echo "Baking display version $THOUGHTSYNC_DISPLAY_VERSION" + INKWELL_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)" + export INKWELL_DISPLAY_VERSION + echo "Baking display version $INKWELL_DISPLAY_VERSION" updater='{}' if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then updater='{"bundle":{"createUpdaterArtifacts":true}}' @@ -382,7 +382,7 @@ jobs: - name: Upload installer uses: actions/upload-artifact@v7 with: - name: thoughtsync-windows + name: inkwell-windows path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe if-no-files-found: error @@ -467,7 +467,7 @@ jobs: # be `key`: this value is matched against bundle filenames. version="$(sh packaging/version.sh key desktop)" # The version a PERSON reads, published beside the manifest as - # `thoughtsync-desktop.json`. The image build reads it to describe the + # `inkwell-desktop.json`. The image build reads it to describe the # bundles it bakes in (packaging/fetch-clients.sh) without re-deriving # anything from its own checkout — which would be a different commit # whenever the desktop did not rebuild. diff --git a/Cargo.lock b/Cargo.lock index b339ffc..b512314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1854,6 +1854,55 @@ dependencies = [ "cfb", ] +[[package]] +name = "inkwell-core" +version = "0.1.0" +dependencies = [ + "chrono", + "log", + "openssl-sys", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "sha2", + "uuid", +] + +[[package]] +name = "inkwell-desktop" +version = "0.2.0" +dependencies = [ + "inkwell-core", + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-global-shortcut", + "tauri-plugin-log", + "tauri-plugin-updater", +] + +[[package]] +name = "inkwell-ffi" +version = "0.1.0" +dependencies = [ + "inkwell-core", + "log", + "serde_json", + "thiserror 2.0.20", + "tokio", + "uniffi", +] + +[[package]] +name = "inkwell-uniffi-bindgen" +version = "0.1.0" +dependencies = [ + "uniffi", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -4215,55 +4264,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "thoughtsync-core" -version = "0.1.0" -dependencies = [ - "chrono", - "log", - "openssl-sys", - "reqwest 0.12.28", - "rusqlite", - "serde", - "serde_json", - "sha2", - "uuid", -] - -[[package]] -name = "thoughtsync-desktop" -version = "0.2.0" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-global-shortcut", - "tauri-plugin-log", - "tauri-plugin-updater", - "thoughtsync-core", -] - -[[package]] -name = "thoughtsync-ffi" -version = "0.1.0" -dependencies = [ - "log", - "serde_json", - "thiserror 2.0.20", - "thoughtsync-core", - "tokio", - "uniffi", -] - -[[package]] -name = "thoughtsync-uniffi-bindgen" -version = "0.1.0" -dependencies = [ - "uniffi", -] - [[package]] name = "time" version = "0.3.55" diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4d86859..f50d7ec 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,7 +16,7 @@ val workspaceRoot: Directory = layout.projectDirectory.dir("../..") val androidAbis = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") /** - * Cross-compile `thoughtsync-ffi` for each Android ABI and drop the resulting + * Cross-compile `inkwell-ffi` for each Android ABI and drop the resulting * `.so` into jniLibs, where AGP packages it. * * `ExecOperations` injected rather than `project.exec`: the latter was REMOVED in @@ -49,7 +49,7 @@ abstract class CargoNdkBuild : DefaultTask() { args += "-t" args += abi } - args += listOf("-o", jniLibsDir.get().asFile.absolutePath, "build", "-p", "thoughtsync-ffi") + args += listOf("-o", jniLibsDir.get().asFile.absolutePath, "build", "-p", "inkwell-ffi") // --locked so an Android build cannot silently re-resolve the workspace // lockfile the desktop lanes are gated on. args += "--locked" @@ -94,7 +94,7 @@ abstract class UniffiBindgen : DefaultTask() { "run", "--locked", "-p", - "thoughtsync-uniffi-bindgen", + "inkwell-uniffi-bindgen", "--", "generate", "--library", @@ -149,7 +149,7 @@ val bindingsOut = layout.buildDirectory.dir("generated/uniffi") val cargoNdk = tasks.register("cargoNdk") { - description = "Cross-compile thoughtsync-ffi for the Android ABIs." + description = "Cross-compile inkwell-ffi for the Android ABIs." rustSources.from(rustInputs) abis.set(androidAbis) cargoProfile.set(rustProfile) @@ -163,7 +163,7 @@ val generateBindings = dependsOn(cargoNdk) // arm64 is arbitrary — every ABI carries the same uniffi metadata, and // reading one is cheaper than reading four. - libraryFile.set(jniLibsOut.map { it.file("arm64-v8a/libthoughtsync_ffi.so") }) + libraryFile.set(jniLibsOut.map { it.file("arm64-v8a/libinkwell_ffi.so") }) workspaceDir.set(workspaceRoot) outputDir.set(bindingsOut) } diff --git a/android/bindgen/Cargo.toml b/android/bindgen/Cargo.toml index b843e06..937d549 100644 --- a/android/bindgen/Cargo.toml +++ b/android/bindgen/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "thoughtsync-uniffi-bindgen" +name = "inkwell-uniffi-bindgen" version = "0.1.0" -description = "Generates the Kotlin bindings for thoughtsync-ffi" +description = "Generates the Kotlin bindings for inkwell-ffi" authors = ["bvandeusen"] edition = "2021" # A crate whose ONLY dependency is uniffi itself. # -# This started life as a `[[bin]]` inside thoughtsync-ffi, which failed: building +# This started life as a `[[bin]]` inside inkwell-ffi, which failed: building # it compiled that crate and therefore the core, reqwest, native-tls and # openssl-sys — for the HOST. The vendored-OpenSSL block in core/Cargo.toml is # scoped to `cfg(target_os = "android")`, so a host build looks for a system diff --git a/android/bindgen/src/main.rs b/android/bindgen/src/main.rs index 23a5ca0..816f2ad 100644 --- a/android/bindgen/src/main.rs +++ b/android/bindgen/src/main.rs @@ -3,8 +3,8 @@ //! Invoked by Gradle (see android/app/build.gradle.kts) as: //! //! ```text -//! cargo run --locked -p thoughtsync-uniffi-bindgen -- \ -//! generate --library \ +//! cargo run --locked -p inkwell-uniffi-bindgen -- \ +//! generate --library \ //! --language kotlin --out-dir //! ``` //! diff --git a/android/ffi/Cargo.toml b/android/ffi/Cargo.toml index 526e5ef..64eab3a 100644 --- a/android/ffi/Cargo.toml +++ b/android/ffi/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "thoughtsync-ffi" +name = "inkwell-ffi" version = "0.1.0" -description = "uniffi bindings exposing thoughtsync-core to the native Android client" +description = "uniffi bindings exposing inkwell-core to the native Android client" authors = ["bvandeusen"] edition = "2021" @@ -10,10 +10,10 @@ edition = "2021" # bindgen binary below — and this crate's own tests — can use the crate normally; # a cdylib-only crate is unusable from Rust. crate-type = ["cdylib", "lib"] -name = "thoughtsync_ffi" +name = "inkwell_ffi" [dependencies] -thoughtsync-core = { path = "../../core" } +inkwell-core = { path = "../../core" } serde_json = { workspace = true } log = { workspace = true } diff --git a/android/ffi/src/lib.rs b/android/ffi/src/lib.rs index 9bdff75..a2e9637 100644 --- a/android/ffi/src/lib.rs +++ b/android/ffi/src/lib.rs @@ -1,4 +1,4 @@ -//! uniffi bindings: `thoughtsync-core` as seen from Kotlin. +//! uniffi bindings: `inkwell-core` as seen from Kotlin. //! //! This crate is to Android what `desktop/src-tauri/src/commands/` is to the desktop //! — a thin shim over the shared core, holding no logic of its own. If something here @@ -38,9 +38,9 @@ pub mod models; use std::path::PathBuf; use std::sync::Arc; -use thoughtsync_core::local::{self, Db}; -use thoughtsync_core::sync::blobs::BlobStore; -use thoughtsync_core::sync::{client, compat, engine, push, state}; +use inkwell_core::local::{self, Db}; +use inkwell_core::sync::blobs::BlobStore; +use inkwell_core::sync::{client, compat, engine, push, state}; use models::{ patch_from, BodyItem, BodyTag, ClientUpdate, Identity, Label, Note, NoteDraft, NoteEdit, @@ -120,14 +120,14 @@ impl ThoughtSync { /// /// `data_dir` comes from Kotlin because only Android knows where its app-private /// storage is; the core must not guess at a platform path. The layout inside is - /// the core's business and matches the desktop's exactly — `thoughtsync.db` and + /// the core's business and matches the desktop's exactly — `inkwell.db` and /// `blobs/` — so a store is readable by any client that opens it. #[uniffi::constructor] pub fn new(data_dir: String) -> Result, CoreError> { let dir = PathBuf::from(data_dir); std::fs::create_dir_all(&dir).map_err(CoreError::store)?; - let db = local::open(&dir.join("thoughtsync.db")).map_err(CoreError::store)?; + let db = local::open(&dir.join("inkwell.db")).map_err(CoreError::store)?; log::info!("local store ready — {}", local::summary(&db)); let blobs = BlobStore::new(dir.join("blobs")).map_err(CoreError::store)?; @@ -583,7 +583,7 @@ pub fn checklist_render(text: String, checked: bool) -> String { /// /// Android has to say so because the core cannot: the same crate is compiled into /// the desktop app, and it used to announce every phone in the field as -/// `thoughtsync-desktop` carrying the CORE crate's version — a number no build +/// `inkwell-desktop` carrying the CORE crate's version — a number no build /// stamps and nobody has seen. The honest value is the installed package's own /// `versionName`, which is what Kotlin passes here. /// @@ -680,7 +680,7 @@ mod tests { use std::sync::atomic::{AtomicU32, Ordering}; static NEXT: AtomicU32 = AtomicU32::new(0); let dir = std::env::temp_dir().join(format!( - "thoughtsync-ffi-{}-{}", + "inkwell-ffi-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); diff --git a/android/ffi/src/models.rs b/android/ffi/src/models.rs index aacf8f0..6e07ff5 100644 --- a/android/ffi/src/models.rs +++ b/android/ffi/src/models.rs @@ -1,6 +1,6 @@ //! The types that cross into Kotlin. //! -//! These MIRROR `thoughtsync_core::local::models` rather than reusing it. The core's +//! These MIRROR `inkwell_core::local::models` rather than reusing it. The core's //! shapes are serde structs whose field names and optionality are contracted with the //! shared Vue frontend; hanging uniffi derives on them would couple two very //! different consumers to one definition and put a `serde_json::Value` (which has no @@ -13,13 +13,13 @@ //! what to do with it. That is the entire reason for the `let Core { .. } = value` //! style here; please keep it. -use thoughtsync_core::local::models as core_models; -use thoughtsync_core::sync::client as core_client; -use thoughtsync_core::sync::compat as core_compat; -use thoughtsync_core::sync::engine as core_engine; -use thoughtsync_core::sync::pull as core_pull; -use thoughtsync_core::sync::push as core_push; -use thoughtsync_core::sync::state as core_state; +use inkwell_core::local::models as core_models; +use inkwell_core::sync::client as core_client; +use inkwell_core::sync::compat as core_compat; +use inkwell_core::sync::engine as core_engine; +use inkwell_core::sync::pull as core_pull; +use inkwell_core::sync::push as core_push; +use inkwell_core::sync::state as core_state; /// A note, with everything needed to render a card or open the editor. /// @@ -60,9 +60,9 @@ pub struct BodyItem { pub checked: bool, } -impl From for BodyItem { - fn from(i: thoughtsync_core::local::derive::DerivedItem) -> Self { - let thoughtsync_core::local::derive::DerivedItem { +impl From for BodyItem { + fn from(i: inkwell_core::local::derive::DerivedItem) -> Self { + let inkwell_core::local::derive::DerivedItem { text, checked, line, @@ -88,9 +88,9 @@ pub struct BodyTag { pub name: String, } -impl From for BodyTag { - fn from(t: thoughtsync_core::local::derive::DerivedTag) -> Self { - let thoughtsync_core::local::derive::DerivedTag { +impl From for BodyTag { + fn from(t: inkwell_core::local::derive::DerivedTag) -> Self { + let inkwell_core::local::derive::DerivedTag { line, start, end, @@ -121,12 +121,12 @@ pub struct ClientUpdate { pub size: i64, } -impl From for ClientUpdate { - fn from(r: thoughtsync_core::sync::client::ClientRelease) -> Self { +impl From for ClientUpdate { + fn from(r: inkwell_core::sync::client::ClientRelease) -> Self { // Destructured exhaustively, like every other conversion in this file: a // field added upstream stops this compiling until Android is told what to // do with it, which turns silent drift into a build error. - let thoughtsync_core::sync::client::ClientRelease { + let inkwell_core::sync::client::ClientRelease { version, version_code, size, diff --git a/android/ffi/uniffi.toml b/android/ffi/uniffi.toml index 8d4c9ce..a22a607 100644 --- a/android/ffi/uniffi.toml +++ b/android/ffi/uniffi.toml @@ -2,4 +2,4 @@ # `com.fabledsword.thoughtsync.core.*` rather than something the app has to alias. [bindings.kotlin] package_name = "com.fabledsword.thoughtsync.core" -cdylib_name = "thoughtsync_ffi" +cdylib_name = "inkwell_ffi" diff --git a/core/Cargo.toml b/core/Cargo.toml index bdc1b87..b07dcd2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "thoughtsync-core" +name = "inkwell-core" version = "0.1.0" -description = "ThoughtSync client core — local-first SQLite store and opt-in sync engine" +description = "Inkwell client core — local-first SQLite store and opt-in sync engine" authors = ["bvandeusen"] edition = "2021" diff --git a/core/src/lib.rs b/core/src/lib.rs index 9f1873f..919557b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,4 @@ -//! ThoughtSync's client core: the on-device SQLite store and the sync engine. +//! Inkwell's client core: the on-device SQLite store and the sync engine. //! //! Deliberately free of any UI framework. The desktop wraps it in Tauri commands; //! the Android client binds it through uniffi. Neither owns it, and a change to diff --git a/core/src/sync/client.rs b/core/src/sync/client.rs index 5d72321..4a286dd 100644 --- a/core/src/sync/client.rs +++ b/core/src/sync/client.rs @@ -427,8 +427,8 @@ mod tests { #[test] fn urls_preserve_a_port_and_subpath() { assert_eq!( - config_url("http://192.168.1.10:8000/thoughtsync"), - "http://192.168.1.10:8000/thoughtsync/api/config" + config_url("http://192.168.1.10:8000/inkwell"), + "http://192.168.1.10:8000/inkwell/api/config" ); } } diff --git a/core/src/sync/compat.rs b/core/src/sync/compat.rs index f10184c..3ef597e 100644 --- a/core/src/sync/compat.rs +++ b/core/src/sync/compat.rs @@ -176,7 +176,7 @@ pub fn evaluate(info: &ServerInfo) -> Compatibility { /// Who this client says it is, set once by the host application at startup. /// /// THE CORE CANNOT KNOW THIS, and the value it used to invent was wrong twice. It -/// was `thoughtsync-desktop/{CARGO_PKG_VERSION}`, and this crate is compiled into +/// was `inkwell-desktop/{CARGO_PKG_VERSION}`, and this crate is compiled into /// the desktop app AND the Android app — so every phone in the field announced /// itself as a desktop. The version was worse: `CARGO_PKG_VERSION` here is the /// version of the CORE crate, a number no build stamps and no user has ever seen, @@ -188,7 +188,7 @@ pub fn evaluate(info: &ServerInfo) -> Compatibility { /// the process and a second caller should be ignored rather than race the first. static CLIENT_AGENT: OnceLock = OnceLock::new(); -/// Name this client for the servers it talks to — `("thoughtsync-android", "2026.08.31.1204")`. +/// Name this client for the servers it talks to — `("inkwell-android", "2026.08.31.1204")`. /// /// Call once at startup, before any sync. Calling twice is not an error and the /// first name wins; not calling it at all is visible in the header rather than @@ -207,13 +207,10 @@ pub fn client_headers() -> [(&'static str, String); 2] { let agent = CLIENT_AGENT .get() .cloned() - .unwrap_or_else(|| "thoughtsync-unidentified/unknown".to_string()); + .unwrap_or_else(|| "inkwell-unidentified/unknown".to_string()); [ - ("X-ThoughtSync-Client", agent), - ( - "X-ThoughtSync-Protocol", - CLIENT_PROTOCOL_VERSION.to_string(), - ), + ("X-Inkwell-Client", agent), + ("X-Inkwell-Protocol", CLIENT_PROTOCOL_VERSION.to_string()), ] } @@ -411,21 +408,19 @@ mod tests { // this one for the OnceLock, and whichever lost would see the other's name. // One test, both branches, in order. assert!( - client_headers()[0] - .1 - .starts_with("thoughtsync-unidentified/"), + client_headers()[0].1.starts_with("inkwell-unidentified/"), "a host that never introduced itself must say so" ); - set_client_agent("thoughtsync-test", "2026.08.31.1204"); + set_client_agent("inkwell-test", "2026.08.31.1204"); let headers = client_headers(); - assert_eq!(headers[0].0, "X-ThoughtSync-Client"); - assert_eq!(headers[0].1, "thoughtsync-test/2026.08.31.1204"); + assert_eq!(headers[0].0, "X-Inkwell-Client"); + assert_eq!(headers[0].1, "inkwell-test/2026.08.31.1204"); assert_eq!(headers[1].1, CLIENT_PROTOCOL_VERSION.to_string()); // First name wins — a second host cannot rename a running process. - set_client_agent("thoughtsync-impostor", "0"); - assert_eq!(client_headers()[0].1, "thoughtsync-test/2026.08.31.1204"); + set_client_agent("inkwell-impostor", "0"); + assert_eq!(client_headers()[0].1, "inkwell-test/2026.08.31.1204"); } #[test] diff --git a/core/src/sync/mod.rs b/core/src/sync/mod.rs index 1ab89c9..a8477f7 100644 --- a/core/src/sync/mod.rs +++ b/core/src/sync/mod.rs @@ -1,4 +1,4 @@ -//! Talking to a ThoughtSync server — entirely opt-in. +//! Talking to an Inkwell server — entirely opt-in. //! //! The app is local-first: `local` is the source of truth and everything works //! unlinked. Nothing in here runs until the user links a server. diff --git a/desktop/packaging/arch/thoughtsync.desktop b/desktop/packaging/arch/inkwell.desktop similarity index 81% rename from desktop/packaging/arch/thoughtsync.desktop rename to desktop/packaging/arch/inkwell.desktop index 9276a1c..820afbb 100644 --- a/desktop/packaging/arch/thoughtsync.desktop +++ b/desktop/packaging/arch/inkwell.desktop @@ -3,10 +3,10 @@ # grouping. Tauri writes the same value into the .deb's generated entry. [Desktop Entry] Type=Application -Name=ThoughtSync +Name=Inkwell Comment=Capture a fleeting thought in a second -Exec=thoughtsync %U -Icon=thoughtsync +Exec=inkwell %U +Icon=inkwell Terminal=false Categories=Utility;Office; -StartupWMClass=thoughtsync +StartupWMClass=inkwell diff --git a/desktop/packaging/arch/package-prebuilt.sh b/desktop/packaging/arch/package-prebuilt.sh index 0d1fa60..13c930c 100755 --- a/desktop/packaging/arch/package-prebuilt.sh +++ b/desktop/packaging/arch/package-prebuilt.sh @@ -29,19 +29,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" SRC_TAURI="$REPO_ROOT/desktop/src-tauri" -BINARY="$REPO_ROOT/target/release/thoughtsync" +BINARY="$REPO_ROOT/target/release/inkwell" OUT_DIR="${1:-$REPO_ROOT/target/release/bundle/arch}" -PKGNAME="thoughtsync" -# The name this package used to ship under. pacman needs both to retire it: without -# them a `pacman -U` of the renamed package installs ALONGSIDE the old one, and two -# packages both own /usr/bin/thoughtsync (issue 2075). -REPLACES=(thoughtsync-desktop) +PKGNAME="inkwell" +# The names this package used to ship under: `thoughtsync` until the app became +# Inkwell (Scribe note 5071), `thoughtsync-desktop` before that. pacman needs both +# conflict and replace to retire them: without them a `pacman -U` of the renamed +# package installs ALONGSIDE the old one (issue 2075). +REPLACES=(thoughtsync thoughtsync-desktop) PKGREL=1 -PKGDESC="ThoughtSync desktop — local-first Keep-style thought capture" -URL="https://git.fabledsword.com/bvandeusen/thoughtsync" +PKGDESC="Inkwell desktop — local-first Keep-style thought capture" +URL="https://git.fabledsword.com/bvandeusen/inkwell" LICENSE="MIT" -PACKAGER="ThoughtSync CI " +PACKAGER="Inkwell CI " # Runtime deps. Deliberately minimal and grounded in what the binary actually # links: tauri is built with features=[] (no tray -> no libayatana-appindicator) @@ -79,25 +80,25 @@ STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT INT TERM # --- lay out the filesystem tree -------------------------------------------- -# /usr/bin/thoughtsync — the same command name the .deb installs and the AppImage +# /usr/bin/inkwell — the same command name the .deb installs and the AppImage # installer symlinks into ~/.local/bin, so it's identical whichever way the app # arrived. The binary already carries this name (Cargo `[[bin]]`), which is also # what the .desktop entry's StartupWMClass has to match. -install -Dm755 "$BINARY" "$STAGE/usr/bin/thoughtsync" -install -Dm644 "$SCRIPT_DIR/thoughtsync.desktop" \ - "$STAGE/usr/share/applications/thoughtsync.desktop" +install -Dm755 "$BINARY" "$STAGE/usr/bin/inkwell" +install -Dm644 "$SCRIPT_DIR/inkwell.desktop" \ + "$STAGE/usr/share/applications/inkwell.desktop" -# Themed icons, so the menu entry's `Icon=thoughtsync` resolves at every size the +# Themed icons, so the menu entry's `Icon=inkwell` resolves at every size the # desktop asks for; pixmaps is the legacy fallback for older launchers. install -Dm644 "$SRC_TAURI/icons/32x32.png" \ - "$STAGE/usr/share/icons/hicolor/32x32/apps/thoughtsync.png" + "$STAGE/usr/share/icons/hicolor/32x32/apps/inkwell.png" install -Dm644 "$SRC_TAURI/icons/128x128.png" \ - "$STAGE/usr/share/icons/hicolor/128x128/apps/thoughtsync.png" + "$STAGE/usr/share/icons/hicolor/128x128/apps/inkwell.png" install -Dm644 "$SRC_TAURI/icons/128x128@2x.png" \ - "$STAGE/usr/share/icons/hicolor/256x256/apps/thoughtsync.png" + "$STAGE/usr/share/icons/hicolor/256x256/apps/inkwell.png" install -Dm644 "$SRC_TAURI/icons/icon.png" \ - "$STAGE/usr/share/icons/hicolor/512x512/apps/thoughtsync.png" -install -Dm644 "$SRC_TAURI/icons/icon.png" "$STAGE/usr/share/pixmaps/thoughtsync.png" + "$STAGE/usr/share/icons/hicolor/512x512/apps/inkwell.png" +install -Dm644 "$SRC_TAURI/icons/icon.png" "$STAGE/usr/share/pixmaps/inkwell.png" # Installed size in KiB, as pacman reports it. INSTALLED_SIZE="$(du -sb "$STAGE" | cut -f1)" diff --git a/desktop/packaging/deb/verify.sh b/desktop/packaging/deb/verify.sh index 9c2fe0a..7241051 100755 --- a/desktop/packaging/deb/verify.sh +++ b/desktop/packaging/deb/verify.sh @@ -13,7 +13,7 @@ # Five checks, cheapest first: # 1. Print the control file + contents — the generated metadata becomes ground # truth in the build log instead of an assumption. -# 2. Naming: the binary is /usr/bin/thoughtsync and the generated .desktop +# 2. Naming: the binary is /usr/bin/inkwell and the generated .desktop # entry's StartupWMClass matches it. The app used to identify itself three # different ways depending on install channel (issue 2075); this is what # keeps the .deb — the only channel whose entry Tauri generates for us — @@ -72,12 +72,17 @@ echo " (binary: ${BIN#"$WORK/root"})" # CANON is the one name the app answers to everywhere: the binary, the CLI # command, the icon, and the WM_CLASS the window reports. Hardcoded here on # purpose — this literal IS the contract the three install channels are held to. -# Deliberately NOT asserted: the control file's `Package:` field, which is -# `thought-sync`. tauri-bundler derives it as kebab-case(productName) with no -# config override, so "ThoughtSync" splits at the hump. Fixing it would mean -# unpacking and rewriting the control archive on every build — a fragile step for -# a cosmetic gain on one uninstall command. Left as a known wart (issue 2075). -CANON="thoughtsync" +# +# The control file's `Package:` field is held to it too. tauri-bundler derives it +# as kebab-case(productName) with no override, which split "ThoughtSync" into +# `thought-sync` (issue 2075). "Inkwell" has no hump to split, so the package and +# the command finally agree, and a productName that brought the split back would +# also break the `conflicts`/`replaces` retiring the old package. +CANON="inkwell" + +package="$(dpkg-deb -f "$DEB" Package 2>/dev/null || true)" +[ "$package" = "$CANON" ] || + note_fail "the package is named '${package:-}', expected '$CANON' (kebab-case of productName)." installed_bin="${BIN#"$WORK/root"}" [ "$installed_bin" = "/usr/bin/$CANON" ] || @@ -107,7 +112,7 @@ if command -v dpkg-shlibdeps >/dev/null 2>&1; then # dpkg-shlibdeps insists on a debian/control in the working directory even with # -O (write to stdout); a stub is enough to let it do the ELF analysis. mkdir -p "$WORK/deb-stub/debian" - printf 'Source: thoughtsync\n\nPackage: thoughtsync\nArchitecture: amd64\n' \ + printf 'Source: inkwell\n\nPackage: inkwell\nArchitecture: amd64\n' \ >"$WORK/deb-stub/debian/control" # `|| true`: pipefail is on and dpkg-shlibdeps still exits non-zero on some # symbol warnings even with --ignore-missing-info. An empty result degrades to diff --git a/desktop/packaging/install.sh b/desktop/packaging/install.sh index 277afaa..0cb1c8a 100755 --- a/desktop/packaging/install.sh +++ b/desktop/packaging/install.sh @@ -1,8 +1,8 @@ #!/bin/sh # -# ThoughtSync desktop — one-command Linux installer. +# Inkwell desktop — one-command Linux installer. # -# curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh +# curl -fsSL https://git.fabledsword.com/bvandeusen/inkwell/raw/branch/dev/desktop/packaging/install.sh | sh # # Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs): # stable (default) — the rolling build from every merge to `main`. @@ -35,7 +35,7 @@ set -eu INSTANCE="https://git.fabledsword.com" -REPO="bvandeusen/thoughtsync" +REPO="bvandeusen/inkwell" API="$INSTANCE/api/v1/repos/$REPO" say() { printf '==> %s\n' "$1"; } @@ -44,7 +44,7 @@ have() { command -v "$1" >/dev/null 2>&1; } usage() { cat <<'USAGE' -ThoughtSync desktop installer. +Inkwell desktop installer. install.sh [--channel stable|dev] @@ -83,11 +83,11 @@ have curl || die "curl is required." arch="$(uname -m)" case "$arch" in x86_64 | amd64) : ;; - *) die "ThoughtSync ships x86_64 Linux builds only right now (this machine: $arch)." ;; + *) die "Inkwell ships x86_64 Linux builds only right now (this machine: $arch)." ;; esac # --- resolve the release for this channel ----------------------------------- -say "Finding the latest ThoughtSync build on the $channel channel…" +say "Finding the latest Inkwell build on the $channel channel…" # ONE lookup, both channels. Each is a release whose tag never moves and whose assets # are pruned to the current build, so the tag alone names the newest build on that @@ -131,10 +131,10 @@ trap 'rm -rf "$tmp"' EXIT INT TERM # The app reads it at startup (src-tauri/src/update.rs, INSTALL_MARKER) and only acts # when the value CHANGED, so switching channel in the app isn't undone on next launch. # -# The directory is Tauri's app-data dir for identifier com.fabledsword.thoughtsync; +# The directory is Tauri's app-data dir for identifier com.fabledsword.inkwell; # both sides hardcode it, so a change to the identifier has to change both. record_channel() { - marker_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.fabledsword.thoughtsync" + marker_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.fabledsword.inkwell" # Best-effort: a failure here costs the channel setting, not the install, and a # native install run as root would only be writing into root's home anyway. mkdir -p "$marker_dir" 2>/dev/null && printf '%s\n' "$channel" > "$marker_dir/install-channel" 2>/dev/null || true @@ -176,7 +176,7 @@ if have pacman && [ -n "$pkg_url" ]; then need_root $sudo pacman -U --noconfirm "$pkg_file" record_channel - say "Done. Launch ThoughtSync from your application menu, or run thoughtsync." + say "Done. Launch Inkwell from your application menu, or run inkwell." native_update_note exit 0 fi @@ -184,33 +184,33 @@ fi # --- native .deb path (Debian/Ubuntu) --------------------------------------- if have dpkg && have apt-get && [ -n "$deb_url" ]; then say "Debian-family system detected — installing the native .deb" - curl -fSL -o "$tmp/thoughtsync.deb" "$deb_url" + curl -fSL -o "$tmp/inkwell.deb" "$deb_url" need_root # apt-get resolves the .deb's dependencies (webkit2gtk etc.). dpkg is the # fallback if this apt is too old for local-file installs — it leaves the deps # unconfigured, so `apt-get -f install` is what actually completes that path. - $sudo apt-get install -y "$tmp/thoughtsync.deb" || - { $sudo dpkg -i "$tmp/thoughtsync.deb" || true; $sudo apt-get -f install -y; } + $sudo apt-get install -y "$tmp/inkwell.deb" || + { $sudo dpkg -i "$tmp/inkwell.deb" || true; $sudo apt-get -f install -y; } record_channel - say "Done. Launch ThoughtSync from your application menu." + say "Done. Launch Inkwell from your application menu." native_update_note exit 0 fi # --- universal AppImage path (user-local, no sudo) -------------------------- -# Install into ~/Applications/ThoughtSync.AppImage — the SAME location the app's +# Install into ~/Applications/Inkwell.AppImage — the SAME location the app's # own self-integration uses (src/integration.rs) — so the running app sees # itself already installed and never makes a second copy or menu entry. say "Installing the de-bundled AppImage (user-local, no sudo)" [ -n "$appimage_url" ] || die "the $channel release (${version:-unknown}) has no AppImage asset." apps_dir="$HOME/Applications" -dest="$apps_dir/ThoughtSync.AppImage" +dest="$apps_dir/Inkwell.AppImage" mkdir -p "$apps_dir" say "Downloading $(basename "$appimage_url")…" -curl -fSL -o "$tmp/ThoughtSync.AppImage" "$appimage_url" -chmod +x "$tmp/ThoughtSync.AppImage" -mv -f "$tmp/ThoughtSync.AppImage" "$dest" +curl -fSL -o "$tmp/Inkwell.AppImage" "$appimage_url" +chmod +x "$tmp/Inkwell.AppImage" +mv -f "$tmp/Inkwell.AppImage" "$dest" # Menu entry — written to match integration.rs verbatim (same paths + fields), # so the app reports is_integrated=true and won't duplicate it. @@ -222,39 +222,39 @@ mkdir -p "$apps_menu" "$icons_dir" # entry looks right immediately. Extraction is a non-GUI unsquash (no FUSE, no # black-window risk); if it fails we fall back to the themed name and the app # writes its embedded icon on first launch anyway. -icon_ref="thoughtsync" +icon_ref="inkwell" if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \ - && cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/thoughtsync.png" 2>/dev/null; then - icon_ref="$icons_dir/thoughtsync.png" + && cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/inkwell.png" 2>/dev/null; then + icon_ref="$icons_dir/inkwell.png" fi rm -rf "$tmp/squashfs-root" 2>/dev/null || true # StartupWMClass is the BINARY name, not the product name and not the AppImage -# filename: the AppImage's AppRun execs usr/bin/thoughtsync, and GTK derives +# filename: the AppImage's AppRun execs usr/bin/inkwell, and GTK derives # WM_CLASS from whatever it ends up running. Anything else here means the window # never associates with this entry and the taskbar shows a second, generic icon. -cat > "$apps_menu/thoughtsync.desktop" < "$apps_menu/inkwell.desktop" </dev/null 2>&1 || true # Convenience CLI launcher. mkdir -p "$HOME/.local/bin" -ln -sf "$dest" "$HOME/.local/bin/thoughtsync" +ln -sf "$dest" "$HOME/.local/bin/inkwell" record_channel say "Installed to $dest" -printf ' Launch it from your application menu, or run \033[1mthoughtsync\033[0m' +printf ' Launch it from your application menu, or run \033[1minkwell\033[0m' printf ' (if ~/.local/bin is on your PATH).\n' # This is the one path where the app can update itself, so say what it will follow. if [ "$channel" = "dev" ]; then diff --git a/desktop/packaging/publish-release.sh b/desktop/packaging/publish-release.sh index af245de..55f5e30 100755 --- a/desktop/packaging/publish-release.sh +++ b/desktop/packaging/publish-release.sh @@ -148,8 +148,8 @@ fi echo "==> Creating release for $TAG" BODY=$(cat < "$work/thoughtsync-desktop.json" -replace_asset "$work/thoughtsync-desktop.json" "thoughtsync-desktop.json" + "$DISPLAY_VERSION" "$APP_VERSION" > "$work/inkwell-desktop.json" +replace_asset "$work/inkwell-desktop.json" "inkwell-desktop.json" # --- TEMPORARY: the one-shot bridge off a retired channel tag ------------------ # @@ -229,7 +229,7 @@ if [ "${PRUNE_OLD_ASSETS:-false}" = "true" ]; then # every desktop push regardless. That is exactly what happened on run # 4098, which swept the APK run 4092 had just published. case "$asset_name" in - latest.json|thoughtsync-desktop.json|thoughtsync.apk|thoughtsync-android.json) continue ;; + latest.json|inkwell-desktop.json|thoughtsync.apk|thoughtsync-android.json) continue ;; *"$APP_VERSION"*) continue ;; esac echo " removing $asset_name" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 875e6d6..18a217d 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "thoughtsync-desktop" +name = "inkwell-desktop" # NOT THE SHIPPED VERSION, and bumping it has no effect on anything a user sees. # # Cargo requires a version here, and Tauri reads one from `tauri.conf.json` — both @@ -13,11 +13,11 @@ name = "thoughtsync-desktop" # what that looked like in the field. A scheme needing a human to remember something # before each release has not removed the decision, only hidden it. version = "0.2.0" -description = "ThoughtSync desktop — local-first Keep-style thought capture" +description = "Inkwell desktop — local-first Keep-style thought capture" authors = ["bvandeusen"] edition = "2021" # The executable is named below, not inferred from this package name. Off so the -# inferred `thoughtsync-desktop` target and the explicit one can't both claim +# inferred `inkwell-desktop` target and the explicit one can't both claim # src/main.rs. autobins = false @@ -25,23 +25,23 @@ autobins = false # main.rs is a thin shim. # # rlib only. The staticlib/cdylib types existed for Tauri mobile, which Android no -# longer uses — it is a native Kotlin client over `thoughtsync-core` instead +# longer uses — it is a native Kotlin client over `inkwell-core` instead # (Scribe note 2730), and the .so it loads is built from that crate, not this one. [lib] -name = "thoughtsync_desktop_lib" +name = "inkwell_desktop_lib" crate-type = ["rlib"] -# The shipped command is `thoughtsync` on every install channel, and the binary's +# The shipped command is `inkwell` on every install channel, and the binary's # own name is what the desktop actually keys on: GTK derives the window's WM_CLASS # from it, and Tauri's generated .desktop file sets StartupWMClass to that same # name. (Written without braces on purpose: `tauri android init` round-trips this # file through a TOML serializer that reformats brace tokens inside comments.) # So the canonical name has to be carried by the build target itself, not just by -# the path it gets installed to (issue 2075). The crate stays `thoughtsync-desktop` -# — only the executable is renamed. `thoughtsync` is also fixed under kebab-casing, -# which the AppImage bundler applies to the binary name on its way in. +# the path it gets installed to (issue 2075). The crate is `inkwell-desktop`, so +# the executable is named here rather than inferred. `inkwell` is also fixed under +# kebab-casing, which the AppImage bundler applies to the binary name on its way in. [[bin]] -name = "thoughtsync" +name = "inkwell" path = "src/main.rs" [build-dependencies] @@ -50,7 +50,7 @@ tauri-build = { version = "2", features = [] } [dependencies] # The framework-free client core: local SQLite store + sync engine. Shared with the # Android client, which binds the same crate through uniffi (Scribe note 2730). -thoughtsync-core = { path = "../../core" } +inkwell-core = { path = "../../core" } tauri = { version = "2", features = [] } serde = { workspace = true } diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index c407dd0..b2b1909 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -7,7 +7,7 @@ fn main() { // why this is easy to forget the day one is added. // // See DISPLAY_VERSION in `src/commands/local.rs`. - println!("cargo::rerun-if-env-changed=THOUGHTSYNC_DISPLAY_VERSION"); + println!("cargo::rerun-if-env-changed=INKWELL_DISPLAY_VERSION"); tauri_build::build() } diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 5f301f1..38bfe97 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -1,7 +1,7 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Core capability for the ThoughtSync windows: the board and the quick-capture window.", + "description": "Core capability for the Inkwell windows: the board and the quick-capture window.", "windows": ["main", "capture"], "permissions": ["core:default"] } diff --git a/desktop/src-tauri/src/capture.rs b/desktop/src-tauri/src/capture.rs index 3ffc5dd..5cd7fc7 100644 --- a/desktop/src-tauri/src/capture.rs +++ b/desktop/src-tauri/src/capture.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; -use thoughtsync_core::local::{store, Db}; +use inkwell_core::local::{store, Db}; const SHORTCUT_PREF: &str = "capture_shortcut"; @@ -43,7 +43,7 @@ pub const CAPTURE_WINDOW: &str = "capture"; /// The two windows hold separate copies of the frontend and therefore separate /// Pinia stores; nothing in the capture window's store can reach the board's. The /// note is already in SQLite by the time this fires — this only says "look again". -pub const CAPTURED_EVENT: &str = "thoughtsync://captured"; +pub const CAPTURED_EVENT: &str = "inkwell://captured"; /// The stored shortcut and whether it is actually live. /// diff --git a/desktop/src-tauri/src/commands/local.rs b/desktop/src-tauri/src/commands/local.rs index 05f8e74..2388069 100644 --- a/desktop/src-tauri/src/commands/local.rs +++ b/desktop/src-tauri/src/commands/local.rs @@ -6,11 +6,11 @@ use serde_json::Value; use tauri::State; -use thoughtsync_core::local::models::*; -use thoughtsync_core::local::retention; -use thoughtsync_core::local::store; -use thoughtsync_core::local::Db; -use thoughtsync_core::sync::state; +use inkwell_core::local::models::*; +use inkwell_core::local::retention; +use inkwell_core::local::store; +use inkwell_core::local::Db; +use inkwell_core::sync::state; // A macro would hide the (very regular) locking; kept explicit so each command reads // as an obvious lock -> delegate -> stringify. @@ -44,7 +44,7 @@ pub fn auth_me() -> User { // The single synthetic local user, so the auth-gated router resolves with no login. User { id: "local".to_string(), - email: "local@thoughtsync.app".to_string(), + email: "local@inkwell.app".to_string(), display_name: "You".to_string(), email_verified: true, is_admin: false, diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 364bd4a..f976ec6 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -1,4 +1,4 @@ -//! The Tauri command surface — the desktop's adapter onto `thoughtsync-core`. +//! The Tauri command surface — the desktop's adapter onto `inkwell-core`. //! //! This is the whole of what couples the store and sync engine to Tauri, and keeping //! it in one place is deliberate: the Android client writes its own adapter (uniffi) diff --git a/desktop/src-tauri/src/commands/sync.rs b/desktop/src-tauri/src/commands/sync.rs index f910c3f..5b38d3f 100644 --- a/desktop/src-tauri/src/commands/sync.rs +++ b/desktop/src-tauri/src/commands/sync.rs @@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize}; use tauri::State; -use thoughtsync_core::local::Db; -use thoughtsync_core::sync::blobs::BlobStore; -use thoughtsync_core::sync::client::{self, Identity, ProbeResult}; -use thoughtsync_core::sync::compat::Compatibility; -use thoughtsync_core::sync::engine; -use thoughtsync_core::sync::push; -use thoughtsync_core::sync::state; +use inkwell_core::local::Db; +use inkwell_core::sync::blobs::BlobStore; +use inkwell_core::sync::client::{self, Identity, ProbeResult}; +use inkwell_core::sync::compat::Compatibility; +use inkwell_core::sync::engine; +use inkwell_core::sync::push; +use inkwell_core::sync::state; /// Ask a server who it is, without committing to anything. The UI calls this as the /// user finishes typing an address, so they see what answered before handing over diff --git a/desktop/src-tauri/src/crossover.rs b/desktop/src-tauri/src/crossover.rs new file mode 100644 index 0000000..eab1eec --- /dev/null +++ b/desktop/src-tauri/src/crossover.rs @@ -0,0 +1,181 @@ +//! One-time move of a ThoughtSync install's data into Inkwell's. +//! +//! The app was renamed ThoughtSync → Inkwell (Scribe note 5071), 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 +//! that directory is the only copy for anyone who never linked a server. +//! +//! So on startup, before the store opens, the old directory's contents move across. +//! The store file was renamed with the app (`thoughtsync.db` → `inkwell.db`), so it +//! is renamed on the way, together with its SQLite `-wal`/`-shm` sidecars. Those +//! 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 +//! call in `lib.rs` once no install is still on the old identifier. + +use std::fs; +use std::io; +use std::path::Path; + +/// The identifier ThoughtSync shipped under. Its app-data dir is a sibling of ours. +pub const LEGACY_IDENTIFIER: &str = "com.fabledsword.thoughtsync"; + +const LEGACY_DB: &str = "thoughtsync.db"; +pub const DB_FILE: &str = "inkwell.db"; + +/// What a crossover did, for the startup log. +#[derive(Debug, PartialEq, Eq)] +pub enum Outcome { + /// No old install beside this one. + NothingToMove, + /// This install already has its own store. The old directory is left alone: + /// merging two stores is not something a rename should decide. + AlreadyHasAStore, + /// Moved this many entries across. + Moved(usize), +} + +/// Move `old_dir`'s contents into `new_dir`, unless `new_dir` already has a store. +/// +/// 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 +/// 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 { + if new_dir.join(DB_FILE).exists() { + return Ok(Outcome::AlreadyHasAStore); + } + if !old_dir.is_dir() { + return Ok(Outcome::NothingToMove); + } + fs::create_dir_all(new_dir)?; + + let mut moved = 0; + for entry in fs::read_dir(old_dir)? { + let entry = entry?; + let name = entry.file_name(); + let target = new_dir.join(renamed(&name.to_string_lossy())); + if target.exists() { + continue; + } + fs::rename(entry.path(), &target)?; + moved += 1; + } + // Only succeeds when empty, which is the point: whatever was skipped stays put. + let _ = fs::remove_dir(old_dir); + Ok(Outcome::Moved(moved)) +} + +/// `thoughtsync.db`, `thoughtsync.db-wal`, `thoughtsync.db-shm` → the `inkwell.db` +/// family. Everything else keeps its name. +fn renamed(name: &str) -> String { + match name.strip_prefix(LEGACY_DB) { + Some(suffix @ ("" | "-wal" | "-shm")) => format!("{DB_FILE}{suffix}"), + _ => name.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU32, Ordering}; + + /// A unique scratch root holding `old/` and `new/`, the shape of a data root + /// with both identifiers' directories in it. + fn root() -> (PathBuf, PathBuf) { + static SEQ: AtomicU32 = AtomicU32::new(0); + let n = SEQ.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!("iw-crossover-{}-{n}", std::process::id())); + (root.join("old"), root.join("new")) + } + + fn write(path: &Path, contents: &str) { + fs::create_dir_all(path.parent().expect("parent")).expect("mkdir"); + fs::write(path, contents).expect("write"); + } + + fn read(path: &Path) -> String { + fs::read_to_string(path).expect("read") + } + + #[test] + fn a_thoughtsync_store_moves_across_with_its_wal_and_blobs() { + let (old, new) = root(); + write(&old.join("thoughtsync.db"), "main"); + write(&old.join("thoughtsync.db-wal"), "uncheckpointed writes"); + write(&old.join("thoughtsync.db-shm"), "index"); + write(&old.join("blobs/ab/abcdef"), "image bytes"); + + assert_eq!( + adopt_legacy_data(&old, &new).expect("move"), + Outcome::Moved(4) + ); + + assert_eq!(read(&new.join("inkwell.db")), "main"); + assert_eq!(read(&new.join("inkwell.db-wal")), "uncheckpointed writes"); + assert_eq!(read(&new.join("inkwell.db-shm")), "index"); + assert_eq!(read(&new.join("blobs/ab/abcdef")), "image bytes"); + assert!(!old.exists(), "an emptied old dir is removed"); + } + + #[test] + fn the_installers_marker_already_in_the_new_dir_wins() { + // install.sh writes the channel marker into the NEW dir before first launch, + // so the new dir exists but holds no store. The crossover still has to run, + // and the fresh marker has to beat the old install's. + let (old, new) = root(); + write(&old.join("thoughtsync.db"), "main"); + write(&old.join("install-channel"), "stable"); + write(&new.join("install-channel"), "dev"); + + assert_eq!( + adopt_legacy_data(&old, &new).expect("move"), + Outcome::Moved(1) + ); + + assert_eq!(read(&new.join("inkwell.db")), "main"); + assert_eq!(read(&new.join("install-channel")), "dev"); + assert_eq!( + read(&old.join("install-channel")), + "stable", + "the loser stays put" + ); + } + + #[test] + fn an_install_with_its_own_store_is_left_alone() { + let (old, new) = root(); + write(&old.join("thoughtsync.db"), "old notes"); + write(&new.join("inkwell.db"), "new notes"); + + assert_eq!( + adopt_legacy_data(&old, &new).expect("check"), + Outcome::AlreadyHasAStore + ); + + assert_eq!(read(&new.join("inkwell.db")), "new notes"); + assert_eq!(read(&old.join("thoughtsync.db")), "old notes"); + } + + #[test] + fn no_old_install_means_nothing_happens() { + let (old, new) = root(); + assert_eq!( + adopt_legacy_data(&old, &new).expect("check"), + Outcome::NothingToMove + ); + assert!(!new.exists(), "nothing created for a fresh install"); + } + + #[test] + fn only_the_store_family_is_renamed() { + assert_eq!(renamed("thoughtsync.db"), "inkwell.db"); + assert_eq!(renamed("thoughtsync.db-wal"), "inkwell.db-wal"); + assert_eq!(renamed("thoughtsync.db-journal"), "thoughtsync.db-journal"); + assert_eq!(renamed("blobs"), "blobs"); + } +} diff --git a/desktop/src-tauri/src/integration.rs b/desktop/src-tauri/src/integration.rs index 1b0824b..06c2b75 100644 --- a/desktop/src-tauri/src/integration.rs +++ b/desktop/src-tauri/src/integration.rs @@ -1,7 +1,7 @@ //! Linux AppImage desktop integration. //! //! A bare AppImage is a portable file with no menu entry — deleting it uninstalls -//! the app. These commands let ThoughtSync install itself into the application menu +//! the app. These commands let Inkwell install itself into the application menu //! (a `.desktop` launcher + icon) and keep a stable copy in `~/Applications`, so it //! behaves like an installed app, and remove that again. No external helper //! (AppImageLauncher / Gear Lever) is required. @@ -20,9 +20,9 @@ use serde::Serialize; // internal layout. Written to the user icon dir on integrate. const APP_ICON: &[u8] = include_bytes!("../app-icon.png"); -const DESKTOP_ENTRY_NAME: &str = "thoughtsync.desktop"; -const ICON_FILE_NAME: &str = "thoughtsync.png"; -const INSTALLED_APPIMAGE_NAME: &str = "ThoughtSync.AppImage"; +const DESKTOP_ENTRY_NAME: &str = "inkwell.desktop"; +const ICON_FILE_NAME: &str = "inkwell.png"; +const INSTALLED_APPIMAGE_NAME: &str = "Inkwell.AppImage"; #[derive(Serialize)] pub struct IntegrationStatus { @@ -89,7 +89,7 @@ pub fn integrate_desktop() -> Result { // 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/thoughtsync` inside it (issue 2075). + // filename — AppRun execs `usr/bin/inkwell` inside it (issue 2075). 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}"))?; @@ -97,13 +97,13 @@ pub fn integrate_desktop() -> Result { let contents = format!( "[Desktop Entry]\n\ Type=Application\n\ - Name=ThoughtSync\n\ + Name=Inkwell\n\ Comment=Capture a fleeting thought in a second\n\ Exec={exec} %U\n\ Icon={icon}\n\ Terminal=false\n\ Categories=Utility;Office;\n\ - StartupWMClass=thoughtsync\n", + StartupWMClass=inkwell\n", exec = installed.to_string_lossy(), icon = icon.to_string_lossy(), ); @@ -122,7 +122,7 @@ pub fn unintegrate_desktop() -> Result { if let Some(icon) = icon_path() { remove_if_exists(&icon)?; } - // Intentionally keep ~/Applications/ThoughtSync.AppImage: it may be the running + // Intentionally keep ~/Applications/Inkwell.AppImage: it may be the running // binary, and removing it would delete the app the user is using. Un-integrate // only removes the menu entry. refresh_desktop_database(&home); diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index bac1e9c..87544de 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,16 +1,17 @@ -//! ThoughtSync desktop (Tauri v2). +//! 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 //! `invoke`. //! //! This crate is the DESKTOP WRAPPER, not the core. The on-device SQLite store and -//! the sync engine live in `thoughtsync-core`, shared with the Android client; what +//! the sync engine live in `inkwell-core`, shared with the Android client; what //! remains here is the Tauri command surface (`commands`), desktop integration //! (menu-entry install for the Linux AppImage), the in-app updater, and boot. mod capture; mod commands; +mod crossover; mod integration; mod update; @@ -37,7 +38,7 @@ mod update; /// 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. -const DISPLAY_VERSION: Option<&str> = option_env!("THOUGHTSYNC_DISPLAY_VERSION"); +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 /// private so no caller can reach past the fallback. @@ -45,11 +46,11 @@ pub(crate) fn display_version() -> &'static str { DISPLAY_VERSION.unwrap_or("unknown") } -// The store and the sync engine live in the shared `thoughtsync-core` crate, which +// 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. -use thoughtsync_core::{local, sync}; +use inkwell_core::{local, sync}; pub fn run() { use tauri_plugin_log::{Target, TargetKind}; @@ -58,7 +59,7 @@ pub fn run() { // The core cannot work this out — it is compiled into the Android app too — so // the header says "desktop" only because the desktop says so here, and carries // the build a person can read rather than the core crate's own version. - sync::compat::set_client_agent("thoughtsync-desktop", display_version()); + sync::compat::set_client_agent("inkwell-desktop", display_version()); #[cfg(target_os = "linux")] harden_linux_webkit_rendering(); @@ -111,11 +112,17 @@ pub fn run() { log_environment(app); paint_window_before_the_webview_does(app); // The on-device store lives in the platform app-data dir (e.g. Linux - // ~/.local/share/com.fabledsword.thoughtsync/thoughtsync.db), created on + // ~/.local/share/com.fabledsword.inkwell/inkwell.db), created on // first launch. This is what makes the app work with no server or login. let dir = app.path().app_data_dir()?; + // Before the store opens: an install from when this app was ThoughtSync + // has its notes in the old identifier's dir, beside this one. + if let Some(root) = dir.parent() { + let legacy = root.join(crossover::LEGACY_IDENTIFIER); + log_crossover(&legacy, crossover::adopt_legacy_data(&legacy, &dir)); + } std::fs::create_dir_all(&dir)?; - let db_path = dir.join("thoughtsync.db"); + let db_path = dir.join(crossover::DB_FILE); log::info!("opening local store: {}", db_path.display()); let db = local::open(&db_path)?; log::info!("local store ready — {}", local::summary(&db)); @@ -261,6 +268,17 @@ fn log_event(level: String, message: String) { } } +/// The startup log line for the ThoughtSync → Inkwell data crossover (crossover.rs). +fn log_crossover(legacy: &std::path::Path, result: std::io::Result) { + match result { + Ok(crossover::Outcome::Moved(n)) => { + log::info!("moved {n} entries from {}", legacy.display()) + } + Ok(outcome) => log::debug!("ThoughtSync data crossover: {outcome:?}"), + Err(e) => log::warn!("could not move data from {}: {e}", legacy.display()), + } +} + /// Log the app version and the environment that determines whether the window /// renders — the first thing to check when a build works on one machine but not /// another. diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 67b61b1..db882da 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -3,5 +3,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - thoughtsync_desktop_lib::run() + inkwell_desktop_lib::run() } diff --git a/desktop/src-tauri/src/update.rs b/desktop/src-tauri/src/update.rs index 0286d30..10d3bc5 100644 --- a/desktop/src-tauri/src/update.rs +++ b/desktop/src-tauri/src/update.rs @@ -12,7 +12,7 @@ //! is the whole point of the change. NOTHING HERE MOVED: this code only ever read //! `/latest.json`, and that is still where the manifest lands. //! -//! The feed lives on Fabled-Git rather than on a ThoughtSync server, deliberately: +//! The feed lives on Fabled-Git rather than on an Inkwell server, 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. @@ -27,10 +27,10 @@ use serde::{Deserialize, Serialize}; use tauri::State; use tauri_plugin_updater::UpdaterExt; -use thoughtsync_core::local::{store, Db}; +use inkwell_core::local::{store, Db}; /// Where the manifests live. Fixed tags, so these URLs are permanent. -const FEED_BASE: &str = "https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download"; +const FEED_BASE: &str = "https://git.fabledsword.com/bvandeusen/inkwell/releases/download"; const CHANNEL_PREF: &str = "update_channel"; @@ -330,7 +330,7 @@ mod tests { use std::sync::atomic::{AtomicU32, Ordering}; fn db() -> Db { - thoughtsync_core::local::open_in_memory().expect("in-memory db") + inkwell_core::local::open_in_memory().expect("in-memory db") } /// A scratch path nothing else will collide with. Process id keeps concurrent diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index acc255c..796465a 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,9 +1,9 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "ThoughtSync", - "mainBinaryName": "thoughtsync", + "productName": "Inkwell", + "mainBinaryName": "inkwell", "version": "0.2.0", - "identifier": "com.fabledsword.thoughtsync", + "identifier": "com.fabledsword.inkwell", "build": { "frontendDist": "../../frontend/dist", "devUrl": "http://localhost:5173", @@ -31,7 +31,7 @@ "plugins": { "updater": { "endpoints": [ - "https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download/stable/latest.json" + "https://git.fabledsword.com/bvandeusen/inkwell/releases/download/stable/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkwRTk2RkVBMkY2RDlCNkEKUldScW0yMHY2bS9wa0VBdWFpM3c1d2trQnlNVUJXUUtwZXBzQjduM3FRVzdGa3dXNGxObkZFV28K", "windows": { @@ -43,8 +43,14 @@ "active": true, "targets": ["deb", "appimage"], "category": "Utility", + "linux": { + "deb": { + "conflicts": ["thought-sync"], + "replaces": ["thought-sync"] + } + }, "shortDescription": "Local-first Keep-style thought capture.", - "longDescription": "ThoughtSync captures a fleeting thought in a second on a Keep-style board and grows into a lightweight second brain. Local-first: works fully offline on an on-device store, and optionally syncs to your self-hosted ThoughtSync server.", + "longDescription": "Inkwell captures a fleeting thought in a second on a Keep-style board and grows into a lightweight second brain. Local-first: works fully offline on an on-device store, and optionally syncs to your self-hosted Inkwell server.", "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/frontend/src/desktop/bridge.ts b/frontend/src/desktop/bridge.ts index 2c4237a..cdcb16e 100644 --- a/frontend/src/desktop/bridge.ts +++ b/frontend/src/desktop/bridge.ts @@ -223,7 +223,7 @@ export interface CaptureShortcut { export const SUGGESTED_CAPTURE_SHORTCUT = "CommandOrControl+Shift+N"; /** Fired at the main window after a capture is saved. */ -const CAPTURED_EVENT = "thoughtsync://captured"; +const CAPTURED_EVENT = "inkwell://captured"; export const capture = { shortcut: () => invoke("capture_shortcut_get"), diff --git a/packaging/fetch-clients.sh b/packaging/fetch-clients.sh index 6d74096..52bdf7a 100755 --- a/packaging/fetch-clients.sh +++ b/packaging/fetch-clients.sh @@ -108,7 +108,7 @@ fi # serves. bake_desktop() { desk="$dest/.desktop-release.json" - if ! fetch "$BASE/thoughtsync-desktop.json" "$desk"; then + if ! fetch "$BASE/inkwell-desktop.json" "$desk"; then echo "::warning::No desktop release on the $channel channel — this image ships without desktop clients." rm -f "$desk" return 0 @@ -132,10 +132,10 @@ bake_desktop() { # # `||` for row in \ - "linux-deb|ThoughtSync_${key}_amd64.deb|inkwell.deb" \ - "linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \ - "linux-appimage|ThoughtSync_${key}_amd64.AppImage|inkwell.AppImage" \ - "windows|ThoughtSync_${key}_x64-setup.exe|inkwell-setup.exe" + "linux-deb|Inkwell_${key}_amd64.deb|inkwell.deb" \ + "linux-pacman|inkwell-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \ + "linux-appimage|Inkwell_${key}_amd64.AppImage|inkwell.AppImage" \ + "windows|Inkwell_${key}_x64-setup.exe|inkwell-setup.exe" do id="${row%%|*}"; rest="${row#*|}" remote="${rest%%|*}"; local_name="${rest#*|}" diff --git a/packaging/release-notes.sh b/packaging/release-notes.sh index 48fecbf..432d012 100755 --- a/packaging/release-notes.sh +++ b/packaging/release-notes.sh @@ -46,7 +46,7 @@ else header="First release." fi -printf 'ThoughtSync %s\n\n%s\n\n' "$tag" "$header" +printf 'Inkwell %s\n\n%s\n\n' "$tag" "$header" # `--no-merges`: a merge commit's subject is "Merge branch ..." and says nothing # about what shipped. The commits it brought in are listed individually, which is