123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package themes
- import (
- "fmt"
- "io"
- "maps"
- "path/filepath"
- "regexp"
- "slices"
- "strings"
- "time"
- "kitty/tools/config"
- "kitty/tools/themes"
- "kitty/tools/tui/loop"
- "kitty/tools/tui/readline"
- "kitty/tools/utils"
- "kitty/tools/wcswidth"
- )
- var _ = fmt.Print
- type State int
- const (
- FETCHING State = iota
- BROWSING
- SEARCHING
- ACCEPTING
- )
- const SEPARATOR = "║"
- type CachedData struct {
- Recent []string `json:"recent"`
- Category string `json:"category"`
- }
- type fetch_data struct {
- themes *themes.Themes
- err error
- closer io.Closer
- }
- var category_filters = map[string]func(*themes.Theme) bool{
- "all": func(*themes.Theme) bool { return true },
- "dark": func(t *themes.Theme) bool { return t.IsDark() },
- "light": func(t *themes.Theme) bool { return !t.IsDark() },
- "user": func(t *themes.Theme) bool { return t.IsUserDefined() },
- }
- func recent_filter(items []string) func(*themes.Theme) bool {
- allowed := utils.NewSetWithItems(items...)
- return func(t *themes.Theme) bool {
- return allowed.Has(t.Name())
- }
- }
- type handler struct {
- lp *loop.Loop
- opts *Options
- cached_data *CachedData
- state State
- fetch_result chan fetch_data
- all_themes *themes.Themes
- themes_closer io.Closer
- themes_list *ThemesList
- category_filters map[string]func(*themes.Theme) bool
- colors_set_once bool
- tabs []string
- rl *readline.Readline
- }
- // fetching {{{
- func (self *handler) fetch_themes() {
- r := fetch_data{}
- r.themes, r.closer, r.err = themes.LoadThemes(time.Duration(self.opts.CacheAge * float64(time.Hour*24)))
- self.lp.WakeupMainThread()
- self.fetch_result <- r
- }
- func (self *handler) on_fetching_key_event(ev *loop.KeyEvent) error {
- if ev.MatchesPressOrRepeat("esc") {
- self.lp.Quit(0)
- ev.Handled = true
- }
- return nil
- }
- func (self *handler) on_wakeup() error {
- r := <-self.fetch_result
- if r.err != nil {
- return r.err
- }
- self.state = BROWSING
- self.all_themes = r.themes
- self.themes_closer = r.closer
- self.redraw_after_category_change()
- return nil
- }
- func (self *handler) draw_fetching_screen() {
- self.lp.Println("Downloading themes from repository, please wait...")
- }
- // }}}
- func (self *handler) finalize() {
- t := self.themes_closer
- if t != nil {
- t.Close()
- self.themes_closer = nil
- }
- }
- func (self *handler) initialize() {
- self.tabs = strings.Split("all dark light recent user", " ")
- self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
- self.themes_list = &ThemesList{}
- self.fetch_result = make(chan fetch_data)
- self.category_filters = make(map[string]func(*themes.Theme) bool, len(category_filters)+1)
- maps.Copy(self.category_filters, category_filters)
- self.category_filters["recent"] = recent_filter(self.cached_data.Recent)
- go self.fetch_themes()
- self.draw_screen()
- }
- func (self *handler) enforce_cursor_state() {
- self.lp.SetCursorVisible(self.state == FETCHING)
- }
- func (self *handler) draw_screen() {
- self.lp.StartAtomicUpdate()
- defer self.lp.EndAtomicUpdate()
- self.lp.ClearScreen()
- self.enforce_cursor_state()
- switch self.state {
- case FETCHING:
- self.draw_fetching_screen()
- case BROWSING, SEARCHING:
- self.draw_browsing_screen()
- case ACCEPTING:
- self.draw_accepting_screen()
- }
- }
- func (self *handler) current_category() string {
- ans := self.cached_data.Category
- if self.category_filters[ans] == nil {
- ans = "all"
- }
- return ans
- }
- func (self *handler) set_current_category(category string) {
- if self.category_filters[category] == nil {
- category = "all"
- }
- self.cached_data.Category = category
- }
- func ReadKittyColorSettings() map[string]string {
- settings := make(map[string]string, 512)
- handle_line := func(key, val string) error {
- if themes.AllColorSettingNames[key] {
- settings[key] = val
- }
- return nil
- }
- cp := config.ConfigParser{LineHandler: handle_line}
- cp.ParseFiles(filepath.Join(utils.ConfigDir(), "kitty.conf"))
- return settings
- }
- func (self *handler) set_colors_to_current_theme() bool {
- if self.themes_list == nil && self.colors_set_once {
- return false
- }
- self.colors_set_once = true
- if self.themes_list != nil {
- t := self.themes_list.CurrentTheme()
- if t != nil {
- raw, err := t.AsEscapeCodes()
- if err == nil {
- self.lp.QueueWriteString(raw)
- return true
- }
- }
- }
- self.lp.QueueWriteString(themes.ColorSettingsAsEscapeCodes(ReadKittyColorSettings()))
- return true
- }
- func (self *handler) redraw_after_category_change() {
- self.themes_list.UpdateThemes(self.all_themes.Filtered(self.category_filters[self.current_category()]))
- self.set_colors_to_current_theme()
- self.draw_screen()
- }
- func (self *handler) on_key_event(ev *loop.KeyEvent) error {
- switch self.state {
- case FETCHING:
- return self.on_fetching_key_event(ev)
- case BROWSING:
- return self.on_browsing_key_event(ev)
- case SEARCHING:
- return self.on_searching_key_event(ev)
- case ACCEPTING:
- return self.on_accepting_key_event(ev)
- }
- return nil
- }
- // browsing ... {{{
- func (self *handler) next_category(delta int) {
- idx := slices.Index(self.tabs, self.current_category()) + delta + len(self.tabs)
- self.set_current_category(self.tabs[idx%len(self.tabs)])
- self.redraw_after_category_change()
- }
- func (self *handler) next(delta int, allow_wrapping bool) {
- if self.themes_list.Next(delta, allow_wrapping) {
- self.set_colors_to_current_theme()
- self.draw_screen()
- } else {
- self.lp.Beep()
- }
- }
- func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
- if ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("q") {
- self.lp.Quit(0)
- ev.Handled = true
- return nil
- }
- for _, cat := range self.tabs {
- if ev.MatchesPressOrRepeat(cat[0:1]) || ev.MatchesPressOrRepeat("alt+"+cat[0:1]) {
- ev.Handled = true
- if cat != self.current_category() {
- self.set_current_category(cat)
- self.redraw_after_category_change()
- return nil
- }
- }
- }
- if ev.MatchesPressOrRepeat("left") || ev.MatchesPressOrRepeat("shift+tab") {
- self.next_category(-1)
- ev.Handled = true
- return nil
- }
- if ev.MatchesPressOrRepeat("right") || ev.MatchesPressOrRepeat("tab") {
- self.next_category(1)
- ev.Handled = true
- return nil
- }
- if ev.MatchesPressOrRepeat("j") || ev.MatchesPressOrRepeat("down") {
- self.next(1, true)
- ev.Handled = true
- return nil
- }
- if ev.MatchesPressOrRepeat("k") || ev.MatchesPressOrRepeat("up") {
- self.next(-1, true)
- ev.Handled = true
- return nil
- }
- if ev.MatchesPressOrRepeat("page_down") {
- ev.Handled = true
- sz, err := self.lp.ScreenSize()
- if err == nil {
- self.next(int(sz.HeightCells)-3, false)
- }
- return nil
- }
- if ev.MatchesPressOrRepeat("page_up") {
- ev.Handled = true
- sz, err := self.lp.ScreenSize()
- if err == nil {
- self.next(3-int(sz.HeightCells), false)
- }
- return nil
- }
- if ev.MatchesPressOrRepeat("s") || ev.MatchesPressOrRepeat("/") {
- ev.Handled = true
- self.start_search()
- return nil
- }
- if ev.MatchesPressOrRepeat("c") || ev.MatchesPressOrRepeat("enter") {
- ev.Handled = true
- if self.themes_list == nil || self.themes_list.Len() == 0 {
- self.lp.Beep()
- } else {
- self.state = ACCEPTING
- self.draw_screen()
- }
- }
- return nil
- }
- func (self *handler) start_search() {
- self.state = SEARCHING
- self.rl.SetText(self.themes_list.current_search)
- self.draw_screen()
- }
- func (self *handler) draw_browsing_screen() {
- self.draw_tab_bar()
- sz, err := self.lp.ScreenSize()
- if err != nil {
- return
- }
- num_rows := int(sz.HeightCells) - 2
- mw := self.themes_list.max_width + 1
- green_fg, _, _ := strings.Cut(self.lp.SprintStyled("fg=green", "|"), "|")
- for _, l := range self.themes_list.Lines(num_rows) {
- line := l.text
- if l.is_current {
- line = strings.ReplaceAll(line, themes.MARK_AFTER, green_fg)
- self.lp.PrintStyled("fg=green", ">")
- self.lp.PrintStyled("fg=green bold", line)
- } else {
- self.lp.PrintStyled("fg=green", " ")
- self.lp.QueueWriteString(line)
- }
- self.lp.MoveCursorHorizontally(mw - l.width)
- self.lp.Println(SEPARATOR)
- num_rows--
- }
- for ; num_rows > 0; num_rows-- {
- self.lp.MoveCursorHorizontally(mw + 1)
- self.lp.Println(SEPARATOR)
- }
- if self.themes_list != nil && self.themes_list.Len() > 0 {
- self.draw_theme_demo()
- }
- if self.state == BROWSING {
- self.draw_bottom_bar()
- } else {
- self.draw_search_bar()
- }
- }
- func (self *handler) draw_bottom_bar() {
- sz, err := self.lp.ScreenSize()
- if err != nil {
- return
- }
- self.lp.MoveCursorTo(1, int(sz.HeightCells))
- self.lp.PrintStyled("reverse", strings.Repeat(" ", int(sz.WidthCells)))
- self.lp.QueueWriteString("\r")
- draw_tab := func(t, sc string) {
- text := self.mark_shortcut(utils.Capitalize(t), sc)
- self.lp.PrintStyled("reverse", " "+text+" ")
- }
- draw_tab("search (/)", "s")
- draw_tab("accept (⏎)", "c")
- self.lp.QueueWriteString("\x1b[m")
- }
- func (self *handler) draw_search_bar() {
- sz, err := self.lp.ScreenSize()
- if err != nil {
- return
- }
- self.lp.MoveCursorTo(1, int(sz.HeightCells))
- self.lp.ClearToEndOfLine()
- self.rl.RedrawNonAtomic()
- }
- func (self *handler) mark_shortcut(text, acc string) string {
- acc_idx := strings.Index(strings.ToLower(text), strings.ToLower(acc))
- return text[:acc_idx] + self.lp.SprintStyled("underline bold", text[acc_idx:acc_idx+1]) + text[acc_idx+1:]
- }
- func (self *handler) draw_tab_bar() {
- sz, err := self.lp.ScreenSize()
- if err != nil {
- return
- }
- self.lp.PrintStyled("reverse", strings.Repeat(` `, int(sz.WidthCells)))
- self.lp.QueueWriteString("\r")
- cc := self.current_category()
- draw_tab := func(text, name, acc string) {
- is_active := name == cc
- if is_active {
- text := self.lp.SprintStyled("italic", fmt.Sprintf("%s #%d", text, self.themes_list.Len()))
- self.lp.Printf(" %s ", text)
- } else {
- text = self.mark_shortcut(text, acc)
- self.lp.PrintStyled("reverse", " "+text+" ")
- }
- }
- for _, title := range self.tabs {
- draw_tab(utils.Capitalize(title), title, string([]rune(title)[0]))
- }
- self.lp.Println("\x1b[m")
- }
- func center_string(x string, width int) string {
- l := wcswidth.Stringwidth(x)
- spaces := int(float64(width-l) / 2)
- return strings.Repeat(" ", utils.Max(0, spaces)) + x + strings.Repeat(" ", utils.Max(0, width-(spaces+l)))
- }
- func (self *handler) draw_theme_demo() {
- ssz, err := self.lp.ScreenSize()
- if err != nil {
- return
- }
- theme := self.themes_list.CurrentTheme()
- if theme == nil {
- return
- }
- xstart := self.themes_list.max_width + 3
- sz := int(ssz.WidthCells) - xstart
- if sz < 20 {
- return
- }
- sz--
- y := 0
- colors := strings.Split(`black red green yellow blue magenta cyan white`, ` `)
- trunc := sz/8 - 1
- pat := regexp.MustCompile(`\s+`)
- next_line := func() {
- self.lp.QueueWriteString("\r")
- y++
- self.lp.MoveCursorTo(xstart, y+1)
- self.lp.QueueWriteString(SEPARATOR + " ")
- }
- write_para := func(text string) {
- text = pat.ReplaceAllLiteralString(text, " ")
- for text != "" {
- t, sp := wcswidth.TruncateToVisualLengthWithWidth(text, sz)
- self.lp.QueueWriteString(t)
- next_line()
- text = text[sp:]
- }
- }
- write_colors := func(bg string) {
- for _, intense := range []bool{false, true} {
- buf := strings.Builder{}
- buf.Grow(1024)
- for _, c := range colors {
- s := c
- if intense {
- s = "bright-" + s
- }
- sTrunc := s
- if len(sTrunc) > trunc {
- sTrunc = sTrunc[:trunc]
- }
- buf.WriteString(self.lp.SprintStyled("fg="+s, sTrunc))
- buf.WriteString(" ")
- }
- text := strings.TrimSpace(buf.String())
- if bg == "" {
- self.lp.QueueWriteString(text)
- } else {
- s := bg
- if intense {
- s = "bright-" + s
- }
- self.lp.PrintStyled("bg="+s, text)
- }
- next_line()
- }
- next_line()
- }
- self.lp.MoveCursorTo(1, 1)
- next_line()
- self.lp.PrintStyled("fg=green bold", center_string(theme.Name(), sz))
- next_line()
- if theme.Author() != "" {
- self.lp.PrintStyled("italic", center_string(theme.Author(), sz))
- next_line()
- }
- if theme.Blurb() != "" {
- next_line()
- write_para(theme.Blurb())
- next_line()
- }
- write_colors("")
- for _, bg := range colors {
- write_colors(bg)
- }
- }
- // }}}
- // accepting {{{
- func (self *handler) on_accepting_key_event(ev *loop.KeyEvent) error {
- if ev.MatchesPressOrRepeat("q") || ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("shift+q") {
- ev.Handled = true
- self.lp.Quit(0)
- return nil
- }
- if ev.MatchesPressOrRepeat("a") || ev.MatchesPressOrRepeat("shift+a") {
- ev.Handled = true
- self.state = BROWSING
- self.draw_screen()
- return nil
- }
- if ev.MatchesPressOrRepeat("p") || ev.MatchesPressOrRepeat("shift+p") {
- ev.Handled = true
- self.themes_list.CurrentTheme().SaveInDir(utils.ConfigDir())
- self.update_recent()
- self.lp.Quit(0)
- return nil
- }
- if ev.MatchesPressOrRepeat("m") || ev.MatchesPressOrRepeat("shift+m") {
- ev.Handled = true
- self.themes_list.CurrentTheme().SaveInConf(utils.ConfigDir(), self.opts.ReloadIn, self.opts.ConfigFileName)
- self.update_recent()
- self.lp.Quit(0)
- return nil
- }
- return nil
- }
- func (self *handler) update_recent() {
- if self.themes_list != nil {
- recent := slices.Clone(self.cached_data.Recent)
- name := self.themes_list.CurrentTheme().Name()
- recent = utils.Remove(recent, name)
- recent = append([]string{name}, recent...)
- if len(recent) > 20 {
- recent = recent[:20]
- }
- self.cached_data.Recent = recent
- }
- }
- func (self *handler) draw_accepting_screen() {
- name := self.themes_list.CurrentTheme().Name()
- name = self.lp.SprintStyled("fg=green bold", name)
- kc := self.lp.SprintStyled("italic", self.opts.ConfigFileName)
- ac := func(x string) string {
- return self.lp.SprintStyled("fg=red", x)
- }
- self.lp.AllowLineWrapping(true)
- defer self.lp.AllowLineWrapping(false)
- self.lp.Printf(`You have chosen the %s theme`, name)
- self.lp.Println()
- self.lp.Println()
- self.lp.Println(`What would you like to do?`)
- self.lp.Println()
- self.lp.Printf(` %sodify %s to load %s`, ac("M"), kc, name)
- self.lp.Println()
- self.lp.Println()
- self.lp.Printf(` %slace the theme file in %s but do not modify %s`, ac("P"), utils.ConfigDir(), kc)
- self.lp.Println()
- self.lp.Println()
- self.lp.Printf(` %sbort and return to list of themes`, ac("A"))
- self.lp.Println()
- self.lp.Println()
- self.lp.Printf(` %suit`, ac("Q"))
- self.lp.Println()
- }
- // }}}
- // searching {{{
- func (self *handler) update_search() {
- text := self.rl.AllText()
- if self.themes_list.UpdateSearch(text) {
- self.set_colors_to_current_theme()
- self.draw_screen()
- } else {
- self.draw_search_bar()
- }
- }
- func (self *handler) on_text(text string, a, b bool) error {
- if self.state == SEARCHING {
- err := self.rl.OnText(text, a, b)
- if err != nil {
- return err
- }
- self.update_search()
- }
- return nil
- }
- func (self *handler) on_searching_key_event(ev *loop.KeyEvent) error {
- if ev.MatchesPressOrRepeat("enter") {
- ev.Handled = true
- self.state = BROWSING
- self.draw_bottom_bar()
- return nil
- }
- if ev.MatchesPressOrRepeat("esc") {
- ev.Handled = true
- self.state = BROWSING
- self.themes_list.UpdateSearch("")
- self.set_colors_to_current_theme()
- self.draw_screen()
- return nil
- }
- err := self.rl.OnKeyEvent(ev)
- if err != nil {
- return err
- }
- if ev.Handled {
- self.update_search()
- }
- return nil
- }
- // }}}
|