Spice is a focus wallpaper manager for Windows and MacOS.
A deep-dive technical guide for implementing new image sources in Spice (v1.1.0+).
Spice uses a Registry Pattern to decouple providers. Providers are standalone packages in pkg/wallpaper/providers/<name>.
pkg/wallpaper/providers/bing/
├── bing.go # Implementation & Registration
├── const.go # Constants (API URL, Regex)
└── bing_test.go # Unit Tests
pkg/provider.ImageProvider)You must implement the following 6 methods.
Name() string:
Title() string:
Type() ProviderType:
provider.TypeOnline (Pexels, Wallhaven), provider.TypeLocal (Filesystem), or provider.TypeAI (Generative).ParseURL(webURL string) (string, error):
bing.com/images/search?q=foo).search:foo) or the input if it’s already compliant.const.go regex here to reject invalid domains.FetchImages(ctx context.Context, apiURL string, page int) ([]Image, error):
ctx.Done() for cancellation.page is 1-indexed. If the API uses offsets, calculate offset = (page-1) * limit.provider.Image.Image struct fields (Path, ID, Attribution, ViewURL).EnrichImage(ctx, img) (Image, error):
FileType, Path, etc.FetchImages, just return img, nil.GetProviderIcon() fyne.Resource:
fyne.NewStaticResource("Name", []byte{...}). Embed the PNG bytes in code or use //go:embed.Do NOT modify the global Config struct. Use fyne.Preferences.
CreateSettingsPanel)Constructs the “General” tab for your provider (e.g., API Keys).
Input: sm setting.SettingsManager.
returns: fyne.CanvasObject (usually a container.NewVBox).
Widget Types:
CreateTextEntrySetting: For strings (API Keys).
fyne.StringValidator (e.g., validator.NewRegexp(...)).func(s string) error for logic validation (e.g., “Key must start with ‘Bearer ‘”).CreateBoolSetting: For toggles.CreateSelectSetting: For dropdowns.CreateButtonWithConfirmationSetting: For dangerous actions (Reset, Clear Cache).CreateQueryPanel)Constructs the image source list. Pattern:
p.cfg.Preferences.QueryList("queries")? NO.p.cfg.Queries (the unified list). Filter by q.Provider == p.Name().Use Standardized Add Button: Use wallpaper.CreateAddQueryButton (in pkg/wallpaper/ui_add_query.go) to create the “Add” button. This helper handles validation, modal creation, and the critical “Apply” button wiring for you.
addBtn := wallpaper.CreateAddQueryButton(
"Add MyProvider Query",
sm,
wallpaper.AddQueryConfig{
Title: "New Query",
URLPlaceholder: "Search term or URL",
DescPlaceholder: "Description",
ValidateFunc: func(url, desc string) error {
if len(url) == 0 {
return errors.New("URL cannot be empty")
}
// Add provider-specific validation here (e.g., regex check)
return nil
},
AddHandler: func(desc, url string, active bool) (string, error) {
return p.cfg.AddMyProviderQuery(desc, url, active)
},
},
func() {
queryList.Refresh()
sm.SetRefreshFlag("queries")
},
)
Spice uses a Strict Deferred-Save Model. Changes made in the UI must NOT be saved immediately to disk. They must be queued and only committed when the user clicks “Apply”.
The SettingsManager now handles the “Closure Trap” and dirty detection automatically via the Registry.
When using helpers like CreateTextEntrySetting, CreateBoolSetting, or CreateSelectSetting, the SettingsManager will:
ApplyFunc only if the value has changed.Example:
pexelsAPIKeyConfig := setting.TextEntrySettingConfig{
Name: "pexelsAPIKey",
InitialValue: p.cfg.GetPexelsAPIKey(),
// ... other fields
ApplyFunc: func(s string) {
p.cfg.SetPexelsAPIKey(s)
},
}
sm.CreateTextEntrySetting(&pexelsAPIKeyConfig, pexHeader)
If you are building a custom list (like a museum collection or query list), you must manually wire into the Registry:
sm.SeedBaseline(key, initialValue) for each item.sm.GetBaseline(key).sm.SetSettingChangedCallback(key, callback) and sm.SetRefreshFlag(key).check.OnChanged = func(on bool) {
baseline := sm.GetBaseline(queryKey).(bool)
if on != baseline {
sm.SetSettingChangedCallback(queryKey, func() {
if on { p.cfg.EnableQuery(id) } else { p.cfg.DisableQuery(id) }
})
sm.SetRefreshFlag(queryKey)
} else {
sm.RemoveSettingChangedCallback(queryKey)
sm.UnsetRefreshFlag(queryKey)
}
sm.GetCheckAndEnableApplyFunc()()
}
p.cfg.Save() inside the OnChanged callback.
on != capturedInitialState.
capturedInitialState is the truth, but the config has updated. Toggling back will mistakenly leave the “Apply” button enabled. Use sm.GetBaseline(key) instead.sm.SetRefreshFlag("queries") on toggle.
UnsetRefreshFlag(uniqueKey). If a user reverts a change, the global flag remains, leaving the “Apply” button stuck on. Only use global flags for destructive actions (Delete) or inside the Apply Callback itself.While standard settings are deferred, Sensitive Credentials (API Keys, Usernames) must follow the Transactional UI Pattern.
CreateSettingsPanel, when the user clicks “Verify & Connect”, the network check is performed and the value is saved to the config immediately upon success.sm.SeedBaseline(key, value) and sm.Refresh() inside the success goroutine. This locks the field and resets the action button.context.WithTimeout (10s) to prevent the UI from hanging on “Verifying…”.APIs often return results in inconsistent orders (e.g., “Page 2” might contain items from “Page 1”). If your provider supports Pagination AND Shuffling, you must implement the “Cache-First Stable Shuffle” pattern.
map[string][]int for already resolved IDs.
sort.Ints(ids).
cfg.GetImgShuffle() is true, shuffle the sorted list using a session-stable seed.
type Provider struct {
// ...
idCache map[string][]int
idCacheMu sync.RWMutex
}
func (p *Provider) resolveIDs(query string) ([]int, error) {
p.idCacheMu.RLock()
if cached, ok := p.idCache[query]; ok {
p.idCacheMu.RUnlock()
return cached, nil
}
p.idCacheMu.RUnlock()
// 1. Fetch
ids, _ := fetchFromAPI(query)
// 2. Sort (Deterministic Baseline)
sort.Ints(ids)
// 3. Shuffle (If User Wants It)
if p.cfg.GetImgShuffle() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
r.Shuffle(len(ids), func(i, j int) {
ids[i], ids[j] = ids[j], ids[i]
})
}
// 4. Cache
p.idCacheMu.Lock()
p.idCache[query] = ids
p.idCacheMu.Unlock()
return ids, nil
}
Spice uses a code generation tool (cmd/util/gen_providers) to automatically register all providers found in pkg/wallpaper/providers/.
### 6.1 The Logic
providers/ directory for subdirectories.cmd/spice/zz_generated_providers.go, which contains the necessary _ imports to trigger the init() functions of your providers.go generate (called by make build or make run).### 6.2 Disabling a Provider
To temporarily disable a provider without deleting the code:
.disabled inside the provider’s directory (e.g., pkg/wallpaper/providers/myprovider/.disabled).go generate ./... (or make gen).zz_generated_providers.go, effectively compiling it out of the final binary.### 6.3 Manual imports (Legacy/Debug)
You do not need to manually edit cmd/spice/main.go anymore. The //go:generate directive at the top of main.go handles this.
ParseURL with table-driven tests.http.Client or usage httptest.Server to test FetchImages without real network calls.If your provider supports “copy-pasting” URLs from the browser (like Wallhaven or Pexels), you can integrate with the Spice Safari/Chrome extension.
pkg/wallpaper/providers/<name>/const.go, define a constant for your URL pattern.
^https://bing.com/images/.*).cmd/spice/main.go (e.g., _ "github.com/.../providers/bing"). will automatically parse main.go, find your enabled provider, and extract the regex from your const.go to inject it into the extension's background.js`.make sync-extension
For cultural institutions (Museums, Archives), Spice provides a standardized “Evangelist” UI template designed to drive engagement rather than just utility.
ui_museum.go)wallpaper.CreateMuseumHeader.
MapURL is provided. Use this to drive foot traffic.Instead of raw database categories, frame collections as curated experiences:
widget.NewCheck) for collections.cfg.Enable/DisableQuery.