Add themes

This commit is contained in:
Anton Medvedev 2022-04-16 21:50:46 +02:00
parent 49756d7ff4
commit ae80a278b2
6 changed files with 274 additions and 87 deletions

6
go.mod
View File

@ -6,6 +6,9 @@ require (
github.com/charmbracelet/bubbles v0.10.3
github.com/charmbracelet/bubbletea v0.20.0
github.com/charmbracelet/lipgloss v0.5.0
github.com/mattn/go-isatty v0.0.14
github.com/mazznoer/colorgrad v0.8.1
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
github.com/stretchr/testify v1.7.1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)
@ -15,11 +18,10 @@ require (
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mazznoer/csscolorparser v0.1.0 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect

5
go.sum
View File

@ -15,6 +15,7 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@ -24,6 +25,10 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mazznoer/colorgrad v0.8.1 h1:Bw/ks+KujOOg9E6YQvPqSqTLryiFnwliAH5VMZarSTI=
github.com/mazznoer/colorgrad v0.8.1/go.mod h1:xCjvoNkXHJIAPOUMSMrXkFdxTGQqk8zMYS3e5hSLghA=
github.com/mazznoer/csscolorparser v0.1.0 h1:xUf1uzU1r24JleIIb2Kz3bl7vATStxy53gm67yuPP+c=
github.com/mazznoer/csscolorparser v0.1.0/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=

55
main.go
View File

@ -6,7 +6,8 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
"golang.org/x/term"
"os"
"path"
@ -16,30 +17,6 @@ import (
type number = json.Number
var colors = struct {
cursor lipgloss.Style
syntax lipgloss.Style
key lipgloss.Style
null lipgloss.Style
boolean lipgloss.Style
number lipgloss.Style
string lipgloss.Style
preview lipgloss.Style
statusBar lipgloss.Style
search lipgloss.Style
}{
cursor: lipgloss.NewStyle().Reverse(true),
syntax: lipgloss.NewStyle(),
key: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")),
null: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")),
boolean: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")),
number: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")),
string: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2")),
preview: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")),
statusBar: lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")),
search: lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")),
}
func main() {
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
@ -52,6 +29,19 @@ func main() {
panic(err)
}
}
themeId, ok := os.LookupEnv("FX_THEME")
if !ok {
themeId = "1"
}
theme, ok := themes[themeId]
if !ok {
theme = themes["1"]
}
if termenv.ColorProfile() == termenv.Ascii {
theme = themes["0"]
}
filePath := ""
var args []string
var dec *json.Decoder
@ -75,12 +65,13 @@ func main() {
panic(err)
}
if len(args) > 0 {
if args[0] == "--print-code" {
tty := isatty.IsTerminal(os.Stdout.Fd())
if len(args) > 0 || !tty {
if len(args) > 0 && args[0] == "--print-code" {
fmt.Print(generateCode(args[1:]))
return
}
reduce(jsonObject, args)
reduce(jsonObject, args, theme)
return
}
@ -105,10 +96,13 @@ func main() {
canBeExpanded[it.path] = len(it.object.(array)) > 0
}
})
input := textinput.New()
input.Prompt = ""
m := &model{
fileName: path.Base(filePath),
theme: theme,
json: jsonObject,
width: 80,
height: 60,
@ -141,6 +135,7 @@ type model struct {
windowHeight int
footerHeight int
wrap bool
theme Theme
fileName string
json interface{}
@ -394,13 +389,13 @@ func (m *model) View() string {
if m.showHelp {
statusBar := "Press Esc or q to close help."
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)))
statusBar = colors.statusBar.Render(statusBar)
statusBar = m.theme.statusBar(statusBar)
return strings.Join(lines, "\n") + extraLines + "\n" + statusBar
}
statusBar := m.cursorPath() + " "
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
statusBar += m.fileName
statusBar = colors.statusBar.Render(statusBar)
statusBar = m.theme.statusBar(statusBar)
output := strings.Join(lines, "\n") + extraLines + "\n" + statusBar
if m.searchInput.Focused() {
output += "\n/" + m.searchInput.View()

View File

@ -2,7 +2,6 @@ package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"strings"
)
@ -28,21 +27,21 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
switch v.(type) {
case nil:
return []string{merge(m.explode("null", searchValue, colors.null, path, selectableValues))}
return []string{merge(m.explode("null", searchValue, m.theme.null, path, selectableValues))}
case bool:
if v.(bool) {
return []string{merge(m.explode("true", searchValue, colors.boolean, path, selectableValues))}
return []string{merge(m.explode("true", searchValue, m.theme.boolean, path, selectableValues))}
} else {
return []string{merge(m.explode("false", searchValue, colors.boolean, path, selectableValues))}
return []string{merge(m.explode("false", searchValue, m.theme.boolean, path, selectableValues))}
}
case number:
return []string{merge(m.explode(v.(number).String(), searchValue, colors.number, path, selectableValues))}
return []string{merge(m.explode(v.(number).String(), searchValue, m.theme.number, path, selectableValues))}
case string:
line := fmt.Sprintf("%q", v)
chunks := m.explode(line, searchValue, colors.string, path, selectableValues)
chunks := m.explode(line, searchValue, m.theme.string, path, selectableValues)
if m.wrap && keyEndPos+width(line) > m.width {
return wrapLines(chunks, keyEndPos, m.width, subident)
}
@ -66,9 +65,10 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
}
m.connect(subpath, lineNumber)
key := fmt.Sprintf("%q", k)
key = merge(m.explode(key, keyRanges, colors.key, subpath, true))
keyTheme := m.theme.key(i, len(keys))
key = merge(m.explode(key, keyRanges, keyTheme, subpath, true))
value, _ := v.(*dict).get(k)
delim := merge(m.explode(": ", delimRanges, colors.syntax, subpath, false))
delim := merge(m.explode(": ", delimRanges, m.theme.syntax, subpath, false))
keyEndPos := width(ident) + width(key) + width(delim)
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
lines[0] = ident + key + delim + lines[0]
@ -110,20 +110,20 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
func (m *model) preview(v interface{}, path string, selectableValues bool) string {
searchResult := m.highlightIndex[path]
previewStyle := colors.preview
previewStyle := m.theme.preview
if selectableValues && m.cursorPath() == path {
previewStyle = colors.cursor
previewStyle = m.theme.cursor
}
printValue := func(value interface{}) string {
switch value.(type) {
case nil, bool, number:
return previewStyle.Render(fmt.Sprintf("%v", value))
return previewStyle(fmt.Sprintf("%v", value))
case string:
return previewStyle.Render(fmt.Sprintf("%q", value))
return previewStyle(fmt.Sprintf("%q", value))
case *dict:
return previewStyle.Render("{\u2026}")
return previewStyle("{\u2026}")
case array:
return previewStyle.Render("[\u2026]")
return previewStyle("[\u2026]")
}
return "..."
}
@ -134,13 +134,13 @@ func (m *model) preview(v interface{}, path string, selectableValues bool) strin
keys := v.(*dict).keys
for _, k := range keys {
key := fmt.Sprintf("%q", k)
output += previewStyle.Render(key + ": ")
output += previewStyle(key + ": ")
value, _ := v.(*dict).get(k)
output += printValue(value)
break
}
if len(keys) > 1 {
output += previewStyle.Render(", \u2026")
output += previewStyle(", \u2026")
}
output += m.printCloseBracket("}", searchResult, path, selectableValues)
return output
@ -153,7 +153,7 @@ func (m *model) preview(v interface{}, path string, selectableValues bool) strin
break
}
if len(slice) > 1 {
output += previewStyle.Render(", \u2026")
output += previewStyle(", \u2026")
}
output += m.printCloseBracket("]", searchResult, path, selectableValues)
return output
@ -189,67 +189,67 @@ func wrapLines(chunks []withStyle, keyEndPos, mWidth int, subident string) []str
}
func (w withStyle) Render(s string) string {
return w.style.Render(s)
return w.style(s)
}
func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
if selectableValues && m.cursorPath() == path {
return colors.cursor.Render(line)
return m.theme.cursor(line)
}
if s != nil && s.openBracket != nil {
if s.openBracket.parent.index == m.searchResultsCursor {
return colors.cursor.Render(line)
return m.theme.cursor(line)
} else {
return colors.search.Render(line)
return m.theme.search(line)
}
} else {
return colors.syntax.Render(line)
return m.theme.syntax(line)
}
}
func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
if selectableValues && m.cursorPath() == path {
return colors.cursor.Render(line)
return m.theme.cursor(line)
}
if s != nil && s.closeBracket != nil {
if s.closeBracket.parent.index == m.searchResultsCursor {
return colors.cursor.Render(line)
return m.theme.cursor(line)
} else {
return colors.search.Render(line)
return m.theme.search(line)
}
} else {
return colors.syntax.Render(line)
return m.theme.syntax(line)
}
}
func (m *model) printComma(line string, s *rangeGroup) string {
if s != nil && s.comma != nil {
if s.comma.parent.index == m.searchResultsCursor {
return colors.cursor.Render(line)
return m.theme.cursor(line)
} else {
return colors.search.Render(line)
return m.theme.search(line)
}
} else {
return colors.syntax.Render(line)
return m.theme.syntax(line)
}
}
type withStyle struct {
value string
style lipgloss.Style
style Color
}
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle lipgloss.Style, path string, selectable bool) []withStyle {
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle Color, path string, selectable bool) []withStyle {
if selectable && m.cursorPath() == path && m.showCursor {
return []withStyle{{line, colors.cursor}}
return []withStyle{{line, m.theme.cursor}}
}
out := make([]withStyle, 0, 1)
pos := 0
for _, r := range highlightRanges {
style := colors.search
style := m.theme.search
if r.parent.index == m.searchResultsCursor {
style = colors.cursor
style = m.theme.cursor
}
out = append(out, withStyle{
value: line[pos:r.start],

View File

@ -104,7 +104,7 @@ func fold(s []string) string {
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
}
func reduce(object interface{}, args []string) {
func reduce(object interface{}, args []string, theme Theme) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("node", "-e", generateCode(args))
cmd.Env = os.Environ()
@ -121,7 +121,7 @@ func reduce(object interface{}, args []string) {
if str, ok := jsonObject.(string); ok {
fmt.Println(str)
} else {
fmt.Println(prettyPrint(jsonObject, 1))
fmt.Println(prettyPrint(jsonObject, 1, theme))
}
} else {
_, _ = fmt.Fprint(os.Stderr, stderr.String())
@ -137,54 +137,54 @@ func reduce(object interface{}, args []string) {
}
}
func prettyPrint(v interface{}, level int) string {
func prettyPrint(v interface{}, level int, theme Theme) string {
ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1)
switch v.(type) {
case nil:
return colors.null.Render("null")
return theme.null("null")
case bool:
if v.(bool) {
return colors.boolean.Render("true")
return theme.boolean("true")
} else {
return colors.boolean.Render("false")
return theme.boolean("false")
}
case number:
return colors.number.Render(v.(number).String())
return theme.number(v.(number).String())
case string:
return colors.string.Render(fmt.Sprintf("%q", v))
return theme.string(fmt.Sprintf("%q", v))
case *dict:
keys := v.(*dict).keys
if len(keys) == 0 {
return colors.syntax.Render("{}")
return theme.syntax("{}")
}
output := colors.syntax.Render("{\n")
output := theme.syntax("{")
output += "\n"
for i, k := range keys {
key := colors.key.Render(fmt.Sprintf("%q", k))
key := theme.key(i, len(keys))(fmt.Sprintf("%q", k))
value, _ := v.(*dict).get(k)
delim := ": "
line := ident + key + delim + prettyPrint(value, level+1)
delim := theme.syntax(": ")
line := ident + key + delim + prettyPrint(value, level+1, theme)
if i < len(keys)-1 {
line += ",\n"
} else {
line += "\n"
line += theme.syntax(",")
}
line += "\n"
output += line
}
return output + subident + colors.syntax.Render("}")
return output + subident + theme.syntax("}")
case array:
slice := v.(array)
if len(slice) == 0 {
return colors.syntax.Render("[]")
return theme.syntax("[]")
}
output := colors.syntax.Render("[\n")
output := theme.syntax("[\n")
for i, value := range v.(array) {
line := ident + prettyPrint(value, level+1)
line := ident + prettyPrint(value, level+1, theme)
if i < len(slice)-1 {
line += ",\n"
} else {
@ -192,7 +192,7 @@ func prettyPrint(v interface{}, level int) string {
}
output += line
}
return output + subident + colors.syntax.Render("]")
return output + subident + theme.syntax("]")
default:
return "unknown type"

185
theme.go Normal file
View File

@ -0,0 +1,185 @@
package main
import (
"github.com/charmbracelet/lipgloss"
"github.com/mazznoer/colorgrad"
"strings"
)
type Theme struct {
cursor Color
syntax Color
preview Color
statusBar Color
search Color
key func(i, len int) Color
string Color
null Color
boolean Color
number Color
}
type Color func(s string) string
func fg(color string) Color {
return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render
}
func boldFg(color string) Color {
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render
}
var (
defaultCursor = lipgloss.NewStyle().Reverse(true).Render
defaultPreview = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")).Render
defaultStatusBar = lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render
defaultSearch = lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render
defaultNull = fg("8")
)
var themes = map[string]Theme{
"0": {
cursor: defaultCursor,
syntax: noColor,
preview: noColor,
statusBar: noColor,
search: defaultSearch,
key: func(_, _ int) Color { return noColor },
string: noColor,
null: noColor,
boolean: noColor,
number: noColor,
},
"1": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return boldFg("4") },
string: boldFg("2"),
null: defaultNull,
boolean: boldFg("3"),
number: boldFg("6"),
},
"2": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return fg("#00F5D4") },
string: fg("#00BBF9"),
null: defaultNull,
boolean: fg("#F15BB5"),
number: fg("#9B5DE5"),
},
"3": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return fg("#faf0ca") },
string: fg("#f4d35e"),
null: defaultNull,
boolean: fg("#ee964b"),
number: fg("#ee964b"),
},
"4": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return fg("#4D96FF") },
string: fg("#6BCB77"),
null: defaultNull,
boolean: fg("#FF6B6B"),
number: fg("#FFD93D"),
},
"5": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return boldFg("42") },
string: boldFg("213"),
null: defaultNull,
boolean: boldFg("201"),
number: boldFg("201"),
},
"6": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return gradient("rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)") },
string: fg("195"),
null: defaultNull,
boolean: fg("195"),
number: fg("195"),
},
"7": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: func(_, _ int) Color { return gradient("rgb(123,216,96)", "rgb(255,255,255)") },
string: noColor,
null: defaultNull,
boolean: noColor,
number: noColor,
},
"8": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: gradientKeys("#ff0000", "#ff8700", "#ffd300", "#deff0a", "#a1ff0a", "#0aff99", "#0aefff", "#147df5", "#580aff", "#be0aff"),
string: noColor,
null: defaultNull,
boolean: noColor,
number: noColor,
},
"9": {
cursor: defaultCursor,
syntax: noColor,
preview: defaultPreview,
statusBar: defaultStatusBar,
search: defaultSearch,
key: gradientKeys("rgb(34,126,34)", "rgb(168,251,60)"),
string: gradient("rgb(34,126,34)", "rgb(168,251,60)"),
null: defaultNull,
boolean: noColor,
number: noColor,
},
}
func noColor(s string) string {
return s
}
func gradient(colors ...string) Color {
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
return func(s string) string {
runes := []rune(s)
colors := grad.ColorfulColors(uint(len(runes)))
var out strings.Builder
for i, r := range runes {
style := lipgloss.NewStyle().Foreground(lipgloss.Color(colors[i].Hex()))
out.WriteString(style.Render(string(r)))
}
return out.String()
}
}
func gradientKeys(colors ...string) func(i, len int) Color {
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
return func(i, len int) Color {
return lipgloss.NewStyle().Foreground(lipgloss.Color(grad.At(float64(i) / float64(len)).Hex())).Render
}
}