package main import ( "fmt" "os" "path/filepath" "slices" "sort" "strconv" "strings" "github.com/BurntSushi/toml" ) type themePair struct { Name string `toml:"name"` Dark string `toml:"dark"` Light string `toml:"light"` } type storedCatalog struct { Pairs []themePair `toml:"pairs"` Themes []string `toml:"themes"` } type currentPair struct { Light string `toml:"light"` Dark string `toml:"dark"` } type catalog struct { FavoritePairs []themePair FavoriteThemes []string OtherPairs []themePair OtherThemes []string } type state struct { Current currentPair Catalog catalog } type storedState struct { Current currentPair `toml:"current"` Favorites storedCatalog `toml:"favorites"` Others storedCatalog `toml:"others"` } func defaultStatePath() string { if path := os.Getenv("TINT_STATE_FILE"); path != "" { return path } config := os.Getenv("XDG_CONFIG_HOME") if config == "" { config = filepath.Join(homeDir(), ".config") } return filepath.Join(config, "tinted-theming", "tinty", "state.toml") } func loadState(path string) (state, error) { var stored storedState metadata, err := toml.DecodeFile(path, &stored) if err != nil { return state{}, err } if unknown := metadata.Undecoded(); len(unknown) != 0 { return state{}, fmt.Errorf("unknown state keys: %s", unknownKeys(unknown)) } value := state{ Current: stored.Current, Catalog: catalog{ FavoritePairs: stored.Favorites.Pairs, FavoriteThemes: stored.Favorites.Themes, OtherPairs: stored.Others.Pairs, OtherThemes: stored.Others.Themes, }, } value.Catalog.normalize() return value, nil } func saveState(path string, value state) error { return writeAtomic(path, []byte(formatState(value)), 0o644) } func formatState(value state) string { value.Catalog.normalize() var output strings.Builder fmt.Fprintf(&output, "[current]\nlight = %s\ndark = %s\n\n", strconv.Quote(value.Current.Light), strconv.Quote(value.Current.Dark)) writePairs := func(name string, pairs []themePair) { for _, pair := range pairs { fmt.Fprintf(&output, "[[%s]]\nname = %s\ndark = %s\nlight = %s\n\n", name, strconv.Quote(pair.Name), strconv.Quote(pair.Dark), strconv.Quote(pair.Light)) } } writeThemes := func(name string, themes []string) { fmt.Fprintf(&output, "[%s]\nthemes = [\n", name) for _, theme := range themes { fmt.Fprintf(&output, "%s,\n", strconv.Quote(theme)) } output.WriteString("]\n\n") } writePairs("favorites.pairs", value.Catalog.FavoritePairs) writeThemes("favorites", value.Catalog.FavoriteThemes) writePairs("others.pairs", value.Catalog.OtherPairs) writeThemes("others", value.Catalog.OtherThemes) return strings.TrimRight(output.String(), "\n") + "\n" } func unknownKeys(keys []toml.Key) string { values := make([]string, len(keys)) for i, key := range keys { values[i] = key.String() } return strings.Join(values, ", ") } func (c *catalog) normalize() { c.FavoritePairs = uniquePairs(c.FavoritePairs) favoritePairNames := make(map[string]bool, len(c.FavoritePairs)) for _, pair := range c.FavoritePairs { favoritePairNames[pair.Name] = true } otherPairs := uniquePairs(c.OtherPairs) c.OtherPairs = c.OtherPairs[:0] for _, pair := range otherPairs { if !favoritePairNames[pair.Name] { c.OtherPairs = append(c.OtherPairs, pair) } } c.FavoriteThemes = uniqueStrings(c.FavoriteThemes) blocked := make(map[string]bool) for _, theme := range c.FavoriteThemes { blocked[theme] = true } for _, pair := range append(slices.Clone(c.FavoritePairs), c.OtherPairs...) { blocked[pair.Dark] = true blocked[pair.Light] = true } otherThemes := uniqueStrings(c.OtherThemes) c.OtherThemes = c.OtherThemes[:0] for _, theme := range otherThemes { if !blocked[theme] { c.OtherThemes = append(c.OtherThemes, theme) } } } func uniquePairs(pairs []themePair) []themePair { byName := make(map[string]themePair, len(pairs)) for _, pair := range pairs { byName[pair.Name] = pair } result := make([]themePair, 0, len(byName)) for _, pair := range byName { result = append(result, pair) } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result } func uniqueStrings(values []string) []string { seen := make(map[string]bool, len(values)) result := make([]string, 0, len(values)) for _, value := range values { if value != "" && !seen[value] { seen[value] = true result = append(result, value) } } sort.Strings(result) return result } func (c *catalog) findPair(name string) (themePair, bool, bool) { for _, pair := range c.FavoritePairs { if pair.Name == name { return pair, true, true } } for _, pair := range c.OtherPairs { if pair.Name == name { return pair, false, true } } return themePair{}, false, false } func (c *catalog) configurePair(pair themePair) { _, favorite, exists := c.findPair(pair.Name) if exists { for _, old := range append(slices.Clone(c.FavoritePairs), c.OtherPairs...) { if old.Name == pair.Name { c.OtherThemes = append(c.OtherThemes, old.Dark, old.Light) } } } c.FavoritePairs = removePair(c.FavoritePairs, pair.Name) c.OtherPairs = removePair(c.OtherPairs, pair.Name) if favorite { c.FavoritePairs = append(c.FavoritePairs, pair) } else { c.OtherPairs = append(c.OtherPairs, pair) } c.normalize() } func (c *catalog) favorite(item string) bool { pair, _, found := c.findPair(item) if found { c.OtherPairs = removePair(c.OtherPairs, item) c.FavoritePairs = append(c.FavoritePairs, pair) c.normalize() return true } c.OtherThemes = removeString(c.OtherThemes, item) c.FavoriteThemes = append(c.FavoriteThemes, item) c.normalize() return false } func (c *catalog) unfavorite(item string) bool { pair, _, found := c.findPair(item) if found { c.FavoritePairs = removePair(c.FavoritePairs, item) c.OtherPairs = append(c.OtherPairs, pair) c.normalize() return true } c.FavoriteThemes = removeString(c.FavoriteThemes, item) c.OtherThemes = append(c.OtherThemes, item) c.normalize() return false } func (c *catalog) record(pair currentPair) { c.OtherThemes = append(c.OtherThemes, pair.Light, pair.Dark) c.normalize() } func removePair(pairs []themePair, name string) []themePair { result := pairs[:0] for _, pair := range pairs { if pair.Name != name { result = append(result, pair) } } return result } func removeString(values []string, target string) []string { result := values[:0] for _, value := range values { if value != target { result = append(result, value) } } return result } func writeAtomic(path string, data []byte, mode os.FileMode) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } file, err := os.CreateTemp(filepath.Dir(path), ".tint-*") if err != nil { return err } tmp := file.Name() defer os.Remove(tmp) if _, err := file.Write(data); err != nil { file.Close() return err } if err := file.Chmod(mode); err != nil { file.Close() return err } if err := file.Close(); err != nil { return err } return os.Rename(tmp, path) } func homeDir() string { if home := os.Getenv("HOME"); home != "" { return home } home, _ := os.UserHomeDir() return home }