diff --git a/gen-config.py b/gen-config.py index a59e5a1e1..6fcc0c2b4 100755 --- a/gen-config.py +++ b/gen-config.py @@ -47,7 +47,7 @@ def main() -> None: all_colors.append(opt.name) patch_color_list('kitty/rc/set_colors.py', nullable_colors, 'NULLABLE') patch_color_list('tools/cmd/at/set_colors.go', nullable_colors, 'NULLABLE') - patch_color_list('kittens/themes/collection.py', all_colors, 'ALL', ' ' * 8) + patch_color_list('tools/themes/collection.go', all_colors, 'ALL') from kittens.diff.options.definition import definition as kd write_output('kittens.diff', kd) diff --git a/kittens/themes/main.py b/kittens/themes/main.py index 0fa05b087..b440c6cc4 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -619,4 +619,5 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['short_desc'] = 'Manage kitty color schemes easily' cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes') diff --git a/shell-integration/ssh/kitty b/shell-integration/ssh/kitty index f98163146..be0c81b2c 100755 --- a/shell-integration/ssh/kitty +++ b/shell-integration/ssh/kitty @@ -24,7 +24,7 @@ exec_kitty() { is_wrapped_kitten() { - wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh" + wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes" [ -n "$1" ] && { case " $wrapped_kittens " in *" $1 "*) printf "%s" "$1" ;; diff --git a/tools/cmd/completion/kitty.go b/tools/cmd/completion/kitty.go index 54df5ec38..175ea1852 100644 --- a/tools/cmd/completion/kitty.go +++ b/tools/cmd/completion/kitty.go @@ -4,11 +4,10 @@ package completion import ( "fmt" - "os/exec" "strings" "kitty/tools/cli" - "kitty/tools/utils" + "kitty/tools/themes" ) var _ = fmt.Print @@ -64,20 +63,7 @@ func complete_plus_open(completions *cli.Completions, word string, arg_num int) } func complete_themes(completions *cli.Completions, word string, arg_num int) { - kitty := utils.KittyExe() - if kitty != "" { - out, err := exec.Command(kitty, "+runpy", "from kittens.themes.collection import *; print_theme_names()").Output() - if err == nil { - mg := completions.AddMatchGroup("Themes") - scanner := utils.NewLineScanner(utils.UnsafeBytesToString(out)) - for scanner.Scan() { - theme_name := strings.TrimSpace(scanner.Text()) - if theme_name != "" && strings.HasPrefix(theme_name, word) { - mg.AddMatch(theme_name) - } - } - } - } + themes.CompleteThemes(completions, word, arg_num) } func EntryPoint(tool_root *cli.Command) { diff --git a/tools/cmd/themes/main.go b/tools/cmd/themes/main.go new file mode 100644 index 000000000..a01278dac --- /dev/null +++ b/tools/cmd/themes/main.go @@ -0,0 +1,62 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package themes + +import ( + "fmt" + "strings" + "time" + + "kitty/tools/cli" + "kitty/tools/themes" + "kitty/tools/utils" +) + +var _ = fmt.Print + +func complete_themes(completions *cli.Completions, word string, arg_num int) { + themes.CompleteThemes(completions, word, arg_num) +} + +func non_interactive(opts *Options, theme_name string) (rc int, err error) { + themes, closer, err := themes.LoadThemes(time.Duration(opts.CacheAge * float64(time.Hour*24))) + if err != nil { + return 1, err + } + defer closer.Close() + theme := themes.ThemeByName(theme_name) + if theme == nil { + theme_name = strings.ReplaceAll(theme_name, `\`, ``) + theme = themes.ThemeByName(theme_name) + if theme == nil { + return 1, fmt.Errorf("No theme named: %s", theme_name) + } + } + if opts.DumpTheme { + code, err := theme.Code() + if err != nil { + return 1, err + } + fmt.Println(code) + } else { + err = theme.SaveInConf(utils.ConfigDir(), opts.ReloadIn, opts.ConfigFileName) + if err != nil { + return 1, err + } + } + return +} + +func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { + if len(args) > 1 { + args = []string{strings.Join(args, ` `)} + } + if len(args) == 1 { + return non_interactive(opts, args[0]) + } + return +} + +func EntryPoint(parent *cli.Command) { + create_cmd(parent, main) +} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 2165949e0..b9775ef05 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -15,6 +15,7 @@ import ( "kitty/tools/cmd/icat" "kitty/tools/cmd/pytest" "kitty/tools/cmd/ssh" + "kitty/tools/cmd/themes" "kitty/tools/cmd/unicode_input" "kitty/tools/cmd/update_self" "kitty/tools/tui" @@ -45,6 +46,8 @@ func KittyToolEntryPoints(root *cli.Command) { ask.EntryPoint(root) // hints hints.EntryPoint(root) + // themes + themes.EntryPoint(root) // __pytest__ pytest.EntryPoint(root) // __hold_till_enter__ diff --git a/tools/themes/collection.go b/tools/themes/collection.go index d221fa02e..b4b89a4de 100644 --- a/tools/themes/collection.go +++ b/tools/themes/collection.go @@ -9,9 +9,6 @@ import ( "fmt" "io" "io/fs" - "kitty/tools/config" - "kitty/tools/utils" - "kitty/tools/utils/style" "net/http" "os" "path" @@ -21,11 +18,304 @@ import ( "strings" "time" + "kitty/tools/cli" + "kitty/tools/config" + "kitty/tools/utils" + "kitty/tools/utils/style" + + "github.com/shirou/gopsutil/v3/process" "golang.org/x/exp/maps" + "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"` @@ -233,6 +523,111 @@ func (self *Theme) load_code() (string, error) { 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 = "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) 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 := 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() @@ -367,7 +762,7 @@ func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) { return nil, err } name_map := make(map[string]*zip.File, len(r.File)) - var themes []ThemeMetadata + var themes []*ThemeMetadata theme_dir := "" for _, file := range r.File { name_map[file.Name] = file @@ -395,7 +790,7 @@ func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) { key := path.Join(theme_dir, theme.Filepath) f := name_map[key] if f != nil { - t := Theme{metadata: &theme, zip_reader: f} + t := Theme{metadata: theme, zip_reader: f} self.name_map[theme.Name] = &t } } @@ -403,7 +798,16 @@ func (self *Themes) add_from_zip_file(zippath string) (io.Closer, error) { } func (self *Themes) ThemeByName(name string) *Theme { - return self.name_map[name] + 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 LoadThemes(cache_age time.Duration) (ans *Themes, closer io.Closer, err error) { @@ -427,3 +831,31 @@ 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) + } + } + } +}