config.go
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// ~/.skraak/config.json schema (reference):
//
// {
// "classify": {
// "reviewer": "string, required. Name stamped into .data file meta on any edit.",
// "color": "bool, optional. Colored spectrograms in the TUI. Default false.",
// "sixel": "bool, optional. Use sixel image protocol. Default false (Kitty).",
// "iterm": "bool, optional. Use iTerm inline-image protocol. Default false.",
// "img_dims": "int, optional. Spectrogram display size in pixels. 0 = default.",
//
// "bindings": {
// "<key>": "Species" // e.g. "c": "comcha"
// "<key>": "Species+CallType" // e.g. "1": "Kiwi+Duet"
// // <key> is a single character. Reserved: ",", ".", "0", " " (space).
// // Pressing <key> labels the current segment (certainty 100, or 0 for
// // "Don't Know"), saves, and advances.
// },
//
// "secondary_bindings": {
// "<primary-key>": {
// "<key>": "CallType" // e.g. "a": "alarm"
// // <key> is a single character, same reserved-key rules as bindings.
// // Outer <primary-key> must also exist in "bindings".
// }
// // Optional. Invoked via Shift+<primary-key>: labels the species with
// // an empty calltype, does NOT advance, and waits for one follow-up
// // key looked up in this inner map. Match -> set calltype, save,
// // advance. Esc -> exit wait mode without advancing. Any other key ->
// // exit wait mode and handle the key normally.
// // Shift+<primary-key> on a primary without a secondary_bindings entry
// // falls back to normal primary behavior.
// }
// }
// }
//
// Example:
//
// {
// "classify": {
// "reviewer": "David",
// "color": true,
// "bindings": {
// "c": "comcha",
// "k": "kea1",
// "x": "Noise",
// "z": "Don't Know",
// "1": "Kiwi+Duet",
// "4": "Kiwi"
// },
// "secondary_bindings": {
// "c": { "a": "alarm", "s": "song", "n": "contact" }
// }
// }
// }
//
// Config holds user-level defaults loaded from ~/.skraak/config.json.
// Per-subcommand sections live as named fields.
type Config struct {
Classify ClassifyFileConfig `json:"classify"`
}
// ClassifyFileConfig holds defaults for `skraak calls classify`.
// Bindings maps a single-character key to "Species" or "Species+CallType".
type ClassifyFileConfig struct {
Reviewer string `json:"reviewer"`
Color bool `json:"color"`
Sixel bool `json:"sixel"`
ITerm bool `json:"iterm"`
ImgDims int `json:"img_dims"`
Bindings map[string]string `json:"bindings"`
// SecondaryBindings extends a primary binding with per-species calltype
// choices. Outer key is the primary binding key; inner map is
// single-char key -> calltype string. Invoked via Shift+primary-key.
SecondaryBindings map[string]map[string]string `json:"secondary_bindings,omitempty"`
}
// ConfigPath returns the absolute path to ~/.skraak/config.json.
func ConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolving home directory: %w", err)
}
return filepath.Join(home, ".skraak", "config.json"), nil
}
// LoadConfig reads ~/.skraak/config.json and returns the parsed config and the
// resolved path (useful for error messages).
func LoadConfig() (Config, string, error) {
var cfg Config
path, err := ConfigPath()
if err != nil {
return cfg, "", err
}
data, err := os.ReadFile(path)
if err != nil {
return cfg, path, fmt.Errorf("reading %s: %w", path, err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, path, fmt.Errorf("parsing %s: %w", path, err)
}
return cfg, path, nil
}