Spice is a focus wallpaper manager for Windows and MacOS.
Status: Current as of v1.1.1 Focus: Concurrency Model, Image Pipeline, and UI Synchronization
Spice employs a Single-Writer, Multiple-Reader (SWMR) concurrency architecture to separate resource-intensive operations (image processing, I/O) from the user interface. This design eliminates UI main-thread blocking (“jank”) and lock contention, ensuring a buttery-smooth user experience even during heavy background downloads.
Spice is built as a modular application where core functionality is delivered via plugins.
graph TD
classDef core fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000,font-size:16px;
classDef plug fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#000,font-size:16px;
App[Spice Application]:::core
PM[Plugin Manager]:::core
subgraph Plugins ["Loaded Plugins"]
WP[Wallpaper Plugin]:::plug
Other[Other Plugins...]:::plug
end
App -->|Initializes| PM
PM -->|Manages Lifecycle| Plugins
WP -->|Injects| SettingsUI[Settings Tab]:::core
WP -->|Injects| TrayUI[Tray Menu Items]:::core
To prevent race conditions and lock contention, only one goroutine is allowed to mutate the global state (Image Store). All other components, including the UI, are Readers or Command Senders.
The User Interface maintains its own local session state (which image am I looking at right now?). It strictly reads from the shared store and sends asynchronous commands to request changes.
The system is divided into two distinct execution contexts:
graph TD
classDef ui fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000,font-size:16px;
classDef pipe fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000,font-size:16px;
classDef store fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000,font-size:16px;
subgraph UI_Context ["UI Context (Plugin)"]
NextBtn[Next Button]:::ui
PrevBtn[Delete Button]:::ui
LocalState["Session State<br/>(Current Index, History)"]:::ui
end
subgraph Shared_Resource [Shared Resources]
Store[(ImageStore)]:::store
end
subgraph Pipeline_Context ["Pipeline Context"]
Workers[["Worker Pool<br/>(Download/Crop)"]]:::pipe
LazyWorker(("Persistent Worker<br/>(Enrichment)")):::pipe
Manager(("State Manager<br/>Loop")):::pipe
end
%% Reads
NextBtn -- 1. RLock (Fast) --> Store
LocalState -- Read Image Info --> Store
%% Async Commands
NextBtn -- 2. CmdMarkSeen (Async) --> Manager
PrevBtn -- CmdDelete (Async) --> Manager
Workers -- Result (New Image) --> Manager
%% Writers
Manager -- 3. Lock (Exclusive) --> Store
%% Feedback
Manager -- Yield (Gosched) --> Manager
pkg/wallpaper/store.go)A thread-safe, stateless container.
sync.RWMutex.
RLock() (Non-blocking).Lock() (Exclusive).RLock() to save to disk without blocking readers.currentIndex).pkg/wallpaper/pipeline.go)The “Brain” of the backend.
resultChan and cmdChan.runtime.Gosched() after every operation to prevent starving readers.pkg/wallpaper/wallpaper.go)The “Controller”.
startEnrichmentWorker)A persistent, single-goroutine worker that “pivots” to the user’s current location.
enrichmentSignal (buffered channel).This flow demonstrates how the UI updates instantly without waiting for a write lock.
sequenceDiagram
participant User
participant UI as UI (Plugin)
participant Store as ImageStore
participant Mgr as State Manager
User->>UI: Click "Next"
activate UI
Note right of UI: 1. Calculate new index (Local)
UI->>Store: Get(newIndex) [RLock]
Store-->>UI: Image
Note right of UI: 2. Optimistic Update
UI->>UI: Update Tray Menu & currentImage
Note right of UI: 3. Async Command
UI--)Mgr: CmdMarkSeen(ImageID)
Note right of UI: 4. Apply Wallpaper
UI->>OS: setWallpaper(path)
deactivate UI
activate Mgr
Note left of Mgr: Process CmdMarkSeen
Mgr->>Store: MarkSeen() [Lock]
deactivate Mgr
Deleting requires modifying the store, handled asynchronously.
sequenceDiagram
participant User
participant UI as UI (Plugin)
participant Mgr as State Manager
participant Store as ImageStore
User->>UI: Click "Delete"
activate UI
UI--)Mgr: CmdRemove(ImageID)
UI->>OS: Remove File (Disk)
Note right of UI: Move to Next
UI->>UI: setNextWallpaper()
deactivate UI
activate Mgr
Note left of Mgr: Process CmdRemove
Mgr->>Store: Remove(ID) [Lock]
deactivate Mgr
| Component | File Path | Responsibility |
|---|---|---|
| Store | pkg/wallpaper/store.go |
Data repository. RWMutex protected. |
| Pipeline | pkg/wallpaper/pipeline.go |
Worker pool & State Manager Loop. |
| Controller | pkg/wallpaper/wallpaper.go |
UI logic, Lifecycle, Optimistic Updates. |
| Processor | pkg/wallpaper/smart_image_processor.go |
Face detection, cropping (Heavy CPU). |
Spice implements a strict “Zero Orphans” policy for resource management. When a wallpaper collection (Query) is deleted:
Config triggers a registered callback (onQueryRemoved).store.RemoveByQueryID(queryID).DeepDelete function is called for every image ID:
Spice supports two distinct provider interaction models:
cache/google_photos/<GUID>.cmdChan pattern can be expanded to a full Event Bus if the application grows complexity (e.g., specific event subscribers).To maintain responsiveness under load, the following optimizations are employed:
idSet map[string]bool to perform existence checks in constant time (45ns) rather than linear scans (470ns+), ensuring that the Writer Loop never lags even with thousands of images.isDownloading = true under lock) before spawning goroutines. This prevents “job storms” and CPU saturation during rapid UI interactions.