diff --git a/cli/qq.go b/cli/qq.go index 4d1a588..2d640a3 100644 --- a/cli/qq.go +++ b/cli/qq.go @@ -18,11 +18,11 @@ func CreateRootCmd() *cobra.Command { var interactive bool var version bool var help bool - var encodings string - for _, t := range codec.SupportedFileTypes { - encodings += t.Ext.String() + ", " - } - encodings = strings.TrimSuffix(encodings, ", ") + var encodings string + for _, t := range codec.SupportedFileTypes { + encodings += t.Ext.String() + ", " + } + encodings = strings.TrimSuffix(encodings, ", ") v := "v0.2.1" desc := fmt.Sprintf("qq is a interoperable configuration format transcoder with jq querying ability powered by gojq. qq is multi modal, and can be used as a replacement for jq or be interacted with via a repl with autocomplete and realtime rendering preview for building queries. Supported formats include %s", encodings) cmd := &cobra.Command{ diff --git a/codec/gron.go b/codec/gron.go index 07b3f09..5277062 100644 --- a/codec/gron.go +++ b/codec/gron.go @@ -129,7 +129,7 @@ func setValueJSON(data map[string]interface{}, key string, value interface{}) { m[part] = value } } else { - // fix index assignment nested map: this is needs optimization + // fix index assignment nested map: this is needs optimization if strings.Contains(part, "[") && strings.Contains(part, "]") { k := strings.Split(part, "[")[0] index := parseArrayIndex(part) @@ -162,4 +162,3 @@ func parseArrayIndex(part string) int { index, _ := strconv.Atoi(indexStr) return index } - diff --git a/internal/tui/interactive.go b/internal/tui/interactive.go index 690a979..8ffcd26 100644 --- a/internal/tui/interactive.go +++ b/internal/tui/interactive.go @@ -8,6 +8,7 @@ import ( "github.com/JFryy/qq/codec" "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/itchyny/gojq" @@ -17,6 +18,7 @@ var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) cursorStyle = focusedStyle previewStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("178")).Italic(true) + outputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("36")) ) type model struct { @@ -29,11 +31,13 @@ type model struct { jqOptions []string suggestedValue string jsonObj interface{} + viewport viewport.Model } func newModel(data string) model { m := model{ - inputs: make([]textinput.Model, 1), + inputs: make([]textinput.Model, 1), + viewport: viewport.New(0, 0), } t := textinput.New() @@ -45,12 +49,11 @@ func newModel(data string) model { t.TextStyle = focusedStyle m.inputs[0] = t m.jsonInput = string(data) - - // Generate jq options based on JSON input m.jqOptions = generateJqOptions(m.jsonInput) m.runJqFilter() m.jsonObj, _ = jsonStrToInterface(m.jsonInput) + return m } @@ -90,18 +93,27 @@ func extractPaths(data interface{}, prefix string, options map[string]struct{}) } func (m model) Init() tea.Cmd { - return nil + return tea.EnterAltScreen } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.WindowSizeMsg: + headerHeight := 2 + footerHeight := 1 + availableHeight := msg.Height - headerHeight - footerHeight + m.viewport.Width = msg.Width + m.viewport.Height = availableHeight + m.updateViewportContent() + return m, nil + case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit - case "tab": // Suggest next jq option + case "tab": if !m.showingPreview { m.showingPreview = true m.currentIndex = 0 @@ -119,10 +131,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.runJqFilter() return m, nil } - // put result and quit m.jsonObj, _ = jsonStrToInterface(m.jsonInput) return m, tea.Quit + case "up": + m.viewport.LineUp(1) + return m, nil + + case "down": + m.viewport.LineDown(1) + return m, nil + + case "pageup": + m.viewport.ViewUp() + return m, nil + case "pagedown": + m.viewport.ViewDown() + return m, nil + default: if m.showingPreview { m.showingPreview = false @@ -158,10 +184,12 @@ func jsonStrToInterface(jsonStr string) (interface{}, error) { } return jsonData, nil } + func (m *model) runJqFilter() { query, err := gojq.Parse(m.inputs[0].Value()) if err != nil { m.jqOutput = fmt.Sprintf("Invalid jq query: %s\n\nLast valid output:\n%s", err, m.lastOutput) + m.updateViewportContent() return } @@ -169,6 +197,7 @@ func (m *model) runJqFilter() { err = json.Unmarshal([]byte(m.jsonInput), &jsonData) if err != nil { m.jqOutput = fmt.Sprintf("Invalid JSON input: %s\n\nLast valid output:\n%s", err, m.lastOutput) + m.updateViewportContent() return } @@ -182,11 +211,13 @@ func (m *model) runJqFilter() { } if err, ok := v.(error); ok { m.jqOutput = fmt.Sprintf("Error executing jq query: %s\n\nLast valid output:\n%s", err, m.lastOutput) + m.updateViewportContent() return } output, err := json.MarshalIndent(v, "", " ") if err != nil { m.jqOutput = fmt.Sprintf("Error formatting output: %s\n\nLast valid output:\n%s", err, m.lastOutput) + m.updateViewportContent() return } if string(output) != "null" { @@ -197,11 +228,22 @@ func (m *model) runJqFilter() { if isNull { m.jqOutput = fmt.Sprintf("Query result is null\n\nLast valid output:\n%s", m.lastOutput) + m.updateViewportContent() return } m.jqOutput = strings.Join(result, "\n") m.lastOutput = m.jqOutput + m.updateViewportContent() +} + +func (m *model) updateViewportContent() { + prettyOutput, err := codec.PrettyFormat(m.jqOutput, codec.JSON, false) + if err != nil { + m.viewport.SetContent(fmt.Sprintf("Error formatting output: %s", err)) + return + } + m.viewport.SetContent(outputStyle.Render(prettyOutput)) } func (m model) View() string { @@ -219,8 +261,7 @@ func (m model) View() string { } b.WriteString("\n") - o, _ := codec.PrettyFormat(m.jqOutput, codec.JSON, false) - b.WriteString(o) + b.WriteString(m.viewport.View()) return b.String() }