Config & Storage
Two simple but essential services: reading configuration, and persisting data between sessions.
Configuration
Tako's config system is deliberately minimal. It's a flat key-value store loaded from a JSON file, with environment variable overrides.
Config File
By default, Tako looks for .tako/config.json inside your app directory:
{
"app": {
"debug": true,
"name": "My App"
},
"search": {
"max_results": 50,
"fuzzy": true
}
}Nested objects are flattened to dot-notation keys (case-insensitive):
app.debug→trueapp.name→"My App"search.max_results→50
Reading Config in a Plugin
func (p *Plugin) OnInit(ctx *tako.Context) error {
cfg := ctx.Config()
debug := cfg.Bool("app.debug")
name := cfg.String("app.name")
maxResults := cfg.Int("search.max_results")
return nil
}Environment Variable Overrides
Any config key can be overridden with an environment variable. The convention is TAKO_ prefix + uppercase key with dots replaced by underscores:
| Config key | Env var |
|---|---|
app.debug | TAKO_APP_DEBUG |
app.name | TAKO_APP_NAME |
search.max_results | TAKO_SEARCH_MAX_RESULTS |
Environment variables take priority over the JSON file. This makes 12-factor style deployments straightforward.
TAKO_APP_DEBUG=true ./myappConfig Limitations
The current contracts.Config interface is intentionally lean:
type Config interface {
String(key string) string
Int(key string) int
Bool(key string) bool
}A few things to be aware of:
- Config is read-only. There's no
Setmethod — plugins cannot write to config at runtime. Intreturns0for missing keys and for parse failures. There's no way to distinguish "not found" from "value is zero". If that matters, useStringand parse manually.- No array access beyond raw values. For complex config, use
Stringto get a JSON string and unmarshal it yourself.
KV Store
The KV Store is for persistent plugin state — data that should survive across sessions. Think: last selected item, scroll position, saved search history, UI preferences.
Using the KV Store
func (p *Plugin) OnInit(ctx *tako.Context) error {
store := ctx.Storage()
// Save a value
if err := store.Set("search.last_query", "golang"); err != nil {
ctx.Logger().Error("failed to save last query", "err", err)
}
// Read it back
var lastQuery string
if err := store.Get("search.last_query", &lastQuery); err != nil {
// Key doesn't exist yet — that's fine on first run
lastQuery = ""
}
// Check existence without reading
if store.Has("search.last_query") {
// ...
}
// Delete a key
_ = store.Delete("search.last_query")
return nil
}How It Works
Under the hood, the store is a JSON file at .tako/kv.json. Writes are debounced — multiple rapid Set calls within a 500ms window are batched into a single write. The write uses an atomic rename (write temp file → os.Rename) to prevent corruption on crash.
Namespacing Keys
There's no automatic namespacing — keys are global across all plugins. Use a prefix that matches your plugin ID to avoid collisions:
// ✅
store.Set("search.history", history)
store.Set("clock.timezone", "UTC")
// ❌ too generic — might conflict
store.Set("history", history)
store.Set("tz", "UTC")Shutdown Safety
store.Close() is registered as an OnDestroy callback in Application.Boot(). When shutdown occurs, the store cancels its debounce goroutine, waits for it to exit, then does a final synchronous flush to disk. Data is never lost due to shutdown timing.
Up next: 03.06 — Stack Guards — middleware for the Focus Stack. Block Push/Pop with guards for access control, unsaved-changes warnings, or any custom condition.
