# 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/ │ │ └──