1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
// Package config persists user preferences (favorite teams, selected leagues,
// active theme) under the XDG config dir (~/.config/sportsball/config.json). Reads are
// forgiving: a missing or corrupt file yields defaults rather than an error, so
// the app always starts.
package config
import (
"encoding/json"
"os"
"path/filepath"
)
// Config is the persisted user state. Fields are optional; zero values are
// valid defaults.
type Config struct {
Favorites []FavTeam `json:"favorites"`
// League preferences (TASK-002 / TASK-019). Leagues auto-show by season;
// these layer per-league overrides on top:
// LeagueOrder — display order (league IDs); empty = catalog order.
// HideLeagues — force-hidden even when in season.
// ShowLeagues — force-shown even when out of season.
// Toggling a league back to what the season would do clears its override.
LeagueOrder []string `json:"league_order,omitempty"`
HideLeagues []string `json:"hide_leagues,omitempty"`
ShowLeagues []string `json:"show_leagues,omitempty"`
Theme string `json:"theme,omitempty"` // active theme name (TASK-005)
}
// FavTeam identifies one favorited team. League+ID is the stable key; Abbr/Name
// are stored for display and as a fallback when ID is unavailable.
type FavTeam struct {
League string `json:"league"`
ID string `json:"id"`
Abbr string `json:"abbr"`
Name string `json:"name"`
}
// dir is the config directory: $XDG_CONFIG_HOME/sportsball, else ~/.config/sportsball.
func dir() (string, error) {
base := os.Getenv("XDG_CONFIG_HOME")
if base == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
base = filepath.Join(home, ".config")
}
return filepath.Join(base, "sportsball"), nil
}
func path() (string, error) {
d, err := dir()
if err != nil {
return "", err
}
return filepath.Join(d, "config.json"), nil
}
// Load reads the config. A missing or unreadable/corrupt file is not an error:
// it returns the zero Config so callers can proceed with defaults.
func Load() Config {
p, err := path()
if err != nil {
return Config{}
}
data, err := os.ReadFile(p)
if err != nil {
return Config{}
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return Config{} // corrupt → defaults
}
return c
}
// Save writes the config atomically (temp file + rename), creating the config
// directory if needed.
func Save(c Config) error {
d, err := dir()
if err != nil {
return err
}
if err := os.MkdirAll(d, 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
p := filepath.Join(d, "config.json")
tmp, err := os.CreateTemp(d, "config-*.json")
if err != nil {
return err
}
tmpName := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmpName)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(tmpName)
return err
}
return os.Rename(tmpName, p)
}
|