Port theme loading code to Go
This commit is contained in:
parent
4eea2fd4fc
commit
0b09d18b36
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user