Spice is a focus wallpaper manager for Windows and MacOS.
Status: Current as of v2.5.1 (Concurrency Model Audit) Focus: Concurrency Model, Image Pipeline, and Actor-based Display Management
Spice employs a hybrid concurrency architecture to separate resource-intensive operations (image processing, I/O) from the user interface. The performance-critical hot path (image download, processing, and ingestion) is serialized through a single-writer pipeline, while infrequent administrative operations (favorites, cache clearing, reconciliation) use direct mutex-protected store access. This design eliminates UI main-thread blocking (“jank”) and ensures a responsive 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
On the performance-critical hot path (image ingestion from workers), mutations are serialized through a single pipeline goroutine (stateManagerLoop). This eliminates lock contention during high-throughput operations like bulk downloading.
Infrequent administrative operations (toggling favorites, cache clearing, startup reconciliation, query removal) mutate the store directly under sync.RWMutex. These operations are inherently synchronous — a user clicks “Clear Cache” and the store must be updated before the UI refreshes.
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 actor fill:#f3e5f5,stroke:#7b1fa2,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_Orchestration ["Plugin Manager & Plugin"]
Plugin[Wallpaper Plugin]:::ui
end
subgraph Actor_Layer ["Actor Model (per Monitor)"]
Monitor1[Monitor Controller 0]:::actor
Monitor2[Monitor Controller 1]:::actor
end
subgraph Shared_Resource ["Shared Resources"]
Store[(ImageStore)]:::store
end
subgraph Pipeline_Layer ["Pipeline Context"]
Workers[["Worker Pool<br/>(Download/Crop)"]]:::pipe
Manager(("State Manager<br/>Loop")):::pipe
end
%% Interaction Flows
Plugin -- "1. Dispatch Cmd" --> Monitor1
Monitor1 -- "2. RLock (Fast)" --> Store
Workers -- "Result (New Image)" --> Manager
Manager -- "3. Lock (Exclusive)" --> Store
%% Feedback
Manager -- "Seen Counter Updates" --> Plugin
Monitor1 -- "Current Wallpaper Change" --> Plugin
Added in v2.0
To support independent multi-monitor wallpapers, Spice moved from a single-controller model to an Actor Model.
ShuffleIDs, RandomPos).selects on a Commands channel.CmdNext, CmdPrev, CmdUpdateShuffle) from the central Plugin.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).Add, MarkSeen, Remove, Clear — serialized through stateManagerLoop.Update, Remove, RemoveByQueryID, ResetFavorites, Wipe, Sync — called directly under mutex from the Plugin for user-initiated or startup operations.pkg/wallpaper/pipeline.go)The serialized writer for the hot path.
resultChan and cmdChan.runtime.Gosched() after every operation to prevent starving readers.Add (from workers), MarkSeen, Remove, and Clear (from command channel). Does not handle admin operations like favorites management or query cleanup, which are performed directly by the Plugin.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 Plugin as Wallpaper Plugin
participant MC as "Monitor Controller (Actor)"
participant Store as ImageStore
participant OS as "OS / Wallpaper API"
User->>Plugin: Click "Next"
activate Plugin
Plugin->>MC: Dispatch(CmdNext)
deactivate Plugin
activate MC
Note right of MC: 1. Calculate next image (Shuffled/History)
MC->>Store: GetByID(nextID) [RLock]
Store-->>MC: Image Info
Note right of MC: 2. Process Change
MC->>OS: setWallpaper(path)
MC->>Store: MarkSeen(path) [Exclusive Lock]
MC->>Plugin: Signal Change (for Tray Menu update)
deactivate MC
Deleting requires modifying the store, handled asynchronously.
sequenceDiagram
participant User
participant Plugin as Wallpaper Plugin
participant MC as "Monitor Controller (Actor)"
participant Store as ImageStore
participant OS as "OS / Wallpaper API"
User->>Plugin: Click "Delete"
activate Plugin
Plugin->>MC: Dispatch(CmdDelete)
deactivate Plugin
activate MC
MC->>OS: Remove File from Disk
MC->>Store: Remove(ID) [Lock]
Note right of MC: Trigger auto-navigation to next
MC->>MC: next()
deactivate MC
| Component | File Path | Responsibility |
|---|---|---|
| Interfaces | pkg/wallpaper/interfaces.go |
Definitions for JobSubmitter and StoreInterface. |
| Store | pkg/wallpaper/store.go |
Data repository. RWMutex protected. |
| Pipeline | pkg/wallpaper/pipeline.go |
Worker pool & State Manager Loop. |
| Actor | pkg/wallpaper/monitor_controller.go |
Monitor-specific state & logic. |
| Controller | pkg/wallpaper/wallpaper.go |
Main plugin orchestrator & UI dispatch. |
| 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>.To manage the growing number of providers, Spice categorizes them into three distinct types (provider.ProviderType), which dictates their UI placement:
TypeAI: Generative or logical providers. Placed in the “AI” tab (Future).
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 pipeline writer 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.Spice’s imaging engine (pkg/wallpaper/smart_image_processor.go) implements the Strategy Pattern to dynamically select the best cropping method based on image content and user settings.
FaceCropStrategy: Used when a high-confidence face is found. Strictly crops around the face.EntropyCropStrategy: Uses SmartCrop (luminance/energy analysis) to find the most interesting area.SmartPanStrategy: Fallback for specific cases (e.g., “Face Boost” centering).EntropyCropStrategy preventing crops of the bottom 20% unless “High Energy” is detected.pkg/wallpaper/tuning.go.To manage evolving configuration schemas (e.g., legacy JSON formats, ID backfilling), Spice uses a Chain of Responsibility pattern.
MigrationStep functions (e.g., UnifyQueriesStep, EnsureFavoritesStep).loadFromPrefs executes the chain. If any step modifies the config, a save is triggered automatically. This ensures data integrity across version upgrades.Added in v2.5
To solve the “Closure Trap” bug and ensure UI consistency across multiple monitor settings, Spice implemented a centralized Settings Registry.
SettingsManager maintains a map[string]interface{} (the Baseline) representing the last-saved state of every UI widget.IsPassword: true for automatic masking of sensitive inputs (e.g., API keys).EnabledIf predicates to programmatically disable widgets based on other values (e.g., locking an API key field until its value is cleared).SeedBaseline with their initial persistent value.OnChanged callbacks compare the “Live” value against the “Baseline” (not the ephemeral Config).sm.SeedBaseline() and sm.Refresh(), which instantly locks the field and enables dependent features (like sync) without requiring an “Apply” click.To prevent ID collisions across different providers (e.g., Pexels and Wallhaven both using numeric IDs), Spice implements a centralized middleware strategy.
IDs are namespaced at the ingestion boundary and de-namespaced when interacting with the original provider.
sequenceDiagram
participant P as Plugin (FetchNewImages)
participant Prov as ImageProvider
participant Pipe as Pipeline/Store
participant W as Worker (enrichImage)
Note over P,Prov: "1. Ingestion Phase"
P->>Prov: FetchImages()
Prov-->>P: []Image (IDs: 123, 456)
Note right of P: "Middleware: Prefix IDs (Provider_123)"
P->>Pipe: Submit(namespacedJob)
Note over W,Prov: "2. Interaction Phase (Lazy Enrichment)"
W->>W: Get namespaced ID (Provider_123)
Note right of W: "Middleware: Strip Prefix (123)"
W->>Prov: EnrichImage(raw_img)
Prov-->>W: raw_modified_img
Note right of W: "Middleware: Restore Prefix (Provider_123)"
W->>Pipe: Save enriched image
ImageStore and FileManager only see and store namespaced IDs (Provider_ID).For verified providers (Museums), Spice treats raw.githubusercontent.com as a Content Delivery Network (CDN).
Remote > Cache > Embed > Hardcoded.