Port theme loading code to Go

This commit is contained in:
Kovid Goyal 2023-02-26 20:40:59 +05:30
parent 4eea2fd4fc
commit 0b09d18b36
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 204 additions and 0 deletions

View File

@ -4,16 +4,23 @@ package themes
import ( import (
"archive/zip" "archive/zip"
"bufio"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"kitty/tools/utils" "kitty/tools/utils"
"kitty/tools/utils/style"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"regexp"
"strings"
"time" "time"
"golang.org/x/exp/maps"
) )
var _ = fmt.Print var _ = fmt.Print
@ -117,3 +124,192 @@ func fetch_cached(name, url string, max_cache_age time.Duration) (string, error)
func FetchCached(max_cache_age time.Duration) (string, error) { func FetchCached(max_cache_age time.Duration) (string, error) {
return fetch_cached("kitty-themes", "https://codeload.github.com/kovidgoyal/kitty-themes/zip/master", max_cache_age) return fetch_cached("kitty-themes", "https://codeload.github.com/kovidgoyal/kitty-themes/zip/master", max_cache_age)
} }
type ThemeMetadata struct {
Name string `json:"name"`
Filepath string `json:"file"`
Is_dark bool `json:"is_dark"`
Num_settings int `json:"num_settings"`
Blurb string `json:"blurb"`
License string `json:"license"`
Upstream string `json:"upstream"`
Author string `json:"author"`
}
func parse_theme_metadata(raw string) *ThemeMetadata {
scanner := bufio.NewScanner(strings.NewReader(raw))
var in_metadata, in_blurb, finished_metadata bool
ans := ThemeMetadata{}
settings := utils.NewSet[string]()
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
is_block := strings.HasPrefix(line, "## ")
if in_metadata && !is_block {
finished_metadata = true
}
if finished_metadata {
if line[0] != '#' {
key, val, found := strings.Cut(line, " ")
if found {
settings.Add(key)
if key == "background" {
val = strings.TrimSpace(val)
if val != "" {
bg, err := style.ParseColor(val)
if err == nil {
ans.Is_dark = utils.Max(bg.Red, bg.Green, bg.Green) < 115
}
}
}
}
}
continue
}
if !in_metadata && is_block {
in_metadata = true
}
if !in_metadata {
continue
}
line = line[3:]
if in_blurb {
ans.Blurb += " " + line
continue
}
key, val, found := strings.Cut(line, ":")
if !found {
continue
}
key = strings.TrimSpace(strings.ToLower(key))
val = strings.TrimSpace(val)
switch key {
case "name":
ans.Name = val
case "author":
ans.Author = val
case "upstream":
ans.Upstream = val
case "blurb":
ans.Blurb = val
in_blurb = true
case "license":
ans.License = val
}
}
ans.Num_settings = settings.Len()
return &ans
}
type Theme struct {
metadata *ThemeMetadata
code string
zip_reader *zip.File
is_user_defined bool
}
type Themes struct {
name_map map[string]*Theme
index_map []string
}
var camel_case_pat = (&utils.Once[*regexp.Regexp]{Run: func() *regexp.Regexp {
return regexp.MustCompile(`[a-z][A-Z]`)
}}).Get
func theme_name_from_file_name(fname string) string {
fname = fname[:len(fname)-len(path.Ext(fname))]
fname = strings.ReplaceAll(fname, "_", " ")
fname = camel_case_pat().ReplaceAllString(fname, "$1 $2")
return strings.Join(utils.Map(strings.Split(fname, " "), strings.Title), " ")
}
func (self *Themes) add_from_dir(dirpath string) error {
entries, err := os.ReadDir(dirpath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return err
}
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".conf") {
confb, err := os.ReadFile(e.Name())
if err != nil {
return err
}
conf := utils.UnsafeBytesToString(confb)
m := parse_theme_metadata(conf)
if m.Name == "" {
m.Name = theme_name_from_file_name(e.Name())
}
t := Theme{metadata: m, is_user_defined: true, code: conf}
self.name_map[m.Name] = &t
}
}
return nil
}
func (self *Themes) add_from_zip_file(zippath string) error {
r, err := zip.OpenReader(zippath)
if err != nil {
return err
}
name_map := make(map[string]*zip.File, len(r.File))
var themes []ThemeMetadata
theme_dir := ""
for _, file := range r.File {
name_map[file.Name] = file
if path.Base(file.Name) == "themes.json" {
theme_dir = path.Dir(file.Name)
fr, err := file.Open()
if err != nil {
return fmt.Errorf("Error while opening %s from the ZIP file: %w", file.Name, err)
}
defer fr.Close()
raw, err := io.ReadAll(fr)
if err != nil {
return fmt.Errorf("Error while reading %s from the ZIP file: %w", file.Name, err)
}
err = json.Unmarshal(raw, &themes)
if err != nil {
return fmt.Errorf("Error while decoding %s: %w", file.Name, err)
}
}
}
if theme_dir == "" {
return fmt.Errorf("No themes.json found in ZIP file")
}
for _, theme := range themes {
key := path.Join(theme_dir, theme.Filepath)
f := name_map[key]
if f != nil {
t := Theme{metadata: &theme, zip_reader: f}
self.name_map[theme.Name] = &t
}
}
return nil
}
func LoadThemes(cache_age_in_days time.Duration, ignore_no_cache bool) (*Themes, error) {
zip_path, err := FetchCached(cache_age_in_days * time.Hour * 24)
ans := Themes{name_map: make(map[string]*Theme)}
if err != nil {
if !errors.Is(err, ErrNoCacheFound) || ignore_no_cache {
return nil, err
}
} else {
if err = ans.add_from_zip_file(zip_path); err != nil {
return nil, err
}
}
if err = ans.add_from_dir(filepath.Join(utils.ConfigDir(), "themes")); err != nil {
return nil, err
}
ans.index_map = maps.Keys(ans.name_map)
ans.index_map = utils.StableSortWithKey(ans.index_map, strings.ToLower)
return &ans, nil
}

View File

@ -57,6 +57,14 @@ func Filter[T any](s []T, f func(x T) bool) []T {
return ans return ans
} }
func Map[T any](s []T, f func(x T) T) []T {
ans := make([]T, 0, len(s))
for _, x := range s {
ans = append(ans, f(x))
}
return ans
}
func Sort[T any](s []T, less func(a, b T) bool) []T { func Sort[T any](s []T, less func(a, b T) bool) []T {
sort.Slice(s, func(i, j int) bool { return less(s[i], s[j]) }) sort.Slice(s, func(i, j int) bool { return less(s[i], s[j]) })
return s return s