diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fe6676..5fb910ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] — 2026-06-02 + +### Added +- **Mobile support — iOS and Android** — Tauri iOS and Android targets with phone stack navigation (full-screen tree, slide-right to note, back button), hamburger menu, long-press context menu, full-screen dialogs, and adaptive layout via `useLayout()` hook. Safe areas handled natively on both platforms. Convenience scripts `ios-dev.sh` and `android-dev.sh` for simulator/emulator launches (#207). + ### Fixed - **AppImage `.DirIcon` symlink** — After bundling, the release workflow now repacks each AppImage replacing the absolute `.DirIcon` symlink baked by tauri-bundler with a relative one, fixing installation failures in app-manager and similar tools (workaround for tauri-apps/tauri#15110, PR #203). @@ -527,6 +532,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Platform-aware menus: macOS app menu, Edit menu with standard shortcuts; Tools menu for Operations Log and Script Manager. - Cross-platform release workflow via GitHub Actions (macOS, Windows, Linux). +[1.2.0]: https://github.com/2pisoftware/krillnotes/compare/v1.1.1...v1.2.0 +[1.1.1]: https://github.com/2pisoftware/krillnotes/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/2pisoftware/krillnotes/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.com/2pisoftware/krillnotes/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/2pisoftware/krillnotes/compare/v0.9.2...v1.0.0 [0.9.2]: https://github.com/2pisoftware/krillnotes/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/2pisoftware/krillnotes/compare/v0.9.0...v0.9.1 diff --git a/MOBILE_README.md b/MOBILE_README.md new file mode 100644 index 00000000..bb2420c3 --- /dev/null +++ b/MOBILE_README.md @@ -0,0 +1,89 @@ +# Mobile Development + +Krillnotes runs on iOS and Android via Tauri v2. The desktop codebase is shared — platform-specific code is gated with `#[cfg(desktop)]` / `#[cfg(not(desktop))]` in Rust and the `useLayout()` hook in React. + +## Requirements + +### Shared (both platforms) + +- **Rust via rustup** (not Homebrew) — Homebrew's cargo lacks cross-compilation targets +- **Node.js + npm** +- **Tauri CLI** — installed via `npm install` in `krillnotes-desktop/` + +### iOS + +- **Xcode** (full install, not just Command Line Tools) +- **iOS simulator runtimes** (installed via Xcode → Settings → Platforms) +- **Rust iOS targets:** + ```bash + rustup target add aarch64-apple-ios aarch64-apple-ios-sim + ``` +- **xcodegen** — `brew install xcodegen` (regenerates Xcode project from `project.yml`) + +### Android + +- **Android Studio** (provides SDK, NDK, and bundled JDK) +- **Android SDK** — default location: `~/Library/Android/sdk` +- **Android NDK** — installed via Android Studio SDK Manager +- **Rust Android targets:** + ```bash + rustup target add aarch64-linux-android + ``` +- **An AVD (emulator)** — create via Android Studio Device Manager (e.g. Pixel 8 API 35) + +## Quick Start + +All commands run from `krillnotes-desktop/`: + +### iOS Simulator + +```bash +./ios-dev.sh # Launches iPhone 17 Pro simulator (default) +./ios-dev.sh 2 # Launches iPad (A16) — pass simulator index +``` + +The script: +1. Kills any lingering Vite dev server on port 1420 +2. Boots the iOS simulator +3. Sets rustup cargo in PATH (required for cross-compilation) +4. Runs `tauri ios dev` which starts Vite + builds Rust + deploys to simulator + +On first run, the full Rust cross-compile takes a few minutes. Subsequent runs are incremental. + +### Android Emulator + +```bash +./android-dev.sh +``` + +The script: +1. Kills any lingering Vite dev server on port 1420 +2. Sets rustup cargo in PATH +3. Configures ANDROID_HOME, NDK_HOME, and JAVA_HOME +4. Runs `tauri android dev` which starts Vite + builds Rust + deploys to emulator + +**Note:** Start the Android emulator first (via Android Studio or `emulator -avd `) before running the script. + +### Desktop (unchanged) + +```bash +npm run tauri dev +``` + +## Important Notes + +- **Always use rustup cargo**, not Homebrew cargo. The scripts handle this automatically. If building manually, prefix with: `PATH="$(dirname $(rustup which cargo)):$PATH"` +- **Port 1420** — Vite dev server runs here. Only one platform can run at a time. Kill the previous before switching. +- **Xcode standalone builds don't work** — `Cmd+R` in Xcode requires the Tauri dev server running. Always launch via `./ios-dev.sh` or `tauri ios dev`. +- **Hot reload** — Vite hot-reloads React/CSS changes on both platforms. Rust changes require a rebuild (the script handles this). +- **`gen/` directories** — `src-tauri/gen/apple/` and `src-tauri/gen/android/` contain platform-specific project files. The iOS `project.yml` has been customized (rustup PATH fix) — don't regenerate without preserving this. + +## Troubleshooting + +| Problem | Fix | +|---------|-----| +| `can't find crate for core` on iOS | Homebrew cargo is being used. Ensure rustup cargo is first in PATH. | +| `Unable to lookup in current state: Shutdown` | Simulator isn't booted. Run `xcrun simctl boot ` or use `./ios-dev.sh` which boots automatically. | +| `failed to read missing addr file` | Tauri dev server isn't running. Don't build from Xcode directly — use `./ios-dev.sh`. | +| Port 1420 already in use | `lsof -ti:1420 \| xargs kill -9` | +| Android `JAVA_HOME` not set | Install Android Studio; the script uses its bundled JDK. | diff --git a/docs/superpowers/plans/2026-04-29-device-id-migration.md b/docs/superpowers/plans/2026-04-29-device-id-migration.md new file mode 100644 index 00000000..cee08beb --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-device-id-migration.md @@ -0,0 +1,145 @@ +# Plan 1a: Device ID Migration + +**Issue:** Child of #171 (Mobile support) +**Branch:** `mobile` +**Spec:** `docs/superpowers/specs/2026-04-29-mobile-support-design.md` § Preliminary: Device ID Migration + +## Context + +The codebase has three device identification mechanisms: + +1. **MAC-based** — `device.rs::get_device_id()` returns `device-<16 hex>` via `mac_address` crate. Called in 21 places across sync/relay commands (invites, swarm, relay_accounts, sync, identity, receive_poll) and export. +2. **Per-identity device UUID** — `identity::ensure_device_uuid(dir)` creates/reads a UUID file in each identity directory. Already persisted, already stable. +3. **Composite** — `{identity_uuid}:{device_uuid}` stored in `workspace_meta.device_id`. Used as the HLC node ID and in `RegisterDevice` operations. + +The MAC-based ID (1) caused relay sync bugs due to instability across network interfaces. It also blocks mobile (no MAC access on iOS/Android). The per-identity UUID (2) is already the right approach — we just need to make (1) use the same mechanism and remove the `mac_address` dependency. + +## Steps + +### Step 1: Create app-level device UUID + +**Files:** `krillnotes-core/src/core/device.rs` + +Replace `get_device_id()` with two functions: + +```rust +/// Read or create the app-level device UUID seed file. +/// File: {data_dir}/device_id (plain text, one UUID). +pub fn get_or_create_seed_device_id(data_dir: &Path) -> Result + +/// Full priority chain for resolving device ID for a workspace. +/// 1. workspace_meta has device_id → use it +/// 2. operations table has local device_id → use it, write to workspace_meta + seed file +/// 3. Seed file exists → use it, write to workspace_meta +/// 4. Generate device-{uuid}, write everywhere +pub fn resolve_device_id(conn: &Connection, data_dir: &Path) -> Result +``` + +`get_or_create_seed_device_id`: +- Read `{data_dir}/device_id` file +- If exists and non-empty → return trimmed contents +- If absent → generate `device-{Uuid::new_v4()}`, write to file, return it + +`resolve_device_id`: +- Query `SELECT value FROM workspace_meta WHERE key = 'device_id'` +- If found → return it (already migrated or set during creation) +- If absent → query `SELECT DISTINCT device_id FROM operations LIMIT 1` +- If found → write to `workspace_meta`, also write to seed file, return it +- If absent → call `get_or_create_seed_device_id(data_dir)`, write to `workspace_meta`, return it + +Remove all `mac_address` crate usage. The old `get_device_id()` function is deleted. + +### Step 2: Update Cargo.toml + +**File:** `krillnotes-core/Cargo.toml` + +- Remove `mac_address = "1.1"` dependency +- Keep `hostname = "0.3"` (used for human-readable device names, has graceful fallback) + +### Step 3: Update call sites in krillnotes-core + +**File:** `krillnotes-core/src/core/export.rs` + +The only core call site. Currently calls `get_device_id()` during export. Replace with: +- Accept `device_id: &str` as a parameter (passed from the Workspace struct which already holds it) +- Or read from `self.device_id` if export is a Workspace method + +### Step 4: Update Workspace::open() and init_core() + +**File:** `krillnotes-core/src/core/workspace/mod.rs` + +`open()`: +- Add `data_dir: &Path` parameter +- After opening DB, call `resolve_device_id(conn, data_dir)` to get/migrate the device ID +- Store result in `self.device_id` +- The existing composite device_id logic (`{identity_uuid}:{device_uuid}`) remains — `resolve_device_id` returns whatever is in workspace_meta, which is already composite for existing workspaces + +`init_core()`: +- Add `data_dir: &Path` parameter +- Use `get_or_create_seed_device_id(data_dir)` when constructing the initial device_id if no identity_dir is provided +- Existing logic for composite `{identity_uuid}:{device_uuid}` format stays as-is + +### Step 5: Update 21 call sites in krillnotes-desktop + +**Files:** `krillnotes-desktop/src-tauri/src/commands/` +- `invites.rs` (2 calls) +- `swarm.rs` (5 calls) +- `relay_accounts.rs` (3 calls) +- `sync.rs` (5 calls) +- `identity.rs` (1 call) +- `receive_poll.rs` (4 calls) + +All these call `get_device_id()` independently. Replace each with: +- Read the device_id from the Workspace instance (already available via AppState) OR +- Call `get_or_create_seed_device_id(&home_dir())` for operations that don't have a workspace context + +Pattern: most of these commands already have access to the workspace via `state.workspaces`. Extract `device_id` from the workspace struct instead of computing it fresh each time. This is more correct anyway — the device_id should come from the workspace context, not be independently derived. + +For commands that operate before a workspace is open (relay account management, identity operations), use `get_or_create_seed_device_id` with the home_dir path. + +### Step 6: Update Workspace::open() callers in lib.rs + +**File:** `krillnotes-desktop/src-tauri/src/lib.rs` (and commands/workspace.rs) + +All calls to `Workspace::open()` and `Workspace::init_core()` need the new `data_dir` parameter. Pass `&home_dir()` on desktop. On mobile (future), the Tauri app will pass the app sandbox directory. + +### Step 7: Migration gate in create_workspace command + +**File:** `krillnotes-desktop/src-tauri/src/commands/workspace.rs` + +In `create_workspace` command: +- Check if seed file exists at `home_dir()/device_id` +- If not → call `list_workspace_files()` (already exists in this file) +- If workspaces exist → return error: "Please open an existing workspace first to migrate your device identity" +- If no workspaces → proceed (fresh install, generate new UUID) + +### Step 8: Update tests + +**File:** `krillnotes-core/src/core/device.rs` (test module) + +- Remove MAC-based tests +- Add tests for `get_or_create_seed_device_id`: creates file on first call, reads same value on second call +- Add tests for `resolve_device_id`: priority chain (workspace_meta > operations > seed file > generate) +- Use temp directories for test isolation + +**Files:** Any existing tests that call `get_device_id()` — update to use new API. + +### Step 9: Verify and test + +- `cargo test -p krillnotes-core` — all tests pass +- `cd krillnotes-desktop && npx tsc --noEmit` — type check +- `cd krillnotes-desktop && npm run tauri dev` — manual test: + - Open existing workspace → device_id migrated to seed file + - Create new workspace → picks up seed file device_id + - Check relay sync still works with migrated ID + +## Commit sequence + +1. `refactor: replace MAC-based device ID with persisted UUID` — steps 1-6 +2. `feat: add migration gate for device ID on new workspace creation` — step 7 +3. `test: add device ID migration tests` — step 8 + +## Risks + +- **Relay identity break:** If the old MAC-based `get_device_id()` value was stored on the relay as the device identifier, switching to a UUID means the relay won't recognize the device. However, the MAC-based ID was already unstable (the problem we're fixing), so relay identity was already unreliable. The composite device_id in workspace_meta (which IS stable) is what matters for workspace sync. +- **Export compatibility:** Exported archives embed a device_id. Old exports have MAC-based IDs. Import should handle both formats (no format validation on the device_id string). diff --git a/docs/superpowers/plans/2026-04-29-mobile-scaffolding-layout.md b/docs/superpowers/plans/2026-04-29-mobile-scaffolding-layout.md new file mode 100644 index 00000000..e38af0a1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-mobile-scaffolding-layout.md @@ -0,0 +1,251 @@ +# Plan 1b: Tauri Mobile Scaffolding + Adaptive Layout + +**Issue:** Child of #171 (Mobile support) +**Branch:** `mobile` +**Depends on:** Plan 1a (Device ID migration) — must be completed first +**Spec:** `docs/superpowers/specs/2026-04-29-mobile-support-design.md` + +## Context + +With the device ID migration complete, the `krillnotes-core` crate compiles cleanly for mobile targets. This plan scaffolds Tauri iOS/Android targets from the existing `krillnotes-desktop` package and implements the adaptive layout with three breakpoints. + +## Prerequisites + +```bash +# Rust mobile targets +rustup target add aarch64-apple-ios aarch64-apple-ios-sim +rustup target add aarch64-linux-android + +# Xcode (full install) — for iOS +# Android Studio + NDK + JDK — for Android +``` + +## Steps + +### Step 1: Initialize Tauri mobile targets + +**Directory:** `krillnotes-desktop/` + +```bash +cd krillnotes-desktop +npm run tauri ios init +npm run tauri android init +``` + +This generates: +- `src-tauri/gen/apple/` — Xcode project +- `src-tauri/gen/android/` — Android Studio project + +Add `gen/` to `.gitignore` if not already (generated, platform-specific). + +Verify bare build compiles: +```bash +npm run tauri ios build -- --debug +npm run tauri android build -- --debug +``` + +### Step 2: Desktop-only guards in Rust + +**File:** `krillnotes-desktop/src-tauri/src/lib.rs` + +Gate desktop-only code with `#[cfg(desktop)]`: + +- Native menu bar construction (the `build_menu()` call and menu event handler) +- `close_window` command +- Multi-window logic in `run()` (window creation for second workspace, etc.) + +Gate the menu module: +**File:** `krillnotes-desktop/src-tauri/src/menu.rs` +- Add `#![cfg(desktop)]` at the top, or gate the import in `lib.rs` + +**File:** `krillnotes-desktop/src-tauri/src/locales.rs` +- Same — only needed for native menu string translation + +The mobile entry point should be minimal: +```rust +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + // ... shared setup ... + + #[cfg(desktop)] + { + // menu, multi-window, system tray + } + + // ... shared event loop ... +} +``` + +### Step 3: Mobile capabilities + +**File:** `krillnotes-desktop/src-tauri/capabilities/default.json` + +Review existing capabilities. May need adjustments for mobile: +- Remove file associations (desktop-only) +- Ensure `core:default` permissions work on mobile +- Add mobile-specific permissions as needed (initially minimal) + +### Step 4: First mobile boot test + +Run on iOS simulator and Android device/emulator: + +```bash +cd krillnotes-desktop +npm run tauri ios dev # iPad simulator +npm run tauri android dev # Android device via USB +``` + +Goal: app launches, existing desktop UI renders (probably badly on phone). This validates the full toolchain: Rust cross-compile, SQLCipher on mobile, React in mobile WebView. + +Fix any compilation or runtime errors before proceeding to layout work. + +### Step 5: `useLayout()` hook + +**File:** `krillnotes-desktop/src/hooks/useLayout.ts` (new) + +```typescript +export type Layout = "phone" | "tablet" | "desktop"; + +export function useLayout(): Layout { + // Listen to window resize + orientation change + // < 640px → "phone" + // 640–1024px → "tablet" + // > 1024px → "desktop" +} +``` + +Uses `window.innerWidth`, `resize` event listener, and `orientationchange` for iPad rotation. Returns reactive state via `useState`. + +### Step 6: `MobileNav.tsx` — phone stack navigation + +**File:** `krillnotes-desktop/src/components/MobileNav.tsx` (new) + +State: `{ screen: "tree" | "note", selectedNoteId: string | null }` + +Renders: +- `screen === "tree"` → full-screen `TreeView` + `SearchBar` +- `screen === "note"` → full-screen `InfoPanel` with back button +- Slide transition between screens (CSS transform + transition) +- Swipe-back gesture (touch event on left edge) + +Props: receives the same props as `WorkspaceView` passes to `TreeView` and `InfoPanel` — note list, selected note, callbacks, etc. + +### Step 7: Bottom navigation bar (phone) + +**File:** `krillnotes-desktop/src/components/BottomNavBar.tsx` (new) + +Four items: Tree (home), Search, Tags, More. + +- Tree → navigates to tree screen in MobileNav +- Search → opens SearchBar overlay or navigates to tree with search focused +- Tags → slides up `TagCloudSheet` (bottom sheet component) +- More → opens dropdown/bottom sheet with workspace actions + +**File:** `krillnotes-desktop/src/components/TagCloudSheet.tsx` (new) + +Bottom sheet that slides up from the bottom bar. Contains the existing tag cloud content. Dismissible via swipe-down or tap-outside. + +### Step 8: Toolbar hamburger menu (tablet + mobile) + +**File:** `krillnotes-desktop/src/components/ToolbarMenu.tsx` (new) + +Hamburger button (☰) that renders in the top toolbar area. Opens a dropdown with actions that normally live in the native menu: +- New note +- Workspace settings +- Export (Phase 3) +- Script manager (Phase 3 — hidden initially) +- About + +Shown when `layout !== "desktop"`. On desktop, the native menu bar handles these. + +### Step 9: Integrate in WorkspaceView + +**File:** `krillnotes-desktop/src/components/WorkspaceView.tsx` + +```typescript +const layout = useLayout(); + +if (layout === "phone") { + return ; +} + +// tablet and desktop share the sidebar layout +// tablet gets: narrower default sidebar, ToolbarMenu, touch divider +// desktop gets: current layout unchanged, native menu +return ( +
+ + + : null} ... /> +
+); +``` + +The existing layout code doesn't change for desktop — `useLayout()` returns `"desktop"` on large viewports and the same code path runs. + +### Step 10: Touch-enable resizable panels + +**File:** `krillnotes-desktop/src/hooks/useResizablePanels.ts` (or wherever the hook lives) + +Add alongside existing mouse event listeners: +- `touchstart` / `touchmove` / `touchend` handlers +- Invisible touch zone: the divider element gets `padding: 0 10px` (20px total touch area) with the visual divider as a thin inner element +- Min width: 120px. Max width: 60% of viewport. + +### Step 11: Touch UX adjustments + +**File:** `krillnotes-desktop/src/components/TreeNode.tsx` +- Increase minimum node height to 44px (Apple HIG) +- Long-press handler for context menu (use `touchstart` + timer, cancel on `touchmove`) +- Context menu renders as bottom sheet on phone, floating menu on tablet + +**File:** `krillnotes-desktop/src/components/ContextMenu.tsx` +- Add a `mode` prop or detect layout: bottom sheet on phone, current floating menu elsewhere + +**File:** `krillnotes-desktop/src/globals.css` (or Tailwind config) +- Safe area insets: `padding-top: env(safe-area-inset-top)`, etc. +- Add to root layout container + +### Step 12: i18n for new components + +**Files:** All 7 locale files in `krillnotes-desktop/src/i18n/locales/` + +Add keys for: +- `mobile.backButton`: "Back" +- `mobile.bottomNav.tree`: "Tree" / "Notes" +- `mobile.bottomNav.search`: "Search" +- `mobile.bottomNav.tags`: "Tags" +- `mobile.bottomNav.more`: "More" +- `mobile.migrationGate`: "Please open an existing workspace first to migrate your device identity" +- `toolbar.menu`: "Menu" +- `toolbar.newNote`: "New Note" +- `toolbar.settings`: "Settings" + +All 7 languages (en, de, es, fr, ja, ko, zh). + +### Step 13: Test on all form factors + +- **iPad simulator** (landscape + portrait rotation) — sidebar layout, touch divider, hamburger menu +- **Android phone** (USB) — stack navigation, bottom bar, tags sheet, swipe-back +- **Desktop** (`npm run tauri dev`) — verify zero regressions, native menu still works +- **Browser resize** — drag browser window small to simulate phone/tablet breakpoints during development + +### Step 14: Commit sequence + +1. `chore: initialize Tauri iOS and Android targets` — steps 1, 3 +2. `refactor: gate desktop-only code with #[cfg(desktop)]` — step 2 +3. `feat: add useLayout hook for adaptive breakpoints` — step 5 +4. `feat: add MobileNav stack navigation for phone layout` — step 6 +5. `feat: add bottom nav bar and tag cloud sheet` — step 7 +6. `feat: add toolbar hamburger menu for tablet/mobile` — step 8 +7. `refactor: integrate adaptive layout in WorkspaceView` — step 9 +8. `feat: touch-enable resizable panels for tablet` — step 10 +9. `feat: touch UX — tap targets, long-press context menu, safe areas` — step 11 +10. `chore: add i18n keys for mobile components` — step 12 + +## Risks + +- **WebView quirks** — WKWebView on iOS has known issues (see gotchas.md): confirm dialogs, scrollbar rendering, drag-drop. Test early. +- **SQLCipher first-open** — verify PRAGMA key works on both iOS and Android WebView contexts. The Rust side should be fine (bundled), but the Tauri bridge timing matters. +- **Keyboard behavior** — virtual keyboard pushing layout around. May need `viewport-fit=cover` meta tag and CSS adjustments. Test with text field editing. +- **gen/ directory churn** — Tauri-generated iOS/Android project files may need manual tweaks for signing, bundle ID, etc. Keep track of manual changes. diff --git a/docs/superpowers/specs/2026-04-29-mobile-support-design.md b/docs/superpowers/specs/2026-04-29-mobile-support-design.md new file mode 100644 index 00000000..65fe6593 --- /dev/null +++ b/docs/superpowers/specs/2026-04-29-mobile-support-design.md @@ -0,0 +1,238 @@ +# Mobile Support Design Spec + +## Overview + +Add iOS and Android support to Krillnotes by building mobile targets from the existing `krillnotes-desktop` package (single-package approach). The core library (`krillnotes-core`) is already Tauri-independent and compiles for mobile targets with one blocker to resolve first (device ID). + +**Branch:** `mobile` (exploratory — pivot to separate package if single-package hits a wall) + +**Target devices:** Android phone, iPad (tablet) + +## Phased Scope + +### MVP (Phase 1) +- Device ID migration (prerequisite — all platforms) +- Tauri mobile scaffolding (iOS + Android targets) +- Adaptive layout (phone / tablet portrait / tablet landscape) +- Note tree browsing, note viewing/editing, search, tags +- Workspace open/create +- Bottom nav bar (phone), toolbar hamburger menu (tablet) + +### Phase 2 +- Multi-device sync (same identity sharing workspace across desktop + mobile) +- Attachments (with camera capture on mobile) + +### Phase 3 +- Script editor (mobile-friendly) +- RBAC permission UI +- Operations log +- Import/export + +### Future: CI/CD & Distribution +- Target: GitHub Actions (consistent with existing desktop CI) +- **Android:** Linux or macOS runner, Android SDK + NDK, `tauri android build` → `.aab`, distribute via Google Play with `fastlane supply`. Signing keystore stored as GitHub secret. +- **iOS:** macOS runner required, Apple Developer account ($99/yr), signing certs + provisioning profiles as GitHub secrets, `tauri ios build` → `.ipa`, distribute via TestFlight with `fastlane pilot`. +- **Tooling:** `fastlane` for both platforms (signing, building, uploading). +- Not designed in this spec — to be addressed when local development stabilizes. + +## Preliminary: Device ID Migration + +### Problem + +`device.rs` uses the `mac_address` crate to derive a stable device ID (`device-`). This doesn't work on mobile (iOS/Android don't expose MAC addresses) and caused relay sync bugs on desktop (different MAC values from different network interfaces). + +### Solution + +Replace MAC-based device ID with a persisted UUID. Source of truth is the workspace database, with an app-level file as seed for new workspaces. + +**Lookup priority (on workspace open):** + +1. `workspace_meta` has `device_id` → use it +2. `operations` table has local `device_id` → use it, write to `workspace_meta` and app-level file +3. App-level file exists → use it, write to `workspace_meta` +4. Nothing found → generate `device-{uuid}`, write to `workspace_meta` and app-level file + +**Migration gate:** On "Create New Workspace", if no app-level `device_id` file exists and `list_workspaces()` returns non-empty, block creation with a message: *"Please open an existing workspace first to migrate your device identity."* This prevents generating a fresh UUID that would conflict with an existing workspace's sync identity. + +**API — two functions in `device.rs`:** + +```rust +/// Reads or creates the app-level device ID seed file. +/// Does NOT touch the database. +pub fn get_or_create_seed_device_id(data_dir: &Path) -> Result + +/// Full priority chain: workspace_meta → operations → seed file → generate. +/// Called during Workspace::open(). Writes back to workspace_meta and seed file as needed. +pub fn resolve_device_id(conn: &Connection, data_dir: &Path) -> Result +``` + +- Desktop: `data_dir` = `home_dir()/.krillnotes/` +- Mobile: `data_dir` = app sandbox directory (passed from Tauri shell) +- Migration gate (block new workspace if existing workspaces but no seed file) lives in the Tauri command layer, not in core + +**Crate changes:** +- Remove `mac_address` dependency entirely +- `device.rs` implements both functions above +- `Workspace::open()` accepts `data_dir` parameter, calls `resolve_device_id()` +- One-time migration reads existing device_id from operations table (no need to recompute MAC) + +## Tauri Mobile Scaffolding + +### Single-Package Approach + +Build mobile from `krillnotes-desktop` rather than a separate crate. Tauri v2 supports this natively — same `src-tauri/` directory, same Cargo project. + +**Existing mobile-ready config:** +- `Cargo.toml` already declares `crate-type = ["staticlib", "cdylib", "rlib"]` +- `lib.rs` supports `#[cfg_attr(mobile, tauri::mobile_entry_point)]` + +**Scaffolding steps:** +- `tauri ios init` → generates `gen/apple/` +- `tauri android init` → generates `gen/android/` +- Add Rust targets: `aarch64-apple-ios`, `aarch64-apple-ios-sim`, `aarch64-linux-android`, `x86_64-linux-android` + +### Desktop-Only Guards + +Features that don't apply on mobile get `#[cfg(desktop)]`: + +- Native menu bar construction (`menu.rs`) +- `close_window` command +- Multi-window workspace management logic in `run()` + +All 100+ Tauri commands remain shared. Mobile simply doesn't exercise the gated code paths. + +### Capabilities + +New `src-tauri/capabilities/mobile.json` for mobile-specific permissions. Initially minimal — same as desktop minus file associations. + +### Build Commands + +```bash +cd krillnotes-desktop +npm run tauri ios dev # iPad simulator +npm run tauri android dev # Android emulator or USB device +``` + +### Known Risk (mitigated) + +SQLCipher on Android x86_64 emulator has a known crash (`__extenddftf2` symbol). Not a concern here — development machine is Apple M2, so Android emulator runs ARM64 images natively. Real Android device is also ARM. + +## Adaptive Layout + +### Breakpoints + +| CSS Width | Layout | Mode | +|-----------|--------|------| +| < 640px | Phone | Stack navigation | +| 640–1024px | Tablet portrait | Compact sidebar + note panel | +| > 1024px | Tablet landscape / Desktop | Full side-by-side | + +These use CSS logical pixels (not physical). Modern phones are 360–430 CSS px wide; tablets start at 744+ CSS px. The 640px boundary aligns with Tailwind's `sm:` breakpoint. + +### `useLayout()` Hook + +```typescript +type Layout = "phone" | "tablet" | "desktop"; +function useLayout(): Layout +``` + +Listens to viewport resize events. Updates reactively on device rotation (iPad portrait ↔ landscape). + +### Phone Layout (< 640px) — Stack Navigation + +**New component: `MobileNav.tsx`** + +Wraps existing `TreeView` and `InfoPanel` in a stack navigator: + +- `screen: "tree"` → full-screen tree view +- Tap note → slide transition to `screen: "note"` → full-screen `InfoPanel` +- Back button / swipe-back → returns to tree + +**Bottom navigation bar:** + +| Icon | Label | Action | +|------|-------|--------| +| 🏠 | Tree | Navigate to tree view | +| 🔍 | Search | Open search | +| 🏷️ | Tags | Slide up tag cloud bottom sheet | +| ☰ | More | Workspace settings, future Phase 3 features | + +Bottom bar is persistent across both tree and note screens. + +### Tablet Portrait (640–1024px) — Compact Sidebar + +Uses existing `WorkspaceView` layout with adjustments: + +- Narrower default sidebar width +- Tree, search, and tag cloud in sidebar (same as desktop) +- Draggable divider with touch support (wider hit target, min/max constraints) +- Hamburger button (☰) in top toolbar for workspace actions (replaces native menu) + +### Tablet Landscape (> 1024px) — Desktop-Like + +Current desktop layout, with: + +- Same draggable divider (touch-enabled) +- Hamburger button (☰) in toolbar (no native menu bar on mobile) +- Touch-friendly tap targets + +### Navigation Summary + +| Element | Phone | Tablet (both) | Desktop | +|---------|-------|---------------|---------| +| Tree | Full screen (stack) | Sidebar | Sidebar | +| Tags | Bottom sheet via bar | In sidebar | In sidebar | +| Search | Bottom bar icon | Top of sidebar | Top of sidebar | +| More/actions | Bottom bar icon | Toolbar ☰ button | Native menu bar | +| Panel divider | N/A (stack nav) | Draggable (touch) | Draggable (mouse) | + +### WorkspaceView Integration + +```typescript +const layout = useLayout(); + +if (layout === "phone") return ; +if (layout === "tablet") return ; +return ; +``` + +Tablet compact layout may not need a separate component — could be the existing layout with different default props. + +## Touch & Mobile UX + +### Interaction Changes + +- **Context menu:** Long-press on tree node (instead of right-click). On phone, renders as bottom sheet; on tablet, floating menu. +- **Tap targets:** Minimum 44px height on tree nodes (Apple HIG). +- **Swipe-back:** Phone stack navigation supports left-edge swipe to go back. +- **Virtual keyboard:** Fields scroll into view when keyboard appears. CSS `env(safe-area-inset-bottom)` prevents content behind keyboard. +- **Safe areas:** Padding for notches, dynamic islands, rounded corners, home indicator bars via `safe-area-inset-*`. + +### Resizable Panels (Tablet) + +Extend existing `useResizablePanels` hook: + +- Add `touchstart/touchmove/touchend` listeners alongside mouse events +- Invisible touch zone (~20px) around the 4px visual divider +- Min width: ~120px. Max width: 60% of viewport. +- Phone layout: divider not rendered (stack navigation) + +### Features Hidden on Phone + +- Script editor (CodeMirror + phone keyboard = painful) — Phase 3 +- Operations log dialog — Phase 3 +- RBAC permission UI — Phase 3 +- Resizable panel divider + +### Features That Work As-Is + +- Search bar, tag pills / tag cloud +- Field display (read-only) and field editing (text, number, boolean, date, select, rating) +- Add note / delete note dialogs +- Workspace open/create dialogs + +## Desktop Impact + +The native menu bar (`menu.rs`) is gated behind `#[cfg(desktop)]` and remains unchanged. Desktop users see no difference. The `useLayout()` hook returns `"desktop"` on large viewports, so desktop renders the existing layout. + +The device ID migration improves desktop stability (no more MAC address drift) and is the only change that affects desktop behavior. diff --git a/krillnotes-core/Cargo.toml b/krillnotes-core/Cargo.toml index 543e439f..60074b09 100644 --- a/krillnotes-core/Cargo.toml +++ b/krillnotes-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "krillnotes-core" -version = "1.1.1" +version = "1.2.0" edition = "2021" authors = ["TripleACS Pty Ltd t/a 2pi Software "] description = "Core library for Krill Notes — a local-first, schema-driven note-taking application" @@ -22,7 +22,6 @@ uuid = { workspace = true } chrono = { workspace = true } thiserror = { workspace = true } log = { workspace = true } -mac_address = "1.1" hostname = "0.3" include_dir = "0.7" zip = { version = "8", default-features = false, features = ["deflate", "aes-crypto"] } diff --git a/krillnotes-core/src/core/device.rs b/krillnotes-core/src/core/device.rs index 1c65bd80..875194f9 100644 --- a/krillnotes-core/src/core/device.rs +++ b/krillnotes-core/src/core/device.rs @@ -4,63 +4,239 @@ // // Copyright (c) 2024-2026 TripleACS Pty Ltd t/a 2pi Software -//! Stable hardware-based device identity for Krillnotes. +//! Persisted device identity for Krillnotes. +//! +//! Replaces the former MAC-address-based approach with a stable UUID +//! persisted to disk. Works on all platforms including iOS/Android +//! where MAC addresses are unavailable. use crate::{KrillnotesError, Result}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use rusqlite::Connection; +use std::path::Path; -/// Returns a stable device identifier derived from the machine's primary MAC address. +const SEED_FILENAME: &str = "device_id"; + +/// Read or create the app-level device UUID seed file. +/// +/// File location: `{data_dir}/device_id` (plain text, one line). /// -/// The MAC address bytes are hashed to produce an opaque identifier of the form -/// `device-<16 hex digits>`. The same hardware always yields the same identifier -/// across process restarts. +/// On first call the file is created with a freshly generated +/// `device-{uuid}` value. Subsequent calls return the same value. +pub fn get_or_create_seed_device_id(data_dir: &Path) -> Result { + let path = data_dir.join(SEED_FILENAME); + + if path.exists() { + let contents = std::fs::read_to_string(&path).map_err(|e| { + KrillnotesError::InvalidWorkspace(format!("Failed to read device seed file: {e}")) + })?; + let trimmed = contents.trim().to_string(); + if !trimmed.is_empty() { + return Ok(trimmed); + } + } + + let id = format!("device-{}", uuid::Uuid::new_v4()); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + KrillnotesError::InvalidWorkspace(format!( + "Failed to create device seed directory: {e}" + )) + })?; + } + std::fs::write(&path, &id).map_err(|e| { + KrillnotesError::InvalidWorkspace(format!("Failed to write device seed file: {e}")) + })?; + Ok(id) +} + +/// Check whether the seed file already exists at `{data_dir}/device_id`. +pub fn seed_file_exists(data_dir: &Path) -> bool { + let path = data_dir.join(SEED_FILENAME); + path.exists() + && std::fs::read_to_string(&path) + .map(|s| !s.trim().is_empty()) + .unwrap_or(false) +} + +/// Resolve the device ID for a workspace using a priority chain: /// -/// # Errors +/// 1. `workspace_meta` has `device_id` → use it +/// 2. `operations` table has a local device_id → use it, write to +/// `workspace_meta` and seed file +/// 3. Seed file exists → use it, write to `workspace_meta` +/// 4. Generate a new `device-{uuid}`, write everywhere /// -/// Returns [`KrillnotesError::InvalidWorkspace`] if the system has no network -/// interfaces or the MAC address cannot be read. -pub fn get_device_id() -> Result { - match mac_address::get_mac_address() { - Ok(Some(mac)) => { - // DefaultHasher is not guaranteed stable across Rust versions, but this - // derivation is battle-tested for multi-device sync. Do NOT change - // without extensive cross-device migration testing. - let mut hasher = DefaultHasher::new(); - mac.bytes().hash(&mut hasher); - let hash = hasher.finish(); - Ok(format!("device-{hash:016x}")) +/// This handles migration from MAC-based IDs: existing workspaces keep +/// whatever device_id they already have in `workspace_meta`. +pub fn resolve_device_id(conn: &Connection, data_dir: &Path) -> Result { + // 1. Check workspace_meta + let from_meta: std::result::Result = conn.query_row( + "SELECT value FROM workspace_meta WHERE key = 'device_id'", + [], + |row| row.get(0), + ); + if let Ok(id) = from_meta { + if !id.is_empty() { + // Also ensure the seed file exists for other workspaces to pick up. + if !seed_file_exists(data_dir) { + // Extract the short device part for the seed file. + // For composite IDs like "device-xxx:identity:yyy", store "device-xxx". + let short = crate::core::identity::device_part_from_device_id(&id); + let _ = write_seed_file(data_dir, short); + } + return Ok(id); } - Ok(None) => Err(KrillnotesError::InvalidWorkspace( - "Could not determine device MAC address".to_string(), - )), - Err(e) => Err(KrillnotesError::InvalidWorkspace(format!( - "Failed to get MAC address: {e}" - ))), } + + // 2. Check operations table for an existing device_id + let from_ops: std::result::Result = conn.query_row( + "SELECT DISTINCT device_id FROM operations WHERE device_id IS NOT NULL AND device_id != '' LIMIT 1", + [], + |row| row.get(0), + ); + if let Ok(id) = from_ops { + if !id.is_empty() { + conn.execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('device_id', ?1)", + [&id], + ) + .map_err(KrillnotesError::Database)?; + let short = crate::core::identity::device_part_from_device_id(&id); + let _ = write_seed_file(data_dir, short); + return Ok(id); + } + } + + // 3. Seed file + if seed_file_exists(data_dir) { + let id = get_or_create_seed_device_id(data_dir)?; + conn.execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('device_id', ?1)", + [&id], + ) + .map_err(KrillnotesError::Database)?; + return Ok(id); + } + + // 4. Generate new + let id = get_or_create_seed_device_id(data_dir)?; + conn.execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('device_id', ?1)", + [&id], + ) + .map_err(KrillnotesError::Database)?; + Ok(id) +} + +pub(crate) fn write_seed_file(data_dir: &Path, device_id: &str) -> Result<()> { + let path = data_dir.join(SEED_FILENAME); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + KrillnotesError::InvalidWorkspace(format!( + "Failed to create device seed directory: {e}" + )) + })?; + } + std::fs::write(&path, device_id).map_err(|e| { + KrillnotesError::InvalidWorkspace(format!("Failed to write device seed file: {e}")) + })?; + Ok(()) } #[cfg(test)] mod tests { use super::*; + use tempfile::TempDir; #[test] - fn test_device_id_is_stable() { - let id1 = get_device_id(); - let id2 = get_device_id(); - - match (id1, id2) { - (Ok(id1), Ok(id2)) => { - assert_eq!(id1, id2, "Device ID should be stable"); - assert!( - id1.starts_with("device-"), - "Device ID should have correct format" - ); - } - (Err(_), Err(_)) => { - // Both failed — acceptable in environments without network interfaces. - } - _ => panic!("Device ID generation is inconsistent"), - } + fn seed_creates_file_on_first_call() { + let dir = TempDir::new().unwrap(); + let id = get_or_create_seed_device_id(dir.path()).unwrap(); + assert!(id.starts_with("device-")); + + let id2 = get_or_create_seed_device_id(dir.path()).unwrap(); + assert_eq!(id, id2, "second call returns same value"); + } + + #[test] + fn seed_file_exists_check() { + let dir = TempDir::new().unwrap(); + assert!(!seed_file_exists(dir.path())); + + get_or_create_seed_device_id(dir.path()).unwrap(); + assert!(seed_file_exists(dir.path())); + } + + #[test] + fn resolve_prefers_workspace_meta() { + let dir = TempDir::new().unwrap(); + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + "CREATE TABLE workspace_meta (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO workspace_meta VALUES ('device_id', 'device-existing'); + CREATE TABLE operations (id INTEGER PRIMARY KEY, device_id TEXT);", + ) + .unwrap(); + + let id = resolve_device_id(&conn, dir.path()).unwrap(); + assert_eq!(id, "device-existing"); + } + + #[test] + fn resolve_falls_back_to_operations() { + let dir = TempDir::new().unwrap(); + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + "CREATE TABLE workspace_meta (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE operations (id INTEGER PRIMARY KEY, device_id TEXT); + INSERT INTO operations (device_id) VALUES ('device-from-ops');", + ) + .unwrap(); + + let id = resolve_device_id(&conn, dir.path()).unwrap(); + assert_eq!(id, "device-from-ops"); + + // Should also have written to workspace_meta + let meta: String = conn + .query_row( + "SELECT value FROM workspace_meta WHERE key = 'device_id'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(meta, "device-from-ops"); + } + + #[test] + fn resolve_falls_back_to_seed_file() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("device_id"), "device-seeded").unwrap(); + + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + "CREATE TABLE workspace_meta (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE operations (id INTEGER PRIMARY KEY, device_id TEXT);", + ) + .unwrap(); + + let id = resolve_device_id(&conn, dir.path()).unwrap(); + assert_eq!(id, "device-seeded"); + } + + #[test] + fn resolve_generates_new_when_nothing_exists() { + let dir = TempDir::new().unwrap(); + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + "CREATE TABLE workspace_meta (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE operations (id INTEGER PRIMARY KEY, device_id TEXT);", + ) + .unwrap(); + + let id = resolve_device_id(&conn, dir.path()).unwrap(); + assert!(id.starts_with("device-")); + + // Seed file should now exist + assert!(seed_file_exists(dir.path())); } } diff --git a/krillnotes-core/src/core/export.rs b/krillnotes-core/src/core/export.rs index defd1edd..ccce1277 100644 --- a/krillnotes-core/src/core/export.rs +++ b/krillnotes-core/src/core/export.rs @@ -21,7 +21,7 @@ use crate::core::note::Note; use crate::core::timestamp::UnixSecs; use crate::core::user_script; use crate::core::workspace::Workspace; -use crate::get_device_id; +use crate::get_or_create_seed_device_id; use crate::Storage; /// The current Krillnotes app version, read from Cargo.toml at compile time. @@ -404,6 +404,7 @@ pub fn import_workspace( workspace_password: &str, identity_uuid: &str, signing_key: ed25519_dalek::SigningKey, + data_dir: &Path, ) -> Result { let mut archive = ZipArchive::new(reader)?; @@ -464,7 +465,8 @@ pub fn import_workspace( .map_err(|e| ExportError::Database(e.to_string()))?; // Insert workspace metadata - let device_id = get_device_id().map_err(|e| ExportError::Database(e.to_string()))?; + let device_id = + get_or_create_seed_device_id(data_dir).map_err(|e| ExportError::Database(e.to_string()))?; storage .connection() .execute( @@ -564,6 +566,7 @@ pub fn import_workspace( signing_key, Box::new(crate::core::permission::AllowAllGate::new("krillnotes/1")), None, + data_dir, ) .map_err(|e| ExportError::Database(e.to_string()))?; workspace diff --git a/krillnotes-core/src/core/export_tests.rs b/krillnotes-core/src/core/export_tests.rs index 04cc34e7..dbb81dcf 100644 --- a/krillnotes-core/src/core/export_tests.rs +++ b/krillnotes-core/src/core/export_tests.rs @@ -62,6 +62,7 @@ fn test_export_workspace_creates_valid_zip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -120,6 +121,7 @@ fn test_peek_import_reads_metadata() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -158,6 +160,7 @@ fn test_round_trip_export_import() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); @@ -194,6 +197,7 @@ fn test_round_trip_export_import() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -214,6 +218,7 @@ fn test_round_trip_export_import() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -253,6 +258,7 @@ fn test_export_archive_is_identity_neutral() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -305,6 +311,7 @@ fn test_round_trip_preserves_script_category() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); @@ -328,6 +335,7 @@ fn test_round_trip_preserves_script_category() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -338,6 +346,7 @@ fn test_round_trip_preserves_script_category() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let scripts = imported_ws.list_user_scripts().unwrap(); @@ -365,6 +374,7 @@ fn test_export_includes_workspace_json() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -391,6 +401,7 @@ fn test_round_trip_preserves_tags() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -407,6 +418,7 @@ fn test_round_trip_preserves_tags() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -417,6 +429,7 @@ fn test_round_trip_preserves_tags() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let tags = imported.get_all_tags().unwrap(); @@ -438,6 +451,7 @@ fn test_import_invalid_zip() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + Path::new("/tmp"), ); assert!(result.is_err()); } @@ -462,6 +476,7 @@ fn test_import_missing_notes_json() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + Path::new("/tmp"), ); assert!(matches!(result, Err(ExportError::InvalidFormat(_)))); } @@ -476,6 +491,7 @@ fn test_export_with_password_creates_encrypted_zip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -504,6 +520,7 @@ fn test_export_without_password_creates_plain_zip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -530,6 +547,7 @@ fn test_read_entry_wrong_password_returns_invalid_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let mut buf = Vec::new(); @@ -550,6 +568,7 @@ fn test_peek_import_returns_encrypted_archive_error_when_no_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -570,6 +589,7 @@ fn test_peek_import_with_correct_password_succeeds() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -591,6 +611,7 @@ fn test_peek_import_with_wrong_password_returns_invalid_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -613,6 +634,7 @@ fn test_import_stamps_importer_identity_on_notes() { key_a.clone(), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); @@ -633,11 +655,20 @@ fn test_import_stamps_importer_identity_on_notes() { "", "identity-b", key_b.clone(), + temp_dst.path().parent().unwrap(), ) .unwrap(); - let ws_b = - Workspace::open(temp_dst.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let ws_b = Workspace::open( + temp_dst.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp_dst.path().parent().unwrap(), + ) + .unwrap(); // Importer is owner assert!(ws_b.is_owner(), "importer should be workspace owner"); @@ -668,6 +699,7 @@ fn test_encrypted_round_trip_import() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -687,6 +719,7 @@ fn test_encrypted_round_trip_import() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); assert_eq!(result.note_count, 1); @@ -699,6 +732,7 @@ fn test_encrypted_round_trip_import() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let notes = imported_ws.list_all_notes().unwrap(); @@ -748,6 +782,7 @@ fn test_import_notes_without_tags_field() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); assert_eq!(result.note_count, 1); @@ -759,6 +794,7 @@ fn test_import_notes_without_tags_field() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let notes = imported_ws.list_all_notes().unwrap(); @@ -811,6 +847,7 @@ fn test_import_archive_without_workspace_json() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); assert_eq!(result.note_count, 1); @@ -827,6 +864,7 @@ fn test_workspace_metadata_roundtrip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); @@ -855,6 +893,7 @@ fn test_workspace_metadata_roundtrip() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -865,6 +904,7 @@ fn test_workspace_metadata_roundtrip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let restored = imported.get_workspace_metadata().unwrap(); @@ -896,6 +936,7 @@ fn test_export_includes_attachments() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -938,6 +979,7 @@ fn test_import_restores_attachments() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + std::path::Path::new("/tmp"), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -957,6 +999,7 @@ fn test_import_restores_attachments() { "newpass", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + dir_dst.path(), ) .unwrap(); @@ -967,6 +1010,7 @@ fn test_import_restores_attachments() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + std::path::Path::new("/tmp"), ) .unwrap(); let notes = ws2.list_all_notes().unwrap(); @@ -1013,6 +1057,7 @@ fn test_workspace_metadata_absent_in_old_archive() { "", "test-identity", ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), + temp_dst.path().parent().unwrap(), ) .unwrap(); @@ -1023,6 +1068,7 @@ fn test_workspace_metadata_absent_in_old_archive() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp_dst.path().parent().unwrap(), ) .unwrap(); let meta = imported.get_workspace_metadata().unwrap(); @@ -1040,6 +1086,7 @@ fn test_peek_import_includes_workspace_metadata() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1110,6 +1157,7 @@ fn test_import_makes_importer_the_owner() { key_a.clone(), test_gate(), None, + temp_src.path().parent().unwrap(), ) .unwrap(); @@ -1132,12 +1180,21 @@ fn test_import_makes_importer_the_owner() { "", "identity-b", key_b.clone(), + temp_dst.path().parent().unwrap(), ) .unwrap(); // Re-open and verify that importer (key B) is now the owner - let imported_ws = - Workspace::open(temp_dst.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let imported_ws = Workspace::open( + temp_dst.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp_dst.path().parent().unwrap(), + ) + .unwrap(); assert_eq!( imported_ws.owner_pubkey(), pubkey_b, diff --git a/krillnotes-core/src/core/mod.rs b/krillnotes-core/src/core/mod.rs index c18dcd59..a918a073 100644 --- a/krillnotes-core/src/core/mod.rs +++ b/krillnotes-core/src/core/mod.rs @@ -43,7 +43,7 @@ pub use contact::{generate_fingerprint, Contact, ContactManager, TrustLevel}; #[doc(inline)] pub use delete::{DeleteResult, DeleteStrategy}; #[doc(inline)] -pub use device::get_device_id; +pub use device::{get_or_create_seed_device_id, resolve_device_id, seed_file_exists}; #[doc(inline)] pub use error::{KrillnotesError, Result}; #[doc(inline)] diff --git a/krillnotes-core/src/core/operation.rs b/krillnotes-core/src/core/operation.rs index bf88b69b..f1257983 100644 --- a/krillnotes-core/src/core/operation.rs +++ b/krillnotes-core/src/core/operation.rs @@ -383,6 +383,22 @@ pub enum Operation { /// Ed25519 signature over the canonical JSON payload (base64). signature: String, }, + /// Workspace-level metadata (author, license, description, etc.) was updated. + /// Owner-only. Carries the full serialized WorkspaceMetadata JSON blob. + UpdateWorkspaceMetadata { + /// Stable UUID for this operation. + operation_id: String, + /// HLC timestamp when the operation was created. + timestamp: HlcTimestamp, + /// ID of the device that performed this operation. + device_id: String, + /// JSON-serialized WorkspaceMetadata. + metadata_json: String, + /// Public key (base64) of the identity that updated the metadata. + modified_by: String, + /// Ed25519 signature over the canonical JSON payload (base64). + signature: String, + }, } impl Operation { @@ -409,7 +425,8 @@ impl Operation { | Self::AddAttachment { operation_id, .. } | Self::RemoveAttachment { operation_id, .. } | Self::RegisterDevice { operation_id, .. } - | Self::SetChecked { operation_id, .. } => operation_id, + | Self::SetChecked { operation_id, .. } + | Self::UpdateWorkspaceMetadata { operation_id, .. } => operation_id, } } @@ -436,7 +453,8 @@ impl Operation { | Self::AddAttachment { timestamp, .. } | Self::RemoveAttachment { timestamp, .. } | Self::RegisterDevice { timestamp, .. } - | Self::SetChecked { timestamp, .. } => *timestamp, + | Self::SetChecked { timestamp, .. } + | Self::UpdateWorkspaceMetadata { timestamp, .. } => *timestamp, } } @@ -463,7 +481,8 @@ impl Operation { | Self::AddAttachment { device_id, .. } | Self::RemoveAttachment { device_id, .. } | Self::RegisterDevice { device_id, .. } - | Self::SetChecked { device_id, .. } => device_id, + | Self::SetChecked { device_id, .. } + | Self::UpdateWorkspaceMetadata { device_id, .. } => device_id, } } @@ -499,6 +518,7 @@ impl Operation { .. } => identity_public_key, Self::SetChecked { modified_by, .. } => modified_by, + Self::UpdateWorkspaceMetadata { modified_by, .. } => modified_by, } } @@ -532,6 +552,7 @@ impl Operation { .. } => *identity_public_key = key, Self::SetChecked { modified_by, .. } => *modified_by = key, + Self::UpdateWorkspaceMetadata { modified_by, .. } => *modified_by = key, } } @@ -555,7 +576,8 @@ impl Operation { | Self::AddAttachment { signature, .. } | Self::RemoveAttachment { signature, .. } | Self::RegisterDevice { signature, .. } - | Self::SetChecked { signature, .. } => *signature = sig, + | Self::SetChecked { signature, .. } + | Self::UpdateWorkspaceMetadata { signature, .. } => *signature = sig, Self::RetractOperation { .. } => {} } } @@ -580,7 +602,8 @@ impl Operation { | Self::AddAttachment { signature, .. } | Self::RemoveAttachment { signature, .. } | Self::RegisterDevice { signature, .. } - | Self::SetChecked { signature, .. } => signature, + | Self::SetChecked { signature, .. } + | Self::UpdateWorkspaceMetadata { signature, .. } => signature, Self::RetractOperation { .. } => "", } } diff --git a/krillnotes-core/src/core/operation_log.rs b/krillnotes-core/src/core/operation_log.rs index 13e56358..992694cb 100644 --- a/krillnotes-core/src/core/operation_log.rs +++ b/krillnotes-core/src/core/operation_log.rs @@ -257,6 +257,7 @@ impl OperationLog { Operation::RemoveAttachment { .. } => "RemoveAttachment", Operation::RegisterDevice { .. } => "RegisterDevice", Operation::SetChecked { .. } => "SetChecked", + Operation::UpdateWorkspaceMetadata { .. } => "UpdateWorkspaceMetadata", } } diff --git a/krillnotes-core/src/core/swarm/sync.rs b/krillnotes-core/src/core/swarm/sync.rs index f08a066c..459d7aee 100644 --- a/krillnotes-core/src/core/swarm/sync.rs +++ b/krillnotes-core/src/core/swarm/sync.rs @@ -456,6 +456,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); @@ -518,6 +519,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -579,6 +581,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -638,6 +641,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); let snap_op = alice_ws @@ -685,6 +689,7 @@ mod tests { SigningKey::from_bytes(&bob_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); @@ -724,6 +729,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); let snap_op = alice_ws @@ -769,6 +775,7 @@ mod tests { SigningKey::from_bytes(&bob_key.to_bytes()), test_gate(), None, + bob_temp.path().parent().unwrap(), ) .unwrap(); let bob_cm_dir = tempfile::tempdir().unwrap(); @@ -807,6 +814,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); @@ -857,6 +865,7 @@ mod tests { SigningKey::from_bytes(&bob_key.to_bytes()), test_gate(), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); let bob_cm_dir = tempfile::tempdir().unwrap(); @@ -910,6 +919,7 @@ mod tests { // Alice uses protocol "wrong/1" Box::new(AllowAllGate::new("wrong/1")), None, + alice_temp.path().parent().unwrap(), ) .unwrap(); @@ -977,6 +987,7 @@ mod tests { SigningKey::from_bytes(&bob_key.to_bytes()), test_gate(), // protocol "test" None, + alice_temp.path().parent().unwrap(), ) .unwrap(); @@ -1019,6 +1030,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_dir.path(), ) .unwrap(); @@ -1083,6 +1095,7 @@ mod tests { &workspace_id, test_gate(), None, + bob_dir.path(), ) .unwrap(); // Adopt Alice's owner_pubkey so the bundle owner check passes. @@ -1157,6 +1170,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_dir.path(), ) .unwrap(); @@ -1185,6 +1199,7 @@ mod tests { &workspace_id, test_gate(), None, + bob_dir.path(), ) .unwrap(); bob_ws.set_owner_pubkey(&alice_pubkey_b64).unwrap(); @@ -1311,6 +1326,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1354,6 +1370,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_dir.path(), ) .unwrap(); @@ -1637,6 +1654,7 @@ mod tests { SigningKey::from_bytes(&alice_key.to_bytes()), test_gate(), None, + alice_dir.path(), ) .unwrap(); @@ -1724,6 +1742,7 @@ mod tests { &workspace_id, test_gate(), None, + bob_dir.path(), ) .unwrap(); bob_ws.set_owner_pubkey(&alice_pubkey_b64).unwrap(); diff --git a/krillnotes-core/src/core/workspace/mod.rs b/krillnotes-core/src/core/workspace/mod.rs index be977531..d3211f0e 100644 --- a/krillnotes-core/src/core/workspace/mod.rs +++ b/krillnotes-core/src/core/workspace/mod.rs @@ -53,6 +53,9 @@ pub struct WorkspaceSnapshot { /// recipient's permission gate can reconstruct access grants. #[serde(default)] pub permission_ops: Vec, + /// Workspace-level metadata (author, license, description, etc.). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_metadata: Option, } /// Controls where a new note is inserted relative to the currently selected note. @@ -136,6 +139,7 @@ pub(crate) struct WorkspaceConfig { } impl Workspace { + #[allow(clippy::too_many_arguments)] fn init_core>( config: WorkspaceConfig, path: P, @@ -144,6 +148,7 @@ impl Workspace { signing_key: ed25519_dalek::SigningKey, permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { let mut storage = Storage::create(&path, password)?; let mut script_registry = ScriptRegistry::new()?; @@ -169,6 +174,13 @@ impl Workspace { identity_uuid.to_string() }; + // Back-populate the seed file so mobile/headless openers can resolve + // a device ID without an identity directory. + if !crate::core::device::seed_file_exists(data_dir) { + let short = crate::core::identity::device_part_from_device_id(&device_id); + let _ = crate::core::device::write_seed_file(data_dir, short); + } + storage.connection().execute( "INSERT INTO workspace_meta (key, value) VALUES (?, ?)", ["device_id", &device_id], @@ -377,6 +389,7 @@ impl Workspace { signing_key: ed25519_dalek::SigningKey, permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { Self::init_core( WorkspaceConfig { @@ -390,12 +403,14 @@ impl Workspace { signing_key, permission_gate, identity_dir, + data_dir, ) } /// Like [`create`] but uses the provided `workspace_id` instead of generating a fresh UUID. /// Use when restoring a workspace from a snapshot so all peers share the same UUID. /// The attachment key derivation uses `workspace_id`, so it must use the supplied ID. + #[allow(clippy::too_many_arguments)] pub fn create_with_id>( path: P, password: &str, @@ -404,6 +419,7 @@ impl Workspace { workspace_id: &str, permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { Self::init_core( WorkspaceConfig { @@ -417,6 +433,7 @@ impl Workspace { signing_key, permission_gate, identity_dir, + data_dir, ) } @@ -432,6 +449,7 @@ impl Workspace { signing_key: ed25519_dalek::SigningKey, permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { Self::init_core( WorkspaceConfig { @@ -445,6 +463,7 @@ impl Workspace { signing_key, permission_gate, identity_dir, + data_dir, ) } @@ -452,6 +471,7 @@ impl Workspace { /// /// Use this when restoring a workspace from a snapshot so all peers share the same UUID, /// and no default root note is inserted (the snapshot import will populate notes itself). + #[allow(clippy::too_many_arguments)] pub fn create_empty_with_id>( path: P, password: &str, @@ -460,6 +480,7 @@ impl Workspace { workspace_id: &str, permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { Self::init_core( WorkspaceConfig { @@ -473,6 +494,7 @@ impl Workspace { signing_key, permission_gate, identity_dir, + data_dir, ) } @@ -491,6 +513,7 @@ impl Workspace { signing_key: ed25519_dalek::SigningKey, mut permission_gate: Box, identity_dir: Option<&Path>, + data_dir: &Path, ) -> Result { let storage = Storage::open(&path, password)?; let script_registry = ScriptRegistry::new()?; @@ -533,6 +556,13 @@ impl Workspace { } } + // Back-populate the seed file so mobile/headless openers can resolve + // a device ID without an identity directory. + if !crate::core::device::seed_file_exists(data_dir) { + let short = crate::core::identity::device_part_from_device_id(&device_id); + let _ = crate::core::device::write_seed_file(data_dir, short); + } + let workspace_root = path .as_ref() .parent() diff --git a/krillnotes-core/src/core/workspace/notes.rs b/krillnotes-core/src/core/workspace/notes.rs index 3981c8bb..25e32a0f 100644 --- a/krillnotes-core/src/core/workspace/notes.rs +++ b/krillnotes-core/src/core/workspace/notes.rs @@ -1065,16 +1065,37 @@ impl Workspace { } /// Persists workspace-level metadata (author, license, description, etc.). + /// Logs an `UpdateWorkspaceMetadata` operation for sync. pub fn set_workspace_metadata(&mut self, metadata: &WorkspaceMetadata) -> Result<()> { if !self.is_owner() { return Err(KrillnotesError::NotOwner); } - let json = serde_json::to_string(metadata) + let metadata_json = serde_json::to_string(metadata) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - self.storage.connection().execute( - "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('workspace_metadata', ?)", - [&json], + + let ts = self.advance_hlc(); + let signing_key = self.signing_key.clone(); + + let tx = self.storage.connection_mut().transaction()?; + tx.execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('workspace_metadata', ?1)", + [&metadata_json], )?; + + Self::save_hlc(&ts, &tx)?; + let mut op = Operation::UpdateWorkspaceMetadata { + operation_id: Uuid::new_v4().to_string(), + timestamp: ts, + device_id: self.device_id.clone(), + metadata_json, + modified_by: String::new(), + signature: String::new(), + }; + Self::sign_op_with(&signing_key, &mut op); + Self::log_op(&self.operation_log, &tx, &op)?; + Self::purge_ops_if_needed(&self.operation_log, &tx)?; + tx.commit()?; + Ok(()) } diff --git a/krillnotes-core/src/core/workspace/sync.rs b/krillnotes-core/src/core/workspace/sync.rs index adad391f..721c3f3c 100644 --- a/krillnotes-core/src/core/workspace/sync.rs +++ b/krillnotes-core/src/core/workspace/sync.rs @@ -25,12 +25,14 @@ impl Workspace { log::debug!(target: "krillnotes::sync", "snapshot: {} notes, {} scripts, {} attachments, {} permission ops", notes.len(), user_scripts.len(), attachments.len(), permission_ops.len()); + let workspace_metadata = self.get_workspace_metadata().ok(); let snapshot = WorkspaceSnapshot { version: 1, notes, user_scripts, attachments, permission_ops, + workspace_metadata, }; Ok(serde_json::to_vec(&snapshot)?) } @@ -608,6 +610,19 @@ impl Workspace { Self::apply_permission_op_via(&*self.permission_gate, &tx, &op)?; } + Operation::UpdateWorkspaceMetadata { + modified_by, + metadata_json, + .. + } => { + if modified_by == &self.owner_pubkey { + tx.execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('workspace_metadata', ?1)", + [metadata_json], + )?; + } + } + // Log-only variants — no working table change in this phase. Operation::JoinWorkspace { .. } | Operation::UpdateSchema { .. } @@ -734,6 +749,7 @@ impl Workspace { Operation::RemoveAttachment { .. } => "RemoveAttachment", Operation::RegisterDevice { .. } => "RegisterDevice", Operation::SetChecked { .. } => "SetChecked", + Operation::UpdateWorkspaceMetadata { .. } => "UpdateWorkspaceMetadata", } } @@ -845,6 +861,17 @@ impl Workspace { tx.commit()?; } + // Apply workspace metadata from the snapshot (bypasses ownership check + // since this is a fresh import from a trusted snapshot). + if let Some(ref meta) = snapshot.workspace_metadata { + let json = serde_json::to_string(meta)?; + self.storage.connection().execute( + "INSERT OR REPLACE INTO workspace_meta (key, value) VALUES ('workspace_metadata', ?)", + [&json], + )?; + log::info!(target: "krillnotes::sync", "applied workspace metadata from snapshot"); + } + log::info!(target: "krillnotes::sync", "snapshot import complete: {} notes", note_count); Ok(note_count) } diff --git a/krillnotes-core/src/core/workspace/sync_events.rs b/krillnotes-core/src/core/workspace/sync_events.rs index 1f7ab14b..6e885eca 100644 --- a/krillnotes-core/src/core/workspace/sync_events.rs +++ b/krillnotes-core/src/core/workspace/sync_events.rs @@ -72,6 +72,7 @@ mod tests { fn make_ws() -> (Workspace, NamedTempFile) { let temp = NamedTempFile::new().unwrap(); + let data_dir = temp.path().parent().unwrap_or(std::path::Path::new("/tmp")); let ws = Workspace::create( temp.path(), "", @@ -79,6 +80,7 @@ mod tests { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), Box::new(AllowAllGate::new("test")), None, + data_dir, ) .expect("workspace"); (ws, temp) diff --git a/krillnotes-core/src/core/workspace/tests.rs b/krillnotes-core/src/core/workspace/tests.rs index 3ee47dc7..2cb6b60d 100644 --- a/krillnotes-core/src/core/workspace/tests.rs +++ b/krillnotes-core/src/core/workspace/tests.rs @@ -37,6 +37,7 @@ fn test_create_workspace() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -66,6 +67,7 @@ fn test_create_and_get_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -89,6 +91,7 @@ fn test_update_note_title() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -113,6 +116,7 @@ fn test_open_existing_workspace() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -127,6 +131,7 @@ fn test_open_existing_workspace() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -146,6 +151,7 @@ fn test_is_expanded_defaults_to_true() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -178,6 +184,7 @@ fn test_is_expanded_persists_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -193,6 +200,7 @@ fn test_is_expanded_persists_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let notes = ws.list_all_notes().unwrap(); @@ -211,6 +219,7 @@ fn test_toggle_note_expansion() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -238,6 +247,7 @@ fn test_toggle_note_expansion_with_child_notes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -267,6 +277,7 @@ fn test_toggle_note_expansion_nonexistent_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -285,6 +296,7 @@ fn test_set_and_get_selected_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -322,6 +334,7 @@ fn test_selected_note_persists_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -336,6 +349,7 @@ fn test_selected_note_persists_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -357,6 +371,7 @@ fn test_set_selected_note_overwrites_previous() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -390,6 +405,7 @@ fn test_create_note_root() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -421,6 +437,7 @@ fn test_create_note_root_invalid_type() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -446,6 +463,7 @@ fn test_sibling_insertion_does_not_create_duplicate_positions() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -493,6 +511,7 @@ fn test_get_note_with_corrupt_fields_json_returns_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -524,6 +543,7 @@ fn test_list_all_notes_with_corrupt_fields_json_returns_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -553,6 +573,7 @@ fn test_sibling_insertion_preserves_correct_order() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -596,6 +617,7 @@ fn test_update_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -638,6 +660,7 @@ fn test_update_note_not_found() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -655,6 +678,7 @@ fn test_count_children() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -689,6 +713,7 @@ fn test_delete_note_recursive() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -734,6 +759,7 @@ fn test_delete_note_recursive_not_found() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let result = ws.delete_note_recursive("nonexistent-id"); @@ -750,6 +776,7 @@ fn test_delete_note_promote() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -798,6 +825,7 @@ fn test_update_contact_rejects_empty_required_fields() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); load_contacts_example(&mut ws); @@ -854,6 +882,7 @@ fn test_delete_note_promote_not_found() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -875,6 +904,7 @@ fn test_delete_note_promote_no_position_collision() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -954,6 +984,7 @@ fn test_update_contact_derives_title_from_hook() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); load_contacts_example(&mut ws); @@ -1019,6 +1050,7 @@ fn test_workspace_created_with_starter_scripts() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let scripts = workspace.list_user_scripts().unwrap(); @@ -1040,6 +1072,7 @@ fn test_create_user_script() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let starter_count = workspace.list_user_scripts().unwrap().len(); @@ -1062,6 +1095,7 @@ fn test_create_user_script_missing_name_fails() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// no name here\nschema(\"X\", #{ version: 1, fields: [] });"; @@ -1079,6 +1113,7 @@ fn test_update_user_script() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: Original\nschema(\"Orig\", #{ version: 1, fields: [] });"; @@ -1102,6 +1137,7 @@ fn test_delete_user_script() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let initial_count = workspace.list_user_scripts().unwrap().len(); @@ -1126,6 +1162,7 @@ fn test_toggle_user_script() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: Toggle\nschema(\"Tog\", #{ version: 1, fields: [] });"; @@ -1147,6 +1184,7 @@ fn test_user_scripts_sorted_by_load_order() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let starter_count = workspace.list_user_scripts().unwrap().len(); @@ -1179,6 +1217,7 @@ fn test_user_scripts_loaded_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); workspace.create_user_script( @@ -1193,6 +1232,7 @@ fn test_user_scripts_loaded_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); assert!(workspace.script_registry().get_schema("OpenType").is_ok()); @@ -1210,6 +1250,7 @@ fn test_disabled_user_scripts_not_loaded_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let (script, _) = workspace.create_user_script( @@ -1225,6 +1266,7 @@ fn test_disabled_user_scripts_not_loaded_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); assert!(workspace.script_registry().get_schema("DisType").is_err()); @@ -1240,6 +1282,7 @@ fn test_delete_note_with_strategy() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1290,6 +1333,7 @@ fn setup_with_children(n: usize) -> (Workspace, String, Vec, NamedTempFi ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -1407,6 +1451,7 @@ fn test_run_view_hook_returns_html_without_hook() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1451,6 +1496,7 @@ fn test_create_user_script_rejects_compile_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1480,6 +1526,7 @@ fn test_update_user_script_rejects_compile_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1522,6 +1569,7 @@ fn test_create_workspace_with_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); // Should have at least one note (the root note) @@ -1538,6 +1586,7 @@ fn test_open_workspace_with_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let ws = Workspace::open( @@ -1547,6 +1596,7 @@ fn test_open_workspace_with_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); assert!(!ws.list_all_notes().unwrap().is_empty()); @@ -1562,6 +1612,7 @@ fn test_open_workspace_wrong_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let result = Workspace::open( @@ -1571,6 +1622,7 @@ fn test_open_workspace_wrong_password() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ); assert!(matches!(result, Err(KrillnotesError::WrongPassword))); } @@ -1585,6 +1637,7 @@ fn test_deep_copy_note_as_child() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1625,6 +1678,7 @@ fn test_deep_copy_note_recursive() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1678,6 +1732,7 @@ fn test_on_add_child_hook_fires_on_create() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1727,6 +1782,7 @@ fn test_on_add_child_hook_fires_for_sibling_under_hooked_parent() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1777,6 +1833,7 @@ fn test_on_add_child_hook_does_not_fire_for_root_level_creation() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1800,6 +1857,7 @@ fn test_on_add_child_hook_fires_on_move() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1854,6 +1912,7 @@ fn test_run_tree_action_reorders_children() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1913,6 +1972,7 @@ fn test_tree_action_create_note_writes_to_db() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1957,6 +2017,7 @@ fn test_tree_action_update_note_writes_to_db() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -1998,6 +2059,7 @@ fn test_tree_action_nested_create_builds_subtree() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -2050,6 +2112,7 @@ fn test_tree_action_error_rolls_back_all_writes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -2090,6 +2153,7 @@ fn test_tree_action_create_child_gated() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -2135,6 +2199,7 @@ fn test_note_tags_round_trip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2156,6 +2221,7 @@ fn test_get_all_tags_empty() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); assert!(ws.get_all_tags().unwrap().is_empty()); @@ -2171,6 +2237,7 @@ fn test_get_all_tags_sorted_distinct() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2195,6 +2262,7 @@ fn test_get_notes_for_tag() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2230,6 +2298,7 @@ fn test_update_note_tags_replaces_existing() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2249,6 +2318,7 @@ fn test_update_note_tags_normalises() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2274,6 +2344,7 @@ fn create_test_workspace_with_schema(schema_script: &str) -> Workspace { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); // Wrap the bare schema call in the required front matter so create_user_script accepts it. @@ -2557,6 +2628,7 @@ fn operations_log_always_records_create_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -2585,6 +2657,7 @@ fn test_workspace_has_attachment_key_when_encrypted() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); assert!( @@ -2604,6 +2677,7 @@ fn test_workspace_has_no_attachment_key_when_unencrypted() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); assert!( @@ -2623,6 +2697,7 @@ fn test_workspace_creates_attachments_directory() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); assert!(dir.path().join("attachments").is_dir()); @@ -2639,6 +2714,7 @@ fn test_workspace_attachment_key_stable_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let key1 = ws1.attachment_key().unwrap().clone(); @@ -2650,6 +2726,7 @@ fn test_workspace_attachment_key_stable_across_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let key2 = ws2.attachment_key().unwrap(); @@ -2669,6 +2746,7 @@ fn test_get_set_workspace_metadata() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -2723,6 +2801,7 @@ fn test_attach_file_stores_metadata_and_file() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let notes = ws.list_all_notes().unwrap(); @@ -2753,6 +2832,7 @@ fn test_get_attachment_bytes_decrypts_correctly() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2776,6 +2856,7 @@ fn test_get_attachments_returns_metadata_list() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2803,6 +2884,7 @@ fn test_delete_attachment_soft_deletes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2850,6 +2932,7 @@ fn test_attach_file_with_signing_key_logs_op_and_pushes_undo() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2887,6 +2970,7 @@ fn test_delete_attachment_with_signing_key_logs_op_and_pushes_undo() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2930,6 +3014,7 @@ fn test_attach_file_enforces_size_limit() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -2954,6 +3039,7 @@ fn test_update_note_cleans_up_replaced_file_field() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); @@ -3011,6 +3097,7 @@ fn test_operation_log_always_records() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3033,6 +3120,7 @@ fn test_update_note_cleans_up_cleared_file_field() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); @@ -3071,6 +3159,7 @@ fn test_can_undo_initially_false() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); assert!(!ws.can_undo()); @@ -3088,6 +3177,7 @@ fn test_collect_subtree_notes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3115,6 +3205,7 @@ fn test_undo_group_collapses_to_one_entry() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3146,6 +3237,7 @@ fn test_undo_create_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3175,6 +3267,7 @@ fn test_undo_update_note_restores_old_title() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3205,6 +3298,7 @@ fn test_undo_delete_note_restores_subtree() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3235,6 +3329,7 @@ fn test_undo_move_note_restores_position() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3275,6 +3370,7 @@ fn test_undo_delete_script_restores_it() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -3306,6 +3402,7 @@ fn test_undo_redo_create_note_cycle() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3341,6 +3438,7 @@ fn test_undo_delete_note_full_cycle() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3367,6 +3465,7 @@ fn test_tree_action_collapses_to_one_undo_entry() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root_id = ws.create_note_root("TextNote").unwrap(); @@ -3400,6 +3499,7 @@ fn test_undo_limit_persists() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); ws.set_undo_limit(10).unwrap(); @@ -3412,6 +3512,7 @@ fn test_undo_limit_persists() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); assert_eq!(ws2.undo_limit, 10); @@ -3428,6 +3529,7 @@ fn test_undo_limit_clamp_and_trim() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -3468,6 +3570,7 @@ fn test_undo_redo_update_script_full_cycle() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -3517,6 +3620,7 @@ fn test_undo_redo_create_script_full_cycle() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -3559,6 +3663,7 @@ fn test_write_info_json_creates_file() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); ws.write_info_json().unwrap(); @@ -3584,6 +3689,7 @@ fn test_write_info_json_counts_notes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); @@ -3610,6 +3716,7 @@ fn test_info_json_written_on_create() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); assert!( @@ -3629,6 +3736,7 @@ fn test_info_json_written_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); std::fs::remove_file(dir.path().join("info.json")).unwrap(); // remove it @@ -3639,6 +3747,7 @@ fn test_info_json_written_on_open() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); assert!( @@ -3660,6 +3769,7 @@ fn test_hlc_counter_increments_for_rapid_ops() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -3696,6 +3806,7 @@ fn test_set_tags_op_logged() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -3722,6 +3833,7 @@ fn test_update_note_op_logged() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -3801,6 +3913,7 @@ fn test_save_pipeline_success() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -3841,6 +3954,7 @@ fn test_save_pipeline_validation_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -3884,6 +3998,7 @@ fn test_save_pipeline_reject_error() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -3941,6 +4056,7 @@ fn test_full_pipeline_groups_validation_reject() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4059,6 +4175,7 @@ fn test_tree_action_validates_created_notes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4118,6 +4235,7 @@ fn migration_renames_field_on_version_bump() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -4221,6 +4339,7 @@ fn migration_skips_up_to_date_notes() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -4259,6 +4378,7 @@ fn migration_chains_across_multiple_versions() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -4331,6 +4451,7 @@ fn schema_version_downgrade_rejected() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -4380,6 +4501,7 @@ fn schema_same_version_reregistration_allowed() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -4434,6 +4556,7 @@ fn test_to_snapshot_json_roundtrip() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); // Add a note so the snapshot has more than just the root. @@ -4452,7 +4575,8 @@ fn test_to_snapshot_json_includes_attachments() { let dir = tempfile::tempdir().unwrap(); let db_path = dir.path().join("notes.db"); let key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = Workspace::create(&db_path, "", "test-id", key, test_gate(), None).unwrap(); + let mut ws = + Workspace::create(&db_path, "", "test-id", key, test_gate(), None, dir.path()).unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); ws.attach_file(&root_id, "test.txt", None, b"hello bytes", None) .unwrap(); @@ -4467,7 +4591,8 @@ fn test_get_latest_operation_id_empty_workspace() { let dir = tempfile::tempdir().unwrap(); let db_path = dir.path().join("notes.db"); let key = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let ws = Workspace::create(&db_path, "", "test-id", key, test_gate(), None).unwrap(); + let ws = + Workspace::create(&db_path, "", "test-id", key, test_gate(), None, dir.path()).unwrap(); // A freshly created workspace has a RegisterDevice operation, so latest is Some. assert!(ws.get_latest_operation_id().unwrap().is_some()); } @@ -4478,8 +4603,17 @@ fn test_create_with_id_preserves_uuid() { let db_path = dir.path().join("notes.db"); let key = ed25519_dalek::SigningKey::from_bytes(&[3u8; 32]); let custom_id = "my-fixed-workspace-uuid"; - let ws = Workspace::create_with_id(&db_path, "", "test-id", key, custom_id, test_gate(), None) - .unwrap(); + let ws = Workspace::create_with_id( + &db_path, + "", + "test-id", + key, + custom_id, + test_gate(), + None, + dir.path(), + ) + .unwrap(); assert_eq!(ws.workspace_id(), custom_id); } @@ -4489,9 +4623,17 @@ fn test_create_empty_with_id_no_root_note() { let db_path = dir.path().join("notes.db"); let key = ed25519_dalek::SigningKey::from_bytes(&[4u8; 32]); let custom_id = "snapshot-workspace-uuid"; - let ws = - Workspace::create_empty_with_id(&db_path, "", "test-id", key, custom_id, test_gate(), None) - .unwrap(); + let ws = Workspace::create_empty_with_id( + &db_path, + "", + "test-id", + key, + custom_id, + test_gate(), + None, + dir.path(), + ) + .unwrap(); assert_eq!(ws.workspace_id(), custom_id); // No root note should be auto-inserted — snapshot restoration will add its own notes. assert_eq!(ws.list_all_notes().unwrap().len(), 0); @@ -4510,6 +4652,7 @@ fn test_create_empty_with_id_skips_starter_scripts() { "skip-scripts-uuid", test_gate(), None, + dir.path(), ) .unwrap(); let count: i64 = ws @@ -4532,6 +4675,7 @@ fn test_import_snapshot_json_round_trip() { ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]), test_gate(), None, + src_temp.path().parent().unwrap(), ) .unwrap(); // Replace the default root note title and add two children. @@ -4551,6 +4695,7 @@ fn test_import_snapshot_json_round_trip() { ed25519_dalek::SigningKey::from_bytes(&[3u8; 32]), test_gate(), None, + dst_temp.path().parent().unwrap(), ) .unwrap(); // Remove the auto-created root so we start from a clean slate. @@ -4579,6 +4724,7 @@ fn test_snapshot_includes_permission_ops() { ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]), test_gate(), None, + src_temp.path().parent().unwrap(), ) .unwrap(); @@ -4634,6 +4780,7 @@ fn test_snapshot_includes_permission_ops() { ed25519_dalek::SigningKey::from_bytes(&[3u8; 32]), test_gate(), None, + dst_temp.path().parent().unwrap(), ) .unwrap(); let dst_root = dst.list_all_notes().unwrap()[0].clone(); @@ -4682,6 +4829,7 @@ fn test_is_leaf_defaults_to_false() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4702,6 +4850,7 @@ fn test_is_leaf_explicit_true() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4722,6 +4871,7 @@ fn test_is_leaf_blocks_create_child() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4754,6 +4904,7 @@ fn test_is_leaf_blocks_move_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4787,6 +4938,7 @@ fn test_is_leaf_blocks_deep_copy() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); ws.create_user_script( @@ -4821,6 +4973,7 @@ fn test_list_peers_info_unknown_peer() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); ws.add_contact_as_peer("AAAAAAAAAAAAAAAA").unwrap(); @@ -4847,6 +5000,7 @@ fn test_list_peers_info_known_contact() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let pubkey = "BBBBBBBBBBBBBBBB"; @@ -4880,6 +5034,7 @@ fn test_add_and_remove_peer() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let pubkey = "CCCCCCCCCCCCCCCC"; @@ -4906,6 +5061,7 @@ fn test_operations_since_empty() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); // A freshly created workspace has a RegisterDevice op, so operations_since is non-empty. @@ -4925,6 +5081,7 @@ fn test_operations_since_watermark() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -4961,6 +5118,7 @@ fn test_operations_since_excludes_device() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -4990,6 +5148,7 @@ fn test_operations_since_filters_local_retract() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let root = ws.list_all_notes().unwrap()[0].clone(); @@ -5056,6 +5215,7 @@ fn test_apply_incoming_create_note() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -5099,6 +5259,7 @@ fn test_apply_incoming_duplicate_is_idempotent() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -5139,6 +5300,7 @@ fn test_apply_incoming_retract_propagate_false_skipped() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -5184,6 +5346,7 @@ fn test_apply_incoming_hlc_advances() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -5245,13 +5408,29 @@ fn test_apply_incoming_hlc_advances() { fn test_create_user_script_rejected_for_non_owner() { let temp = NamedTempFile::new().unwrap(); let key_a = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let workspace = - Workspace::create(temp.path(), "", "identity-a", key_a, test_gate(), None).unwrap(); + let workspace = Workspace::create( + temp.path(), + "", + "identity-a", + key_a, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let source = "// @name: Evil Script\nschema(\"Evil\", #{ version: 1, fields: [] });"; let result = workspace.create_user_script(source); @@ -5270,6 +5449,7 @@ fn test_update_user_script_rejected_for_non_owner() { key_a.clone(), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: My Script\nschema(\"MyType\", #{ version: 1, fields: [] });"; @@ -5278,8 +5458,16 @@ fn test_update_user_script_rejected_for_non_owner() { drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let result = workspace.update_user_script( &script_id, "// @name: Hacked\nschema(\"Hacked\", #{ version: 1, fields: [] });", @@ -5299,6 +5487,7 @@ fn test_delete_user_script_rejected_for_non_owner() { key_a.clone(), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: My Script\nschema(\"MyType\", #{ version: 1, fields: [] });"; @@ -5307,8 +5496,16 @@ fn test_delete_user_script_rejected_for_non_owner() { drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let result = workspace.delete_user_script(&script_id); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("owner")); @@ -5325,6 +5522,7 @@ fn test_toggle_user_script_rejected_for_non_owner() { key_a.clone(), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: My Script\nschema(\"MyType\", #{ version: 1, fields: [] });"; @@ -5333,8 +5531,16 @@ fn test_toggle_user_script_rejected_for_non_owner() { drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let result = workspace.toggle_user_script(&script_id, false); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("owner")); @@ -5351,6 +5557,7 @@ fn test_reorder_user_script_rejected_for_non_owner() { key_a.clone(), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); let source = "// @name: My Script\nschema(\"MyType\", #{ version: 1, fields: [] });"; @@ -5359,8 +5566,16 @@ fn test_reorder_user_script_rejected_for_non_owner() { drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let result = workspace.reorder_user_script(&script_id, 0); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("owner")); @@ -5370,13 +5585,29 @@ fn test_reorder_user_script_rejected_for_non_owner() { fn test_reorder_all_user_scripts_rejected_for_non_owner() { let temp = NamedTempFile::new().unwrap(); let key_a = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let workspace = - Workspace::create(temp.path(), "", "identity-a", key_a, test_gate(), None).unwrap(); + let workspace = Workspace::create( + temp.path(), + "", + "identity-a", + key_a, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let mut workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let mut workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let result = workspace.reorder_all_user_scripts(&[]); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("owner")); @@ -5386,8 +5617,16 @@ fn test_reorder_all_user_scripts_rejected_for_non_owner() { fn test_owner_pubkey_matches_creator() { let temp = NamedTempFile::new().unwrap(); let key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let workspace = - Workspace::create(temp.path(), "", "test-id", key.clone(), test_gate(), None).unwrap(); + let workspace = Workspace::create( + temp.path(), + "", + "test-id", + key.clone(), + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); assert_eq!(workspace.owner_pubkey(), workspace.identity_pubkey()); assert!(workspace.is_owner()); @@ -5397,13 +5636,29 @@ fn test_owner_pubkey_matches_creator() { fn test_is_owner_false_for_different_identity() { let temp = NamedTempFile::new().unwrap(); let key_a = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let workspace = - Workspace::create(temp.path(), "", "identity-a", key_a, test_gate(), None).unwrap(); + let workspace = Workspace::create( + temp.path(), + "", + "identity-a", + key_a, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); drop(workspace); let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); assert!(!workspace.is_owner()); } @@ -5418,6 +5673,7 @@ fn test_open_legacy_workspace_without_owner_pubkey_assigns_opener() { key_a.clone(), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); // Manually remove owner_pubkey to simulate a pre-existing workspace @@ -5429,8 +5685,16 @@ fn test_open_legacy_workspace_without_owner_pubkey_assigns_opener() { // Re-open — opener should become owner let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); - let workspace = - Workspace::open(temp.path(), "", "identity-b", key_b, test_gate(), None).unwrap(); + let workspace = Workspace::open( + temp.path(), + "", + "identity-b", + key_b, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); assert!(workspace.is_owner()); assert_eq!(workspace.owner_pubkey(), workspace.identity_pubkey()); } @@ -5439,8 +5703,16 @@ fn test_open_legacy_workspace_without_owner_pubkey_assigns_opener() { fn test_apply_incoming_script_op_from_owner_applied() { let temp = NamedTempFile::new().unwrap(); let key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut workspace = - Workspace::create(temp.path(), "", "test-id", key.clone(), test_gate(), None).unwrap(); + let mut workspace = Workspace::create( + temp.path(), + "", + "test-id", + key.clone(), + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let owner_pubkey = workspace.identity_pubkey().to_string(); // Build a CreateUserScript op signed by the owner @@ -5474,8 +5746,16 @@ fn test_apply_incoming_script_op_from_owner_applied() { fn test_apply_incoming_script_op_from_non_owner_skipped() { let temp = NamedTempFile::new().unwrap(); let key_a = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut workspace = - Workspace::create(temp.path(), "", "identity-a", key_a, test_gate(), None).unwrap(); + let mut workspace = Workspace::create( + temp.path(), + "", + "identity-a", + key_a, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); // Build a CreateUserScript op signed by a different identity let key_b = ed25519_dalek::SigningKey::from_bytes(&[2u8; 32]); @@ -5522,8 +5802,16 @@ fn test_apply_incoming_script_op_from_non_owner_skipped() { fn test_verify_sender_authored_op_valid_signature() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); // Create and sign an op with a known key let sender_key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]); @@ -5548,8 +5836,16 @@ fn test_verify_sender_authored_op_valid_signature() { fn test_verify_sender_authored_op_tampered_rejected() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let sender_key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]); let sender_pubkey = { @@ -5592,8 +5888,16 @@ fn test_verify_sender_authored_op_tampered_rejected() { fn test_verify_vouched_relayed_op_accepted() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); // Op authored by key_a, relayed (vouched) by key_b let key_a = ed25519_dalek::SigningKey::from_bytes(&[10u8; 32]); @@ -5617,8 +5921,16 @@ fn test_verify_vouched_relayed_op_accepted() { fn test_verify_unvouched_relayed_op_rejected() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); // Op authored by key_a, sent by key_b without vouch let key_a = ed25519_dalek::SigningKey::from_bytes(&[10u8; 32]); @@ -5642,8 +5954,16 @@ fn test_verify_unvouched_relayed_op_rejected() { fn test_verify_vouch_mismatch_rejected() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let key_a = ed25519_dalek::SigningKey::from_bytes(&[10u8; 32]); let key_b = ed25519_dalek::SigningKey::from_bytes(&[11u8; 32]); @@ -5674,8 +5994,16 @@ fn test_verify_retract_op_vouched_accepted() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let sender_key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]); let sender_pubkey = { @@ -5712,8 +6040,16 @@ fn test_verify_retract_op_unvouched_rejected() { let temp = NamedTempFile::new().unwrap(); let ws_key = ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]); - let mut ws = - Workspace::create(temp.path(), "", "ws-device", ws_key, test_gate(), None).unwrap(); + let mut ws = Workspace::create( + temp.path(), + "", + "ws-device", + ws_key, + test_gate(), + None, + temp.path().parent().unwrap(), + ) + .unwrap(); let sender_key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]); let sender_pubkey = { @@ -5755,6 +6091,7 @@ fn test_list_peers_by_channel() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); @@ -5792,6 +6129,7 @@ fn test_undo_add_attachment() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -5876,6 +6214,7 @@ fn test_undo_delete_attachment() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); @@ -5973,6 +6312,7 @@ fn test_register_device_emitted_once_on_create() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); } @@ -5984,6 +6324,7 @@ fn test_register_device_emitted_once_on_create() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6010,6 +6351,7 @@ fn test_register_device_fields_are_correct() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6061,6 +6403,7 @@ fn test_set_note_checked() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6100,6 +6443,7 @@ fn test_c5_delete_note_logs_operation_atomically() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6128,6 +6472,7 @@ fn test_m4_undo_delete_schema_script_restores_category() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + &path.parent().unwrap(), ) .unwrap(); @@ -6158,6 +6503,7 @@ fn test_m5_purge_retains_more_than_100_ops() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6190,6 +6536,7 @@ fn test_apply_incoming_create_note_stores_seconds_timestamp() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6256,6 +6603,7 @@ fn test_m6_set_note_checked_stores_seconds_timestamp() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6289,6 +6637,7 @@ fn test_self_authored_ops_have_verified_by() { signing_key, test_gate(), None, + temp.path().parent().unwrap(), ) .unwrap(); @@ -6325,6 +6674,7 @@ fn test_restore_attachment_rejects_invalid_salt_hex() { ed25519_dalek::SigningKey::from_bytes(&[1u8; 32]), test_gate(), None, + dir.path(), ) .unwrap(); let root_id = ws.list_all_notes().unwrap()[0].id.clone(); diff --git a/krillnotes-core/src/lib.rs b/krillnotes-core/src/lib.rs index 2de31679..9e7548b3 100644 --- a/krillnotes-core/src/lib.rs +++ b/krillnotes-core/src/lib.rs @@ -20,7 +20,7 @@ pub use core::{ accepted_invite::{AcceptedInvite, AcceptedInviteManager, AcceptedInviteStatus}, attachment::AttachmentMeta, delete::{DeleteResult, DeleteStrategy}, - device::get_device_id, + device::{get_or_create_seed_device_id, resolve_device_id, seed_file_exists}, error::{KrillnotesError, Result}, export::{ export_workspace, import_workspace, peek_import, ExportError, ExportNotes, ImportResult, diff --git a/krillnotes-core/src/system_scripts/00_text_note.schema.rhai b/krillnotes-core/src/system_scripts/00_text_note.schema.rhai index 1a89d7cd..11e08c9d 100644 --- a/krillnotes-core/src/system_scripts/00_text_note.schema.rhai +++ b/krillnotes-core/src/system_scripts/00_text_note.schema.rhai @@ -34,4 +34,4 @@ register_menu("Add Child Note", ["TextNote"], |note| { let child = create_child(note.id, "TextNote"); set_title(child.id, "New note"); commit(); -}); \ No newline at end of file +}); diff --git a/krillnotes-core/src/system_scripts/01_todo_item.schema.rhai b/krillnotes-core/src/system_scripts/01_todo_item.schema.rhai index 12b2a159..8669a6dc 100644 --- a/krillnotes-core/src/system_scripts/01_todo_item.schema.rhai +++ b/krillnotes-core/src/system_scripts/01_todo_item.schema.rhai @@ -13,7 +13,8 @@ schema("TodoItem", #{ show_checkbox: true, is_leaf: true, fields: [ - #{ name: "body", type: "textarea", required: false }, + #{ name: "notes", type: "textarea", required: false }, + #{ name: "due_date", type: "date", required: false }, ] }); diff --git a/krillnotes-core/tests/relay_integration.rs b/krillnotes-core/tests/relay_integration.rs index bfde3324..9090f25e 100644 --- a/krillnotes-core/tests/relay_integration.rs +++ b/krillnotes-core/tests/relay_integration.rs @@ -55,6 +55,7 @@ fn b64_pubkey(key: &SigningKey) -> String { /// Create an in-memory (temp-file backed) workspace. fn make_workspace(key: &SigningKey, identity_id: &str) -> (NamedTempFile, Workspace) { let tmp = NamedTempFile::new().expect("tempfile"); + let data_dir = tmp.path().parent().unwrap_or(std::path::Path::new("/tmp")); let ws = Workspace::create( tmp.path(), "", @@ -62,6 +63,7 @@ fn make_workspace(key: &SigningKey, identity_id: &str) -> (NamedTempFile, Worksp SigningKey::from_bytes(&key.to_bytes()), test_gate(), None, + data_dir, ) .expect("Workspace::create"); (tmp, ws) @@ -270,6 +272,7 @@ fn relay_delta_roundtrip() { SigningKey::from_bytes(&bob_key.to_bytes()), test_gate(), None, + alice_tmp.path().parent().unwrap(), ) .expect("Workspace::open"); let (_bob_cm_dir, mut bob_cm) = make_contact_manager([0xBBu8; 32]); @@ -331,6 +334,7 @@ fn folder_channel_delta_roundtrip() { alice_ws.workspace_id(), test_gate(), None, + bob_tmp.path().parent().unwrap(), ) .expect("Workspace::create_with_id for Bob"); bob_ws diff --git a/krillnotes-core/tests/watermark_recovery.rs b/krillnotes-core/tests/watermark_recovery.rs index f0fb2554..34d5a6d5 100644 --- a/krillnotes-core/tests/watermark_recovery.rs +++ b/krillnotes-core/tests/watermark_recovery.rs @@ -46,6 +46,7 @@ fn b64_pubkey(key: &SigningKey) -> String { fn make_workspace(key: &SigningKey, identity_id: &str) -> (NamedTempFile, Workspace) { let tmp = NamedTempFile::new().expect("tempfile"); + let data_dir = tmp.path().parent().unwrap_or(std::path::Path::new("/tmp")); let ws = Workspace::create( tmp.path(), "", @@ -53,6 +54,7 @@ fn make_workspace(key: &SigningKey, identity_id: &str) -> (NamedTempFile, Worksp SigningKey::from_bytes(&key.to_bytes()), test_gate(), None, + data_dir, ) .expect("Workspace::create"); (tmp, ws) @@ -300,6 +302,7 @@ fn ack_behind_watermark_resets_alice_watermark() { alice_ws.workspace_id(), test_gate(), None, + bob_tmp.path().parent().unwrap(), ) .expect("bob create_with_id"); bob_ws @@ -389,6 +392,7 @@ fn ack_unknown_op_resets_alice_watermark_to_none() { alice_ws.workspace_id(), test_gate(), None, + bob_tmp.path().parent().unwrap(), ) .expect("bob create_with_id"); bob_ws @@ -473,6 +477,7 @@ fn no_ack_in_delta_resets_alice_watermark_to_none() { alice_ws.workspace_id(), test_gate(), None, + bob_tmp.path().parent().unwrap(), ) .expect("bob create_with_id"); bob_ws diff --git a/krillnotes-desktop/android-dev.sh b/krillnotes-desktop/android-dev.sh new file mode 100755 index 00000000..156d186e --- /dev/null +++ b/krillnotes-desktop/android-dev.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Launch Android dev build on emulator or physical device +# Usage: ./android-dev.sh # auto-detect (prefers running emulator) +# ./android-dev.sh --device # target USB-connected physical device +# ./android-dev.sh --emulator # target emulator only + +# Kill any lingering Vite dev server +lsof -ti:1420 | xargs kill -9 2>/dev/null + +# Use rustup cargo (not Homebrew) for cross-compilation +export PATH="$(dirname $(rustup which cargo)):$PATH" + +# Android SDK / NDK / Java +export ANDROID_HOME="$HOME/Library/Android/sdk" +export NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264" +export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" +export PATH="$JAVA_HOME/bin:$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$ANDROID_HOME/platform-tools:$PATH" + +case "${1:-}" in + --device) + if ! adb devices | grep -qw "device$"; then + echo "❌ No physical device found. Check USB connection and USB debugging." >&2 + exit 1 + fi + SERIAL=$(adb devices | awk '/\tdevice$/ {print $1; exit}') + echo "📱 Targeting physical device: $SERIAL" + ;; + --emulator) + if ! adb devices | grep -q "^emulator-"; then + AVD=$("$ANDROID_HOME/emulator/emulator" -list-avds 2>/dev/null | head -1) + if [ -z "$AVD" ]; then + echo "❌ No AVDs found. Create one in Android Studio first." >&2 + exit 1 + fi + echo "📱 Starting emulator: $AVD" + "$ANDROID_HOME/emulator/emulator" -avd "$AVD" & + echo "⏳ Waiting for emulator to boot..." + adb wait-for-device + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do sleep 1; done + echo "✅ Emulator ready" + else + echo "📱 Targeting running emulator" + fi + ;; + *) + echo "📱 Auto-detecting target (use --device or --emulator to override)" + ;; +esac + +npm run tauri android dev diff --git a/krillnotes-desktop/index.html b/krillnotes-desktop/index.html index ff93803b..530728d6 100644 --- a/krillnotes-desktop/index.html +++ b/krillnotes-desktop/index.html @@ -3,7 +3,7 @@ - + Tauri + React + Typescript diff --git a/krillnotes-desktop/ios-dev.sh b/krillnotes-desktop/ios-dev.sh new file mode 100755 index 00000000..906e7410 --- /dev/null +++ b/krillnotes-desktop/ios-dev.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Launch iOS simulator dev build +# Usage: ./ios-dev.sh [simulator-index] +# Default: 22 (iPhone 17 Pro iOS 26.3) + +SIMULATOR_INDEX="${1:-22}" + +# Kill any lingering Vite dev server +lsof -ti:1420 | xargs kill -9 2>/dev/null + +# Ensure simulator is booted +xcrun simctl boot FB65129A-E323-4719-8018-8A23C1B0713E 2>/dev/null +open -a Simulator + +# Use rustup cargo (not Homebrew) for cross-compilation +export PATH="$(dirname $(rustup which cargo)):$PATH" + +echo "$SIMULATOR_INDEX" | npm run tauri ios dev diff --git a/krillnotes-desktop/src-tauri/Cargo.toml b/krillnotes-desktop/src-tauri/Cargo.toml index 9c631148..e1877ff8 100644 --- a/krillnotes-desktop/src-tauri/Cargo.toml +++ b/krillnotes-desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "krillnotes-desktop" -version = "1.1.1" +version = "1.2.0" description = "Krill Notes desktop application (Tauri)" authors = ["TripleACS Pty Ltd t/a 2pi Software "] edition = "2021" diff --git a/krillnotes-desktop/src-tauri/gen/android/.editorconfig b/krillnotes-desktop/src-tauri/gen/android/.editorconfig new file mode 100644 index 00000000..ebe51d3b --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/.gitignore b/krillnotes-desktop/src-tauri/gen/android/.gitignore new file mode 100644 index 00000000..1c636c39 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/.gitignore @@ -0,0 +1,20 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties +keystore.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/.gitignore b/krillnotes-desktop/src-tauri/gen/android/app/.gitignore new file mode 100644 index 00000000..6c4d56b4 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/**/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/build.gradle.kts b/krillnotes-desktop/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 00000000..b1a01745 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,71 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "io.opswarm.krillnotes" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "io.opswarm.krillnotes" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.lifecycle:lifecycle-process:2.10.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/proguard-rules.pro b/krillnotes-desktop/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0810d022 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/java/io/opswarm/krillnotes/MainActivity.kt b/krillnotes-desktop/src-tauri/gen/android/app/src/main/java/io/opswarm/krillnotes/MainActivity.kt new file mode 100644 index 00000000..cf675d5f --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/java/io/opswarm/krillnotes/MainActivity.kt @@ -0,0 +1,22 @@ +package io.opswarm.krillnotes + +import android.os.Bundle +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val contentView = findViewById(android.R.id.content) + ViewCompat.setOnApplyWindowInsetsListener(contentView) { view, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.updatePadding( + top = systemBars.top, + bottom = systemBars.bottom + ) + insets + } + } +} diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4fc24441 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..28f1aa11 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..85d0c88a Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..28f1aa11 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..73e48dbf Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..13dd2147 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..73e48dbf Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..1d98044f Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a888b336 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1d98044f Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..08183246 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a2a838e7 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..08183246 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..b18bceb6 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..3f8a57f3 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b18bceb6 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..836c86b0 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/colors.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/strings.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..e79dc15e --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Krillnotes + Krillnotes + \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/themes.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..836c86b0 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..782d63b9 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/android/build.gradle.kts b/krillnotes-desktop/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 00000000..607240bc --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/krillnotes-desktop/src-tauri/gen/android/buildSrc/build.gradle.kts b/krillnotes-desktop/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 00000000..5c55bba7 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/BuildTask.kt b/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/BuildTask.kt new file mode 100644 index 00000000..a3de1256 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/BuildTask.kt @@ -0,0 +1,68 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """npm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("run", "--", "tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/RustPlugin.kt b/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/RustPlugin.kt new file mode 100644 index 00000000..4aa7fcaf --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/buildSrc/src/main/java/com/twopisoftware/krillnotes/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/gradle.properties b/krillnotes-desktop/src-tauri/gen/android/gradle.properties new file mode 100644 index 00000000..2a7ec695 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..c5f9a53c --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/krillnotes-desktop/src-tauri/gen/android/gradlew b/krillnotes-desktop/src-tauri/gen/android/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/krillnotes-desktop/src-tauri/gen/android/gradlew.bat b/krillnotes-desktop/src-tauri/gen/android/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/krillnotes-desktop/src-tauri/gen/android/settings.gradle b/krillnotes-desktop/src-tauri/gen/android/settings.gradle new file mode 100644 index 00000000..39391166 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/krillnotes-desktop/src-tauri/gen/apple/.gitignore b/krillnotes-desktop/src-tauri/gen/apple/.gitignore new file mode 100644 index 00000000..6726e2f8 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/.gitignore @@ -0,0 +1,3 @@ +xcuserdata/ +build/ +Externals/ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 00000000..a6ac2a8c Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 00000000..2869541f Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png new file mode 100644 index 00000000..2869541f Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png new file mode 100644 index 00000000..cf265a45 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png new file mode 100644 index 00000000..29c9746c Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png new file mode 100644 index 00000000..a4e68c8d Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 00000000..a4e68c8d Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 00000000..e4adcbce Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 00000000..2869541f Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png new file mode 100644 index 00000000..a414e65b Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png new file mode 100644 index 00000000..a414e65b Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png new file mode 100644 index 00000000..a0807e5d Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 00000000..704c9291 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png new file mode 100644 index 00000000..a0807e5d Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 00000000..2a9fbc26 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 00000000..2cdf1848 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 00000000..4723e4b4 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 00000000..f26fee45 Binary files /dev/null and b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png differ diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..90eea7ec --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "AppIcon-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "AppIcon-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon-29x29@2x-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "AppIcon-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "AppIcon-20x20@2x-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon-40x40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "AppIcon-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "AppIcon-512@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/Contents.json b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/apple/ExportOptions.plist b/krillnotes-desktop/src-tauri/gen/apple/ExportOptions.plist new file mode 100644 index 00000000..0428a171 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/ExportOptions.plist @@ -0,0 +1,8 @@ + + + + + method + debugging + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/LaunchScreen.storyboard b/krillnotes-desktop/src-tauri/gen/apple/LaunchScreen.storyboard new file mode 100644 index 00000000..81b5f90e --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/Podfile b/krillnotes-desktop/src-tauri/gen/apple/Podfile new file mode 100644 index 00000000..9faeff5f --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project + +target 'krillnotes-desktop_iOS' do +platform :ios, '14.0' + # Pods for krillnotes-desktop_iOS +end + +target 'krillnotes-desktop_macOS' do +platform :osx, '11.0' + # Pods for krillnotes-desktop_macOS +end + +# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' + end + end +end diff --git a/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/bindings/bindings.h b/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/bindings/bindings.h new file mode 100644 index 00000000..51522007 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/bindings/bindings.h @@ -0,0 +1,8 @@ +#pragma once + +namespace ffi { + extern "C" { + void start_app(); + } +} + diff --git a/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/main.mm b/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/main.mm new file mode 100644 index 00000000..7793a9d5 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/Sources/krillnotes-desktop/main.mm @@ -0,0 +1,6 @@ +#include "bindings/bindings.h" + +int main(int argc, char * argv[]) { + ffi::start_app(); + return 0; +} diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.pbxproj b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.pbxproj new file mode 100644 index 00000000..314a91e0 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.pbxproj @@ -0,0 +1,548 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0107BADC0FBABF417EC33956 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9765E7095C18478F0A3A197 /* Security.framework */; }; + 1BB96392759E85E724C2CFC2 /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 63272F042AD7E68E8BBA9CDE /* libapp.a */; }; + 37820C3D86607F8F5806AB1A /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1413595D99675777D5420D7 /* MetalKit.framework */; }; + 4E95074CDB64644C5DD30AC8 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6399E5C5F1661AA19BA14DF0 /* CoreGraphics.framework */; }; + 894C367082E33084CEFE1825 /* libapp.a in Resources */ = {isa = PBXBuildFile; fileRef = 2978395842B75B3118436775 /* libapp.a */; }; + A9FE0B3A1FF94522D9421EC7 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 59431B8D5D960B579B151274 /* main.mm */; }; + AC857CF3EA55914664AC7B8B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CF40B3A1C01CFCECE4F8B5E /* UIKit.framework */; }; + B3413D7A5625B75605955EAB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 026D4C28FD09FAA689B75517 /* assets */; }; + C0DF81211849B324CDAB0536 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 214778EE03827CCB4CBE54AA /* QuartzCore.framework */; }; + C590F03E61C990C3C6132E66 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7410E73F319CF0201AB2A668 /* WebKit.framework */; }; + C6B2CAD8298C7EA89C82AB51 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCC531ED8A871E740986544E /* Metal.framework */; }; + CC774315B2BF0D88C1E5BA72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 712DA10D26CDF5BD6773EF09 /* Assets.xcassets */; }; + ECEDF5EA243DA761B4B03955 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCFD6D2163E9723CF7C9541D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0192F505136CE826F29D1FAB /* main.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = main.rs; sourceTree = ""; }; + 026D4C28FD09FAA689B75517 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + 05DCEE871F04ACD66B10049A /* permissions.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = permissions.rs; sourceTree = ""; }; + 0CF40B3A1C01CFCECE4F8B5E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 0DBB61357E42712F898F3FE3 /* Krillnotes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Krillnotes.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0EFB6723798B39776A3B8757 /* invites.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = invites.rs; sourceTree = ""; }; + 214778EE03827CCB4CBE54AA /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 2978395842B75B3118436775 /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 3B23B0336F55EAFEB1D67FD6 /* notes.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = notes.rs; sourceTree = ""; }; + 570058DC499E0F508E37AFE1 /* scripts.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = scripts.rs; sourceTree = ""; }; + 57B7F81A9C99175C1EAC10C6 /* identity.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = identity.rs; sourceTree = ""; }; + 59431B8D5D960B579B151274 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + 5BB6CDA58D5A7C723A19C4B1 /* attachments.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = attachments.rs; sourceTree = ""; }; + 63272F042AD7E68E8BBA9CDE /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 6389E32C1E3E59FA1D32B382 /* contacts.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = contacts.rs; sourceTree = ""; }; + 6399E5C5F1661AA19BA14DF0 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 6F98260B27DFB07D5988365E /* accepted_invites.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = accepted_invites.rs; sourceTree = ""; }; + 712DA10D26CDF5BD6773EF09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7272EC764B21A1270A35BA5C /* mod.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = mod.rs; sourceTree = ""; }; + 7410E73F319CF0201AB2A668 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 9185BF75F3F8A668B2796DBF /* lib.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = lib.rs; sourceTree = ""; }; + 9281F0498634AC3F1A22BCDA /* locales.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = locales.rs; sourceTree = ""; }; + 98C0945BB5932F9771A40DE7 /* settings.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = settings.rs; sourceTree = ""; }; + A1413595D99675777D5420D7 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + A94D52343C9BE4C5C1F60444 /* swarm.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = swarm.rs; sourceTree = ""; }; + AAAB1B27CF5E0E2C3903F5C5 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + AB64A86424E45AF4C4D583DE /* themes.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = themes.rs; sourceTree = ""; }; + AD14DED15CF1923F0B521470 /* relay_accounts.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = relay_accounts.rs; sourceTree = ""; }; + B097ED3575B09B59B1CFD70E /* krillnotes-desktop_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "krillnotes-desktop_iOS.entitlements"; sourceTree = ""; }; + BCC531ED8A871E740986544E /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + C9765E7095C18478F0A3A197 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + D1F8A4D90BCA605E4B11B8A4 /* menu.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = menu.rs; sourceTree = ""; }; + DCFD6D2163E9723CF7C9541D /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + E446EFEE920D3DB711D0D00F /* receive_poll.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = receive_poll.rs; sourceTree = ""; }; + E965C3754235B188DCB92400 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + FC6495BDEA744AB051A1B0AB /* sync.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = sync.rs; sourceTree = ""; }; + FD179C1932F1746D0D3E09A2 /* workspace.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = workspace.rs; sourceTree = ""; }; + FDD1B2CE5AD70571CC5D5699 /* scripting.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = scripting.rs; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 60D4CAA9EEC992D21F3852EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BB96392759E85E724C2CFC2 /* libapp.a in Frameworks */, + 4E95074CDB64644C5DD30AC8 /* CoreGraphics.framework in Frameworks */, + C6B2CAD8298C7EA89C82AB51 /* Metal.framework in Frameworks */, + 37820C3D86607F8F5806AB1A /* MetalKit.framework in Frameworks */, + C0DF81211849B324CDAB0536 /* QuartzCore.framework in Frameworks */, + 0107BADC0FBABF417EC33956 /* Security.framework in Frameworks */, + AC857CF3EA55914664AC7B8B /* UIKit.framework in Frameworks */, + C590F03E61C990C3C6132E66 /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 027A308CA52E0EB0287115D3 /* src */ = { + isa = PBXGroup; + children = ( + 9185BF75F3F8A668B2796DBF /* lib.rs */, + 9281F0498634AC3F1A22BCDA /* locales.rs */, + 0192F505136CE826F29D1FAB /* main.rs */, + D1F8A4D90BCA605E4B11B8A4 /* menu.rs */, + 98C0945BB5932F9771A40DE7 /* settings.rs */, + AB64A86424E45AF4C4D583DE /* themes.rs */, + 98E78E784EBA2029F8746F54 /* commands */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + 045EBCEFF5E0B788FF776A85 /* Products */ = { + isa = PBXGroup; + children = ( + 0DBB61357E42712F898F3FE3 /* Krillnotes.app */, + ); + name = Products; + sourceTree = ""; + }; + 11CBD4F5A6587743AAEE0225 /* Externals */ = { + isa = PBXGroup; + children = ( + 56EFC4375B268E49BD7C0D96 /* arm64 */, + ); + path = Externals; + sourceTree = ""; + }; + 1A745B24E8BE0C5E67C6C245 /* krillnotes-desktop_iOS */ = { + isa = PBXGroup; + children = ( + E965C3754235B188DCB92400 /* Info.plist */, + B097ED3575B09B59B1CFD70E /* krillnotes-desktop_iOS.entitlements */, + ); + path = "krillnotes-desktop_iOS"; + sourceTree = ""; + }; + 3F596B0EE1C21BD9D2447756 = { + isa = PBXGroup; + children = ( + 026D4C28FD09FAA689B75517 /* assets */, + 712DA10D26CDF5BD6773EF09 /* Assets.xcassets */, + DCFD6D2163E9723CF7C9541D /* LaunchScreen.storyboard */, + 11CBD4F5A6587743AAEE0225 /* Externals */, + 1A745B24E8BE0C5E67C6C245 /* krillnotes-desktop_iOS */, + D8D95D54C1775F764576A741 /* Sources */, + 027A308CA52E0EB0287115D3 /* src */, + FE5EA6005D1CC1D949E69494 /* Frameworks */, + 045EBCEFF5E0B788FF776A85 /* Products */, + ); + sourceTree = ""; + }; + 442642A00E14075A38F4560A /* debug */ = { + isa = PBXGroup; + children = ( + 2978395842B75B3118436775 /* libapp.a */, + ); + path = debug; + sourceTree = ""; + }; + 56EFC4375B268E49BD7C0D96 /* arm64 */ = { + isa = PBXGroup; + children = ( + 442642A00E14075A38F4560A /* debug */, + ); + path = arm64; + sourceTree = ""; + }; + 98E78E784EBA2029F8746F54 /* commands */ = { + isa = PBXGroup; + children = ( + 6F98260B27DFB07D5988365E /* accepted_invites.rs */, + 5BB6CDA58D5A7C723A19C4B1 /* attachments.rs */, + 6389E32C1E3E59FA1D32B382 /* contacts.rs */, + 57B7F81A9C99175C1EAC10C6 /* identity.rs */, + 0EFB6723798B39776A3B8757 /* invites.rs */, + 7272EC764B21A1270A35BA5C /* mod.rs */, + 3B23B0336F55EAFEB1D67FD6 /* notes.rs */, + 05DCEE871F04ACD66B10049A /* permissions.rs */, + E446EFEE920D3DB711D0D00F /* receive_poll.rs */, + AD14DED15CF1923F0B521470 /* relay_accounts.rs */, + FDD1B2CE5AD70571CC5D5699 /* scripting.rs */, + 570058DC499E0F508E37AFE1 /* scripts.rs */, + A94D52343C9BE4C5C1F60444 /* swarm.rs */, + FC6495BDEA744AB051A1B0AB /* sync.rs */, + FD179C1932F1746D0D3E09A2 /* workspace.rs */, + ); + path = commands; + sourceTree = ""; + }; + B5111395BE79BEDF8DCF655B /* krillnotes-desktop */ = { + isa = PBXGroup; + children = ( + 59431B8D5D960B579B151274 /* main.mm */, + D6093E10C8F79F4AB9A02BD8 /* bindings */, + ); + path = "krillnotes-desktop"; + sourceTree = ""; + }; + D6093E10C8F79F4AB9A02BD8 /* bindings */ = { + isa = PBXGroup; + children = ( + AAAB1B27CF5E0E2C3903F5C5 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + D8D95D54C1775F764576A741 /* Sources */ = { + isa = PBXGroup; + children = ( + B5111395BE79BEDF8DCF655B /* krillnotes-desktop */, + ); + path = Sources; + sourceTree = ""; + }; + FE5EA6005D1CC1D949E69494 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6399E5C5F1661AA19BA14DF0 /* CoreGraphics.framework */, + 63272F042AD7E68E8BBA9CDE /* libapp.a */, + BCC531ED8A871E740986544E /* Metal.framework */, + A1413595D99675777D5420D7 /* MetalKit.framework */, + 214778EE03827CCB4CBE54AA /* QuartzCore.framework */, + C9765E7095C18478F0A3A197 /* Security.framework */, + 0CF40B3A1C01CFCECE4F8B5E /* UIKit.framework */, + 7410E73F319CF0201AB2A668 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3BFCA31082EDC9B6394EA15C /* krillnotes-desktop_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4BF2CF87C0A2C0B10B2C4F7E /* Build configuration list for PBXNativeTarget "krillnotes-desktop_iOS" */; + buildPhases = ( + 6B2FF1987D488AF06BCE0102 /* Build Rust Code */, + CBC688EEE9814D54CC29E2DB /* Sources */, + 334ACA1984082F3A88796802 /* Resources */, + 60D4CAA9EEC992D21F3852EF /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "krillnotes-desktop_iOS"; + packageProductDependencies = ( + ); + productName = "krillnotes-desktop_iOS"; + productReference = 0DBB61357E42712F898F3FE3 /* Krillnotes.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5D16CDB6635D1C4182CA2BDC /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + }; + buildConfigurationList = 27444EC7E7F951152080386B /* Build configuration list for PBXProject "krillnotes-desktop" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 3F596B0EE1C21BD9D2447756; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 045EBCEFF5E0B788FF776A85 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3BFCA31082EDC9B6394EA15C /* krillnotes-desktop_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 334ACA1984082F3A88796802 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CC774315B2BF0D88C1E5BA72 /* Assets.xcassets in Resources */, + ECEDF5EA243DA761B4B03955 /* LaunchScreen.storyboard in Resources */, + B3413D7A5625B75605955EAB /* assets in Resources */, + 894C367082E33084CEFE1825 /* libapp.a in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6B2FF1987D488AF06BCE0102 /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "RUSTUP_CARGO=\"$(dirname $(rustup which cargo))\" && export PATH=\"$RUSTUP_CARGO:/opt/homebrew/bin:$PATH\" && export CARGO=\"$RUSTUP_CARGO/cargo\" && export RUSTC=\"$RUSTUP_CARGO/../bin/rustc\" && npm run -- tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CBC688EEE9814D54CC29E2DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A9FE0B3A1FF94522D9421EC7 /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3E374E586CC6DB60985C4DBB /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + 51A49E82312FC0CB9342E135 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + 8B3CF08CC68248D946018B0D /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 25AJM98B7U; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "krillnotes-desktop_iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.twopisoftware.krillnotes; + PRODUCT_NAME = "Krillnotes"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; + AA8A50C2DEFDAAF2495FF902 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 25AJM98B7U; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "krillnotes-desktop_iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)", + "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.twopisoftware.krillnotes; + PRODUCT_NAME = "Krillnotes"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 27444EC7E7F951152080386B /* Build configuration list for PBXProject "krillnotes-desktop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51A49E82312FC0CB9342E135 /* debug */, + 3E374E586CC6DB60985C4DBB /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + 4BF2CF87C0A2C0B10B2C4F7E /* Build configuration list for PBXNativeTarget "krillnotes-desktop_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA8A50C2DEFDAAF2495FF902 /* debug */, + 8B3CF08CC68248D946018B0D /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5D16CDB6635D1C4182CA2BDC /* Project object */; +} diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..ac90d5ac --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + BuildSystemType + Original + DisableBuildSystemDeprecationDiagnostic + + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/xcshareddata/xcschemes/krillnotes-desktop_iOS.xcscheme b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/xcshareddata/xcschemes/krillnotes-desktop_iOS.xcscheme new file mode 100644 index 00000000..5c4a5c8b --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop.xcodeproj/xcshareddata/xcschemes/krillnotes-desktop_iOS.xcscheme @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/Info.plist b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/Info.plist new file mode 100644 index 00000000..b361a6e6 --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/Info.plist @@ -0,0 +1,94 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.1.0 + CFBundleVersion + 1.1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UTExportedTypeDeclarations + + + UTTypeIdentifier + com.careck.krillnotes-archive + UTTypeDescription + Krillnotes Archive + UTTypeConformsTo + + public.archive + public.data + + UTTypeTagSpecification + + public.filename-extension + + krillnotes + + public.mime-type + application/x-krillnotes + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + krillnotes + + CFBundleTypeName + Krillnotes Archive + CFBundleTypeRole + Editor + LSHandlerRank + Default + + + CFBundleTypeExtensions + + swarm + + CFBundleTypeName + Krillnotes Sync Bundle + CFBundleTypeRole + Editor + LSHandlerRank + Default + + + + \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/krillnotes-desktop/src-tauri/gen/apple/project.yml b/krillnotes-desktop/src-tauri/gen/apple/project.yml new file mode 100644 index 00000000..f931e7bd --- /dev/null +++ b/krillnotes-desktop/src-tauri/gen/apple/project.yml @@ -0,0 +1,88 @@ +name: krillnotes-desktop +options: + bundleIdPrefix: io.opswarm.krillnotes + deploymentTarget: + iOS: 14.0 +fileGroups: [../../src] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: Krillnotes + PRODUCT_BUNDLE_IDENTIFIER: io.opswarm.krillnotes +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + krillnotes-desktop_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: Assets.xcassets + - path: Externals + - path: krillnotes-desktop_iOS + - path: assets + buildPhase: resources + type: folder + - path: LaunchScreen.storyboard + info: + path: krillnotes-desktop_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: 1.1.0 + CFBundleVersion: "1.1.0" + entitlements: + path: krillnotes-desktop_iOS/krillnotes-desktop_iOS.entitlements + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + base: + ENABLE_BITCODE: false + ARCHS: [arm64] + VALID_ARCHS: arm64 + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + groups: [app] + dependencies: + - framework: libapp.a + embed: false + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: WebKit.framework + preBuildScripts: + - script: RUSTUP_CARGO="$(dirname $(rustup which cargo))" && export PATH="$RUSTUP_CARGO:/opt/homebrew/bin:$PATH" && export CARGO="$RUSTUP_CARGO/cargo" && export RUSTC="$RUSTUP_CARGO/../bin/rustc" && npm run -- tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a \ No newline at end of file diff --git a/krillnotes-desktop/src-tauri/src/commands/identity.rs b/krillnotes-desktop/src-tauri/src/commands/identity.rs index 1748fe71..2a4a36f3 100644 --- a/krillnotes-desktop/src-tauri/src/commands/identity.rs +++ b/krillnotes-desktop/src-tauri/src/commands/identity.rs @@ -270,7 +270,7 @@ pub fn unlock_identity( let ids = state.unlocked_identities.lock().expect("Mutex poisoned"); if let (Some(id), Ok(device_id)) = ( ids.get(&uuid), - krillnotes_core::core::device::get_device_id(), + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()), ) { let device_sk = id.device_signing_key(&device_id); let dpk_hex = hex::encode(device_sk.verifying_key().to_bytes()); diff --git a/krillnotes-desktop/src-tauri/src/commands/invites.rs b/krillnotes-desktop/src-tauri/src/commands/invites.rs index 04481879..ea1a7815 100644 --- a/krillnotes-desktop/src-tauri/src/commands/invites.rs +++ b/krillnotes-desktop/src-tauri/src/commands/invites.rs @@ -174,7 +174,7 @@ pub fn create_invite( // Stamp the inviter's composite device ID so the acceptor can route the // acceptance bundle via the relay's findByDeviceId fallback. - if let Ok(short) = krillnotes_core::core::device::get_device_id() { + if let Ok(short) = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) { file.inviter_device_id = Some(format!("{}:identity:{}", short, uuid)); // Re-sign after mutation. let payload = serde_json::to_value(&file).map_err(|e| e.to_string())?; @@ -262,7 +262,8 @@ pub fn save_invite_file( inviter_public_key: pubkey_b64, inviter_declared_name: declared_name, inviter_device_id: { - let short = krillnotes_core::core::device::get_device_id().ok(); + let short = + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()).ok(); short.map(|s| format!("{}:identity:{}", s, uuid)) }, expires_at: record.expires_at.map(|dt| dt.to_rfc3339()), diff --git a/krillnotes-desktop/src-tauri/src/commands/notes.rs b/krillnotes-desktop/src-tauri/src/commands/notes.rs index d8bb3dd5..09291ba9 100644 --- a/krillnotes-desktop/src-tauri/src/commands/notes.rs +++ b/krillnotes-desktop/src-tauri/src/commands/notes.rs @@ -352,30 +352,37 @@ pub fn get_notes_for_tag( #[tauri::command] pub fn set_paste_menu_enabled( - state: State<'_, AppState>, + _state: State<'_, AppState>, _window: tauri::Window, - enabled: bool, + _enabled: bool, ) -> std::result::Result<(), String> { - #[cfg(target_os = "macos")] + #[cfg(desktop)] { - let items = state.paste_menu_items.lock().expect("Mutex poisoned"); - if let Some((child_item, sibling_item)) = items.get("macos") { - child_item.set_enabled(enabled).map_err(|e| e.to_string())?; - sibling_item - .set_enabled(enabled) - .map_err(|e| e.to_string())?; + #[cfg(target_os = "macos")] + { + let items = _state.paste_menu_items.lock().expect("Mutex poisoned"); + if let Some((child_item, sibling_item)) = items.get("macos") { + child_item + .set_enabled(_enabled) + .map_err(|e| e.to_string())?; + sibling_item + .set_enabled(_enabled) + .map_err(|e| e.to_string())?; + } } - } - #[cfg(not(target_os = "macos"))] - { - let label = _window.label().to_string(); - let items = state.paste_menu_items.lock().expect("Mutex poisoned"); - if let Some((child_item, sibling_item)) = items.get(&label) { - child_item.set_enabled(enabled).map_err(|e| e.to_string())?; - sibling_item - .set_enabled(enabled) - .map_err(|e| e.to_string())?; + #[cfg(all(desktop, not(target_os = "macos")))] + { + let label = _window.label().to_string(); + let items = _state.paste_menu_items.lock().expect("Mutex poisoned"); + if let Some((child_item, sibling_item)) = items.get(&label) { + child_item + .set_enabled(_enabled) + .map_err(|e| e.to_string())?; + sibling_item + .set_enabled(_enabled) + .map_err(|e| e.to_string())?; + } } } diff --git a/krillnotes-desktop/src-tauri/src/commands/receive_poll.rs b/krillnotes-desktop/src-tauri/src/commands/receive_poll.rs index d032033b..925b8324 100644 --- a/krillnotes-desktop/src-tauri/src/commands/receive_poll.rs +++ b/krillnotes-desktop/src-tauri/src/commands/receive_poll.rs @@ -192,7 +192,8 @@ pub async fn poll_receive_workspace( }; let device_id = { - let short = krillnotes_core::core::device::get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, identity_uuid) }; @@ -799,7 +800,8 @@ pub async fn poll_receive_identity( // Build RelayConnections and call core function inside spawn_blocking let temp_dir = std::env::temp_dir(); let device_id = { - let short = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, uuid) }; @@ -906,7 +908,9 @@ pub async fn poll_all_identity_snapshots( } }; - let short_device_id = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let short_device_id = + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; let device_id = format!("{}:identity:{}", short_device_id, uuid); let relay_accounts_clone = relay_accounts.clone(); let device_id_clone = device_id.clone(); @@ -1028,7 +1032,8 @@ pub async fn poll_all_identity_snapshots( let temp_dir = std::env::temp_dir(); let device_id = { - let short = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, uuid) }; let result = tokio::task::spawn_blocking(move || { diff --git a/krillnotes-desktop/src-tauri/src/commands/relay_accounts.rs b/krillnotes-desktop/src-tauri/src/commands/relay_accounts.rs index dce47425..38d8b8eb 100644 --- a/krillnotes-desktop/src-tauri/src/commands/relay_accounts.rs +++ b/krillnotes-desktop/src-tauri/src/commands/relay_accounts.rs @@ -91,8 +91,8 @@ pub async fn register_relay_account( let id = m .get(&uuid) .ok_or("Identity is not unlocked — please unlock your identity first")?; - let device_id = - krillnotes_core::core::device::get_device_id().map_err(|e| e.to_string())?; + let device_id = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; let device_sk = id.device_signing_key(&device_id); let dpk = hex::encode(device_sk.verifying_key().to_bytes()); let identity_sk = crate::Ed25519SigningKey::from_bytes(&id.signing_key.to_bytes()); @@ -101,7 +101,8 @@ pub async fn register_relay_account( }; let composite_device_id = { - let short = krillnotes_core::core::device::get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, uuid) }; let identity_uuid_str = identity_uuid.clone(); @@ -213,8 +214,8 @@ pub async fn login_relay_account( let id = m .get(&uuid) .ok_or("Identity is not unlocked — please unlock your identity first")?; - let device_id = - krillnotes_core::core::device::get_device_id().map_err(|e| e.to_string())?; + let device_id = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; let device_sk = id.device_signing_key(&device_id); let dpk = hex::encode(device_sk.verifying_key().to_bytes()); let composite = format!("{}:identity:{}", device_id, uuid); diff --git a/krillnotes-desktop/src-tauri/src/commands/swarm.rs b/krillnotes-desktop/src-tauri/src/commands/swarm.rs index 364ebb34..616519dc 100644 --- a/krillnotes-desktop/src-tauri/src/commands/swarm.rs +++ b/krillnotes-desktop/src-tauri/src/commands/swarm.rs @@ -276,7 +276,9 @@ pub async fn create_snapshot_for_peers( id.display_name.clone(), ) }; - let source_device_id = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let source_device_id = + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; // 2. Decode recipient verifying keys from base64. let recipient_vks: Vec = peer_public_keys @@ -479,6 +481,7 @@ pub async fn apply_swarm_snapshot( &parsed.workspace_id, super::workspace::create_permission_gate(owner_pubkey), Some(&swarm_identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| e.to_string())?; @@ -713,7 +716,9 @@ pub async fn send_snapshot_via_relay( id.display_name.clone(), ) }; - let source_device_id = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let source_device_id = + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; let sender_composite_device_id = format!("{}:identity:{}", source_device_id, identity_uuid); // 2. Decode recipient verifying keys from base64. @@ -1075,7 +1080,8 @@ pub async fn send_self_snapshot_via_relay( // 1. Sender signing key + display name + per-device key. let source_device_id = { - let short = krillnotes_core::get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, identity_uuid_parsed) }; let (signing_key, source_display_name, own_device_pubkey_hex) = { @@ -1298,8 +1304,8 @@ pub async fn list_devices_on_relay( let id = ids .get(&identity_uuid_parsed) .ok_or("Identity not unlocked")?; - let device_id = - krillnotes_core::core::device::get_device_id().map_err(|e| e.to_string())?; + let device_id = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; let device_sk = id.device_signing_key(&device_id); let own = hex::encode(device_sk.verifying_key().to_bytes()); let identity = hex::encode(id.signing_key.verifying_key().to_bytes()); diff --git a/krillnotes-desktop/src-tauri/src/commands/sync.rs b/krillnotes-desktop/src-tauri/src/commands/sync.rs index 1630417a..2db5a815 100644 --- a/krillnotes-desktop/src-tauri/src/commands/sync.rs +++ b/krillnotes-desktop/src-tauri/src/commands/sync.rs @@ -9,10 +9,7 @@ use crate::AppState; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use krillnotes_core::core::sync::relay::{RelayAccount, RelayChannel, RelayClient}; -use krillnotes_core::core::{ - device::get_device_id, - sync::{FolderChannel, SyncContext, SyncEngine, SyncEvent}, -}; +use krillnotes_core::core::sync::{FolderChannel, SyncContext, SyncEngine, SyncEvent}; use std::sync::Arc; use tauri::{Emitter, State, Window}; use uuid::Uuid; @@ -88,7 +85,8 @@ pub async fn poll_sync( }; let device_id = { - let short = get_device_id().map_err(|e| e.to_string())?; + let short = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) + .map_err(|e| e.to_string())?; format!("{}:identity:{}", short, identity_uuid) }; @@ -331,7 +329,7 @@ pub async fn share_invite_link( // Stamp the inviter's composite device ID so the acceptor can route the // acceptance bundle via the relay's findByDeviceId fallback. - if let Ok(short) = krillnotes_core::core::device::get_device_id() { + if let Ok(short) = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()) { file.inviter_device_id = Some(format!("{}:identity:{}", short, uuid)); let payload = serde_json::to_value(&file).map_err(|e| e.to_string())?; file.signature = krillnotes_core::core::invite::sign_payload(&payload, &signing_key); @@ -486,7 +484,8 @@ pub async fn create_relay_invite( inviter_public_key: pubkey_b64, inviter_declared_name: declared_name, inviter_device_id: { - let short = krillnotes_core::core::device::get_device_id().ok(); + let short = + krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()).ok(); short.map(|s| format!("{}:identity:{}", s, uuid)) }, expires_at: record.expires_at.map(|dt| dt.to_rfc3339()), @@ -722,7 +721,7 @@ pub async fn send_invite_response_via_relay( // Also upload an Accept bundle so the inviter can discover the response // via list_bundles() during polling. This is best-effort — the invite URL // was already created successfully above. - let device_id = krillnotes_core::core::device::get_device_id().unwrap_or_default(); + let device_id = krillnotes_core::get_or_create_seed_device_id(&crate::settings::home_dir()).unwrap_or_default(); let accept_bundle_bytes = krillnotes_core::core::swarm::invite::create_accept_bundle( krillnotes_core::core::swarm::invite::AcceptParams { protocol: "krillnotes/1".to_string(), diff --git a/krillnotes-desktop/src-tauri/src/commands/workspace.rs b/krillnotes-desktop/src-tauri/src/commands/workspace.rs index 2de84962..5c9041e1 100644 --- a/krillnotes-desktop/src-tauri/src/commands/workspace.rs +++ b/krillnotes-desktop/src-tauri/src/commands/workspace.rs @@ -54,22 +54,32 @@ pub struct WorkspaceInfo { /// Appends a numeric suffix (`-2`, `-3`, …) until the label is absent /// from the currently open workspace labels in `state`. pub fn generate_unique_label(state: &AppState, path: &Path) -> String { - let filename = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("untitled"); + // Mobile is single-window — always use "main" so commands can find the workspace. + #[cfg(not(desktop))] + { + let _ = (state, path); + return "main".to_string(); + } - let workspaces = state.workspaces.lock().expect("Mutex poisoned"); + #[cfg(desktop)] + { + let filename = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("untitled"); - let mut label = filename.to_string(); - let mut counter = 2; + let workspaces = state.workspaces.lock().expect("Mutex poisoned"); - while workspaces.contains_key(&label) { - label = format!("{filename}-{counter}"); - counter += 1; - } + let mut label = filename.to_string(); + let mut counter = 2; + + while workspaces.contains_key(&label) { + label = format!("{filename}-{counter}"); + counter += 1; + } - label + label + } } /// Returns the window label for a workspace already open at `path`, if any. @@ -102,6 +112,7 @@ fn focus_window(app: &AppHandle, label: &str) -> std::result::Result<(), String> /// /// Used when the user opens a `.krillnotes` file while all launcher windows /// have been closed (only workspace windows remain open). +#[cfg(desktop)] pub fn create_main_window(app: &AppHandle) { let lang = crate::settings::load_settings().language; let strings = crate::locales::menu_strings(&lang); @@ -131,65 +142,70 @@ pub fn create_main_window(app: &AppHandle) { /// Returns an error string if Tauri fails to build the menu or the window. pub fn create_workspace_window( app: &AppHandle, - label: &str, - caller: &tauri::Window, + _label: &str, + _caller: &tauri::Window, ) -> std::result::Result { - let lang = crate::settings::load_settings().language; - let strings = crate::locales::menu_strings(&lang); - let menu_result = - crate::menu::build_menu(app, &strings).map_err(|e| format!("Failed to build menu: {e}"))?; - - // Enable workspace-specific menu items for this new workspace window. - // On macOS the menu bar is global, so we update the shared handles stored - // under "macos". On Windows each window owns its own menu bar, so we - // enable the items in the freshly-built menu before attaching it. - #[cfg(target_os = "macos")] + #[cfg(desktop)] { - let state = app.state::(); - let items = state.workspace_menu_items.lock().expect("Mutex poisoned"); - if let Some(ws_items) = items.get("macos") { - for item in ws_items { + let lang = crate::settings::load_settings().language; + let strings = crate::locales::menu_strings(&lang); + let menu_result = crate::menu::build_menu(app, &strings) + .map_err(|e| format!("Failed to build menu: {e}"))?; + + #[cfg(target_os = "macos")] + { + let state = app.state::(); + let items = state.workspace_menu_items.lock().expect("Mutex poisoned"); + if let Some(ws_items) = items.get("macos") { + for item in ws_items { + item.set_enabled(true) + .map_err(|e| format!("Failed to enable menu item: {e}"))?; + } + } + } + #[cfg(not(target_os = "macos"))] + { + for item in &menu_result.workspace_items { item.set_enabled(true) .map_err(|e| format!("Failed to enable menu item: {e}"))?; } + let state = app.state::(); + state + .paste_menu_items + .lock() + .expect("Mutex poisoned") + .insert( + _label.to_string(), + (menu_result.paste_as_child, menu_result.paste_as_sibling), + ); } - } - #[cfg(not(target_os = "macos"))] - { - // Enable workspace items in this window's private menu before attaching it. - for item in &menu_result.workspace_items { - item.set_enabled(true) - .map_err(|e| format!("Failed to enable menu item: {e}"))?; - } - // Store the paste handles per window label so set_paste_menu_enabled can find them. - let state = app.state::(); - state - .paste_menu_items - .lock() - .expect("Mutex poisoned") - .insert( - label.to_string(), - (menu_result.paste_as_child, menu_result.paste_as_sibling), - ); - } - let mut builder = - tauri::WebviewWindowBuilder::new(app, label, tauri::WebviewUrl::App("index.html".into())) - .title(format!("Krillnotes - {label}")) - .inner_size(1024.0, 768.0) - .disable_drag_drop_handler() - .menu(menu_result.menu); - - // Cascade new windows when opening from an existing workspace window. - if caller.label() != "main" { - if let Ok(pos) = caller.outer_position() { - builder = builder.position((pos.x + 30) as f64, (pos.y + 30) as f64); + let mut builder = tauri::WebviewWindowBuilder::new( + app, + _label, + tauri::WebviewUrl::App("index.html".into()), + ) + .title(format!("Krillnotes - {_label}")) + .inner_size(1024.0, 768.0) + .disable_drag_drop_handler() + .menu(menu_result.menu); + + if _caller.label() != "main" { + if let Ok(pos) = _caller.outer_position() { + builder = builder.position((pos.x + 30) as f64, (pos.y + 30) as f64); + } } + + builder + .build() + .map_err(|e| format!("Failed to create window: {e}")) } - builder - .build() - .map_err(|e| format!("Failed to create window: {e}")) + #[cfg(not(desktop))] + { + app.get_webview_window("main") + .ok_or_else(|| "No main window found".to_string()) + } } /// Rebuilds and reapplies the native menu for all open windows using `lang`. @@ -198,6 +214,7 @@ pub fn create_workspace_window( /// paste/workspace handles in AppState are updated. /// On Windows each window owns its own menu bar: every open window gets a freshly /// built menu, with workspace items pre-enabled for workspace windows. +#[cfg(desktop)] pub fn rebuild_menus( app: &AppHandle, state: &AppState, @@ -425,6 +442,30 @@ pub async fn create_workspace( return Err("Workspace already exists. Use Open Workspace instead.".to_string()); } + // Migration gate: if no device seed file exists but existing workspaces are + // present, the user must open one first so the device ID can be extracted + // and persisted to the seed file. + let home = crate::settings::home_dir(); + if !krillnotes_core::seed_file_exists(&home) { + let mgr = state.identity_manager.lock().expect("Mutex poisoned"); + let has_existing = mgr.list_identities().unwrap_or_default().iter().any(|id| { + mgr.identity_base_dir(&id.uuid) + .and_then(|dir| std::fs::read_dir(dir).ok()) + .map(|rd| { + rd.filter_map(|e| e.ok()) + .any(|e| e.path().join("notes.db").exists()) + }) + .unwrap_or(false) + }); + drop(mgr); + if has_existing { + return Err( + "Please open an existing workspace first to migrate your device identity." + .to_string(), + ); + } + } + match find_window_for_path(&state, &folder) { Some(existing_label) => { focus_window(&app, &existing_label)?; @@ -473,6 +514,7 @@ pub async fn create_workspace( signing_key, gate, Some(&identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| format!("Failed to create: {e}"))?; @@ -494,10 +536,16 @@ pub async fn create_workspace( let new_window = create_workspace_window(&app, &label, &window)?; store_workspace(&state, label.clone(), workspace, folder.clone(), uuid); + #[cfg(not(desktop))] + { + let _ = new_window.emit("workspace-opened", ()); + } + new_window .set_title(&format!("Krillnotes - {label}")) .map_err(|e| e.to_string())?; + #[cfg(desktop)] if window.label() == "main" { window.close().map_err(|e| e.to_string())?; } @@ -593,6 +641,7 @@ pub async fn open_workspace( signing_key, gate, Some(&identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| match e { KrillnotesError::WrongPassword => "WRONG_PASSWORD".to_string(), @@ -614,6 +663,11 @@ pub async fn open_workspace( identity_uuid, ); + #[cfg(not(desktop))] + { + let _ = new_window.emit("workspace-opened", ()); + } + // Emit one event per migrated schema type so the frontend can show a toast. for (schema_name, from_version, to_version, notes_migrated) in &migration_results { let _ = new_window.emit( @@ -631,6 +685,7 @@ pub async fn open_workspace( .set_title(&format!("Krillnotes - {label}")) .map_err(|e| e.to_string())?; + #[cfg(desktop)] if window.label() == "main" { window.close().map_err(|e| e.to_string())?; } @@ -774,6 +829,7 @@ pub async fn execute_import( &workspace_password, &uuid.to_string(), Ed25519SigningKey::from_bytes(&import_seed), + &folder, ) .map_err(|e| e.to_string())?; @@ -799,6 +855,7 @@ pub async fn execute_import( import_signing_key, gate, Some(&identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| e.to_string())?; @@ -820,10 +877,16 @@ pub async fn execute_import( let new_window = create_workspace_window(&app, &label, &window)?; store_workspace(&state, label.clone(), workspace, folder, uuid); + #[cfg(not(desktop))] + { + let _ = new_window.emit("workspace-opened", ()); + } + new_window .set_title(&format!("Krillnotes - {label}")) .map_err(|e| e.to_string())?; + #[cfg(desktop)] if window.label() == "main" { window.close().map_err(|e| e.to_string())?; } @@ -904,7 +967,7 @@ pub fn update_settings( patch: serde_json::Value, ) -> std::result::Result<(), String> { let current = crate::settings::load_settings(); - let old_lang = current.language.clone(); + let _old_lang = current.language.clone(); let mut current_value = serde_json::to_value(¤t).map_err(|e| format!("Failed to serialize settings: {e}"))?; @@ -919,7 +982,8 @@ pub fn update_settings( .map_err(|e| format!("Failed to deserialize merged settings: {e}"))?; crate::settings::save_settings(&updated)?; - if updated.language != old_lang { + #[cfg(desktop)] + if updated.language != _old_lang { rebuild_menus(&app, &state, &updated.language)?; } @@ -1251,6 +1315,7 @@ pub fn duplicate_workspace( source_signing_key, gate, Some(©_identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| e.to_string())?; @@ -1275,6 +1340,7 @@ pub fn duplicate_workspace( &new_password, &identity_uuid, Ed25519SigningKey::from_bytes(©_seed), + &dest_folder, ) .map_err(|e| e.to_string())?; @@ -1293,6 +1359,7 @@ pub fn duplicate_workspace( dest_signing_key, dest_gate, Some(©_identity_dir), + &crate::settings::home_dir(), ) .map_err(|e| format!("Failed to open new workspace: {e}"))?; let _ = new_ws.write_info_json(); @@ -1328,18 +1395,22 @@ pub fn is_workspace_owner( #[tauri::command] pub fn close_window( - window: tauri::Window, - state: State<'_, AppState>, + _window: tauri::Window, + _state: State<'_, AppState>, ) -> std::result::Result<(), String> { - let label = window.label().to_string(); - state - .closing_windows - .lock() - .expect("Mutex poisoned") - .insert(label); - window - .destroy() - .map_err(|e| format!("Failed to close window: {e}")) + #[cfg(desktop)] + { + let label = _window.label().to_string(); + _state + .closing_windows + .lock() + .expect("Mutex poisoned") + .insert(label); + _window + .destroy() + .map_err(|e| format!("Failed to close window: {e}"))?; + } + Ok(()) } #[cfg(test)] diff --git a/krillnotes-desktop/src-tauri/src/lib.rs b/krillnotes-desktop/src-tauri/src/lib.rs index 786c0432..de79fea1 100644 --- a/krillnotes-desktop/src-tauri/src/lib.rs +++ b/krillnotes-desktop/src-tauri/src/lib.rs @@ -10,7 +10,9 @@ //! Each command is scoped to the calling window's workspace via //! [`AppState`] and the window label. +#[cfg(desktop)] pub mod locales; +#[cfg(desktop)] pub mod menu; pub mod settings; pub mod themes; @@ -29,7 +31,9 @@ use uuid::Uuid; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; -use tauri::{AppHandle, Manager}; +#[cfg(desktop)] +use tauri::AppHandle; +use tauri::Manager; /// Per-process state shared across all workspace windows. /// @@ -72,9 +76,7 @@ pub struct AppState { /// In-memory map of currently unlocked identities (UUID → unlocked state). /// Entries are removed when an identity is locked or the app exits. pub unlocked_identities: Arc>>, - /// Paste menu item handles for dynamic enable/disable. - /// On macOS: one global pair keyed by "macos" (the menu bar is shared). - /// On Windows: keyed by window label (each window owns its own menu bar). + #[cfg(desktop)] pub paste_menu_items: Arc< Mutex< HashMap< @@ -86,18 +88,12 @@ pub struct AppState { >, >, >, - /// Workspace-specific menu item handles (Add Note, Delete Note, Copy Note, - /// Manage Scripts, Operations Log, Export Workspace). - /// On macOS: one global list keyed by "macos" — enabled when a workspace - /// opens, disabled when the last workspace window closes. - /// On Windows: keyed by window label — `rebuild_menus` stores into this map during - /// language changes, but items are enabled at build time so the stored handles are - /// never read back to toggle enabled state. + #[cfg(desktop)] pub workspace_menu_items: Arc>>>>, - /// Handle to the "Export Workspace" menu item, toggled based on ownership. - /// On macOS: stored once at setup. On Windows: stored per-window at open time. + #[cfg(desktop)] pub export_menu_item: Arc>>>, /// Handle to the "Manage Scripts" menu item, toggled based on ownership. + #[cfg(desktop)] pub manage_scripts_menu_item: Arc>>>, /// Window labels that have been approved for closing by the frontend. /// When a label is in this set, the next `CloseRequested` event for @@ -105,13 +101,14 @@ pub struct AppState { pub closing_windows: Arc>>, } -/// Maps raw menu event IDs to the user-facing message strings emitted to the frontend. +#[cfg(desktop)] const MENU_MESSAGES: &[(&str, &str)] = &[ ("file_new", "File > New Workspace clicked"), ("file_open", "File > Open Workspace clicked"), ("file_export", "File > Export Workspace clicked"), ("file_import", "File > Import Workspace clicked"), - ("edit_add_note", "Edit > Add Note clicked"), + ("edit_add_note", "Edit > Add Child Note clicked"), + ("edit_add_sibling", "Edit > Add Sibling Note clicked"), ("edit_delete_note", "Edit > Delete Note clicked"), ("view_refresh", "View > Refresh clicked"), ("help_about", "Help > About Krillnotes clicked"), @@ -133,17 +130,7 @@ const MENU_MESSAGES: &[(&str, &str)] = &[ ("file_sync_now", "File > Sync Now clicked"), ]; -/// Translates a native [`tauri::menu::MenuEvent`] into a `"menu-action"` event -/// emitted only to the window that was most recently focused. -/// -/// [`tauri::Emitter::emit_to`] with [`tauri::EventTarget::WebviewWindow`] -/// delivers the event exclusively to that window's -/// `getCurrentWebviewWindow().listen()` handler on the frontend, so multiple -/// open workspace windows do not all react to the same menu click. -/// -/// This also fixes Windows, where clicking a native menu item briefly -/// unfocuses the application window before the event fires, making async -/// focus checks in the frontend unreliable. +#[cfg(desktop)] fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { let Some((_, message)) = MENU_MESSAGES .iter() @@ -189,7 +176,19 @@ fn set_home_dir_path(path: String) -> std::result::Result<(), String> { /// Panics if the Tauri runtime fails to start. #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + #[cfg(not(desktop))] + { + let data_dir = dirs::data_dir() + .or_else(|| { + // Android: dirs::data_dir() returns None. + // Use the standard app-private files directory. + std::env::var("HOME").ok().map(std::path::PathBuf::from) + }) + .unwrap_or_else(|| std::path::PathBuf::from("/data/data/io.opswarm.krillnotes/files")); + settings::set_mobile_data_dir(data_dir); + } + + let builder = tauri::Builder::default() .plugin( tauri_plugin_log::Builder::new() .level(if cfg!(debug_assertions) { @@ -216,9 +215,13 @@ pub fn run() { received_response_managers: Arc::new(Mutex::new(HashMap::new())), sync_engines: Arc::new(Mutex::new(HashMap::new())), unlocked_identities: Arc::new(Mutex::new(HashMap::new())), + #[cfg(desktop)] paste_menu_items: Arc::new(Mutex::new(HashMap::new())), + #[cfg(desktop)] workspace_menu_items: Arc::new(Mutex::new(HashMap::new())), + #[cfg(desktop)] export_menu_item: Arc::new(Mutex::new(None)), + #[cfg(desktop)] manage_scripts_menu_item: Arc::new(Mutex::new(None)), closing_windows: Arc::new(Mutex::new(HashSet::new())), }) @@ -329,47 +332,51 @@ pub fn run() { } }) .setup(|app| { - let lang = settings::load_settings().language; - let strings = locales::menu_strings(&lang); - let menu_result = menu::build_menu(app.handle(), &strings)?; - app.set_menu(menu_result.menu)?; - - // On macOS the menu bar is global (shared by all windows). - // Store handles under the "macos" key so they can be found from - // any window later (set_paste_menu_enabled, workspace enable/disable). - #[cfg(target_os = "macos")] + #[cfg(desktop)] { - let state = app.state::(); - state - .paste_menu_items - .lock() - .expect("Mutex poisoned") - .insert( - "macos".to_string(), - (menu_result.paste_as_child, menu_result.paste_as_sibling), - ); - state - .workspace_menu_items - .lock() - .expect("Mutex poisoned") - .insert("macos".to_string(), menu_result.workspace_items); - *state.export_menu_item.lock().expect("Mutex poisoned") = - Some(menu_result.export_item); - *state - .manage_scripts_menu_item - .lock() - .expect("Mutex poisoned") = Some(menu_result.manage_scripts_item); + let lang = settings::load_settings().language; + let strings = locales::menu_strings(&lang); + let menu_result = menu::build_menu(app.handle(), &strings)?; + app.set_menu(menu_result.menu)?; + + #[cfg(target_os = "macos")] + { + let state = app.state::(); + state + .paste_menu_items + .lock() + .expect("Mutex poisoned") + .insert( + "macos".to_string(), + (menu_result.paste_as_child, menu_result.paste_as_sibling), + ); + state + .workspace_menu_items + .lock() + .expect("Mutex poisoned") + .insert("macos".to_string(), menu_result.workspace_items); + *state.export_menu_item.lock().expect("Mutex poisoned") = + Some(menu_result.export_item); + *state + .manage_scripts_menu_item + .lock() + .expect("Mutex poisoned") = Some(menu_result.manage_scripts_item); + } } - // Ensure home directory exists + let _ = &app; let home = settings::home_dir(); if !home.exists() { std::fs::create_dir_all(&home).expect("Failed to create Krillnotes home directory"); } Ok(()) - }) - .on_menu_event(handle_menu_event) + }); + + #[cfg(desktop)] + let builder = builder.on_menu_event(handle_menu_event); + + builder .invoke_handler(tauri::generate_handler![ create_workspace, open_workspace, diff --git a/krillnotes-desktop/src-tauri/src/menu.rs b/krillnotes-desktop/src-tauri/src/menu.rs index b79edbe0..a6075629 100644 --- a/krillnotes-desktop/src-tauri/src/menu.rs +++ b/krillnotes-desktop/src-tauri/src/menu.rs @@ -194,7 +194,6 @@ fn build_file_menu( ) -> Result, tauri::Error> { let new_item = MenuItemBuilder::with_id("file_new", s(strings, "newWorkspace", "New Workspace")) - .accelerator("CmdOrCtrl+N") .build(app)?; let open_item = MenuItemBuilder::with_id( "file_open", @@ -267,10 +266,18 @@ fn build_edit_menu( app: &AppHandle, strings: &Value, ) -> Result, tauri::Error> { - let add_note = MenuItemBuilder::with_id("edit_add_note", s(strings, "addNote", "Add Note")) - .accelerator("CmdOrCtrl+Shift+N") - .enabled(false) - .build(app)?; + let add_note = + MenuItemBuilder::with_id("edit_add_note", s(strings, "addNote", "Add Child Note")) + .accelerator("CmdOrCtrl+Shift+N") + .enabled(false) + .build(app)?; + let add_sibling = MenuItemBuilder::with_id( + "edit_add_sibling", + s(strings, "addSiblingNote", "Add Sibling Note"), + ) + .accelerator("CmdOrCtrl+N") + .enabled(false) + .build(app)?; let delete_note = MenuItemBuilder::with_id("edit_delete_note", s(strings, "deleteNote", "Delete Note")) .accelerator("CmdOrCtrl+Backspace") @@ -302,6 +309,7 @@ fn build_edit_menu( let builder = SubmenuBuilder::new(app, s(strings, "edit", "Edit")).items(&[ &add_note, + &add_sibling, &delete_note, &sep1, ©_note, @@ -327,7 +335,7 @@ fn build_edit_menu( submenu, paste_as_child: paste_child, paste_as_sibling: paste_sibling, - workspace_items: vec![add_note, delete_note, copy_note], + workspace_items: vec![add_note, add_sibling, delete_note, copy_note], }) } diff --git a/krillnotes-desktop/src-tauri/src/settings.rs b/krillnotes-desktop/src-tauri/src/settings.rs index 7ba46f38..db3fbe21 100644 --- a/krillnotes-desktop/src-tauri/src/settings.rs +++ b/krillnotes-desktop/src-tauri/src/settings.rs @@ -13,6 +13,14 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use std::sync::OnceLock; + +static MOBILE_DATA_DIR: OnceLock = OnceLock::new(); + +/// Called from setup() on mobile to provide the app sandbox data directory. +pub fn set_mobile_data_dir(path: PathBuf) { + let _ = MOBILE_DATA_DIR.set(path); +} /// Persisted application settings. /// @@ -65,6 +73,9 @@ impl Default for AppSettings { /// - macOS / Linux: `~/.config/krillnotes/` /// - Windows: `%APPDATA%/Krillnotes/` fn breadcrumb_dir() -> PathBuf { + if let Some(mobile_dir) = MOBILE_DATA_DIR.get() { + return mobile_dir.join("krillnotes_config"); + } #[cfg(target_os = "windows")] { dirs::config_dir() @@ -87,8 +98,10 @@ fn breadcrumb_path() -> PathBuf { // ── Home directory ─────────────────────────────────────────────────── -/// Returns the default home directory: `~/Krillnotes/`. fn default_home_dir() -> PathBuf { + if let Some(mobile_dir) = MOBILE_DATA_DIR.get() { + return mobile_dir.join("Krillnotes"); + } dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("Krillnotes") diff --git a/krillnotes-desktop/src-tauri/tauri.conf.json b/krillnotes-desktop/src-tauri/tauri.conf.json index 9da6a76c..62203fbf 100644 --- a/krillnotes-desktop/src-tauri/tauri.conf.json +++ b/krillnotes-desktop/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Krillnotes", - "version": "1.1.1", - "identifier": "com.2pisoftware.krillnotes", + "version": "1.2.0", + "identifier": "io.opswarm.krillnotes", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", @@ -14,7 +14,7 @@ { "title": "Krillnotes", "width": 800, - "height": 600, + "height": 700, "dragDropEnabled": false } ], diff --git a/krillnotes-desktop/src/App.tsx b/krillnotes-desktop/src/App.tsx index 7b64a5d2..06df3215 100644 --- a/krillnotes-desktop/src/App.tsx +++ b/krillnotes-desktop/src/App.tsx @@ -235,7 +235,27 @@ function App() { return (
- {workspace ? setShowWorkspacePeers(true)} sharingIndicatorMode={sharingIndicatorMode} /> :
} + {workspace ? ( + setShowWorkspacePeers(true)} + sharingIndicatorMode={sharingIndicatorMode} + onNewWorkspace={() => setShowNewWorkspace(true)} + onOpenWorkspace={() => setShowOpenWorkspace(true)} + onManageIdentities={() => setShowIdentityManager(true)} + onSettings={() => setShowSettings(true)} + /> + ) : ( +
+ setShowNewWorkspace(true)} + onOpenWorkspace={() => setShowOpenWorkspace(true)} + onManageIdentities={() => setShowIdentityManager(true)} + onSettings={() => setShowSettings(true)} + /> +
+ )} {status && } (''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const selectRef = useRef(null); const availableTypes = useMemo( () => getAvailableTypes(position, referenceNoteId, notes, schemas), @@ -32,15 +33,19 @@ function AddNoteDialog({ isOpen, onClose, onNoteCreated, referenceNoteId, positi ); useEffect(() => { - if (availableTypes.length > 0 && !availableTypes.includes(noteSchema)) { - setNoteSchema(availableTypes[0]); - } - }, [availableTypes, noteSchema]); + if (availableTypes.length === 0) return; + const refNote = referenceNoteId ? notes.find(n => n.id === referenceNoteId) : null; + const preferred = position === 'sibling' && refNote && availableTypes.includes(refNote.schema) + ? refNote.schema + : availableTypes[0]; + setNoteSchema(preferred); + }, [availableTypes, referenceNoteId, position, notes]); useEffect(() => { if (isOpen) { setError(''); setLoading(false); + requestAnimationFrame(() => selectRef.current?.focus()); } }, [isOpen]); @@ -80,6 +85,7 @@ function AddNoteDialog({ isOpen, onClose, onNoteCreated, referenceNoteId, positi

{t('notes.noAllowedTypes')}

) : ( {selectedNote.title} +

{selectedNote.title}

) : null )} -
+
{isEditing ? ( <> @@ -333,7 +337,7 @@ function InfoPanel({ selectedNote, onNoteUpdated, onDeleteRequest, requestEditMo {canEdit && ( @@ -465,7 +469,8 @@ function InfoPanel({ selectedNote, onNoteUpdated, onDeleteRequest, requestEditMo autoCapitalize="off" spellCheck={false} onKeyDown={e => { - if (e.key === 'Enter' || e.key === 'Tab') { + const hasContent = tagInput.trim() || tagSuggestions.length > 0; + if ((e.key === 'Enter' || e.key === 'Tab') && hasContent) { e.preventDefault(); if (tagSuggestions.length > 0) addTag(tagSuggestions[0]); else if (tagInput.trim()) addTag(tagInput); diff --git a/krillnotes-desktop/src/components/OperationsLogDialog.tsx b/krillnotes-desktop/src/components/OperationsLogDialog.tsx index 6882db96..4683b362 100644 --- a/krillnotes-desktop/src/components/OperationsLogDialog.tsx +++ b/krillnotes-desktop/src/components/OperationsLogDialog.tsx @@ -6,9 +6,10 @@ import { useState, useEffect, useCallback } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { ListFilter, Trash2, X } from 'lucide-react'; +import { ListFilter, Trash2, X, ChevronDown, ChevronRight } from 'lucide-react'; import type { OperationSummary, SyncEventRecord } from '../types'; import { useTranslation } from 'react-i18next'; +import { useLayout } from '../hooks/useLayout'; interface OperationsLogDialogProps { isOpen: boolean; @@ -171,6 +172,8 @@ function OperationDetailPanel({ function OperationsLogDialog({ isOpen, onClose }: OperationsLogDialogProps) { const { t, i18n } = useTranslation(); + const opLayout = useLayout(); + const isPhone = opLayout === 'phone'; const [activeTab, setActiveTab] = useState<'operations' | 'syncEvents'>('operations'); const [operations, setOperations] = useState([]); const [syncEvents, setSyncEvents] = useState([]); @@ -357,13 +360,48 @@ function OperationsLogDialog({ isOpen, onClose }: OperationsLogDialogProps) { {/* Content area */} {activeTab === 'operations' ? (
- {/* Operations list */}
{operations.length === 0 ? (
{t('log.noOperations')}
+ ) : isPhone ? ( + /* Phone: card list with inline expand */ +
+ {operations.map((op) => { + const isExpanded = selectedOpId === op.operationId; + return ( +
+ + {isExpanded && opDetail && ( +
+
+
{t('log.author')}
{op.authorKey || '—'}
+
{t('log.verifiedBy')}
{op.verifiedBy || '—'}
+ {Object.entries(opDetail).filter(([k]) => !METADATA_KEYS.has(k)).map(([k, v]) => ( +
{formatKey(k)}
+ ))} +
+
+ )} +
+ ); + })} +
) : ( + /* Desktop: table layout */ @@ -413,8 +451,8 @@ function OperationsLogDialog({ isOpen, onClose }: OperationsLogDialogProps) { )} - {/* Detail panel */} - {opDetail && ( + {/* Detail panel — desktop only */} + {!isPhone && opDetail && ( op.operationId === selectedOpId)?.authorKey ?? ''} diff --git a/krillnotes-desktop/src/components/TreeNode.tsx b/krillnotes-desktop/src/components/TreeNode.tsx index e4b65ad8..cafefb46 100644 --- a/krillnotes-desktop/src/components/TreeNode.tsx +++ b/krillnotes-desktop/src/components/TreeNode.tsx @@ -4,7 +4,7 @@ // // Copyright (c) 2024-2026 TripleACS Pty Ltd t/a 2pi Software -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type { TreeNode as TreeNodeType, Note, DropIndicator, SchemaInfo } from '../types'; @@ -37,6 +37,7 @@ function TreeNode({ onHoverStart, onHoverEnd, onToggleChecked, effectiveRoles, shareAnchorIds, showSharingIndicators, }: TreeNodeProps) { const { t } = useTranslation(); + const longPressTimer = useRef | null>(null); const hasChildren = node.children.length > 0; const isSelected = node.note.id === selectedNoteId; @@ -224,9 +225,20 @@ function TreeNode({ } ${isDragged ? 'opacity-40' : ''} ${ isDropTarget && dropIndicator?.position === 'child' ? 'bg-blue-500/20 ring-1 ring-blue-500/40' : '' }`} - style={{ paddingLeft: `${indentPx}px` }} + style={{ paddingLeft: `${indentPx}px`, WebkitUserSelect: 'none', userSelect: 'none', WebkitTouchCallout: 'none' }} onClick={isGhost ? undefined : () => onSelect(noteId)} onContextMenu={isGhost ? undefined : (e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(e, noteId); }} + onTouchStart={isGhost ? undefined : (e) => { + const touch = e.touches[0]; + const syntheticX = touch.clientX; + const syntheticY = touch.clientY; + longPressTimer.current = setTimeout(() => { + longPressTimer.current = null; + onContextMenu({ preventDefault: () => {}, stopPropagation: () => {}, clientX: syntheticX, clientY: syntheticY } as unknown as React.MouseEvent, noteId); + }, 500); + }} + onTouchMove={() => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }} + onTouchEnd={() => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }} onMouseEnter={(e) => { const rect = e.currentTarget.getBoundingClientRect(); onHoverStart(node.note.id, rect.top + rect.height / 2); diff --git a/krillnotes-desktop/src/components/TreeView.tsx b/krillnotes-desktop/src/components/TreeView.tsx index 50b9ac4e..b7b82a89 100644 --- a/krillnotes-desktop/src/components/TreeView.tsx +++ b/krillnotes-desktop/src/components/TreeView.tsx @@ -77,7 +77,7 @@ function TreeView({ if (tree.length === 0) { return (
{ if (!isOpen) return; + invoke('is_workspace_owner').then(setIsOwner).catch(() => setIsOwner(false)); invoke('get_workspace_metadata') .then(meta => { setAuthorName(meta.authorName ?? ''); @@ -133,7 +135,7 @@ function WorkspacePropertiesDialog({ isOpen, onClose }: WorkspacePropertiesDialo
); - const inputClass = 'w-full bg-secondary border border-secondary rounded px-3 py-1.5 text-sm'; + const inputClass = `w-full bg-secondary border border-secondary rounded px-3 py-1.5 text-sm ${!isOwner ? 'opacity-60 cursor-default' : ''}`; return (
@@ -143,21 +145,30 @@ function WorkspacePropertiesDialog({ isOpen, onClose }: WorkspacePropertiesDialo {t('workspace.propertiesHint')}

+ {!isOwner && ( +
+ {t('workspace.propertiesReadOnly')} +
+ )} + {field(t('workspace.authorName'), ( setAuthorName(e.target.value)} className={inputClass} placeholder={t('workspace.authorNamePlaceholder')} + readOnly={!isOwner} disabled={!isOwner} autoCorrect="off" autoCapitalize="off" spellCheck={false} /> ))} {field(t('workspace.authorOrg'), ( setAuthorOrg(e.target.value)} className={inputClass} placeholder={t('workspace.authorOrgPlaceholder')} + readOnly={!isOwner} disabled={!isOwner} autoCorrect="off" autoCapitalize="off" spellCheck={false} /> ))} {field(t('workspace.homepageUrl'), ( setHomepageUrl(e.target.value)} className={inputClass} placeholder={t('workspace.homepageUrlPlaceholder')} + readOnly={!isOwner} disabled={!isOwner} autoCorrect="off" autoCapitalize="off" spellCheck={false} /> ))} @@ -165,19 +176,22 @@ function WorkspacePropertiesDialog({ isOpen, onClose }: WorkspacePropertiesDialo