Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ theme:
name: "nord" # controls the color scheme
chromaStyleOverrides: # override parts of the chroma style
kc: "#009900 underline" # keys use the chroma short names
indent: 2 # number of spaces used to indent JSON (defaults to 4)
```

## Indentation

By default, `jqp` indents the input data and query results with 4 spaces. You can change the number of spaces used per indentation level with the `indent` option in your [configuration file](#configuration). This can make deeply nested JSON easier to read by fitting more data on screen.

```yaml
indent: 2
```

## Themes
Expand Down
10 changes: 8 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,16 @@ You can provide the input JSON or NDJSON either through a file or via standard i
}
}

// indent controls how many spaces are used per level when prettifying
// JSON output. An unset or non-positive value falls back to the default.
indent := viper.GetInt(configKeysName.indent)

if isInputFromPipe() {
stdin, err := streamToBytes(os.Stdin)
if err != nil {
return err
}
bubble, err := jqplayground.New(stdin, "STDIN", query, jqtheme)
bubble, err := jqplayground.New(stdin, "STDIN", query, jqtheme, indent)
if err != nil {
return err
}
Expand Down Expand Up @@ -102,7 +106,7 @@ You can provide the input JSON or NDJSON either through a file or via standard i
return err
}

bubble, err := jqplayground.New(data, fi.Name(), query, jqtheme)
bubble, err := jqplayground.New(data, fi.Name(), query, jqtheme, indent)
if err != nil {
return err
}
Expand Down Expand Up @@ -169,10 +173,12 @@ var configKeysName = struct {
themeName string
themeOverrides string
styleOverrides string
indent string
}{
themeName: "theme.name",
themeOverrides: "theme.chromaStyleOverrides",
styleOverrides: "theme.styleOverrides",
indent: "indent",
}

var cfgFile string
Expand Down
6 changes: 6 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,12 @@
},
"minProperties": 1,
"additionalProperties": false
},
"indent": {
"title": "indent",
"description": "Number of spaces used to indent prettified JSON output (defaults to 4)\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#indentation",
"type": "integer",
"minimum": 1
}
},
"minProperties": 1,
Expand Down
6 changes: 4 additions & 2 deletions tui/bubbles/inputdata/inputdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ type Bubble struct {
highlightedJSON *bytes.Buffer
filename string
theme theme.Theme
indent int
setInitialContentSub chan setPrettifiedContentMsg
}

func New(inputJSON []byte, filename string, jqtheme theme.Theme) (Bubble, error) {
func New(inputJSON []byte, filename string, jqtheme theme.Theme, indent int) (Bubble, error) {
styles := DefaultStyles()
styles.containerStyle = styles.containerStyle.BorderForeground(jqtheme.Inactive)
styles.infoStyle = styles.infoStyle.BorderForeground(jqtheme.Inactive)
Expand All @@ -39,6 +40,7 @@ func New(inputJSON []byte, filename string, jqtheme theme.Theme) (Bubble, error)
inputJSON: inputJSON,
filename: filename,
theme: jqtheme,
indent: indent,
setInitialContentSub: make(chan setPrettifiedContentMsg),
}
return b, nil
Expand Down Expand Up @@ -101,7 +103,7 @@ type setPrettifiedContentMsg struct {
// sent through the channel to ensure the prettified data is available without blocking other operations.
func (b Bubble) prettifyContentCmd(sub chan setPrettifiedContentMsg, isJSONLines bool) tea.Cmd {
return func() tea.Msg {
prettifiedData, _ := utils.Prettify(b.inputJSON, b.theme.ChromaStyle, isJSONLines)
prettifiedData, _ := utils.Prettify(b.inputJSON, b.theme.ChromaStyle, isJSONLines, b.indent)
sub <- setPrettifiedContentMsg{Content: prettifiedData}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion tui/bubbles/jqplayground/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (b *Bubble) executeQueryCommand(ctx context.Context) tea.Cmd {
if err != nil {
return errorMsg{error: err}
}
highlightedOutput, err := utils.Prettify([]byte(results), b.theme.ChromaStyle, true)
highlightedOutput, err := utils.Prettify([]byte(results), b.theme.ChromaStyle, true, b.indent)
if err != nil {
return errorMsg{error: err}
}
Expand Down
6 changes: 4 additions & 2 deletions tui/bubbles/jqplayground/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ type Bubble struct {
results string
cancel func()
theme theme.Theme
indent int
ExitMessage string
isJSONLines bool
showInputPanel bool
}

func New(inputJSON []byte, filename string, query string, jqtheme theme.Theme) (Bubble, error) {
func New(inputJSON []byte, filename string, query string, jqtheme theme.Theme, indent int) (Bubble, error) {
workingDirectory, err := os.Getwd()
if err != nil {
return Bubble{}, err
Expand All @@ -49,7 +50,7 @@ func New(inputJSON []byte, filename string, query string, jqtheme theme.Theme) (

fs.SetInput(workingDirectory)

inputData, err := inputdata.New(inputJSON, filename, jqtheme)
inputData, err := inputdata.New(inputJSON, filename, jqtheme, indent)
if err != nil {
return Bubble{}, err
}
Expand All @@ -68,6 +69,7 @@ func New(inputJSON []byte, filename string, query string, jqtheme theme.Theme) (
statusbar: sb,
fileselector: fs,
theme: jqtheme,
indent: indent,
showInputPanel: true,
}
return b, nil
Expand Down
33 changes: 26 additions & 7 deletions tui/utils/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"strings"

"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
Expand All @@ -15,6 +16,20 @@ import (

const FourSpaces = " "

// DefaultIndent is the number of spaces used to indent prettified JSON when the
// configured indent is unset or invalid.
const DefaultIndent = 4

// indentUnit returns the string used to indent one level of prettified JSON for
// the given number of spaces, falling back to FourSpaces when spaces is not a
// positive number.
func indentUnit(spaces int) string {
if spaces < 1 {
return FourSpaces
}
return strings.Repeat(" ", spaces)
}

// IsValidInput checks the validity of input data as JSON or JSON lines.
// It takes a byte slice 'data' and returns two boolean values indicating
// whether the data is valid JSON and valid JSON lines, along with an error
Expand Down Expand Up @@ -73,21 +88,21 @@ func IsValidJSONLines(input []byte) error {
return nil
}

func indentJSON(input *[]byte, output *bytes.Buffer) error {
func indentJSON(input *[]byte, output *bytes.Buffer, indent string) error {
err := IsValidJSON(*input)
if err != nil {
return nil
}
err = json.Indent(output, []byte(*input), "", FourSpaces)
err = json.Indent(output, []byte(*input), "", indent)
if err != nil {
return err
}
return nil
}

func prettifyJSON(input []byte, chromaStyle *chroma.Style) (*bytes.Buffer, error) {
func prettifyJSON(input []byte, chromaStyle *chroma.Style, indent string) (*bytes.Buffer, error) {
var indentedBuf bytes.Buffer
err := indentJSON(&input, &indentedBuf)
err := indentJSON(&input, &indentedBuf, indent)
if err != nil {
return nil, err
}
Expand All @@ -106,14 +121,18 @@ func prettifyJSON(input []byte, chromaStyle *chroma.Style) (*bytes.Buffer, error
return &highlightedBuf, nil
}

func Prettify(inputJSON []byte, chromaStyle *chroma.Style, isJSONLines bool) (*bytes.Buffer, error) {
// Prettify indents and syntax-highlights the input JSON (or NDJSON when
// isJSONLines is true). The indent argument is the number of spaces used for
// each indentation level; values below 1 fall back to DefaultIndent.
func Prettify(inputJSON []byte, chromaStyle *chroma.Style, isJSONLines bool, indent int) (*bytes.Buffer, error) {
unit := indentUnit(indent)
if !isJSONLines {
return prettifyJSON(inputJSON, chromaStyle)
return prettifyJSON(inputJSON, chromaStyle, unit)
}

var buf bytes.Buffer
processLine := func(line []byte) error {
hightlighedLine, err := prettifyJSON(line, chromaStyle)
hightlighedLine, err := prettifyJSON(line, chromaStyle, unit)
if err != nil {
return err
}
Expand Down
81 changes: 81 additions & 0 deletions tui/utils/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package utils

import (
"bytes"
"regexp"
"testing"
)

var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`)

func TestIndentUnit(t *testing.T) {
tests := []struct {
name string
spaces int
want string
}{
{name: "two spaces", spaces: 2, want: " "},
{name: "four spaces", spaces: 4, want: FourSpaces},
{name: "one space", spaces: 1, want: " "},
{name: "zero falls back to default", spaces: 0, want: FourSpaces},
{name: "negative falls back to default", spaces: -3, want: FourSpaces},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := indentUnit(tt.spaces); got != tt.want {
t.Errorf("indentUnit(%d) = %q, want %q", tt.spaces, got, tt.want)
}
})
}
}

func assertIndentJSON(t *testing.T, indent, want string) {
t.Helper()

data := []byte(`{"a":{"b":1}}`)
var out bytes.Buffer
if err := indentJSON(&data, &out, indent); err != nil {
t.Fatalf("indentJSON returned error: %v", err)
}
if out.String() != want {
t.Errorf("indentJSON with %q =\n%q\nwant\n%q", indent, out.String(), want)
}
}

func TestIndentJSON(t *testing.T) {
t.Run("two spaces", func(t *testing.T) {
assertIndentJSON(t, " ", "{\n \"a\": {\n \"b\": 1\n }\n}")
})
t.Run("four spaces", func(t *testing.T) {
assertIndentJSON(t, FourSpaces, "{\n \"a\": {\n \"b\": 1\n }\n}")
})
}

func TestIndentJSONInvalidInputIsNoop(t *testing.T) {
var out bytes.Buffer
data := []byte("not json")
if err := indentJSON(&data, &out, " "); err != nil {
t.Fatalf("indentJSON returned error for invalid input: %v", err)
}
if out.Len() != 0 {
t.Errorf("expected no output for invalid input, got %q", out.String())
}
}

func TestPrettifyRespectsIndent(t *testing.T) {
input := []byte(`{"a":{"b":1}}`)

buf, err := Prettify(input, nil, false, 2)
if err != nil {
t.Fatalf("Prettify returned error: %v", err)
}

// Highlighting wraps tokens in ANSI escapes, so strip them before comparing
// against the expected 2-space indented output.
got := ansiEscape.ReplaceAllString(buf.String(), "")
want := "{\n \"a\": {\n \"b\": 1\n }\n}"
if got != want {
t.Errorf("Prettify with indent 2 =\n%q\nwant\n%q", got, want)
}
}