rename: the desktop is Inkwell — crates, Tauri identity, data move, packaging
Step 3 of milestone 481 (Scribe note 5071: a full rename).
- crates thoughtsync-{core,desktop,ffi,uniffi-bindgen} → inkwell-*, the
Cargo.lock entries moved to match (checked with `cargo metadata --locked`)
- Tauri: productName "Inkwell", identifier com.fabledsword.inkwell, binary
`inkwell`, updater feed on bvandeusen/inkwell, store file inkwell.db
- client agent inkwell-desktop, headers X-Inkwell-Client/-Protocol (the server
reads neither), capture event inkwell://captured, display-version env
- .deb: conflicts + replaces thought-sync, so the updater's install retires the
old package instead of colliding on it. kebab-case("Inkwell") is `inkwell`, so
the package name finally matches the command and verify.sh now asserts it
- pacman: inkwell, conflicting with and replacing thoughtsync and
thoughtsync-desktop
- AppImage ~/Applications/Inkwell.AppImage, menu entry inkwell.desktop,
installer, release titles, desktop asset names in fetch-clients.sh
The one shim, chosen by the operator because it is the only copy of a
local-first user's notes: crossover.rs moves the old
com.fabledsword.thoughtsync app-data dir's contents into the new one on startup,
before the store opens, renaming thoughtsync.db and its -wal/-shm with it. It
skips when the new dir already has a store, and anything already in the new dir
wins (the installer writes its channel marker there first). Tested.
Android's Kotlin side (package, applicationId, uniffi class) is step 4. Its
release asset names stay thoughtsync.* until then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Outcome> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<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/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<IntegrationStatus, String> {
|
||||
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<IntegrationStatus, String> {
|
||||
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);
|
||||
|
||||
@@ -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<crossover::Outcome>) {
|
||||
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.
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
thoughtsync_desktop_lib::run()
|
||||
inkwell_desktop_lib::run()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! 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 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user