cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / themepicker / themepicker.go
themepicker.go 5.5K
1// Package themepicker is File > Settings > Theme: every theme in a2// picker, fuzzy searched by name, narrowed to one kind by a search3// starting with "dark" or "light". The highlighted theme4// is drawn live; Enter keeps it, Esc goes back to the theme there was. It5// knows the UI only through Host, which draws and saves themes.6package themepicker78import (9 "strings"1011 tea "charm.land/bubbletea/v2"1213 "src.rest/cat/012/internal/ui/overlay"14 "src.rest/cat/012/internal/ui/picker"15 "src.rest/cat/012/internal/ui/theme"16)1718// Host is what the theme picker needs of the UI: a picker's host, and19// the themes.20type Host interface {21 picker.Host22 // Current is the theme configured for the terminal's background.23 Current() string24 // ThemesDir is where the user's own themes are.25 ThemesDir() string26 // Preview draws the theme named name; "" goes back to the configured27 // one.28 Preview(name string)29 // Keep closes the picker and makes name the theme, saving it.30 Keep(name string)31 // KeyReleases reports whether the terminal says when a key is let32 // go (the kitty keyboard protocol), so Space can be held to peek.33 KeyReleases() bool34}3536// Picker is the open theme picker.37type Picker struct {38 *picker.Picker39 h Host40 open bool // not yet closed or kept41 shown string // the theme being previewed42 peek bool // Space is held: the list is hidden to show the sheet43}4445// New returns a picker of every theme with the current one highlighted46// and drawn.47func New(h Host) *Picker {48 tp := &Picker{h: h, open: true}49 current := h.Current()50 entries := theme.List(h.ThemesDir())51 items := make([]picker.Item, 0, len(entries))52 sel := 053 for _, e := range entries {54 kind := "light"55 if e.Dark {56 kind = "dark"57 }58 desc := "A " + kind + " color scheme, drawn in its own colors with solid bars"59 switch {60 case e.Name == theme.Terminal:61 kind, desc = "your terminal's colors", "The terminal's own 16-color palette, following its light or dark background"62 case e.Name == theme.HighContrast:63 kind, desc = "high contrast", "White on black or black on white, following the terminal, with WCAG AAA contrast"64 case e.User:65 kind += ", your file"66 }67 if strings.EqualFold(e.Name, current) {68 sel = len(items)69 }70 name := e.Name71 items = append(items, picker.Item{Title: name, Name: len(name), Detail: kind, Desc: desc,72 Pick: func() tea.Cmd {73 tp.open = false74 h.Keep(name)75 return nil76 }})77 }78 tp.Picker = picker.New(closing{h, tp}, "Theme", "Type a name, dark or light", 64, items)79 tp.Action = "keep"80 tp.Narrow = byKind(entries)81 tp.Sel = sel82 tp.preview()83 return tp84}8586// byKind narrows a search starting with the word "dark" or "light" to87// the schemes of that kind (by their meta.isDark), the rest of it then88// matching their names. So "light" lists light schemes, not dark ones89// named like "Bright Lights", and "light sol" finds Solarized Light.90// The terminal's theme is neither kind.91func byKind(entries []theme.Entry) func(string) (string, func(*picker.Item) bool) {92 dark := make(map[string]bool, len(entries))93 for _, e := range entries {94 if e.Name != theme.Terminal {95 dark[e.Name] = e.Dark96 }97 }98 return func(q string) (string, func(*picker.Item) bool) {99 word, rest, _ := strings.Cut(q, " ")100 var want bool101 switch strings.ToLower(word) {102 case "dark":103 want = true104 case "light":105 default:106 return q, nil107 }108 return rest, func(it *picker.Item) bool {109 d, ok := dark[it.Title]110 return ok && d == want111 }112 }113}114115// closing is the host as the inner picker sees it: closing it closes the116// theme picker.117type closing struct {118 Host119 tp *Picker120}121122func (c closing) Close() {123 c.tp.open = false124 c.Host.Close()125}126127// Holding Space, on a terminal that reports key releases, hides the list128// so the whole sheet shows in the highlighted theme; letting go brings129// the list back. It needs an empty search, so a space can still be typed130// into one. Elsewhere Space is typed, as any other key.131132// holdsSpace reports whether k, a press of Space, starts or continues a133// hold rather than typing.134func (tp *Picker) holdsSpace(k tea.KeyPressMsg) bool {135 return k.String() == "space" && tp.h.KeyReleases() && (tp.peek || tp.h.Line().Text() == "")136}137138// Release ends a hold of Space.139func (tp *Picker) Release(k tea.KeyReleaseMsg) tea.Cmd {140 if k.String() == "space" {141 tp.peek = false142 }143 return nil144}145146func (tp *Picker) Layout() []overlay.Box {147 if tp.peek {148 return nil149 }150 return tp.Picker.Layout()151}152153func (tp *Picker) Cursor() (int, int) {154 if tp.peek {155 return -1, 0156 }157 return tp.Picker.Cursor()158}159160func (tp *Picker) Status() (string, string) {161 desc, keys := tp.Picker.Status()162 th := tp.h.Theme()163 switch {164 case tp.peek:165 return "Theme " + tp.shown, th.KeyHints("Space", "let go to go back")166 case tp.h.KeyReleases() && tp.h.Line().Text() == "":167 keys = th.KeyHints("Space", "hold to peek") + " " + keys168 }169 return desc, keys170}171172func (tp *Picker) Key(k tea.KeyPressMsg) tea.Cmd {173 if tp.holdsSpace(k) {174 tp.peek = true175 return nil176 }177 tp.peek = false178 if k.String() == "esc" {179 tp.h.Preview("")180 }181 cmd := tp.Picker.Key(k)182 if tp.open {183 tp.preview()184 }185 return cmd186}187188func (tp *Picker) Mouse(e overlay.MouseEvent) tea.Cmd {189 if e.Box != picker.ID && e.Kind == overlay.MousePress {190 tp.h.Preview("")191 }192 cmd := tp.Picker.Mouse(e)193 if tp.open {194 tp.preview()195 }196 return cmd197}198199func (tp *Picker) Changed() {200 tp.Picker.Changed()201 tp.preview()202}203204// preview draws the highlighted theme.205func (tp *Picker) preview() {206 it := tp.Selected()207 if it == nil || it.Title == tp.shown {208 return209 }210 tp.shown = it.Title211 tp.h.Preview(it.Title)212}