// 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) }