vhs/themes.go
Carlos Alexandro Becker ce7286f30c
fix: lint issues
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-03-12 11:03:49 -03:00

204 lines
5.3 KiB
Go

// Package main theme.go contains the information about a terminal theme.
// It stores the 16 base colors as well as the background and foreground colors
// of the terminal theme.
//
// It can be changed through the Set command.
//
// Set Theme {"background": "#171717"}
// Set Theme "Catppuccin Mocha"
//
//go:generate make all
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/charmbracelet/glamour/ansi"
_ "embed"
"github.com/agnivade/levenshtein"
)
// Theme is a terminal theme for xterm.js
// It is used for marshalling between the xterm.js readable json format and a
// valid go struct.
// https://xtermjs.org/docs/api/terminal/interfaces/itheme/
type Theme struct {
Name string `json:"name"`
Background string `json:"background"`
Foreground string `json:"foreground"`
Selection string `json:"selection"`
Cursor string `json:"cursor"`
CursorAccent string `json:"cursorAccent"`
Black string `json:"black"`
BrightBlack string `json:"brightBlack"`
Red string `json:"red"`
BrightRed string `json:"brightRed"`
Green string `json:"green"`
BrightGreen string `json:"brightGreen"`
Yellow string `json:"yellow"`
BrightYellow string `json:"brightYellow"`
Blue string `json:"blue"`
BrightBlue string `json:"brightBlue"`
Magenta string `json:"magenta"`
BrightMagenta string `json:"brightMagenta"`
Cyan string `json:"cyan"`
BrightCyan string `json:"brightCyan"`
White string `json:"white"`
BrightWhite string `json:"brightWhite"`
}
func (t Theme) String() string {
ts, err := json.Marshal(t)
if err != nil {
dts, _ := json.Marshal(DefaultTheme)
return string(dts)
}
return string(ts)
}
// DefaultTheme is the default theme to use for recording demos and
// screenshots.
//
// Taken from https://github.com/meowgorithm/dotfiles.
var DefaultTheme = Theme{
Background: Background,
Foreground: Foreground,
Cursor: Foreground,
CursorAccent: Background,
Black: Black,
BrightBlack: BrightBlack,
Red: Red,
BrightRed: BrightRed,
Green: Green,
BrightGreen: BrightGreen,
Yellow: Yellow,
BrightYellow: BrightYellow,
Blue: Blue,
BrightBlue: BrightBlue,
Magenta: Magenta,
BrightMagenta: BrightMagenta,
Cyan: Cyan,
BrightCyan: BrightCyan,
White: White,
BrightWhite: BrightWhite,
}
const margin = 2
// GlamourTheme is the theme for printing out the manual page ('vhs man').
var GlamourTheme = ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockPrefix: "\n",
BlockSuffix: "\n",
},
Margin: uintPtr(margin),
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
Color: stringPtr("99"),
Bold: boolPtr(true),
},
},
Item: ansi.StylePrimitive{Prefix: "· "},
Emph: ansi.StylePrimitive{Color: stringPtr(BrightBlack)},
Strong: ansi.StylePrimitive{Bold: boolPtr(true)},
Link: ansi.StylePrimitive{Color: stringPtr("42"), Underline: boolPtr(true)},
LinkText: ansi.StylePrimitive{Color: stringPtr("207")},
Code: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Color: stringPtr("204")}},
}
func boolPtr(b bool) *bool { return &b }
func stringPtr(s string) *string { return &s }
func uintPtr(u uint) *uint { return &u }
//go:embed themes.json
var themesBts []byte
// ThemeNotFoundError is returned when a requested theme is not found.
type ThemeNotFoundError struct {
Theme string
Suggestions []string
}
func (e ThemeNotFoundError) Error() string {
if len(e.Suggestions) == 0 {
return fmt.Sprintf("invalid `Set Theme %q`: theme does not exist", e.Theme)
}
return fmt.Sprintf("invalid `Set Theme %q`: did you mean %q",
e.Theme,
strings.Join(e.Suggestions, ", "),
)
}
// sortedThemeNames returns the names of the themes, sorted.
func sortedThemeNames() ([]string, error) {
var keys []string
for _, bts := range [][]byte{themesBts} {
themes, err := parseThemes(bts)
if err != nil {
return nil, err
}
for _, theme := range themes {
keys = append(keys, theme.Name)
}
}
sort.Slice(keys, func(i, j int) bool {
return strings.ToLower(keys[i]) < strings.ToLower(keys[j])
})
return keys, nil
}
const distance = 2
// findTheme return the given theme, if it exists.
func findTheme(name string) (Theme, error) {
for _, bts := range [][]byte{themesBts} {
themes, err := parseThemes(bts)
if err != nil {
return DefaultTheme, err
}
for _, theme := range themes {
if theme.Name == name {
return theme, nil
}
}
}
// not found, lets find similar themes!
keys, err := sortedThemeNames()
if err != nil {
return DefaultTheme, err
}
suggestions := []string{}
lname := strings.ToLower(name)
for _, theme := range keys {
ltheme := strings.ToLower(theme)
levenshteinDistance := levenshtein.ComputeDistance(lname, ltheme)
suggestByLevenshtein := levenshteinDistance <= distance
suggestByPrefix := strings.HasPrefix(lname, ltheme)
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, theme)
}
}
return DefaultTheme, ThemeNotFoundError{name, suggestions}
}
func parseThemes(bts []byte) ([]Theme, error) {
var themes []Theme
if err := json.Unmarshal(bts, &themes); err != nil {
return nil, fmt.Errorf("could not load themes.json: %w", err)
}
return themes, nil
}