Load color tag presets from an editable presets.json file
The presets move from app.js to presets.json, built into the binary and served by /api/presets. PRESETS_FILE (default /data/presets.json) replaces the list when present; it is read again each time Settings opens and the built-in list is used if it is invalid. docs/presets.md (EN/FR) explains each preset and the file format. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4225a2c870
commit
688a7dc2e6
14 files changed
+680
-126
No files matched your search
+110
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Built-in tag presets, used when no presets file is found (PRESETS_FILE,
|
||||
// /data/presets.json by default). See docs/presets.md.
|
||||
//
|
||||
//go:embed presets.json
|
||||
var builtinPresets []byte
|
||||
|
||||
// i18nText is a text per language ({"en": "...", "fr": "..."}) or one plain
|
||||
// string for every language.
|
||||
type i18nText map[string]string
|
||||
|
||||
func (t *i18nText) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if json.Unmarshal(b, &s) == nil {
|
||||
*t = i18nText{"en": s}
|
||||
return nil
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return errors.New("expected a string or an object of strings per language")
|
||||
}
|
||||
*t = m
|
||||
return nil
|
||||
}
|
||||
|
||||
type PresetTag struct {
|
||||
Label i18nText `json:"label,omitempty"`
|
||||
Pattern string `json:"pattern"`
|
||||
Color string `json:"color"`
|
||||
Regex *bool `json:"regex,omitempty"` // regular expression unless false
|
||||
WholeWord bool `json:"wholeWord,omitempty"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
type Preset struct {
|
||||
ID string `json:"id"`
|
||||
Name i18nText `json:"name"`
|
||||
Tags []PresetTag `json:"tags"`
|
||||
}
|
||||
|
||||
type PresetGroup struct {
|
||||
Group i18nText `json:"group"`
|
||||
Presets []Preset `json:"presets"`
|
||||
}
|
||||
|
||||
// parsePresets decodes and checks a presets file with the same rules as the
|
||||
// tags, so that every preset can be added as is.
|
||||
func parsePresets(b []byte) ([]PresetGroup, error) {
|
||||
var groups []PresetGroup
|
||||
if err := json.Unmarshal(b, &groups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, g := range groups {
|
||||
for _, p := range g.Presets {
|
||||
if p.ID == "" || seen[p.ID] {
|
||||
return nil, fmt.Errorf("preset %q: missing or duplicate id", p.ID)
|
||||
}
|
||||
seen[p.ID] = true
|
||||
if len(p.Tags) == 0 {
|
||||
return nil, fmt.Errorf("preset %q: no tags", p.ID)
|
||||
}
|
||||
for i, pt := range p.Tags {
|
||||
t := Tag{Pattern: pt.Pattern, Color: pt.Color, Regex: pt.Regex == nil || *pt.Regex}
|
||||
if err := t.validate(); err != nil {
|
||||
return nil, fmt.Errorf("preset %q, tag %d: %w", p.ID, i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// presetsResponse is what GET /api/presets returns.
|
||||
type presetsResponse struct {
|
||||
Source string `json:"source"` // "file" or "builtin"
|
||||
File string `json:"file"`
|
||||
Error string `json:"error,omitempty"` // the file is invalid: built-in presets are used
|
||||
Groups []PresetGroup `json:"groups"`
|
||||
}
|
||||
|
||||
// loadPresets reads the presets file on every call, so that edits apply
|
||||
// without a restart, and falls back to the built-in presets.
|
||||
func loadPresets(path string) presetsResponse {
|
||||
res := presetsResponse{Source: "builtin", File: path}
|
||||
if path != "" {
|
||||
b, err := os.ReadFile(path)
|
||||
switch {
|
||||
case err == nil:
|
||||
if res.Groups, err = parsePresets(b); err == nil {
|
||||
res.Source = "file"
|
||||
return res
|
||||
}
|
||||
res.Error = err.Error()
|
||||
case !errors.Is(err, os.ErrNotExist):
|
||||
res.Error = err.Error()
|
||||
}
|
||||
}
|
||||
res.Groups, _ = parsePresets(builtinPresets) // checked by the tests
|
||||
return res
|
||||
}
|
||||
Reference in new issue
Block a user