MF6A3QKQ6SU7PYAHJOO7YFCRB47DO7FHRA7OIZ7KHQJYJR77PAIQC DOME2A7KFYV4EH4RISTE5LMF6UF3U6KNNKPV6UGCKCM5PYEPOP5AC UVDEG6RN6CEKMV76ROQ7DDF6DKJJPTT36OEW53FEW3IS4UB57MCQC 4DKGEM6MTXFJ4LET56PNJR7KPDDTALHWKX7EMXXQHCRE7SBMYHIAC 77NADKVL6Q6KOSJBVFFRNURE6CCKWON6YBEB7B2GTKI5GJOOPXBAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC 5QRZQIBFN5KJZ4PZO22POEKLUVYYQ436NAVKGNQOMPIWKQX7CZFAC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC 7SMHQHQGGCPR44NNBHAELM46EOREVGRF32YB66FYZALQSDRC3CJQC SB4FZEB6ZLUHQNM3M76OGNNJY6THOF55S6JO6Q7IGXWE7OA7INFAC RUF5K5CL542GK5UIIIBHPIMGGCXU72IWS5OFBVTI5DRX36OSPJDAC 2TDG53JBZHZA6ZPYONPINKVDV4UXLP4T4CI5C2MEZIIYO7DQE5RAC GQNMVJQBC6DRV5XGK3K5L7YWG2GJUXR7EQE3OHNW72XK6BFY3AHQC 5KIKDA72HM6JFIPKOWGLM2EO7D5PTSK7WEVYV3YZWGMG3M34PJXQC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC D4W5FSXXDKYXSJGRO7D6VWG2LC5FB23DZMIK4FKNCTGO6TO54BQAC L52ACWG2RC4CLT5ZNOHDMCYUP6DLWHTOFLOPTOXI2ZCTTWBQUF2AC MFURT7K56GUJH64Z6XRKWKUI4VZZQYR72U2QEQNWR4B3TSUEXPQAC GSHWXPDB4GATIWWGKZXMM7VOK5O5IDFKBV4SKXRFOWNMYKYX527AC M3RD4OYBFPVFM3LASIVNTENIVF2YPNQMVROMG7CJCS2O6SST5YNQC 45ZCMOGI2DFPAMVU2YEZE7OSIY6SGRPT2AVCHGCWQ3RCC5JFVEJAC HBQ2J2ANYVMLSE6LBWTSCGJNOVZPMNZOPRPEPMYBXQCWODBHZIJQC GQ2FYLG2VGDDKQ7TJTCFM2XPLJOAX2N737OZWPRCNKSFIH7YJ6EQC HHT7M27I3YKGGJOTVTMRVWXATDWUZKIVVLM7IVI7SJRB7FLT2DAQC QIDT44NKTA6VM3IV7EDEKF5OZOXO243BI7XQJZH37I746VSXGQHAC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC package utilsimport ("encoding/json""fmt""os""path/filepath")// 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"`}// 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 Configpath, 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}
fmt.Fprintf(os.Stderr, " --file <path> Path to a single .data file (required, or --folder)\n")fmt.Fprintf(os.Stderr, " --reviewer <name> Reviewer name for labelling attribution (required)\n")fmt.Fprintf(os.Stderr, " --bind <key=value> Key binding for species label (required, repeatable)\n")
fmt.Fprintf(os.Stderr, " --file <path> Path to a single .data file (required, or --folder)\n")
fmt.Fprintf(os.Stderr, " --color Enable color spectrogram output\n")fmt.Fprintf(os.Stderr, " --sixel Use Sixel protocol for spectrogram images\n")fmt.Fprintf(os.Stderr, " --iterm Use iTerm2 protocol for spectrogram images\n")
fmt.Fprintf(os.Stderr, " --img-dims <int> Image dimensions in characters (optional)\n")fmt.Fprintf(os.Stderr, "\nBind format:\n")fmt.Fprintf(os.Stderr, " --bind k=Species Bind key 'k' to label as Species (any call type)\n")fmt.Fprintf(os.Stderr, " --bind d=Species+CallType Bind key 'd' to label as Species with specific call type\n")
fmt.Fprintf(os.Stderr, "\nConfig (required): ~/.skraak/config.json\n")fmt.Fprintf(os.Stderr, " Provides reviewer, keybindings, and display flags (color/sixel/iterm/img_dims).\n")fmt.Fprintf(os.Stderr, " Example:\n")fmt.Fprintf(os.Stderr, " {\n")fmt.Fprintf(os.Stderr, " \"classify\": {\n")fmt.Fprintf(os.Stderr, " \"reviewer\": \"David\",\n")fmt.Fprintf(os.Stderr, " \"color\": true,\n")fmt.Fprintf(os.Stderr, " \"bindings\": {\n")fmt.Fprintf(os.Stderr, " \"k\": \"Kiwi\",\n")fmt.Fprintf(os.Stderr, " \"1\": \"Kiwi+Duet\",\n")fmt.Fprintf(os.Stderr, " \"x\": \"Noise\"\n")fmt.Fprintf(os.Stderr, " }\n")fmt.Fprintf(os.Stderr, " }\n")fmt.Fprintf(os.Stderr, " }\n")
fmt.Fprintf(os.Stderr, " # Classify kiwi calls in a folder\n")fmt.Fprintf(os.Stderr, " skraak calls classify --folder /path/to/data --reviewer dave \\\n")fmt.Fprintf(os.Stderr, " --bind k=Kiwi --bind d=Kiwi+Duet --bind n=NotBird --sixel\n\n")fmt.Fprintf(os.Stderr, " # Classify a single file with filter\n")fmt.Fprintf(os.Stderr, " skraak calls classify --file /path/to/file.data --reviewer dave \\\n")fmt.Fprintf(os.Stderr, " --filter opensoundscape-kiwi-1.2 --bind k=Kiwi --color\n\n")fmt.Fprintf(os.Stderr, " # Scope to only Kiwi Duet calls within a filter\n")fmt.Fprintf(os.Stderr, " skraak calls classify --folder /path/to/data --reviewer dave \\\n")fmt.Fprintf(os.Stderr, " --filter opensoundscape-kiwi-1.2 --species Kiwi+Duet --bind k=Kiwi\n")
fmt.Fprintf(os.Stderr, " skraak calls classify --folder /path/to/data\n")fmt.Fprintf(os.Stderr, " skraak calls classify --file /path/to/file.data --filter opensoundscape-kiwi-1.2\n")fmt.Fprintf(os.Stderr, " skraak calls classify --folder /path/to/data --species Kiwi+Duet\n")
var folder, file, filter, reviewer, species, gotoFile stringvar color, sixel, iterm boolvar imgDims, certainty intvar bindings []tools.KeyBinding
var folder, file, filter, species, gotoFile stringvar certainty int
case "--reviewer":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --reviewer requires a value\n")os.Exit(1)}reviewer = args[i+1]i += 2case "--color":color = truei++case "--sixel":sixel = truei++case "--iterm":iterm = truei++
case "--help", "-h":printClassifyUsage()os.Exit(0)
i += 2case "--img-dims":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --img-dims requires a value\n")os.Exit(1)}v, err := strconv.Atoi(args[i+1])if err != nil {fmt.Fprintf(os.Stderr, "Error: --img-dims must be an integer\n")os.Exit(1)}imgDims = vi += 2case "--bind":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --bind requires a value\n")os.Exit(1)}// Parse key="Species" or key="Species"+"CallType" formatbinding := parseBind(args[i+1])bindings = append(bindings, binding)
if reviewer == "" {missing = append(missing, "--reviewer")
// Load reviewer, bindings, and display flags from ~/.skraak/config.json.cfg, cfgPath, err := utils.LoadConfig()if err != nil {fmt.Fprintf(os.Stderr, "Error: %v\n", err)fmt.Fprintf(os.Stderr, "Create %s with a \"classify\" section; run `skraak calls classify --help` for an example.\n", cfgPath)os.Exit(1)
if len(missing) > 0 {fmt.Fprintf(os.Stderr, "Error: missing required flags: %v\n\n", missing)printClassifyUsage()
if len(cfg.Classify.Bindings) == 0 {fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.bindings\" (need at least one key)\n", cfgPath)
// Convert config bindings map -> []tools.KeyBinding via existing parseBind.bindings := make([]tools.KeyBinding, 0, len(cfg.Classify.Bindings))for key, value := range cfg.Classify.Bindings {if len(key) != 1 {fmt.Fprintf(os.Stderr, "Error: binding key %q in %s must be a single character\n", key, cfgPath)os.Exit(1)}bindings = append(bindings, parseBind(key+"="+value))}
Reviewer: reviewer,Color: color,ImageSize: imgDims,Sixel: sixel,ITerm: iterm,
Reviewer: cfg.Classify.Reviewer,Color: cfg.Classify.Color,ImageSize: cfg.Classify.ImgDims,Sixel: cfg.Classify.Sixel,ITerm: cfg.Classify.ITerm,
./skraak calls classify --folder . --filter opensoundscape-kiwi-1.2 --species Kiwi+Male \--reviewer David \--bind k=Kiwi \--bind d="Kiwi+Duet" \--bind f="Kiwi+Female" \--bind m="Kiwi+Male" \--bind n="Don't Know" \--bind p=Morepork \--bind w=W.eka \--bind g=Gecko \--color \--img-dims 224
./skraak calls classify --folder . --filter opensoundscape-kiwi-1.2 --species Kiwi+Male./skraak calls classify --folder . --filter opensoundscape-multi-1.0
./skraak calls classify --folder . --reviewer David --color --filter opensoundscape-multi-1.0 \--bind a=eurbla \--bind b=nezbel1 \--bind c=comcha \--bind d=saddle3 \--bind e=pipipi1 \--bind f=nezfan1 \--bind g=gryger1 \--bind i=tui1 \--bind j=nezkak1 \--bind k=kea1 \--bind l=lotkoe1 \--bind m=morepo2 \--bind n=nezrob3 \--bind o=soioys1 \--bind p=malpar2 \--bind r=riflem1 \--bind s=silver3 \--bind t=tomtit1 \--bind u=nezpig2 \--bind w=weka1 \--bind x=Noise \--bind z="Don't Know" \--bind 1=Kiwi+Duet \--bind 2=Kiwi+Female \--bind 3=Kiwi+Male \--bind 4=Kiwi \--bind 5=Gecko
Reviewer, keybindings, and display flags (color/sixel/iterm/img_dims) are loadedfrom `~/.skraak/config.json` — create it once before first use:```json{"classify": {"reviewer": "David","color": true,"bindings": {"k": "Kiwi","d": "Kiwi+Duet","n": "Don't Know","1": "Kiwi+Duet","2": "Kiwi+Female","3": "Kiwi+Male","4": "Kiwi","x": "Noise"}}}```Path resolves to `~/.skraak/config.json` on Linux/macOS and`C:\Users\<name>\.skraak\config.json` on Windows via `os.UserHomeDir()`.
# Launch TUI for reviewing and classifying segments (folder, reviewer and 1 key bind required)./skraak calls classify --folder ./data --reviewer David \--bind k=Kiwi --bind d='Kiwi+Duet' --bind n='Don''t Know'
# Launch TUI for reviewing and classifying segments./skraak calls classify --folder ./data
./skraak calls classify --folder ./data --reviewer David --bind k=Kiwi \--filter opensoundscape-kiwi-1.2 --species Kiwi+Duet# With color and custom image size (clamps to 224px to 896px)./skraak calls classify --folder ./data --reviewer David --bind k=Kiwi --color --img-dims 224
./skraak calls classify --folder ./data --filter opensoundscape-kiwi-1.2 --species Kiwi+Duet
**Key bindings format:**- `k=Kiwi` - Press 'k' to classify as Kiwi (species only)- `d=Kiwi+Duet` - Press 'd' to classify as Kiwi with Duet call type
**Key bindings format** (values in `classify.bindings` in `~/.skraak/config.json`):- `"k": "Kiwi"` - Press 'k' to classify as Kiwi (species only)- `"d": "Kiwi+Duet"` - Press 'd' to classify as Kiwi with Duet call type- Keys must be a single character. Digits (`"1"`, `"2"`, ...) work for numpad use.
## [2026-04-18] `calls classify` reviewer, bindings, and display flags moved to config file**Breaking CLI change.** `skraak calls classify` no longer accepts `--reviewer`,`--bind`, `--color`, `--sixel`, `--iterm`, or `--img-dims`. These values are nowloaded from `~/.skraak/config.json`.Rationale: users (e.g. David) were typing the same ~25 `--bind` flags on everyinvocation. Moving stable, personal defaults into a config file eliminates thatrepetition. Per-invocation flags (`--folder`, `--file`, `--filter`, `--species`,`--certainty`, `--goto`) stay on the CLI.Path works cross-platform via `os.UserHomeDir()` — resolves to`~/.skraak/config.json` on Linux/macOS and `C:\Users\<name>\.skraak\config.json`on Windows.
Config shape:```json{"classify": {"reviewer": "David","color": true,"sixel": false,"iterm": false,"img_dims": 0,"bindings": {"k": "Kiwi","1": "Kiwi+Duet","x": "Noise","z": "Don't Know"}}}````bindings` values use the same `Species` or `Species+CallType` grammar the old`--bind key=value` flag accepted — parsing is shared (`cmd/calls_classify.go:parseBind`).**Files added:**- `utils/config.go` — `Config`, `ClassifyFileConfig`, `LoadConfig`, `ConfigPath`.Named `LoadConfig` (not `LoadClassifyConfig`) so future subcommands can addtheir own sections to the same file.**Files changed:**- `cmd/calls_classify.go` — Removed six flag cases, added config load after argparsing (so `--help` still works without a config), added `--help`/`-h` case,added single-character validation on binding keys.