Docs: update all markdown files to reflect workspace-settings-overhaul changes

Update CLAUDE.md, PLAN.md, README.md, docs/API.md, and docs/DEVELOPMENT.md
to reflect UUID-keyed workspaces, checksum-based conflict resolution,
auto-sync lifecycle, tauri-plugin-credentials, .onyx-workspace.json rename,
and new GUI features (remote folder browsing, workspace rename, sync
interval, loading screen, safe area insets, task dedup).
This commit is contained in:
Tristan Michael 2026-04-05 19:21:33 -07:00
parent 7cc899470f
commit eaab66609c
5 changed files with 97 additions and 54 deletions

View file

@ -31,19 +31,19 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility. - **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility.
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`. - **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android. - **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android. `tauri-plugin-credentials/` provides cross-platform credential storage (Android Keystore via EncryptedSharedPreferences, desktop via keyring crate).
### Key patterns ### Key patterns
- **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. - **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files.
- **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here. - **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here.
- **Config** (`config.rs`): `AppConfig` manages named workspaces with paths, mode (local/webdav), theme, and WebDAV URL. Stored in platform-specific config dirs via the `directories` crate. - **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate.
- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Auto-appends `Onyx/` to WebDAV URL. Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows. - **Sync** (`sync.rs`): Three-way diff sync with offline queue. Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix. Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows.
- **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Credentials stored via `keyring` crate (feature-gated). `Zeroizing<String>` for credential fields. Scoped keyring keys (`com.onyx.webdav.<domain>::<username>`); auto-migrates legacy dot-separated format on load. 10MB PROPFIND response cap. - **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Rejects non-HTTPS URLs. `Zeroizing<String>` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB PROPFIND response cap. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain).
### On-disk format ### On-disk format
Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories.
### Tauri GUI structure ### Tauri GUI structure
@ -66,9 +66,9 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
### Current state (2026-04-05) ### Current state (2026-04-05)
- **Phase 1** (Core + CLI): Complete - **Phase 1** (Core + CLI): Complete
- **Phase 2** (WebDAV sync): In progress — reworking to let users browse and pick a remote folder instead of hardcoding `Onyx/` subfolder - **Phase 2** (WebDAV sync): Complete — remote folder browsing, checksum-based conflict resolution, auto-sync lifecycle, per-workspace sync interval
- **Phase 3** (GUI MVP): Complete - **Phase 3** (GUI MVP): Complete
- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build - **Phase 4** (Mobile): Tauri Android cfg-gated with tauri-plugin-credentials and safe area insets; needs `tauri android init` + build
### GUI features done ### GUI features done
@ -87,21 +87,26 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
- Group-by-due-date toggle per list (main panel kebab) - Group-by-due-date toggle per list (main panel kebab)
- Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs) - Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs)
- Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus) - Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus)
- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option - Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option, remote folder browsing
- WebDAV setup flow with connection test, credential storage in system keychain - WebDAV setup flow with connection test, credential storage via tauri-plugin-credentials (Android Keystore + desktop keychain)
- WebDAV sync: auto-creates `Onyx/` subfolder on remote, 60s hard timeout, sync error display in settings - WebDAV sync: user-selected remote folder, 60s hard timeout, checksum-based conflict resolution (remote wins, local recovered as duplicate)
- Auto-sync lifecycle: periodic polling (configurable interval), debounced file-change (5s), window-focus (30s stale threshold); sync status dot (idle/synced/error/offline) and manual sync button in drawer
- Initial sync loading screen for new WebDAV workspaces
- Per-workspace sync interval (configurable in settings)
- Upload/download counts in drawer footer (hidden when zero)
- Transient sync error suppression (connectivity issues update status dot only, no error banner)
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes) - File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
- Sync status indicators (last-sync time + upload/download counts chip) - WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces
- Push/pull/full sync mode selection (session-only, in settings) - Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog)
- WorkspaceMode enum (local/webdav) with per-workspace config
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
- Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation - Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation
- Safe area insets for mobile (CSS variables --safe-top/--safe-bottom, viewport-fit=cover)
- Task deduplication on load (handles sync conflict duplicates)
- Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning) - Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning)
- Custom confirmation dialogs (ConfirmDialog component replaces native confirm()) - Custom confirmation dialogs (ConfirmDialog component replaces native confirm())
### GUI features NOT yet done ### GUI features NOT yet done
- Workspace retarget/migrate
- Search/filter tasks - Search/filter tasks
- Desktop packaging for macOS - Desktop packaging for macOS

63
PLAN.md
View file

@ -106,12 +106,18 @@ TaskList {
} }
AppConfig { AppConfig {
workspaces: HashMap<String, WorkspaceConfig>, workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
current_workspace: Option<String>, current_workspace: Option<String>, // UUID
} }
WorkspaceConfig { WorkspaceConfig {
name: String, // Display name
path: PathBuf, path: PathBuf,
mode: WorkspaceMode, // Local or Webdav
webdav_url: Option<String>,
webdav_path: Option<String>, // User-selected remote folder
theme: Option<String>,
sync_interval_secs: Option<u64>,
} }
``` ```
@ -119,7 +125,7 @@ WorkspaceConfig {
``` ```
~/Documents/Tasks/ # User-selected folder ~/Documents/Tasks/ # User-selected folder
├── .metadata.json # Global: list ordering, last opened list ├── .onyx-workspace.json # Global: list ordering, last opened list
├── My Tasks/ # Task list folder ├── My Tasks/ # Task list folder
│ ├── .listdata.json # List metadata: task order, id, timestamps │ ├── .listdata.json # List metadata: task order, id, timestamps
│ ├── Buy groceries.md # Title: "Buy groceries" (without .md) │ ├── Buy groceries.md # Title: "Buy groceries" (without .md)
@ -132,7 +138,7 @@ WorkspaceConfig {
**Note**: Task titles are derived from filenames by removing the `.md` extension. **Note**: Task titles are derived from filenames by removing the `.md` extension.
**`.metadata.json` (root level)**: **`.onyx-workspace.json` (root level)**:
```json ```json
{ {
"version": 1, "version": 1,
@ -169,14 +175,20 @@ WorkspaceConfig {
```json ```json
{ {
"workspaces": { "workspaces": {
"personal": { "a1b2c3d4-...": {
"path": "/home/user/Documents/Tasks" "name": "personal",
"path": "/home/user/Documents/Tasks",
"mode": "local"
}, },
"shared": { "e5f6g7h8-...": {
"path": "/home/user/Dropbox/TeamTasks" "name": "shared",
"path": "/home/user/Dropbox/TeamTasks",
"mode": "webdav",
"webdav_url": "https://nextcloud.example.com/remote.php/dav/files/user/",
"webdav_path": "TeamTasks"
} }
}, },
"current_workspace": "personal" "current_workspace": "a1b2c3d4-..."
} }
``` ```
@ -394,7 +406,7 @@ $ onyx workspace migrate personal ~/Dropbox/Tasks
⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks ⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks
Continue? (y/n): y Continue? (y/n): y
Moving files... Moving files...
Moved .metadata.json Moved .onyx-workspace.json
Moved My Tasks/ (15 files) Moved My Tasks/ (15 files)
Moved Work/ (8 files) Moved Work/ (8 files)
✓ Migrated 23 files to ~/Dropbox/Tasks ✓ Migrated 23 files to ~/Dropbox/Tasks
@ -453,11 +465,16 @@ cargo run -p onyx-cli -- workspace list
Add WebDAV support to `onyx-core`: Add WebDAV support to `onyx-core`:
```rust ```rust
// Update WorkspaceConfig to include WebDAV // WorkspaceConfig with WebDAV support (UUID-keyed in AppConfig)
WorkspaceConfig { WorkspaceConfig {
name: String,
path: PathBuf, path: PathBuf,
mode: WorkspaceMode, // Local or Webdav
webdav_url: Option<String>, webdav_url: Option<String>,
webdav_path: Option<String>, // User-selected remote folder
last_sync: Option<DateTime>, last_sync: Option<DateTime>,
theme: Option<String>,
sync_interval_secs: Option<u64>,
} }
// AppConfig remains the same (workspaces + current_workspace) // AppConfig remains the same (workspaces + current_workspace)
@ -484,8 +501,8 @@ pub fn delete_credentials(domain: &str) -> Result<()>;
``` ```
#### Sync Strategy #### Sync Strategy
- **Trigger**: On app start (if connected), background timer (every 5 min), on modification (debounced) - **Trigger**: Auto-sync lifecycle — periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold)
- **Conflict Resolution**: Last-write-wins with timestamp - **Conflict Resolution**: Checksum-based — downloads remote, compares SHA-256. Identical = false conflict (skipped). Different = remote wins, local recovered as duplicate with new UUID and `[RECOVERED FROM CONFLICT]` prefix
- **Offline Support**: Queue operations when offline, sync when online - **Offline Support**: Queue operations when offline, sync when online
#### Authentication #### Authentication
@ -664,20 +681,22 @@ apps/tauri/
#### App Configuration (Phase 3+) #### App Configuration (Phase 3+)
**Update AppConfig** to include UI preferences: **AppConfig** with UI preferences (theme is per-workspace):
```rust ```rust
AppConfig { AppConfig {
workspaces: HashMap<String, WorkspaceConfig>, // From Phase 1 workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
current_workspace: Option<String>, current_workspace: Option<String>, // UUID
theme: Theme, // NEW: light/dark mode
window_size: Option<(u32, u32)>, // NEW: remember window size
last_opened_list_per_workspace: HashMap<String, Uuid>, // NEW: per-workspace last view
} }
WorkspaceConfig { WorkspaceConfig {
name: String, // Display name
path: PathBuf, path: PathBuf,
webdav_url: Option<String>, // From Phase 2 mode: WorkspaceMode, // Local or Webdav
webdav_url: Option<String>,
webdav_path: Option<String>, // User-selected remote folder
last_sync: Option<DateTime>, last_sync: Option<DateTime>,
theme: Option<String>, // Per-workspace theme
sync_interval_secs: Option<u64>, // Auto-sync interval
} }
``` ```
@ -1011,6 +1030,6 @@ This project is free and open-source software licensed under GPL v3.
--- ---
**Last Updated**: 2026-04-03 **Last Updated**: 2026-04-05
**Document Version**: 4.2 **Document Version**: 4.3
**Status**: Ready to Implement - Milestone-Driven Plan **Status**: Ready to Implement - Milestone-Driven Plan

View file

@ -40,6 +40,7 @@ onyx/
- Subtask hierarchy (parent_id) - Subtask hierarchy (parent_id)
- WebDAV sync with three-way diff and offline queue - WebDAV sync with three-way diff and offline queue
- Platform keychain credential storage (feature-gated for Android) - Platform keychain credential storage (feature-gated for Android)
- Checksum-based conflict resolution (remote wins, local recovered as duplicate)
### CLI (`onyx-cli`) ### CLI (`onyx-cli`)
- Workspace management (init, add, list, switch, remove, retarget, migrate) - Workspace management (init, add, list, switch, remove, retarget, migrate)
@ -62,7 +63,7 @@ onyx/
- Keyboard shortcuts (Escape priority chain) - Keyboard shortcuts (Escape priority chain)
- WebDAV setup flow with credential auto-population - WebDAV setup flow with credential auto-population
- File watcher (auto-reloads on external changes) - File watcher (auto-reloads on external changes)
- Sync status indicators and push/pull/full mode selection - Auto-sync with configurable interval, status indicators
- Custom confirmation dialogs - Custom confirmation dialogs
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
@ -182,7 +183,7 @@ Task description and notes go here in **markdown** format.
``` ```
~/Documents/Tasks/ # User-selected folder ~/Documents/Tasks/ # User-selected folder
├── .metadata.json # Global: list ordering, last opened list ├── .onyx-workspace.json # Workspace metadata: list ordering, detection
├── My Tasks/ # Task list folder ├── My Tasks/ # Task list folder
│ ├── .listdata.json # List metadata: task order, id, timestamps │ ├── .listdata.json # List metadata: task order, id, timestamps
│ ├── Buy groceries.md # Individual task files │ ├── Buy groceries.md # Individual task files

View file

@ -64,12 +64,12 @@ pub struct TaskList {
#### AppConfig #### AppConfig
Global application configuration supporting multiple workspaces. Global application configuration supporting multiple workspaces. Workspaces are keyed by UUID string.
```rust ```rust
pub struct AppConfig { pub struct AppConfig {
pub workspaces: HashMap<String, WorkspaceConfig>, pub workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
pub current_workspace: Option<String>, pub current_workspace: Option<String>, // UUID of active workspace
} }
``` ```
@ -87,14 +87,18 @@ use onyx_core::AppConfig;
let config_path = AppConfig::get_config_path(); let config_path = AppConfig::get_config_path();
let mut config = AppConfig::load_from_file(&config_path)?; let mut config = AppConfig::load_from_file(&config_path)?;
// Add workspace // Add workspace (returns generated UUID)
config.add_workspace( let id = config.add_workspace(
"personal".to_string(), WorkspaceConfig::new("personal".to_string(), PathBuf::from("/home/user/tasks"))
WorkspaceConfig::new(PathBuf::from("/home/user/tasks"))
); );
// Set current workspace // Set current workspace by ID
config.set_current_workspace("personal".to_string())?; config.set_current_workspace(id)?;
// Find workspace by display name
if let Some((id, ws)) = config.find_by_name("personal") {
println!("Found: {} at {:?}", id, ws.path);
}
// Save config // Save config
config.save_to_file(&config_path)?; config.save_to_file(&config_path)?;
@ -106,9 +110,14 @@ Configuration for a single workspace.
```rust ```rust
pub struct WorkspaceConfig { pub struct WorkspaceConfig {
pub name: String, // Display name
pub path: PathBuf, pub path: PathBuf,
pub mode: WorkspaceMode, // Local or Webdav
pub webdav_url: Option<String>, pub webdav_url: Option<String>,
pub webdav_path: Option<String>, // User-selected remote folder path
pub last_sync: Option<DateTime<Utc>>, pub last_sync: Option<DateTime<Utc>>,
pub theme: Option<String>,
pub sync_interval_secs: Option<u64>, // Auto-sync polling interval
} }
``` ```
@ -267,7 +276,7 @@ Each list folder contains a `.listdata.json` file:
### Root Metadata ### Root Metadata
The root folder contains a `.metadata.json` file: The root folder contains a `.onyx-workspace.json` file:
```json ```json
{ {
@ -318,7 +327,7 @@ let status = get_sync_status(Path::new("/home/user/tasks"))?;
Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service). Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service).
Keyring service keys use the format `com.onyx.webdav.<domain>::<username>` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format (`com.onyx.webdav.<domain>.<username>`) are automatically migrated to the scoped format and the old entries are removed. **Core library** (`onyx-core::webdav`): Keyring service keys use the format `com.onyx.webdav.<domain>::<username>` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format are automatically migrated.
```rust ```rust
use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials}; use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials};
@ -333,6 +342,8 @@ let (username, password) = load_credentials("nextcloud.example.com")?;
delete_credentials("nextcloud.example.com")?; delete_credentials("nextcloud.example.com")?;
``` ```
**Tauri GUI**: Uses `tauri-plugin-credentials` instead of direct keyring calls. This plugin provides cross-platform support: EncryptedSharedPreferences (Android Keystore) on Android, keyring crate on desktop. Plugin crate at `apps/tauri/tauri-plugin-credentials/`.
### WebDAV Client ### WebDAV Client
```rust ```rust
@ -342,7 +353,7 @@ let client = WebDavClient::new(
"https://nextcloud.example.com/remote.php/dav/files/user/Tasks", "https://nextcloud.example.com/remote.php/dav/files/user/Tasks",
"username", "username",
"password", "password",
); )?; // Returns Result — rejects non-HTTPS URLs
// Test connection // Test connection
client.test_connection().await?; client.test_connection().await?;
@ -361,11 +372,13 @@ client.delete_file("old-task.md").await?;
### Sync Strategy ### Sync Strategy
- **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote) - **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote, conflict)
- **Conflict resolution**: Last-write-wins using file timestamps - **Conflict resolution**: Checksum-based — downloads remote file and compares SHA-256 checksums. Identical content is a false conflict (skipped). When different, remote wins and the local version is recovered as a duplicate task with a new UUID and `[RECOVERED FROM CONFLICT]` prefix, inserted adjacent to the original in `.listdata.json`
- **Offline queue**: Pending operations are queued and replayed when connectivity returns - **Offline queue**: Pending operations are queued and replayed when connectivity returns
- **Sync state**: Stored in `.syncstate.json` within the workspace directory - **Sync state**: Stored in `.syncstate.json` within the workspace directory
- **Auto-sync**: Periodic polling (configurable `sync_interval_secs`), debounced file-change trigger (5s), window-focus trigger (30s stale threshold)
- **Response size cap**: PROPFIND responses are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers - **Response size cap**: PROPFIND responses are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers
- **Syncable files**: Only processes files at expected depths — `.onyx-workspace.json` at root (depth 1), `.listdata.json` and `*.md` inside list directories (depth 2)
## Error Handling ## Error Handling
@ -424,8 +437,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure workspace // Configure workspace
let mut config = AppConfig::new(); let mut config = AppConfig::new();
config.add_workspace("personal".to_string(), WorkspaceConfig::new(path)); let ws_id = config.add_workspace(WorkspaceConfig::new("personal".to_string(), path));
config.set_current_workspace("personal".to_string())?; config.set_current_workspace(ws_id)?;
config.save_to_file(&AppConfig::get_config_path())?; config.save_to_file(&AppConfig::get_config_path())?;
Ok(()) Ok(())

View file

@ -76,6 +76,11 @@ onyx/
│ │ ├── components/ # Reusable UI components │ │ ├── components/ # Reusable UI components
│ │ ├── stores/ # Svelte state (app.svelte.ts) │ │ ├── stores/ # Svelte state (app.svelte.ts)
│ │ └── types.ts # TypeScript type definitions │ │ └── types.ts # TypeScript type definitions
│ ├── tauri-plugin-credentials/ # Cross-platform credential storage plugin
│ │ ├── Cargo.toml
│ │ ├── src/
│ │ │ └── lib.rs # Desktop (keyring) + plugin API
│ │ └── android/ # Android (EncryptedSharedPreferences)
│ └── src-tauri/ # Rust backend (Tauri commands) │ └── src-tauri/ # Rust backend (Tauri commands)
│ ├── Cargo.toml │ ├── Cargo.toml
│ ├── tauri.conf.json │ ├── tauri.conf.json