mirror of
https://github.com/charmbracelet/vhs.git
synced 2025-11-08 23:05:05 -06:00
204 lines
5.3 KiB
Go
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
|
|
}
|