123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package themes
- import (
- "archive/zip"
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "sync"
- "time"
- "kitty/tools/cli"
- "kitty/tools/config"
- "kitty/tools/tui/loop"
- "kitty/tools/tui/subseq"
- "kitty/tools/utils"
- "kitty/tools/utils/style"
- "github.com/shirou/gopsutil/v3/process"
- "golang.org/x/exp/maps"
- "golang.org/x/exp/slices"
- "golang.org/x/sys/unix"
- )
- var _ = fmt.Print
- var AllColorSettingNames = map[string]bool{ // {{{
- // generated by gen-config.py do not edit
- // ALL_COLORS_START
- "active_border_color": true,
- "active_tab_background": true,
- "active_tab_foreground": true,
- "background": true,
- "bell_border_color": true,
- "color0": true,
- "color1": true,
- "color10": true,
- "color100": true,
- "color101": true,
- "color102": true,
- "color103": true,
- "color104": true,
- "color105": true,
- "color106": true,
- "color107": true,
- "color108": true,
- "color109": true,
- "color11": true,
- "color110": true,
- "color111": true,
- "color112": true,
- "color113": true,
- "color114": true,
- "color115": true,
- "color116": true,
- "color117": true,
- "color118": true,
- "color119": true,
- "color12": true,
- "color120": true,
- "color121": true,
- "color122": true,
- "color123": true,
- "color124": true,
- "color125": true,
- "color126": true,
- "color127": true,
- "color128": true,
- "color129": true,
- "color13": true,
- "color130": true,
- "color131": true,
- "color132": true,
- "color133": true,
- "color134": true,
- "color135": true,
- "color136": true,
- "color137": true,
- "color138": true,
- "color139": true,
- "color14": true,
- "color140": true,
- "color141": true,
- "color142": true,
- "color143": true,
- "color144": true,
- "color145": true,
- "color146": true,
- "color147": true,
- "color148": true,
- "color149": true,
- "color15": true,
- "color150": true,
- "color151": true,
- "color152": true,
- "color153": true,
- "color154": true,
- "color155": true,
- "color156": true,
- "color157": true,
- "color158": true,
- "color159": true,
- "color16": true,
- "color160": true,
- "color161": true,
- "color162": true,
- "color163": true,
- "color164": true,
- "color165": true,
- "color166": true,
- "color167": true,
- "color168": true,
- "color169": true,
- "color17": true,
- "color170": true,
- "color171": true,
- "color172": true,
- "color173": true,
- "color174": true,
- "color175": true,
- "color176": true,
- "color177": true,
- "color178": true,
- "color179": true,
- "color18": true,
- "color180": true,
- "color181": true,
- "color182": true,
- "color183": true,
- "color184": true,
- "color185": true,
- "color186": true,
- "color187": true,
- "color188": true,
- "color189": true,
- "color19": true,
- "color190": true,
- "color191": true,
- "color192": true,
- "color193": true,
- "color194": true,
- "color195": true,
- "color196": true,
- "color197": true,
- "color198": true,
- "color199": true,
- "color2": true,
- "color20": true,
- "color200": true,
- "color201": true,
- "color202": true,
- "color203": true,
- "color204": true,
- "color205": true,
- "color206": true,
- "color207": true,
- "color208": true,
- "color209": true,
- "color21": true,
- "color210": true,
- "color211": true,
- "color212": true,
- "color213": true,
- "color214": true,
- "color215": true,
- "color216": true,
- "color217": true,
- "color218": true,
- "color219": true,
- "color22": true,
- "color220": true,
- "color221": true,
- "color222": true,
- "color223": true,
- "color224": true,
- "color225": true,
- "color226": true,
- "color227": true,
- "color228": true,
- "color229": true,
- "color23": true,
- "color230": true,
- "color231": true,
- "color232": true,
- "color233": true,
- "color234": true,
- "color235": true,
- "color236": true,
- "color237": true,
- "color238": true,
- "color239": true,
- "color24": true,
- "color240": true,
- "color241": true,
- "color242": true,
- "color243": true,
- "color244": true,
- "color245": true,
- "color246": true,
- "color247": true,
- "color248": true,
- "color249": true,
- "color25": true,
- "color250": true,
- "color251": true,
- "color252": true,
- "color253": true,
- "color254": true,
- "color255": true,
- "color26": true,
- "color27": true,
- "color28": true,
- "color29": true,
- "color3": true,
- "color30": true,
- "color31": true,
- "color32": true,
- "color33": true,
- "color34": true,
- "color35": true,
- "color36": true,
- "color37": true,
- "color38": true,
- "color39": true,
- "color4": true,
- "color40": true,
- "color41": true,
- "color42": true,
- "color43": true,
- "color44": true,
- "color45": true,
- "color46": true,
- "color47": true,
- "color48": true,
- "color49": true,
- "color5": true,
- "color50": true,
- "color51": true,
- "color52": true,
- "color53": true,
- "color54": true,
- "color55": true,
- "color56": true,
- "color57": true,
- "color58": true,
- "color59": true,
- "color6": true,
- "color60": true,
- "color61": true,
- "color62": true,
- "color63": true,
- "color64": true,
- "color65": true,
- "color66": true,
- "color67": true,
- "color68": true,
- "color69": true,
- "color7": true,
- "color70": true,
- "color71": true,
- "color72": true,
- "color73": true,
- "color74": true,
- "color75": true,
- "color76": true,
- "color77": true,
- "color78": true,
- "color79": true,
- "color8": true,
- "color80": true,
- "color81": true,
- "color82": true,
- "color83": true,
- "color84": true,
- "color85": true,
- "color86": true,
- "color87": true,
- "color88": true,
- "color89": true,
- "color9": true,
- "color90": true,
- "color91": true,
- "color92": true,
- "color93": true,
- "color94": true,
- "color95": true,
- "color96": true,
- "color97": true,
- "color98": true,
- "color99": true,
- "cursor": true,
- "cursor_text_color": true,
- "foreground": true,
- "inactive_border_color": true,
- "inactive_tab_background": true,
- "inactive_tab_foreground": true,
- "macos_titlebar_color": true,
- "mark1_background": true,
- "mark1_foreground": true,
- "mark2_background": true,
- "mark2_foreground": true,
- "mark3_background": true,
- "mark3_foreground": true,
- "selection_background": true,
- "selection_foreground": true,
- "tab_bar_background": true,
- "tab_bar_margin_color": true,
- "url_color": true,
- "visual_bell_color": true,
- "wayland_titlebar_color": true, // ALL_COLORS_END
- } // }}}
- type JSONMetadata struct {
- Etag string `json:"etag"`
- Timestamp string `json:"timestamp"`
- }
- var ErrNoCacheFound = errors.New("No cache found and max cache age is negative")
- func set_comment_in_zip_file(path string, comment string) error {
- src, err := zip.OpenReader(path)
- if err != nil {
- return err
- }
- defer src.Close()
- buf := bytes.Buffer{}
- dest := zip.NewWriter(&buf)
- if err = dest.SetComment(comment); err != nil {
- return err
- }
- for _, sf := range src.File {
- err = dest.Copy(sf)
- if err != nil {
- return err
- }
- }
- dest.Close()
- return utils.AtomicUpdateFile(path, buf.Bytes(), 0o644)
- }
- func fetch_cached(name, url, cache_path string, max_cache_age time.Duration) (string, error) {
- cache_path = filepath.Join(cache_path, name+".zip")
- zf, err := zip.OpenReader(cache_path)
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return "", err
- }
- var jm JSONMetadata
- if err == nil {
- defer zf.Close()
- if err = json.Unmarshal(utils.UnsafeStringToBytes(zf.Comment), &jm); err == nil {
- if max_cache_age < 0 {
- return cache_path, nil
- }
- cache_age, err := utils.ISO8601Parse(jm.Timestamp)
- if err == nil {
- if time.Now().Before(cache_age.Add(max_cache_age)) {
- return cache_path, nil
- }
- }
- }
- }
- if max_cache_age < 0 {
- return "", ErrNoCacheFound
- }
- req, err := http.NewRequest(http.MethodGet, url, nil)
- if err != nil {
- return "", err
- }
- if jm.Etag != "" {
- req.Header.Add("If-None-Match", jm.Etag)
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- if resp.StatusCode == http.StatusNotModified {
- jm.Timestamp = utils.ISO8601Format(time.Now())
- comment, _ := json.Marshal(jm)
- err = set_comment_in_zip_file(cache_path, utils.UnsafeBytesToString(comment))
- if err != nil {
- return "", err
- }
- return cache_path, nil
- }
- return "", fmt.Errorf("Failed to download %s with HTTP error: %s", url, resp.Status)
- }
- var tf, tf2 *os.File
- tf, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
- if err == nil {
- tf2, err = os.CreateTemp(filepath.Dir(cache_path), name+".temp-*")
- }
- defer func() {
- if tf != nil {
- tf.Close()
- os.Remove(tf.Name())
- tf = nil
- }
- if tf2 != nil {
- tf2.Close()
- os.Remove(tf2.Name())
- tf2 = nil
- }
- }()
- if err != nil {
- return "", fmt.Errorf("Failed to create temp file in %s with error: %w", filepath.Dir(cache_path), err)
- }
- _, err = io.Copy(tf, resp.Body)
- if err != nil {
- return "", fmt.Errorf("Failed to download %s with error: %w", url, err)
- }
- r, err := zip.OpenReader(tf.Name())
- if err != nil {
- return "", fmt.Errorf("Failed to open downloaded zip file with error: %w", err)
- }
- defer r.Close()
- w := zip.NewWriter(tf2)
- jm.Etag = resp.Header.Get("ETag")
- jm.Timestamp = utils.ISO8601Format(time.Now())
- comment, _ := json.Marshal(jm)
- if err = w.SetComment(utils.UnsafeBytesToString(comment)); err != nil {
- return "", err
- }
- for _, file := range r.File {
- err = w.Copy(file)
- if err != nil {
- return "", fmt.Errorf("Failed to copy zip file from source to destination archive")
- }
- }
- err = w.Close()
- if err != nil {
- return "", err
- }
- tf2.Close()
- err = os.Rename(tf2.Name(), cache_path)
- if err != nil {
- return "", fmt.Errorf("Failed to atomic rename temp file to %s with error: %w", cache_path, err)
- }
- tf2 = nil
- return cache_path, nil
- }
- func FetchCached(max_cache_age time.Duration) (string, error) {
- return fetch_cached("kitty-themes", "https://codeload.github.com/kovidgoyal/kitty-themes/zip/master", utils.CacheDir(), 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 ParseThemeMetadata(path string) (*ThemeMetadata, map[string]string, error) {
- var in_metadata, in_blurb, finished_metadata bool
- ans := ThemeMetadata{Is_dark: true} // the default background in kitty is dark
- settings := map[string]string{}
- read_is_dark := func(key, val string) (err error) {
- settings[key] = val
- if key == "background" {
- if val != "" {
- bg, err := style.ParseColor(val)
- if err == nil {
- ans.Is_dark = utils.Max(bg.Red, bg.Green, bg.Blue) < 115
- }
- }
- }
- return
- }
- read_metadata := func(line string) (err error) {
- is_block := strings.HasPrefix(line, "## ")
- if in_metadata && !is_block {
- finished_metadata = true
- }
- if finished_metadata {
- return
- }
- if !in_metadata && is_block {
- in_metadata = true
- }
- if !in_metadata {
- return
- }
- line = line[3:]
- if in_blurb {
- ans.Blurb += " " + line
- return
- }
- key, val, found := strings.Cut(line, ":")
- if !found {
- return
- }
- key = strings.TrimSpace(strings.ToLower(key))
- val = strings.TrimSpace(val)
- switch key {
- case "name":
- if val != "The name of the theme (if not present, derived from filename)" {
- 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
- }
- return
- }
- cp := config.ConfigParser{LineHandler: read_is_dark, CommentsHandler: read_metadata}
- err := cp.ParseFiles(path)
- if err != nil {
- return nil, nil, err
- }
- ans.Num_settings = len(settings)
- return &ans, settings, nil
- }
- type Theme struct {
- metadata *ThemeMetadata
- code string
- settings map[string]string
- zip_reader *zip.File
- is_user_defined bool
- path_for_user_defined_theme string
- }
- func (self *Theme) Name() string { return self.metadata.Name }
- func (self *Theme) Author() string { return self.metadata.Author }
- func (self *Theme) Blurb() string { return self.metadata.Blurb }
- func (self *Theme) IsDark() bool { return self.metadata.Is_dark }
- func (self *Theme) IsUserDefined() bool { return self.is_user_defined }
- func (self *Theme) load_code() (string, error) {
- if self.zip_reader != nil {
- f, err := self.zip_reader.Open()
- self.zip_reader = nil
- if err != nil {
- return "", err
- }
- defer f.Close()
- data, err := io.ReadAll(f)
- if err != nil {
- return "", err
- }
- self.code = utils.UnsafeBytesToString(data)
- }
- if self.is_user_defined && self.path_for_user_defined_theme != "" && self.code == "" {
- raw, err := os.ReadFile(self.path_for_user_defined_theme)
- if err != nil {
- return "", err
- }
- self.code = utils.UnsafeBytesToString(raw)
- }
- return self.code, nil
- }
- func (self *Theme) Code() (string, error) {
- return self.load_code()
- }
- func patch_conf(text, theme_name string) string {
- addition := fmt.Sprintf("# BEGIN_KITTY_THEME\n# %s\ninclude current-theme.conf\n# END_KITTY_THEME", theme_name)
- pat := utils.MustCompile(`(?ms)^# BEGIN_KITTY_THEME.+?# END_KITTY_THEME`)
- replaced := false
- ntext := pat.ReplaceAllStringFunc(text, func(string) string {
- replaced = true
- return addition
- })
- if !replaced {
- if text != "" {
- text += "\n\n"
- }
- ntext = text + addition
- }
- pat = utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(maps.Keys(AllColorSettingNames), "|")))
- return pat.ReplaceAllString(ntext, `# $1`)
- }
- func is_kitty_gui_cmdline(cmd ...string) bool {
- if len(cmd) == 0 {
- return false
- }
- if filepath.Base(cmd[0]) != "kitty" {
- return false
- }
- if len(cmd) == 1 {
- return true
- }
- s := cmd[1][:1]
- switch s {
- case `@`:
- return false
- case `+`:
- if cmd[1] == `+` {
- return len(cmd) > 2 && cmd[2] == `open`
- }
- return cmd[1] == `+open`
- }
- return true
- }
- type ReloadDestination string
- const (
- RELOAD_IN_PARENT ReloadDestination = "parent"
- RELOAD_IN_ALL ReloadDestination = "all"
- )
- func reload_config(reload_in ReloadDestination) bool {
- switch reload_in {
- case RELOAD_IN_PARENT:
- if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
- if p, err := process.NewProcess(int32(pid)); err == nil {
- if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
- return p.SendSignal(unix.SIGUSR1) == nil
- }
- }
- }
- case RELOAD_IN_ALL:
- if all, err := process.Processes(); err == nil {
- for _, p := range all {
- if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
- _ = p.SendSignal(unix.SIGUSR1)
- }
- }
- return true
- }
- }
- return false
- }
- func (self *Theme) SaveInDir(dirpath string) (err error) {
- path := filepath.Join(dirpath, self.Name()+".conf")
- code, err := self.Code()
- if err != nil {
- return err
- }
- return utils.AtomicUpdateFile(path, utils.UnsafeStringToBytes(code), 0o644)
- }
- func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) {
- _ = os.MkdirAll(config_dir, 0o755)
- path := filepath.Join(config_dir, `current-theme.conf`)
- code, err := self.Code()
- if err != nil {
- return err
- }
- err = utils.AtomicUpdateFile(path, utils.UnsafeStringToBytes(code), 0o644)
- if err != nil {
- return err
- }
- confpath := config_file_name
- if !filepath.IsAbs(config_file_name) {
- confpath = filepath.Join(config_dir, config_file_name)
- }
- if q, err := filepath.EvalSymlinks(confpath); err == nil {
- confpath = q
- }
- raw, err := os.ReadFile(confpath)
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return err
- }
- nraw := patch_conf(utils.UnsafeBytesToString(raw), self.metadata.Name)
- if len(raw) > 0 {
- _ = os.WriteFile(confpath+".bak", raw, 0o600)
- }
- err = utils.AtomicUpdateFile(confpath, utils.UnsafeStringToBytes(nraw), 0o600)
- if err != nil {
- return err
- }
- reload_config(ReloadDestination(reload_in))
- return
- }
- func (self *Theme) Settings() (map[string]string, error) {
- if self.zip_reader != nil {
- code, err := self.load_code()
- if err != nil {
- return nil, err
- }
- self.settings = make(map[string]string, 64)
- scanner := utils.NewLineScanner(code)
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if line != "" && line[0] != '#' {
- key, val, found := strings.Cut(line, " ")
- if found {
- self.settings[key] = val
- }
- }
- }
- }
- return self.settings, nil
- }
- func (self *Theme) AsEscapeCodes() (string, error) {
- settings, err := self.Settings()
- if err != nil {
- return "", err
- }
- return ColorSettingsAsEscapeCodes(settings), nil
- }
- func ColorSettingsAsEscapeCodes(settings map[string]string) string {
- w := strings.Builder{}
- w.Grow(4096)
- set_color := func(i int, sharp string) {
- w.WriteByte(';')
- w.WriteString(strconv.Itoa(i))
- w.WriteByte(';')
- w.WriteString(sharp)
- }
- set_default_color := func(name, defval string, num loop.DefaultColor) {
- w.WriteString("\033]")
- defer func() { w.WriteString("\033\\") }()
- val, found := settings[name]
- if !found {
- val = defval
- }
- if val != "" {
- rgba, err := style.ParseColor(val)
- if err == nil {
- w.WriteString(strconv.Itoa(int(num)))
- w.WriteByte(';')
- w.WriteString(rgba.AsRGBSharp())
- return
- }
- }
- w.WriteByte('1')
- w.WriteString(strconv.Itoa(int(num)))
- }
- set_default_color("foreground", style.DefaultColors.Foreground, loop.FOREGROUND)
- set_default_color("background", style.DefaultColors.Background, loop.BACKGROUND)
- set_default_color("cursor", style.DefaultColors.Cursor, loop.CURSOR)
- set_default_color("selection_background", style.DefaultColors.SelectionBg, loop.SELECTION_BG)
- set_default_color("selection_foreground", style.DefaultColors.SelectionFg, loop.SELECTION_FG)
- w.WriteString("\033]4")
- for i := 0; i < 256; i++ {
- key := "color" + strconv.Itoa(i)
- val := settings[key]
- if val != "" {
- rgba, err := style.ParseColor(val)
- if err == nil {
- set_color(i, rgba.AsRGBSharp())
- continue
- }
- }
- rgba := style.RGBA{}
- rgba.FromRGB(style.ColorTable[i])
- set_color(i, rgba.AsRGBSharp())
- }
- w.WriteString("\033\\")
- return w.String()
- }
- type Themes struct {
- name_map map[string]*Theme
- index_map []string
- }
- func (self *Themes) Copy() *Themes {
- ans := &Themes{name_map: make(map[string]*Theme, len(self.name_map)), index_map: slices.Clone(self.index_map)}
- maps.Copy(ans.name_map, self.name_map)
- return ans
- }
- var camel_case_pat = sync.OnceValue(func() *regexp.Regexp {
- return regexp.MustCompile(`([a-z])([A-Z])`)
- })
- func ThemeNameFromFileName(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.Title, strings.Split(fname, " ")), " ")
- }
- func (self *Themes) Len() int { return len(self.name_map) }
- func (self *Themes) At(x int) *Theme {
- if x >= len(self.index_map) || x < 0 {
- return nil
- }
- return self.name_map[self.index_map[x]]
- }
- func (self *Themes) Names() []string { return self.index_map }
- func (self *Themes) create_index_map() {
- self.index_map = maps.Keys(self.name_map)
- self.index_map = utils.StableSortWithKey(self.index_map, strings.ToLower)
- }
- func (self *Themes) Filtered(is_ok func(*Theme) bool) *Themes {
- themes := utils.Filter(maps.Values(self.name_map), is_ok)
- ans := Themes{name_map: make(map[string]*Theme, len(themes))}
- for _, theme := range themes {
- ans.name_map[theme.metadata.Name] = theme
- }
- ans.create_index_map()
- return &ans
- }
- func (self *Themes) AddFromFile(path string) (*Theme, error) {
- m, conf, err := ParseThemeMetadata(path)
- if err != nil {
- return nil, err
- }
- if m.Name == "" {
- m.Name = ThemeNameFromFileName(filepath.Base(path))
- }
- t := Theme{metadata: m, is_user_defined: true, settings: conf, path_for_user_defined_theme: path}
- self.name_map[m.Name] = &t
- return &t, nil
- }
- 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") {
- path := filepath.Join(dirpath, e.Name())
- // ignore files if they are the STDOUT of the current processes
- // allows using kitten theme --dump-theme name > ~/.config/kitty/themes/name.conf
- if utils.Samefile(path, os.Stdout) {
- continue
- }
- if _, err = self.AddFromFile(path); err != nil {
- return err
- }
- }
- }
- return nil
- }
- func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) {
- r, err := zip.OpenReader(zippath)
- if err != nil {
- return nil, 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 nil, 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 nil, fmt.Errorf("Error while reading %s from the ZIP file: %w", file.Name, err)
- }
- err = json.Unmarshal(raw, &themes)
- if err != nil {
- return nil, fmt.Errorf("Error while decoding %s: %w", file.Name, err)
- }
- }
- }
- if theme_dir == "" {
- return nil, 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 r, nil
- }
- func (self *Themes) ThemeByName(name string) *Theme {
- ans := self.name_map[name]
- if ans == nil {
- q := strings.ToLower(name)
- for k, t := range self.name_map {
- if strings.ToLower(k) == q {
- return t
- }
- }
- }
- return ans
- }
- func match(expression string, items []string) []*subseq.Match {
- matches := subseq.ScoreItems(expression, items, subseq.Options{Level1: " "})
- matches = utils.StableSort(matches, func(a, b *subseq.Match) int {
- if b.Score < a.Score {
- return -1
- }
- if b.Score > a.Score {
- return 1
- }
- return 0
- })
- return matches
- }
- const (
- MARK_BEFORE = "\033[33m"
- MARK_AFTER = "\033[39m"
- )
- func (self *Themes) ApplySearch(expression string, marks ...string) []string {
- mark_before, mark_after := MARK_BEFORE, MARK_AFTER
- if len(marks) == 2 {
- mark_before, mark_after = marks[0], marks[1]
- }
- results := utils.Filter(match(expression, self.index_map), func(x *subseq.Match) bool { return x.Score > 0 })
- name_map := make(map[string]*Theme, len(results))
- for _, m := range results {
- name_map[m.Text] = self.name_map[m.Text]
- }
- self.name_map = name_map
- self.index_map = self.index_map[:0]
- ans := make([]string, 0, len(results))
- for _, m := range results {
- text := m.Text
- positions := m.Positions
- for i := len(positions) - 1; i >= 0; i-- {
- p := positions[i]
- text = text[:p] + mark_before + text[p:p+1] + mark_after + text[p+1:]
- }
- ans = append(ans, text)
- self.index_map = append(self.index_map, m.Text)
- }
- return ans
- }
- func LoadThemes(cache_age time.Duration) (ans *Themes, closer io.Closer, err error) {
- zip_path, err := FetchCached(cache_age)
- ans = &Themes{name_map: make(map[string]*Theme)}
- if err != nil {
- return nil, nil, err
- }
- if closer, err = ans.add_from_zip_file(zip_path); err != nil {
- return nil, nil, err
- }
- if err = ans.add_from_dir(filepath.Join(utils.ConfigDir(), "themes")); err != nil {
- return nil, nil, err
- }
- ans.create_index_map()
- return ans, closer, nil
- }
- func ThemeFromFile(path string) (*Theme, error) {
- ans := &Themes{name_map: make(map[string]*Theme)}
- return ans.AddFromFile(path)
- }
- func GetThemeNames(cache_age time.Duration) (ans []string, err error) {
- themes, closer, err := LoadThemes(cache_age)
- if err != nil {
- if errors.Is(err, ErrNoCacheFound) {
- return []string{"Default"}, nil
- }
- return nil, err
- }
- defer closer.Close()
- for name := range themes.name_map {
- ans = append(ans, name)
- }
- return
- }
- func CompleteThemes(completions *cli.Completions, word string, arg_num int) {
- names, err := GetThemeNames(-1)
- if err == nil {
- mg := completions.AddMatchGroup("Themes")
- for _, theme_name := range names {
- theme_name = strings.TrimSpace(theme_name)
- if theme_name != "" && strings.HasPrefix(theme_name, word) {
- mg.AddMatch(theme_name)
- }
- }
- }
- }
|