Move part of the code to pkg

This commit is contained in:
Anton Medvedev 2022-04-17 22:57:12 +02:00
parent aa0964a12e
commit bf166fafda
21 changed files with 552 additions and 513 deletions

26
dict.go
View File

@ -1,26 +0,0 @@
package main
type dict struct {
keys []string
values map[string]interface{}
}
func newDict() *dict {
return &dict{
keys: make([]string, 0),
values: make(map[string]interface{}),
}
}
func (d *dict) get(key string) (interface{}, bool) {
val, exists := d.values[key]
return val, exists
}
func (d *dict) set(key string, value interface{}) {
_, exists := d.values[key]
if !exists {
d.keys = append(d.keys, key)
}
d.values[key] = value
}

66
main.go
View File

@ -3,21 +3,23 @@ package main
import (
"encoding/json"
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/antonmedv/fx/pkg/reducer"
. "github.com/antonmedv/fx/pkg/theme"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
"golang.org/x/term"
"io/fs"
"os"
"path"
"io/fs"
"runtime/pprof"
"strings"
)
type number = json.Number
func main() {
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
@ -35,12 +37,12 @@ func main() {
if !ok {
themeId = "1"
}
theme, ok := themes[themeId]
theme, ok := Themes[themeId]
if !ok {
theme = themes["1"]
theme = Themes["1"]
}
if termenv.ColorProfile() == termenv.Ascii {
theme = themes["0"]
theme = Themes["0"]
}
filePath := ""
@ -71,7 +73,7 @@ func main() {
os.Exit(1)
}
dec.UseNumber()
jsonObject, err := parse(dec)
jsonObject, err := Parse(dec)
if err != nil {
panic(err)
}
@ -79,17 +81,17 @@ func main() {
tty := isatty.IsTerminal(os.Stdout.Fd())
if len(args) > 0 || !tty {
if len(args) > 0 && args[0] == "--print-code" {
fmt.Print(generateCode(args[1:]))
fmt.Print(reducer.GenerateCode(args[1:]))
return
}
reduce(jsonObject, args, theme)
reducer.Reduce(jsonObject, args, theme)
return
}
expand := map[string]bool{
"": true,
}
if array, ok := jsonObject.(array); ok {
if array, ok := jsonObject.(Array); ok {
for i := range array {
expand[accessor("", i)] = true
}
@ -97,14 +99,14 @@ func main() {
parents := map[string]string{}
children := map[string][]string{}
canBeExpanded := map[string]bool{}
dfs(jsonObject, func(it iterator) {
parents[it.path] = it.parent
children[it.parent] = append(children[it.parent], it.path)
switch it.object.(type) {
case *dict:
canBeExpanded[it.path] = len(it.object.(*dict).keys) > 0
case array:
canBeExpanded[it.path] = len(it.object.(array)) > 0
Dfs(jsonObject, func(it Iterator) {
parents[it.Path] = it.Parent
children[it.Parent] = append(children[it.Parent], it.Path)
switch it.Object.(type) {
case *Dict:
canBeExpanded[it.Path] = len(it.Object.(*Dict).Keys) > 0
case Array:
canBeExpanded[it.Path] = len(it.Object.(Array)) > 0
}
})
@ -152,7 +154,7 @@ type model struct {
json interface{}
lines []string
mouseWheelDelta int // number of lines the mouse wheel will scroll
mouseWheelDelta int // Number of lines the mouse wheel will scroll
offset int // offset is the vertical scroll position
keyMap KeyMap
@ -161,9 +163,9 @@ type model struct {
expandedPaths map[string]bool // set of expanded paths
canBeExpanded map[string]bool // set of path => can be expanded (i.e. dict or array)
paths []string // array of paths on screen
pathToLineNumber map[string]int // map of path => line number
pathToLineNumber map[string]int // map of path => line Number
pathToIndex map[string]int // map of path => index in m.paths
lineNumberToPath map[int]string // map of line number => path
lineNumberToPath map[int]string // map of line Number => path
parents map[string]string // map of subpath => parent path
children map[string][]string // map of path => child paths
nextSiblings, prevSiblings map[string]string // map of path => sibling path
@ -342,10 +344,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.render()
case key.Matches(msg, m.keyMap.ExpandAll):
dfs(m.json, func(it iterator) {
switch it.object.(type) {
case *dict, array:
m.expandedPaths[it.path] = true
Dfs(m.json, func(it Iterator) {
switch it.Object.(type) {
case *Dict, Array:
m.expandedPaths[it.Path] = true
}
})
m.render()
@ -400,13 +402,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 = m.theme.statusBar(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 = m.theme.statusBar(statusBar)
statusBar = m.theme.StatusBar(statusBar)
output := strings.Join(lines, "\n") + extraLines + "\n" + statusBar
if m.searchInput.Focused() {
output += "\n/" + m.searchInput.View()
@ -506,22 +508,22 @@ func (m *model) collapseRecursively(path string) {
func (m *model) collectSiblings(v interface{}, path string) {
switch v.(type) {
case *dict:
case *Dict:
prev := ""
for _, k := range v.(*dict).keys {
for _, k := range v.(*Dict).Keys {
subpath := path + "." + k
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
value, _ := v.(*dict).get(k)
value, _ := v.(*Dict).Get(k)
m.collectSiblings(value, subpath)
}
case array:
case Array:
prev := ""
for i, value := range v.(array) {
for i, value := range v.(Array) {
subpath := fmt.Sprintf("%v[%v]", path, i)
if prev != "" {
m.nextSiblings[prev] = subpath

View File

@ -1,50 +0,0 @@
package main
import (
"encoding/json"
"strings"
"testing"
)
func Test_parse(t *testing.T) {
input := `{
"a": 1,
"b": 2,
"a": 3,
"slice": [{"z": "z", "1": "1"}]
}`
p, err := parse(json.NewDecoder(strings.NewReader(input)))
if err != nil {
t.Error("JSON parse error", err)
}
o := p.(*dict)
expectedKeys := []string{
"a",
"b",
"slice",
}
for i := range o.keys {
if o.keys[i] != expectedKeys[i] {
t.Error("Wrong key order ", i, o.keys[i], "!=", expectedKeys[i])
}
}
s, ok := o.get("slice")
if !ok {
t.Error("slice missing")
}
a := s.(array)
z := a[0].(*dict)
expectedKeys = []string{
"z",
"1",
}
for i := range z.keys {
if z.keys[i] != expectedKeys[i] {
t.Error("Wrong key order for nested map ", i, z.keys[i], "!=", expectedKeys[i])
}
}
}

26
pkg/dict/dict.go Normal file
View File

@ -0,0 +1,26 @@
package dict
type Dict struct {
Keys []string
Values map[string]interface{}
}
func NewDict() *Dict {
return &Dict{
Keys: make([]string, 0),
Values: make(map[string]interface{}),
}
}
func (d *Dict) Get(key string) (interface{}, bool) {
val, exists := d.Values[key]
return val, exists
}
func (d *Dict) Set(key string, value interface{}) {
_, exists := d.Values[key]
if !exists {
d.Keys = append(d.Keys, key)
}
d.Values[key] = value
}

View File

@ -1,26 +1,26 @@
package main
package dict
import "testing"
func Test_dict(t *testing.T) {
d := newDict()
d.set("number", 3)
v, _ := d.get("number")
d := NewDict()
d.Set("number", 3)
v, _ := d.Get("number")
if v.(int) != 3 {
t.Error("Set number")
}
// string
d.set("string", "x")
v, _ = d.get("string")
d.Set("string", "x")
v, _ = d.Get("string")
if v.(string) != "x" {
t.Error("Set string")
}
// string slice
d.set("strings", []string{
d.Set("strings", []string{
"t",
"u",
})
v, _ = d.get("strings")
v, _ = d.Get("strings")
if v.([]string)[0] != "t" {
t.Error("Set strings first index")
}
@ -28,11 +28,11 @@ func Test_dict(t *testing.T) {
t.Error("Set strings second index")
}
// mixed slice
d.set("mixed", []interface{}{
d.Set("mixed", []interface{}{
1,
"1",
})
v, _ = d.get("mixed")
v, _ = d.Get("mixed")
if v.([]interface{})[0].(int) != 1 {
t.Error("Set mixed int")
}
@ -40,19 +40,19 @@ func Test_dict(t *testing.T) {
t.Error("Set mixed string")
}
// overriding existing key
d.set("number", 4)
v, _ = d.get("number")
d.Set("number", 4)
v, _ = d.Get("number")
if v.(int) != 4 {
t.Error("Override existing key")
}
// keys
// Keys
expectedKeys := []string{
"number",
"string",
"strings",
"mixed",
}
for i, key := range d.keys {
for i, key := range d.Keys {
if key != expectedKeys[i] {
t.Error("Keys method", key, "!=", expectedKeys[i])
}

View File

@ -1,10 +1,11 @@
package main
package json
import (
"encoding/json"
. "github.com/antonmedv/fx/pkg/dict"
)
func parse(dec *json.Decoder) (interface{}, error) {
func Parse(dec *json.Decoder) (interface{}, error) {
token, err := dec.Token()
if err != nil {
return nil, err
@ -20,8 +21,8 @@ func parse(dec *json.Decoder) (interface{}, error) {
return token, nil
}
func decodeDict(dec *json.Decoder) (*dict, error) {
d := newDict()
func decodeDict(dec *json.Decoder) (*Dict, error) {
d := NewDict()
for {
token, err := dec.Token()
if err != nil {
@ -50,14 +51,12 @@ func decodeDict(dec *json.Decoder) (*dict, error) {
}
}
}
d.set(key, value)
d.Set(key, value)
}
}
type array = []interface{}
func decodeArray(dec *json.Decoder) ([]interface{}, error) {
slice := make(array, 0)
slice := make(Array, 0)
for index := 0; ; index++ {
token, err := dec.Token()
if err != nil {

51
pkg/json/parse_test.go Normal file
View File

@ -0,0 +1,51 @@
package json
import (
"encoding/json"
. "github.com/antonmedv/fx/pkg/dict"
"strings"
"testing"
)
func Test_parse(t *testing.T) {
input := `{
"a": 1,
"b": 2,
"a": 3,
"slice": [{"z": "z", "1": "1"}]
}`
p, err := Parse(json.NewDecoder(strings.NewReader(input)))
if err != nil {
t.Error("JSON parse error", err)
}
o := p.(*Dict)
expectedKeys := []string{
"a",
"b",
"slice",
}
for i := range o.Keys {
if o.Keys[i] != expectedKeys[i] {
t.Error("Wrong key order ", i, o.Keys[i], "!=", expectedKeys[i])
}
}
s, ok := o.Get("slice")
if !ok {
t.Error("slice missing")
}
a := s.(Array)
z := a[0].(*Dict)
expectedKeys = []string{
"z",
"1",
}
for i := range z.Keys {
if z.Keys[i] != expectedKeys[i] {
t.Error("Wrong key order for nested map ", i, z.Keys[i], "!=", expectedKeys[i])
}
}
}

71
pkg/json/pretty_print.go Normal file
View File

@ -0,0 +1,71 @@
package json
import (
"encoding/json"
"fmt"
"github.com/antonmedv/fx/pkg/dict"
"github.com/antonmedv/fx/pkg/theme"
"strings"
)
func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1)
switch v.(type) {
case nil:
return theme.Null("null")
case bool:
if v.(bool) {
return theme.Boolean("true")
} else {
return theme.Boolean("false")
}
case json.Number:
return theme.Number(v.(json.Number).String())
case string:
return theme.String(fmt.Sprintf("%q", v))
case *dict.Dict:
keys := v.(*dict.Dict).Keys
if len(keys) == 0 {
return theme.Syntax("{}")
}
output := theme.Syntax("{")
output += "\n"
for i, k := range keys {
key := theme.Key(i, len(keys))(fmt.Sprintf("%q", k))
value, _ := v.(*dict.Dict).Get(k)
delim := theme.Syntax(": ")
line := ident + key + delim + PrettyPrint(value, level+1, theme)
if i < len(keys)-1 {
line += theme.Syntax(",")
}
line += "\n"
output += line
}
return output + subident + theme.Syntax("}")
case []interface{}:
slice := v.([]interface{})
if len(slice) == 0 {
return theme.Syntax("[]")
}
output := theme.Syntax("[\n")
for i, value := range v.([]interface{}) {
line := ident + PrettyPrint(value, level+1, theme)
if i < len(slice)-1 {
line += ",\n"
} else {
line += "\n"
}
output += line
}
return output + subident + theme.Syntax("]")
default:
return "unknown type"
}
}

View File

@ -1,10 +1,11 @@
package main
package json
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
)
func stringify(v interface{}) string {
func Stringify(v interface{}) string {
switch v.(type) {
case nil:
return "null"
@ -16,28 +17,28 @@ func stringify(v interface{}) string {
return "false"
}
case number:
return v.(number).String()
case Number:
return v.(Number).String()
case string:
return fmt.Sprintf("%q", v)
case *dict:
case *Dict:
result := "{"
for i, key := range v.(*dict).keys {
line := fmt.Sprintf("%q", key) + ": " + stringify(v.(*dict).values[key])
if i < len(v.(*dict).keys)-1 {
for i, key := range v.(*Dict).Keys {
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.(*Dict).Values[key])
if i < len(v.(*Dict).Keys)-1 {
line += ","
}
result += line
}
return result + "}"
case array:
case Array:
result := "["
for i, value := range v.(array) {
line := stringify(value)
if i < len(v.(array))-1 {
for i, value := range v.(Array) {
line := Stringify(value)
if i < len(v.(Array))-1 {
line += ","
}
result += line

View File

@ -1,28 +1,31 @@
package main
package json
import "testing"
import (
. "github.com/antonmedv/fx/pkg/dict"
"testing"
)
func Test_stringify(t *testing.T) {
t.Run("dict", func(t *testing.T) {
arg := newDict()
arg.set("a", number("1"))
arg.set("b", number("2"))
arg := NewDict()
arg.Set("a", Number("1"))
arg.Set("b", Number("2"))
want := `{"a": 1,"b": 2}`
if got := stringify(arg); got != want {
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})
t.Run("array", func(t *testing.T) {
arg := array{number("1"), number("2")}
arg := Array{Number("1"), Number("2")}
want := `[1,2]`
if got := stringify(arg); got != want {
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})
t.Run("array_with_dict", func(t *testing.T) {
arg := array{newDict(), array{}}
arg := Array{NewDict(), Array{}}
want := `[{},[]]`
if got := stringify(arg); got != want {
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})

47
pkg/json/traverse.go Normal file
View File

@ -0,0 +1,47 @@
package json
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
)
type Iterator struct {
Object interface{}
Path, Parent string
}
func Dfs(object interface{}, f func(it Iterator)) {
sub(Iterator{Object: object}, f)
}
func sub(it Iterator, f func(it Iterator)) {
f(it)
switch it.Object.(type) {
case *Dict:
keys := it.Object.(*Dict).Keys
for _, k := range keys {
subpath := it.Path + "." + k
value, _ := it.Object.(*Dict).Get(k)
sub(Iterator{
Object: value,
Path: subpath,
Parent: it.Path,
}, f)
}
case Array:
slice := it.Object.(Array)
for i, value := range slice {
subpath := accessor(it.Path, i)
sub(Iterator{
Object: value,
Path: subpath,
Parent: it.Path,
}, f)
}
}
}
func accessor(path string, to interface{}) string {
return fmt.Sprintf("%v[%v]", path, to)
}

6
pkg/json/types.go Normal file
View File

@ -0,0 +1,6 @@
package json
import "encoding/json"
type Number = json.Number
type Array = []interface{}

View File

@ -1,10 +1,12 @@
package main
package reducer
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/theme"
"os"
"os/exec"
"regexp"
@ -16,7 +18,7 @@ var template string
var flatMapRegex = regexp.MustCompile("^(\\.\\w*)+\\[]")
func generateCode(args []string) string {
func GenerateCode(args []string) string {
rs := "\n"
for i, a := range args {
rs += " try {"
@ -74,9 +76,9 @@ func generateCode(args []string) string {
}
pointer := fmt.Sprintf(
"%v %v %v",
strings.Repeat(" ", width(pre)),
strings.Repeat("^", width(a)),
strings.Repeat(" ", width(post)),
strings.Repeat(" ", len(pre)),
strings.Repeat("^", len(a)),
strings.Repeat(" ", len(post)),
)
rs += fmt.Sprintf(
" throw `\\n"+
@ -104,24 +106,24 @@ func fold(s []string) string {
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
}
func reduce(object interface{}, args []string, theme Theme) {
func Reduce(object interface{}, args []string, theme Theme) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("node", "-e", generateCode(args))
cmd := exec.Command("node", "-e", GenerateCode(args))
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "NODE_OPTIONS=--max-old-space-size=8192")
cmd.Stdin = strings.NewReader(stringify(object))
cmd.Stdin = strings.NewReader(Stringify(object))
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err == nil {
dec := json.NewDecoder(&stdout)
dec.UseNumber()
jsonObject, err := parse(dec)
jsonObject, err := Parse(dec)
if err == nil {
if str, ok := jsonObject.(string); ok {
fmt.Println(str)
} else {
fmt.Println(prettyPrint(jsonObject, 1, theme))
fmt.Println(PrettyPrint(jsonObject, 1, theme))
}
} else {
_, _ = fmt.Fprint(os.Stderr, stderr.String())
@ -136,65 +138,3 @@ func reduce(object interface{}, args []string, theme Theme) {
os.Exit(exitCode)
}
}
func prettyPrint(v interface{}, level int, theme Theme) string {
ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1)
switch v.(type) {
case nil:
return theme.null("null")
case bool:
if v.(bool) {
return theme.boolean("true")
} else {
return theme.boolean("false")
}
case number:
return theme.number(v.(number).String())
case string:
return theme.string(fmt.Sprintf("%q", v))
case *dict:
keys := v.(*dict).keys
if len(keys) == 0 {
return theme.syntax("{}")
}
output := theme.syntax("{")
output += "\n"
for i, k := range keys {
key := theme.key(i, len(keys))(fmt.Sprintf("%q", k))
value, _ := v.(*dict).get(k)
delim := theme.syntax(": ")
line := ident + key + delim + prettyPrint(value, level+1, theme)
if i < len(keys)-1 {
line += theme.syntax(",")
}
line += "\n"
output += line
}
return output + subident + theme.syntax("}")
case array:
slice := v.(array)
if len(slice) == 0 {
return theme.syntax("[]")
}
output := theme.syntax("[\n")
for i, value := range v.(array) {
line := ident + prettyPrint(value, level+1, theme)
if i < len(slice)-1 {
line += ",\n"
} else {
line += "\n"
}
output += line
}
return output + subident + theme.syntax("]")
default:
return "unknown type"
}
}

185
pkg/theme/theme.go Normal file
View File

@ -0,0 +1,185 @@
package theme
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
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 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
}
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
}
}

View File

@ -2,6 +2,9 @@ package main
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/antonmedv/fx/pkg/theme"
"strings"
)
@ -27,34 +30,34 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
switch v.(type) {
case nil:
return []string{merge(m.explode("null", searchValue, m.theme.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, m.theme.boolean, path, selectableValues))}
return []string{merge(m.explode("true", searchValue, m.theme.Boolean, path, selectableValues))}
} else {
return []string{merge(m.explode("false", searchValue, m.theme.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, m.theme.number, path, selectableValues))}
case Number:
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, m.theme.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)
}
// No wrap
return []string{merge(chunks)}
case *dict:
case *Dict:
if !m.expandedPaths[path] {
return []string{m.preview(v, path, selectableValues)}
}
output := []string{m.printOpenBracket("{", highlight, path, selectableValues)}
lineNumber++ // bracket is on separate line
keys := v.(*dict).keys
keys := v.(*Dict).Keys
for i, k := range keys {
subpath := path + "." + k
highlight := m.highlightIndex[subpath]
@ -65,10 +68,10 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
}
m.connect(subpath, lineNumber)
key := fmt.Sprintf("%q", k)
keyTheme := m.theme.key(i, len(keys))
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, m.theme.syntax, subpath, false))
value, _ := v.(*Dict).Get(k)
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]
@ -81,13 +84,13 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
output = append(output, subident+m.printCloseBracket("}", highlight, path, false))
return output
case array:
case Array:
if !m.expandedPaths[path] {
return []string{m.preview(v, path, selectableValues)}
}
output := []string{m.printOpenBracket("[", highlight, path, selectableValues)}
lineNumber++ // bracket is on separate line
slice := v.(array)
slice := v.(Array)
for i, value := range slice {
subpath := fmt.Sprintf("%v[%v]", path, i)
s := m.highlightIndex[subpath]
@ -110,32 +113,32 @@ 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 := m.theme.preview
previewStyle := m.theme.Preview
if selectableValues && m.cursorPath() == path {
previewStyle = m.theme.cursor
previewStyle = m.theme.Cursor
}
printValue := func(value interface{}) string {
switch value.(type) {
case nil, bool, number:
case nil, bool, Number:
return previewStyle(fmt.Sprintf("%v", value))
case string:
return previewStyle(fmt.Sprintf("%q", value))
case *dict:
case *Dict:
return previewStyle("{\u2026}")
case array:
case Array:
return previewStyle("[\u2026]")
}
return "..."
}
switch v.(type) {
case *dict:
case *Dict:
output := m.printOpenBracket("{", searchResult, path, selectableValues)
keys := v.(*dict).keys
keys := v.(*Dict).Keys
for _, k := range keys {
key := fmt.Sprintf("%q", k)
output += previewStyle(key + ": ")
value, _ := v.(*dict).get(k)
value, _ := v.(*Dict).Get(k)
output += printValue(value)
break
}
@ -145,9 +148,9 @@ func (m *model) preview(v interface{}, path string, selectableValues bool) strin
output += m.printCloseBracket("}", searchResult, path, selectableValues)
return output
case array:
case Array:
output := m.printOpenBracket("[", searchResult, path, selectableValues)
slice := v.(array)
slice := v.(Array)
for _, value := range slice {
output += printValue(value)
break
@ -194,62 +197,62 @@ func (w withStyle) Render(s string) string {
func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
if selectableValues && m.cursorPath() == path {
return m.theme.cursor(line)
return m.theme.Cursor(line)
}
if s != nil && s.openBracket != nil {
if s.openBracket.parent.index == m.searchResultsCursor {
return m.theme.cursor(line)
return m.theme.Cursor(line)
} else {
return m.theme.search(line)
return m.theme.Search(line)
}
} else {
return m.theme.syntax(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 m.theme.cursor(line)
return m.theme.Cursor(line)
}
if s != nil && s.closeBracket != nil {
if s.closeBracket.parent.index == m.searchResultsCursor {
return m.theme.cursor(line)
return m.theme.Cursor(line)
} else {
return m.theme.search(line)
return m.theme.Search(line)
}
} else {
return m.theme.syntax(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 m.theme.cursor(line)
return m.theme.Cursor(line)
} else {
return m.theme.search(line)
return m.theme.Search(line)
}
} else {
return m.theme.syntax(line)
return m.theme.Syntax(line)
}
}
type withStyle struct {
value string
style Color
style theme.Color
}
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle Color, path string, selectable bool) []withStyle {
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle theme.Color, path string, selectable bool) []withStyle {
if selectable && m.cursorPath() == path && m.showCursor {
return []withStyle{{line, m.theme.cursor}}
return []withStyle{{line, m.theme.Cursor}}
}
out := make([]withStyle, 0, 1)
pos := 0
for _, r := range highlightRanges {
style := m.theme.search
style := m.theme.Search
if r.parent.index == m.searchResultsCursor {
style = m.theme.cursor
style = m.theme.Cursor
}
out = append(out, withStyle{
value: line[pos:r.start],

View File

@ -2,6 +2,8 @@ package main
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"regexp"
)
@ -54,7 +56,7 @@ func (m *model) doSearch(s string) {
m.searchInput.Blur()
return
}
indexes := re.FindAllStringIndex(stringify(m.json), -1)
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
m.indexSearchResults()
m.searchInput.Blur()
@ -79,8 +81,8 @@ func (m *model) remapSearchResult(object interface{}, path string, pos int, inde
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case number:
s := object.(number).String()
case Number:
s := object.(Number).String()
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
@ -89,10 +91,10 @@ func (m *model) remapSearchResult(object interface{}, path string, pos int, inde
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case *dict:
case *Dict:
id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
pos++ // {
for i, k := range object.(*dict).keys {
for i, k := range object.(*Dict).Keys {
subpath := path + "." + k
key := fmt.Sprintf("%q", k)
@ -103,8 +105,8 @@ func (m *model) remapSearchResult(object interface{}, path string, pos int, inde
id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
pos += len(delim)
pos, id, current = m.remapSearchResult(object.(*dict).values[k], subpath, pos, indexes, id, current)
if i < len(object.(*dict).keys)-1 {
pos, id, current = m.remapSearchResult(object.(*Dict).Values[k], subpath, pos, indexes, id, current)
if i < len(object.(*Dict).Keys)-1 {
comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)
@ -114,13 +116,13 @@ func (m *model) remapSearchResult(object interface{}, path string, pos int, inde
pos++ // }
return pos, id, current
case array:
case Array:
id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
pos++ // [
for i, v := range object.(array) {
for i, v := range object.(Array) {
subpath := fmt.Sprintf("%v[%v]", path, i)
pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
if i < len(object.(array))-1 {
if i < len(object.(Array))-1 {
comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)

View File

@ -1,6 +1,8 @@
package main
import (
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/stretchr/testify/require"
"regexp"
"testing"
@ -15,7 +17,7 @@ func Test_search_values(t *testing.T) {
{name: "null", object: nil},
{name: "true", object: true},
{name: "false", object: false},
{name: "number", object: number("42")},
{name: "Number", object: Number("42")},
{name: "string", object: "Hello, World!"},
}
for _, tt := range tests {
@ -24,7 +26,7 @@ func Test_search_values(t *testing.T) {
json: tt.object,
}
re, _ := regexp.Compile(".+")
str := stringify(m.json)
str := Stringify(m.json)
indexes := re.FindAllStringIndex(str, -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
@ -47,10 +49,10 @@ func Test_search_array(t *testing.T) {
^^^^^ ^^^^^^
`
m := &model{
json: array{"first", "second"},
json: Array{"first", "second"},
}
re, _ := regexp.Compile("\\w+")
indexes := re.FindAllStringIndex(stringify(m.json), -1)
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s1 := &searchResult{path: "[0]"}
@ -82,10 +84,10 @@ func Test_search_between_array(t *testing.T) {
^^^^^^^^^^^^^^
`
m := &model{
json: array{"first", "second"},
json: Array{"first", "second"},
}
re, _ := regexp.Compile("\\w.+\\w")
indexes := re.FindAllStringIndex(stringify(m.json), -1)
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: "[0]"}
@ -120,13 +122,13 @@ func Test_search_dict(t *testing.T) {
{"key": "hello world"}
^^^^^ ^^^^^^^^^^^^^
`
d := newDict()
d.set("key", "hello world")
d := NewDict()
d.Set("key", "hello world")
m := &model{
json: d,
}
re, _ := regexp.Compile("\"[\\w\\s]+\"")
indexes := re.FindAllStringIndex(stringify(m.json), -1)
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s1 := &searchResult{path: ".key"}
@ -157,14 +159,14 @@ func Test_search_dict_with_array(t *testing.T) {
{"first": [1,2],"second": []}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`
d := newDict()
d.set("first", array{number("1"), number("2")})
d.set("second", array{})
d := NewDict()
d.Set("first", Array{Number("1"), Number("2")})
d.Set("second", Array{})
m := &model{
json: d,
}
re, _ := regexp.Compile(".+")
indexes := re.FindAllStringIndex(stringify(m.json), -1)
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: ""}

185
theme.go
View File

@ -1,185 +0,0 @@
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
}
}

View File

@ -1,38 +0,0 @@
package main
type iterator struct {
object interface{}
path, parent string
}
func dfs(object interface{}, f func(it iterator)) {
sub(iterator{object: object}, f)
}
func sub(it iterator, f func(it iterator)) {
f(it)
switch it.object.(type) {
case *dict:
keys := it.object.(*dict).keys
for _, k := range keys {
subpath := it.path + "." + k
value, _ := it.object.(*dict).get(k)
sub(iterator{
object: value,
path: subpath,
parent: it.path,
}, f)
}
case array:
slice := it.object.(array)
for i, value := range slice {
subpath := accessor(it.path, i)
sub(iterator{
object: value,
path: subpath,
parent: it.path,
}, f)
}
}
}

View File

@ -81,8 +81,8 @@ func (m *model) LineDown(n int) {
return
}
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we actually have left before we reach
// Make sure the Number of lines by which we're going to scroll isn't
// greater than the Number of lines we actually have left before we reach
// the bottom.
m.SetOffset(m.offset + n)
}
@ -92,8 +92,8 @@ func (m *model) LineUp(n int) {
return
}
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we are from the top.
// Make sure the Number of lines by which we're going to scroll isn't
// greater than the Number of lines we are from the top.
m.SetOffset(m.offset - n)
}