Files
kitty/tools/themes/collection.go
mcrmck ab3a8ca56a Add pane title bar feature for window splits
Add an optional title bar that displays above or below each window pane
when multiple windows are visible in a tab. This is similar to tmux's
pane-border-format or Terminator's pane title bars.

New configuration options:
- pane_title_bar: none/top/bottom (default: none)
- pane_title_template: f-string template (same syntax as tab_title_template)
- active_pane_title_template: override for active pane
- pane_title_bar_active_fg/bg: colors for active pane title
- pane_title_bar_inactive_fg/bg: colors for inactive pane titles
- pane_title_bar_align: left/center/right text alignment

The title bars are rendered using virtual Screen objects registered with
the GPU, following the same model as the tab bar. Title bars are
automatically hidden when only a single window is visible.

Ref: https://github.com/kovidgoyal/kitty/discussions/9448

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-01 23:52:24 -05:00

944 lines
27 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package themes
import (
"archive/zip"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"maps"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/subseq"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
)
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,
"cursor_trail_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,
"pane_title_bar_active_bg": true,
"pane_title_bar_active_fg": true,
"pane_title_bar_inactive_bg": true,
"pane_title_bar_inactive_fg": true,
"scrollbar_handle_color": true,
"scrollbar_track_color": 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, bytes.NewReader(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 (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, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644)
}
func (self *Theme) SaveInFile(config_dir, config_file_name string) (err error) {
_ = os.MkdirAll(config_dir, 0o755)
path := filepath.Join(config_dir, config_file_name)
code, err := self.Code()
if err != nil {
return err
}
return utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644)
}
func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) {
if err = self.SaveInFile(config_dir, `current-theme.conf`); err != nil {
return err
}
confpath := config_file_name
if !filepath.IsAbs(config_file_name) {
confpath = filepath.Join(config_dir, config_file_name)
}
patcher := config.Patcher{Write_backup: true}
if _, err = patcher.Patch(
confpath, "KITTY_THEME", fmt.Sprintf("# %s\ninclude current-theme.conf", self.metadata.Name),
utils.Keys(AllColorSettingNames)...); err != nil {
return
}
switch reload_in {
case "parent":
config.ReloadConfigInKitty(true)
case "all":
config.ReloadConfigInKitty(false)
}
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 := range 256 {
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 = utils.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(utils.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)
}
}
}
}