Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
256fba3610 | ||
|
|
fe6f0746b2 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<CargoNdkBuild>("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)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
Adaptive icon. minSdk is 26, so this is the ONLY icon Android will ask for —
|
||||
no legacy raster fallback is needed.
|
||||
|
||||
The foreground is the shared maskable asset the web app already ships
|
||||
(frontend/public/icon-maskable-512.png), which is drawn with the safe-zone
|
||||
padding adaptive icons require. Reusing it means the phone, the web app and the
|
||||
desktop all wear the same face rather than three near-misses.
|
||||
The foreground is the inkwell mark alone on transparency, inside the 66dp safe
|
||||
circle; the yellow is the background colour. It is rendered by
|
||||
packaging/icons.py from the same drawing as the web and desktop icons, so all
|
||||
three wear one face. Transparent rather than a full-bleed tile because the
|
||||
monochrome layer below reuses it: a themed icon draws its alpha as a silhouette,
|
||||
and an opaque foreground would come out as a solid square.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.9 KiB |
@@ -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
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! Invoked by Gradle (see android/app/build.gradle.kts) as:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --locked -p thoughtsync-uniffi-bindgen -- \
|
||||
//! generate --library <path/to/libthoughtsync_ffi.so> \
|
||||
//! cargo run --locked -p inkwell-uniffi-bindgen -- \
|
||||
//! generate --library <path/to/libinkwell_ffi.so> \
|
||||
//! --language kotlin --out-dir <build/generated/uniffi>
|
||||
//! ```
|
||||
//!
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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<Arc<Self>, 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)
|
||||
));
|
||||
|
||||
@@ -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<thoughtsync_core::local::derive::DerivedItem> for BodyItem {
|
||||
fn from(i: thoughtsync_core::local::derive::DerivedItem) -> Self {
|
||||
let thoughtsync_core::local::derive::DerivedItem {
|
||||
impl From<inkwell_core::local::derive::DerivedItem> 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<thoughtsync_core::local::derive::DerivedTag> for BodyTag {
|
||||
fn from(t: thoughtsync_core::local::derive::DerivedTag) -> Self {
|
||||
let thoughtsync_core::local::derive::DerivedTag {
|
||||
impl From<inkwell_core::local::derive::DerivedTag> 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<thoughtsync_core::sync::client::ClientRelease> for ClientUpdate {
|
||||
fn from(r: thoughtsync_core::sync::client::ClientRelease) -> Self {
|
||||
impl From<inkwell_core::sync::client::ClientRelease> 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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = 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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -45,13 +45,14 @@ cd desktop/src-tauri && cargo tauri dev
|
||||
## Build (Linux)
|
||||
|
||||
```sh
|
||||
cargo tauri icon "$(git rev-parse --show-toplevel)/frontend/public/icon.svg"
|
||||
cd desktop/src-tauri && cargo tauri build # -> .deb + .AppImage
|
||||
```
|
||||
|
||||
App icons are **generated** from the frontend's `icon.svg` via `cargo tauri icon`
|
||||
(not committed; CI does this before `cargo tauri build`). Bundle targets: `deb`,
|
||||
`appimage` (Linux-first; Windows/macOS later, no code changes expected).
|
||||
App icons are **generated and committed**: `python3 packaging/icons.py` draws the
|
||||
inkwell mark once and renders every web, desktop and Android icon from it,
|
||||
including `app-icon.png` and `icons/*.png` here. The Windows lane derives its
|
||||
`.ico` from `app-icon.png` with `cargo tauri icon` at build time. Bundle targets:
|
||||
`deb`, `appimage` (Linux-first; Windows/macOS later, no code changes expected).
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 <noreply@fabledsword.com>"
|
||||
PACKAGER="Inkwell CI <noreply@fabledsword.com>"
|
||||
|
||||
# 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)"
|
||||
|
||||
@@ -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:-<unset>}', 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
|
||||
|
||||
@@ -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" <<EOF
|
||||
cat > "$apps_menu/inkwell.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ThoughtSync
|
||||
Name=Inkwell
|
||||
Comment=Capture a fleeting thought in a second
|
||||
Exec=$dest %U
|
||||
Icon=$icon_ref
|
||||
Terminal=false
|
||||
Categories=Utility;Office;
|
||||
StartupWMClass=thoughtsync
|
||||
StartupWMClass=inkwell
|
||||
EOF
|
||||
|
||||
have update-desktop-database && update-desktop-database "$apps_menu" >/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
|
||||
|
||||
@@ -148,8 +148,8 @@ fi
|
||||
|
||||
echo "==> Creating release for $TAG"
|
||||
BODY=$(cat <<JSON
|
||||
{"tag_name":"$TAG","name":"ThoughtSync $CHANNEL_LABEL","draft":false,"prerelease":$RELEASE_PRERELEASE,
|
||||
"body":"ThoughtSync $CHANNEL_LABEL.\n\n**Desktop**\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | $INSTALL_TAIL\n\`\`\`\n\n**Android** — \`thoughtsync.apk\`. Copy it and \`thoughtsync-android.json\` into your server's \`/var/thoughtsync/client/\` and the server will offer it to your devices; see docs/android-distribution.md.$CHANNEL_NOTE"}
|
||||
{"tag_name":"$TAG","name":"Inkwell $CHANNEL_LABEL","draft":false,"prerelease":$RELEASE_PRERELEASE,
|
||||
"body":"Inkwell $CHANNEL_LABEL.\n\n**Desktop**\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | $INSTALL_TAIL\n\`\`\`\n\n**Android** — \`thoughtsync.apk\`. Copy it and \`thoughtsync-android.json\` into your server's \`/var/inkwell/client/\` and the server will offer it to your devices; see docs/android-distribution.md.$CHANNEL_NOTE"}
|
||||
JSON
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ JSON
|
||||
# whether it has a JSON encoder; this script cannot assume python3 is on PATH in
|
||||
# every image that sources it.
|
||||
if [ -n "${RELEASE_BODY_JSON:-}" ]; then
|
||||
BODY="{\"tag_name\":\"$TAG\",\"name\":\"ThoughtSync $CHANNEL_LABEL\",\"draft\":false,\"prerelease\":$RELEASE_PRERELEASE,\"body\":\"$RELEASE_BODY_JSON\"}"
|
||||
BODY="{\"tag_name\":\"$TAG\",\"name\":\"Inkwell $CHANNEL_LABEL\",\"draft\":false,\"prerelease\":$RELEASE_PRERELEASE,\"body\":\"$RELEASE_BODY_JSON\"}"
|
||||
fi
|
||||
# 409 = a release for this tag already exists (re-run) — fall through to lookup.
|
||||
release="$(ALLOW_CODES=409 api POST "$API/releases" -H "Content-Type: application/json" -d "$BODY")"
|
||||
|
||||
@@ -158,8 +158,8 @@ replace_asset "$work/latest.json" "latest.json"
|
||||
# downloads a bundle measures the bytes it actually got, which is the only way to
|
||||
# tell a truncated download from a whole one.
|
||||
printf '{\n "version_name": "%s",\n "version_code": "%s"\n}\n' \
|
||||
"$DISPLAY_VERSION" "$APP_VERSION" > "$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"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 37 KiB |
@@ -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"]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 17 KiB |
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,24 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Inkwell">
|
||||
<!-- Maskable variant: full-bleed background so platform masks (circle, squircle,
|
||||
rounded-square) never clip the glyph. The graph sits inside the central 80%
|
||||
safe zone. Same mark as icon.svg. -->
|
||||
rounded-square) never clip the mark, which sits inside the central 80% safe
|
||||
zone. Same mark as icon.svg. -->
|
||||
<rect width="512" height="512" fill="#F5C518"/>
|
||||
<g stroke="#18181b" stroke-linecap="round" fill="none">
|
||||
<g stroke-width="14" opacity="0.26">
|
||||
<path d="M256 150 L166 336"/>
|
||||
<path d="M166 336 L346 336"/>
|
||||
<path d="M346 336 L256 150"/>
|
||||
</g>
|
||||
<g stroke-width="22">
|
||||
<path d="M256 268 L256 150"/>
|
||||
<path d="M256 268 L166 336"/>
|
||||
<path d="M256 268 L346 336"/>
|
||||
<g transform="translate(256 256) scale(0.8) translate(-262 -226)">
|
||||
<defs>
|
||||
<mask id="inkwell-cuts" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
||||
<rect width="512" height="512" fill="#fff"/>
|
||||
<g stroke="#000" fill="none">
|
||||
<g transform="translate(256 232) rotate(34)">
|
||||
<path d="M1 -52 L3 -232" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 -128 L30 -108 M64 -176 L32 -152" stroke-width="8"/>
|
||||
</g>
|
||||
<path d="M170 344 C 172 318 188 300 214 292" stroke-width="10" stroke-linecap="round"/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask="url(#inkwell-cuts)">
|
||||
<g transform="translate(256 232) rotate(34)">
|
||||
<path d="M0 6 L0 -46" stroke="#18181b" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M0 -34 C 46 -78 58 -176 4 -252 C -34 -196 -40 -104 0 -34 Z" fill="#18181b"/>
|
||||
</g>
|
||||
<path fill="#18181b" d="M222 262 C 166 268 140 300 140 338 L140 374 C140 396 156 410 178 410
|
||||
L334 410 C356 410 372 396 372 374 L372 338 C372 300 346 268 290 262 Z"/>
|
||||
<rect x="214" y="236" width="84" height="34" rx="9" fill="#18181b"/>
|
||||
<rect x="200" y="226" width="112" height="22" rx="11" fill="#18181b"/>
|
||||
</g>
|
||||
<g fill="#18181b">
|
||||
<circle cx="256" cy="150" r="34"/>
|
||||
<circle cx="166" cy="336" r="34"/>
|
||||
<circle cx="346" cy="336" r="34"/>
|
||||
<circle cx="256" cy="268" r="26"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,25 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Inkwell">
|
||||
<!-- Rounded-tile app icon: brand-yellow background + a small knowledge-graph
|
||||
constellation (a hub node linked to three thoughts) evoking linked notes. -->
|
||||
<!-- Rounded-tile app icon: brand yellow, and the inkwell — a black inkpot with a
|
||||
quill standing in it. Source for every raster icon; see step 5 of milestone 481. -->
|
||||
<rect width="512" height="512" rx="112" fill="#F5C518"/>
|
||||
<g stroke="#18181b" stroke-linecap="round" fill="none">
|
||||
<!-- faint outer web -->
|
||||
<g stroke-width="14" opacity="0.26">
|
||||
<path d="M256 150 L166 336"/>
|
||||
<path d="M166 336 L346 336"/>
|
||||
<path d="M346 336 L256 150"/>
|
||||
</g>
|
||||
<!-- hub-and-spoke links -->
|
||||
<g stroke-width="22">
|
||||
<path d="M256 268 L256 150"/>
|
||||
<path d="M256 268 L166 336"/>
|
||||
<path d="M256 268 L346 336"/>
|
||||
<g transform="translate(256 256) scale(0.92) translate(-262 -226)">
|
||||
<defs>
|
||||
<mask id="inkwell-cuts" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
||||
<rect width="512" height="512" fill="#fff"/>
|
||||
<g stroke="#000" fill="none">
|
||||
<g transform="translate(256 232) rotate(34)">
|
||||
<path d="M1 -52 L3 -232" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 -128 L30 -108 M64 -176 L32 -152" stroke-width="8"/>
|
||||
</g>
|
||||
<path d="M170 344 C 172 318 188 300 214 292" stroke-width="10" stroke-linecap="round"/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask="url(#inkwell-cuts)">
|
||||
<g transform="translate(256 232) rotate(34)">
|
||||
<path d="M0 6 L0 -46" stroke="#18181b" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M0 -34 C 46 -78 58 -176 4 -252 C -34 -196 -40 -104 0 -34 Z" fill="#18181b"/>
|
||||
</g>
|
||||
<path fill="#18181b" d="M222 262 C 166 268 140 300 140 338 L140 374 C140 396 156 410 178 410
|
||||
L334 410 C356 410 372 396 372 374 L372 338 C372 300 346 268 290 262 Z"/>
|
||||
<rect x="214" y="236" width="84" height="34" rx="9" fill="#18181b"/>
|
||||
<rect x="200" y="226" width="112" height="22" rx="11" fill="#18181b"/>
|
||||
</g>
|
||||
<g fill="#18181b">
|
||||
<circle cx="256" cy="150" r="34"/>
|
||||
<circle cx="166" cy="336" r="34"/>
|
||||
<circle cx="346" cy="336" r="34"/>
|
||||
<circle cx="256" cy="268" r="26"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 1.5 KiB |
@@ -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<CaptureShortcut>("capture_shortcut_get"),
|
||||
|
||||
@@ -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() {
|
||||
#
|
||||
# `<platform id>|<published name>|<name on disk>`
|
||||
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#*|}"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Every icon Inkwell ships, from one drawing.
|
||||
|
||||
The mark is a black inkpot with a quill standing in it, on the brand yellow
|
||||
(Scribe note 5071). It is drawn ONCE below, in a 512 box, and each variant places it
|
||||
with its own scale, because each platform crops differently:
|
||||
|
||||
- icon.svg the rounded tile: web favicon/PWA "any", desktop app icon
|
||||
- icon-maskable.svg full bleed, mark inside the central 80% (PWA "maskable")
|
||||
- Android foreground mark alone on transparency inside the 66dp safe circle of a
|
||||
108dp adaptive canvas; the yellow is the background layer, and
|
||||
the transparent mark is also what themed (monochrome) icons need
|
||||
|
||||
Run from the repo root after changing the drawing; needs rsvg-convert (librsvg).
|
||||
Generated output is committed, like any other generated file.
|
||||
|
||||
python3 packaging/icons.py
|
||||
"""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INK = "#18181b"
|
||||
GOLD = "#F5C518"
|
||||
|
||||
# The quill is drawn upright at the pot mouth (0,0), then rotated into place.
|
||||
QUILL_AT = "translate(256 232) rotate(34)"
|
||||
|
||||
QUILL_INK = f'''
|
||||
<g transform="{QUILL_AT}">
|
||||
<path d="M0 6 L0 -46" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M0 -34 C 46 -78 58 -176 4 -252 C -34 -196 -40 -104 0 -34 Z" fill="{INK}"/>
|
||||
</g>'''
|
||||
|
||||
POT_INK = f'''
|
||||
<path fill="{INK}" d="M222 262 C 166 268 140 300 140 338 L140 374 C140 396 156 410 178 410
|
||||
L334 410 C356 410 372 396 372 374 L372 338 C372 300 346 268 290 262 Z"/>
|
||||
<rect x="214" y="236" width="84" height="34" rx="9" fill="{INK}"/>
|
||||
<rect x="200" y="226" width="112" height="22" rx="11" fill="{INK}"/>'''
|
||||
|
||||
# The detail — the quill's shaft, two splits in its vane, the glint on the pot — is
|
||||
# CUT OUT of the ink rather than painted over it in yellow. On the tile the two look
|
||||
# the same; on Android's transparent foreground only a cut-out is right, because a
|
||||
# themed icon draws the foreground's alpha as a silhouette, and painted detail
|
||||
# would come out as solid, with the vane splits sticking out past its edge.
|
||||
CUTS = '''
|
||||
<g transform="{at}">
|
||||
<path d="M1 -52 L3 -232" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 -128 L30 -108 M64 -176 L32 -152" stroke-width="8"/>
|
||||
</g>
|
||||
<path d="M170 344 C 172 318 188 300 214 292" stroke-width="10" stroke-linecap="round"/>'''.format(at=QUILL_AT)
|
||||
|
||||
GLYPH = f'''
|
||||
<defs>
|
||||
<mask id="inkwell-cuts" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
||||
<rect width="512" height="512" fill="#fff"/>
|
||||
<g stroke="#000" fill="none">{CUTS}
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask="url(#inkwell-cuts)">{QUILL_INK}{POT_INK}
|
||||
</g>'''
|
||||
# Glyph bounds, measured from a render: centre the mark, then scale per variant.
|
||||
CX, CY = 262, 226
|
||||
|
||||
def place(scale, cy=256):
|
||||
return f'translate(256 {cy}) scale({scale}) translate({-CX} {-CY})'
|
||||
|
||||
def svg(body, label=True):
|
||||
aria = ' role="img" aria-label="Inkwell"' if label else ''
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"{aria}>\n{body}\n</svg>\n'
|
||||
|
||||
def tile(scale=0.92):
|
||||
return svg(f''' <!-- Rounded-tile app icon: brand yellow, and the inkwell — a black inkpot with a
|
||||
quill standing in it. Source for every raster icon; see step 5 of milestone 481. -->
|
||||
<rect width="512" height="512" rx="112" fill="{GOLD}"/>
|
||||
<g transform="{place(scale)}">{GLYPH}
|
||||
</g>''')
|
||||
|
||||
def maskable(scale=0.8):
|
||||
return svg(f''' <!-- Maskable variant: full-bleed background so platform masks (circle, squircle,
|
||||
rounded-square) never clip the mark, which sits inside the central 80% safe
|
||||
zone. Same mark as icon.svg. -->
|
||||
<rect width="512" height="512" fill="{GOLD}"/>
|
||||
<g transform="{place(scale)}">{GLYPH}
|
||||
</g>''')
|
||||
|
||||
def foreground(scale=0.64):
|
||||
# Android adaptive foreground: the mark alone on transparency, inside the 66dp
|
||||
# safe circle of a 108dp canvas. The yellow lives in the background layer.
|
||||
return svg(f''' <g transform="{place(scale)}">{GLYPH}
|
||||
</g>''', label=False)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
WEB = ROOT / "frontend" / "public"
|
||||
DESKTOP = ROOT / "desktop" / "src-tauri"
|
||||
ANDROID = ROOT / "android" / "app" / "src" / "main" / "res" / "mipmap-xxxhdpi"
|
||||
|
||||
|
||||
def render(svg_text: str, size: int, out: Path, background: str | None = None) -> None:
|
||||
cmd = ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out)]
|
||||
if background:
|
||||
cmd += ["-b", background]
|
||||
subprocess.run(cmd, input=svg_text.encode(), check=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
icon, mask, fg = tile(), maskable(), foreground()
|
||||
(WEB / "icon.svg").write_text(icon)
|
||||
(WEB / "icon-maskable.svg").write_text(mask)
|
||||
|
||||
render(icon, 32, WEB / "favicon-32.png")
|
||||
render(icon, 192, WEB / "icon-192.png")
|
||||
render(icon, 512, WEB / "icon-512.png")
|
||||
render(mask, 512, WEB / "icon-maskable-512.png")
|
||||
# iOS rounds the corners itself and shows transparency as black.
|
||||
render(mask, 180, WEB / "apple-touch-icon.png")
|
||||
|
||||
render(icon, 1024, DESKTOP / "app-icon.png")
|
||||
render(icon, 32, DESKTOP / "icons" / "32x32.png")
|
||||
render(icon, 128, DESKTOP / "icons" / "128x128.png")
|
||||
render(icon, 256, DESKTOP / "icons" / "128x128@2x.png")
|
||||
render(icon, 512, DESKTOP / "icons" / "icon.png")
|
||||
|
||||
render(fg, 512, ANDROID / "ic_launcher_foreground.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
|
||||