diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..f9f3916 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,798 @@ +# Smoothie — Architecture Specification + +**Status:** As-built (v0.1.2) +**Scope:** Desktop application for discovering, installing, isolating, and launching Blender builds. +**Audience:** Maintainers and contributors. + +--- + +## 1. Overview + +Smoothie is a Tauri 2 desktop application that manages multiple Blender installations from a single, portable library folder. + +The application: + +- Discovers official Blender builds from `download.blender.org`. +- Downloads and extracts builds into a user-configurable library. +- Isolates each install's Blender user resources so versions and templates do not share configuration. +- Launches Blender with per-install environment variables and optional application templates. +- Registers builds with the operating system and creates desktop shortcuts (Linux). +- Generates per-install icons and splash banners. +- Keeps update, tray, notification, and window-state behavior in the Tauri host. + +### Goals + +1. **Portability.** All versions, configuration, and templates live under one library directory that can be moved, backed up, or copied. +2. **Isolation.** Each Blender build reads and writes its own `BLENDER_USER_RESOURCES` directory. +3. **Low-friction management.** Install, launch, favorite, delete, and set default from one GUI. +4. **Desktop-native behavior.** Tray support, OS shortcuts, file associations, signed auto-updates. +5. **Thin native layer.** Rust is used only for capabilities the webview cannot perform reliably. + +### Non-goals + +- Building or compiling Blender from source. +- Hosting or mirroring Blender binaries. +- Multi-user or cloud-synced library management. +- Plugin/add-on marketplace functionality. +- Mobile support (the Tauri entry point exists, but the capabilities and UX target desktop). + +--- + +## 2. System Context + +```mermaid +flowchart TB + User[User] --> UI[Svelte 5 UI] + UI --> Services[JS service modules] + Services --> Stores[Svelte stores] + Services --> TauriJS[Tauri JS APIs] + Services --> Commands[Rust commands] + TauriJS --> Plugins[Tauri plugins] + Plugins --> Net[download.blender.org] + Plugins --> OS[OS: dialogs, notifications, shell, tray] + Commands --> FS[(Local filesystem)] + Plugins --> FS + Commands --> Blender[Blender process] + Plugins --> Updater[Update endpoint] + FS --> Library[Portable Blender library] +``` + +Smoothie has no application server. The only runtime network dependencies are: + +- `https://download.blender.org` for release discovery and downloads. +- The configured updater endpoint for signed application updates. + +--- + +## 3. Technology Stack + +| Layer | Technology | Notes | +| --- | --- | --- | +| Desktop shell | Tauri 2 (Rust, edition 2021) | One window, one main process | +| Frontend | SvelteKit 2 + Svelte 5 runes | `adapter-static`, prerendered, SSR disabled | +| Bundler | Vite 6 | Fixed dev port `1420` | +| Package manager | Bun | `bun.lock` committed | +| UI | Hand-written CSS + `@webtui/css` | Splitpanes, tabs, custom components | +| Persistence | `@tauri-apps/plugin-store` | `settings.json`, `versions.json` | +| Downloads | `@tauri-apps/plugin-upload` | Streaming download with progress | +| HTTP | `@tauri-apps/plugin-http` | Scoped to Blender hosts | +| Filesystem | `@tauri-apps/plugin-fs` + persisted scope | Library and shortcut operations | +| OS integration | dialog, os, shell, notification, opener, window-state | Linux shortcut creation in JS | +| Updates | `@tauri-apps/plugin-updater` | Signed artifacts | +| Archives | Rust crates: `zip`, `tar`, `flate2`, `xz2`, `bzip2` | Custom Tauri commands | + +--- + +## 4. High-Level Architecture + +Smoothie is organized into four layers: + +```mermaid +flowchart TB + subgraph Presentation + Routes[routes/+page.svelte] + Components[lib/components] + Styles[app.css and component styles] + end + + subgraph Application + Settings[settings.js] + Fetch[blenderfetch.js] + Download[download.js] + Library[library.js] + FileUtils[file_utils.js] + Tray[trayicon.js] + Notify[notification.js] + end + + subgraph PlatformBoundary + TauriAPI[Tauri core API] + TauriPlugins[Tauri plugins] + RustCommands[Rust commands] + end + + subgraph State + SvelteStores[Svelte writable stores] + LazyStores[plugin-store JSON files] + Disk[Library filesystem] + end + + Presentation --> Application + Application --> PlatformBoundary + Application --> SvelteStores + Application --> LazyStores + PlatformBoundary --> Disk +``` + +### Architectural principles + +1. **Filesystem-first.** The library directory is the source of truth for installed versions. There is no installation database to drift out of sync. +2. **Stateless UI over persistent services.** Components read Svelte stores; service modules perform I/O and refresh stores. +3. **Thin Rust boundary.** Rust handles archive extraction and process spawning. All orchestration and file layout remain in JavaScript. +4. **Explicit per-version isolation.** Launches inject `BLENDER_USER_RESOURCES` pointing at a version-specific config folder. +5. **Idempotent maintenance.** Missing icons, banners, templates, and shortcuts are regenerated when versions are rescanned. +6. **Graceful degradation.** Missing executables are skipped; failed shortcuts and banners log warnings rather than aborting startup. + +--- + +## 5. Repository Layout + +```text +smoothie/ +├── src/ # SvelteKit frontend +│ ├── app.css # Global styles and theme variables +│ ├── app.html # Webview shell document +│ ├── routes/ +│ │ ├── +layout.js # prerender = true, ssr = false +│ │ ├── +layout.svelte +│ │ └── +page.svelte # Main application shell and bootstrap +│ └── lib/ +│ ├── assets/ # Base icon and embedded font +│ ├── components/ # UI components +│ ├── blenderfetch.js # Release discovery and filename parsing +│ ├── download.js # Download/extract pipeline +│ ├── file_utils.js # Icons, banners, paths, copy/move helpers +│ ├── library.js # Installed versions, launch, templates, shortcuts +│ ├── notification.js # Transient notification store +│ ├── settings.js # Settings, defaults, platform detection +│ ├── transition.js # Crossfade transition +│ └── trayicon.js # Tray menu and close-to-tray behavior +├── src-tauri/ +│ ├── src/ +│ │ ├── lib.rs # Commands and plugin registration +│ │ └── main.rs # Entry point +│ ├── capabilities/desktop.json # Permissions and scopes +│ ├── icons/ # App and platform icons +│ ├── Cargo.toml +│ ├── build.rs +│ └── tauri.conf.json # App, window, bundle, updater config +├── static/ # Static web assets +├── .gitea/workflows/build.yml # Release build workflow +├── package.json +├── svelte.config.js +└── vite.config.js +``` + +--- + +## 6. Frontend Architecture + +### 6.1 Application shell + +`src/routes/+page.svelte` owns: + +- The splash screen and initialization sequence. +- The top-level tab state (`library`, `download`, `settings`). +- Subscription wiring between Svelte stores and component props. +- Global CSS variables and layout. + +Bootstrap order: + +```mermaid +sequenceDiagram + participant UI as +page.svelte + participant App as Tauri app API + participant Settings as settings.js + participant Fetch as blenderfetch.js + participant Library as library.js + participant Tray as trayicon.js + + UI->>App: getVersion() + UI->>Settings: getSettings() + Settings->>Settings: detect platform/arch + Settings->>Settings: create library structure + UI->>Fetch: getBlenderReleases() + Fetch->>Fetch: scrape and parse release pages + UI->>Library: getInstalledVersions() + Library->>Library: scan library, find executables + Library->>Library: generate icons/banners, shortcuts + UI->>Tray: setupTrayIcon() + UI->>UI: subscribe stores and show main UI +``` + +Initialization is intentionally serial and surfaced through splash status text. No routing is used beyond the single route; tabs are local component state. + +### 6.2 Components + +| Component | Responsibility | +| --- | --- | +| `Library.svelte` | Two-pane view: installed/favorite versions on the left, templates for the selected version on the right | +| `Download.svelte` | Lists major releases and their available minor versions | +| `Release.svelte` | Per-release row: version selector, progress, download/installed state | +| `VersionCard.svelte` | Version tile: favorite, select, launch, delete, set default | +| `TemplateCard.svelte` | Launch base Blender or a template; delete template | +| `Dropzone.svelte` | Accepts dropped `.blend` files as templates for the selected version | +| `Settings.svelte` | Displays settings and moves the library directory | +| `Splash.svelte` | Startup status display | +| `Dialog.svelte` | Promise-based confirmation dialog (`show({...})`) | +| `Popup.svelte` | Transient success/error toast | +| `Menu.svelte`, `MenuItem.svelte` | Context menu primitives | +| `Progress.svelte` | Download/extraction progress display | +| `Button.svelte` | Shared button styles and variants | + +### 6.3 State management + +State is split into two categories: + +**In-memory Svelte stores** + +| Store | Module | Shape | +| --- | --- | --- | +| `currentSettings` | `settings.js` | `Settings` | +| `installedVersionsStore` | `library.js` | `blenderVersion[]` | +| `selectedVersionStore` | `library.js` | `blenderVersion \| null` | +| `blenderReleases` | `blenderfetch.js` | `BlenderRelease[]` | +| `downloadTasksStore` | `download.js` | `DownloadTask[]` | +| `notification` | `notification.js` | `{ message, timeout } \| null` | + +**Persisted stores (`plugin-store`)** + +| File | Keys | Purpose | +| --- | --- | --- | +| `settings.json` | Setting names | User preferences | +| `versions.json` | Version string → boolean | Favorite flags | + +The UI does not cache installed versions independently of `installedVersionsStore`; components rescan through `getInstalledVersions()` after mutations. + +### 6.4 Service modules + +#### `settings.js` + +- Defines the `Settings` type and defaults. +- Detects host platform and architecture via `plugin-os`. +- Persists settings through `LazyStore`. +- Creates the library structure when the library directory changes. +- Exposes `BASE_LIBRARY_DIR = "smoothie-library"`. + +#### `blenderfetch.js` + +- Fetches `https://download.blender.org/release/`. +- Parses the index with `DOMParser`. +- Selects version folders matching `Blender/` and greater than `2.78`. +- Fetches each version folder in parallel and parses filenames of the form: + + ```text + blender---. + ``` + +- Filters to the selected platform and architecture. +- Sorts releases with a numeric dot-segment comparator. +- Emits `BlenderRelease[]` and `FileLink[]`. + +This module is deliberately source-specific. It is the only component that understands Blender's download directory naming convention. + +#### `download.js` + +Implements the install pipeline: + +```mermaid +sequenceDiagram + participant UI as Release.svelte + participant DL as download.js + participant Store as plugin-upload + participant Rust as extract_archive + participant Lib as library.js + + UI->>DL: downloadBlenderVersion(link) + DL->>DL: read settings and compute target dir + DL->>Store: download(url, target/archive) + loop progress + Store-->>DL: loaded / total + DL->>DL: updateDownloadProgress(version) + end + DL->>Rust: extract_archive(archive, target) + Rust->>Rust: spawn_blocking extraction + Rust->>Rust: strip single top-level directory + DL->>DL: remove archive unless keepDownloadedArchives + DL->>Lib: getInstalledVersions() + DL-->>UI: { success, message, path } +``` + +Key behaviors: + +- Target directory: `/smoothie-library/blender/`. +- Progress is keyed by version string, not by task ID. +- On download failure, an empty target directory is removed. +- On extraction failure, the archive is retained and the error is returned. +- Extraction is delegated to a single Rust command; the JS layer only decides retention and refresh. + +#### `library.js` + +The domain core. Responsibilities: + +- Scan `/blender/*` and build `blenderVersion` objects. +- Resolve the executable for the current platform. +- Load templates and ensure template directories exist. +- Generate missing icons and banners. +- Create/remove Linux `.desktop` entries. +- Launch Blender with isolated environment variables. +- Register a build as the system default (`blender --register`). +- Maintain favorites. +- Delete versions and clean up associated resources. +- Refresh `selectedVersionStore` after scans. + +#### `file_utils.js` + +- Canvas-based icon generation with the embedded base icon and `Europa-Mono` font. +- Canvas/figlet-based 600×90 banner generation. +- Directory creation, recursive copy, move, and existence checks. +- Single-top-level-directory flattening. +- `.blend` drag-and-drop import as templates. + +#### `trayicon.js` + +- Creates a tray icon with "Show/Hide" and "Quit". +- Intercepts window close and hides to tray when `closeToTray` is enabled. + +#### `notification.js` + +- Tiny writable store consumed by `+page.svelte` and rendered through `Popup.svelte`. + +--- + +## 7. Native (Rust) Layer + +`src-tauri/src/lib.rs` exposes seven commands and registers twelve plugins. + +### 7.1 Commands + +| Command | Signature | Behavior | +| --- | --- | --- | +| `extract_zip` | `(archive_path, target_dir)` | Extracts ZIP entries, creating parent directories | +| `extract_tar` | `(archive_path, target_dir)` | Extracts uncompressed TAR | +| `extract_tar_gz` | `(archive_path, target_dir)` | Extracts gzip-compressed TAR | +| `extract_tar_xz` | `(archive_path, target_dir)` | Extracts xz-compressed TAR | +| `extract_tar_bz2` | `(archive_path, target_dir)` | Extracts bzip2-compressed TAR | +| `extract_archive` | `(archive_path, target_dir)` | Async dispatcher; detects format by filename; runs in `spawn_blocking` | +| `launch_binary` | `(path, args, env?)` | Spawns a detached process with null stdio | + +Format detection in `extract_archive`: + +1. Multi-part suffixes: `.tar.xz` / `.txz`, `.tar.gz` / `.tgz`, `.tar.bz2` / `.tbz2` / `.tbz`. +2. Single suffix: `.tar`, `.zip`. +3. Unknown suffix: try ZIP, then uncompressed TAR. + +All archive extractors call `strip_single_top_level_directory`, which flattens an archive that produced exactly one directory. This is required because Blender archives contain a versioned wrapping folder. + +### 7.2 Plugins + +Registered in `lib.rs` and declared in the frontend dependency set: + +- `updater`, `upload`, `shell`, `fs`, `persisted-scope`, `store`, `os`, `notification`, `http`, `dialog`, `window-state`, `opener`. + +`window-state` is registered twice (once in the builder chain and once in `setup`); this is redundant but harmless and should be consolidated. + +### 7.3 Process model + +Blender is launched as a detached child process with: + +- `stdin`, `stdout`, `stderr` set to null. +- Optional environment variables. +- No process tracking or lifecycle supervision. + +Smoothie does not wait for Blender and does not receive exit status. + +--- + +## 8. Data Model + +### 8.1 `Settings` + +```js +{ + libraryDir: string, // user-visible library root + defaultPlatform: 'windows' | 'linux' | 'macos', + defaultArch: string, // host architecture string + keepDownloadedArchives: boolean, // default false + autoCreateShortcuts: boolean, // default true (Linux desktop entries) + theme: 'light' | 'dark', // default 'light' + closeToTray: boolean // default true +} +``` + +Defaults are derived at runtime from `appDataDir()`, `plugin-os` platform, and `plugin-os` architecture. Missing persisted fields are patched from defaults on load. + +### 8.2 `BlenderRelease` + +```js +{ + original: string, // original href, e.g. "Blender4.2/" + version: string, // major series, e.g. "4.2" + fullPath: string, // absolute folder URL + lastModified: null, // reserved + links: FileLink[] +} +``` + +### 8.3 `FileLink` + +```js +{ + version: string, // full version, e.g. "4.2.1" + downloadUrl: string, + platform: 'linux' | 'windows' | 'macos', + architecture: 'x64' | 'arm64', + fileExtension: 'tar.xz' | 'zip' | ..., + status: 'pending' | 'downloading' | 'completed' | 'error', + percent: number +} +``` + +### 8.4 `blenderVersion` + +```js +{ + version: string, + path: string, + executable: string, + platform: string, + favourite: boolean, + templates: string[] // template folder names +} +``` + +`installDate` is declared in the JSDoc but not populated. + +### 8.5 `DownloadTask` + +```js +{ + version: string, + loaded: number, + total: number, + percent: number +} +``` + +--- + +## 9. Storage Layout + +### 9.1 Library + +`BASE_LIBRARY_DIR = "smoothie-library"` is created under the configured `libraryDir`. + +```text +/ +└── smoothie-library/ + ├── blender/ + │ └── / + │ ├── blender # or blender.exe + │ ├── icon.png # generated + │ ├── banner.png # generated + │ ├── / + │ │ └── scripts/ + │ │ └── startup/ + │ │ └── bl_app_templates_system/ + │ │ └──