293 lines
7.1 KiB
Go
293 lines
7.1 KiB
Go
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
|
|
}
|