├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── release.yml
│ ├── spellchecker.yml
│ └── test-snap-can-build.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── _typos.toml
├── cmd
├── root.go
└── utils.go
├── go.mod
├── go.sum
├── main.go
├── schema.json
├── snap
├── local
│ └── jqp.png
└── snapcraft.yaml
└── tui
├── bubbles
├── fileselector
│ ├── fileselector.go
│ └── styles.go
├── help
│ ├── help.go
│ ├── keys.go
│ └── styles.go
├── inputdata
│ ├── inputdata.go
│ └── styles.go
├── jqplayground
│ ├── commands.go
│ ├── init.go
│ ├── model.go
│ ├── update.go
│ └── view.go
├── output
│ ├── output.go
│ └── styles.go
├── queryinput
│ ├── queryinput.go
│ └── styles.go
├── state
│ └── state.go
└── statusbar
│ ├── statusbar.go
│ └── styles.go
├── theme
└── theme.go
└── utils
├── json.go
├── scan.go
└── terminal.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain golang dependencies defined in go.mod
4 | # These would open PR, these PR would be tested with the CI
5 | # They will have to be merged manually by a maintainer
6 | - package-ecosystem: "gomod"
7 | directory: "/"
8 | open-pull-requests-limit: 10 # avoid spam, if no one reacts
9 | schedule:
10 | interval: "daily"
11 | time: "11:00"
12 |
13 | # Maintain dependencies for GitHub Actions
14 | # These would open PR, these PR would be tested with the CI
15 | # They will have to be merged manually by a maintainer
16 | - package-ecosystem: "github-actions"
17 | directory: "/"
18 | open-pull-requests-limit: 10 # avoid spam, if no one reacts
19 | schedule:
20 | interval: "daily"
21 | time: "11:00"
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ v* ]
7 | pull_request:
8 | types: [opened, synchronize, reopened]
9 |
10 | jobs:
11 | test:
12 | strategy:
13 | matrix:
14 | go-version: ["1.22.x"]
15 | platform: [ubuntu-latest]
16 | runs-on: ${{ matrix.platform }}
17 | steps:
18 | - name: Install Go
19 | uses: actions/setup-go@v5
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 |
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Run tests with race detector
27 | run: go test -count=1 -race ./...
28 |
29 | - name: Run golangci-lint
30 | uses: golangci/golangci-lint-action@v6
31 | with:
32 | version: latest
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | # run only against tags
6 | tags:
7 | - '*'
8 |
9 | permissions:
10 | contents: write
11 | # packages: write
12 | # issues: write
13 |
14 | jobs:
15 | goreleaser:
16 | runs-on: ubuntu-latest
17 | steps:
18 | -
19 | name: Checkout
20 | uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 | -
24 | name: Fetch all tags
25 | run: git fetch --force --tags
26 | -
27 | name: Set up Go
28 | uses: actions/setup-go@v5
29 | with:
30 | go-version: 1.22
31 | -
32 | name: Run GoReleaser
33 | uses: goreleaser/goreleaser-action@v6
34 | with:
35 | distribution: goreleaser
36 | version: latest
37 | args: release --clean
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/spellchecker.yml:
--------------------------------------------------------------------------------
1 | name: spell checking
2 | on: [pull_request]
3 |
4 | jobs:
5 | typos:
6 | name: Spell Check with Typos
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout Actions Repository
10 | uses: actions/checkout@v4
11 |
12 | - name: typos-action
13 | uses: crate-ci/typos@v1.24.5
14 |
15 |
--------------------------------------------------------------------------------
/.github/workflows/test-snap-can-build.yml:
--------------------------------------------------------------------------------
1 | name: 🧪 Test snap can be built on x86_64
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [20.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - uses: snapcore/action-build@v1
20 | id: build
21 |
22 | - uses: diddlesnaps/snapcraft-review-action@v1
23 | with:
24 | snap: ${{ steps.build.outputs.snap }}
25 | isClassic: 'false'
26 | # Plugs and Slots declarations to override default denial (requires store assertion to publish)
27 | # plugs: ./plug-declaration.json
28 | # slots: ./slot-declaration.json
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | jqp
2 | dist/
3 | debug.log
4 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Options for analysis running.
3 | run:
4 | # Timeout for analysis, e.g. 30s, 5m.
5 | # Default: 1m
6 | timeout: 5m
7 |
8 | # Include test files or not.
9 | # Default: true
10 | tests: true
11 |
12 | issues:
13 | # Maximum issues count per one linter.
14 | # Set to 0 to disable.
15 | # Default: 50
16 | max-issues-per-linter: 0
17 | # Maximum count of issues with the same text.
18 | # Set to 0 to disable.
19 | # Default: 3
20 | max-same-issues: 0
21 |
22 | linters:
23 | enable:
24 | # check when errors are compared without errors.Is
25 | - errorlint
26 |
27 | # check imports order and makes it always deterministic.
28 | - gci
29 |
30 | # linter to detect errors invalid key values count
31 | - loggercheck
32 |
33 | # Very Basic spell error checker
34 | - misspell
35 |
36 | # Forbid some imports
37 | - depguard
38 |
39 | # simple security check
40 | - gosec
41 |
42 | # Copyloopvar is a linter detects places where loop variables are copied.
43 | # this hack was needed before golang 1.22
44 | - copyloopvar
45 |
46 | # Fast, configurable, extensible, flexible, and beautiful linter for Go.
47 | # Drop-in replacement of golint.
48 | - revive
49 |
50 | # Finds sending http request without context.Context
51 | - noctx
52 |
53 | # make sure to use t.Helper() when needed
54 | - thelper
55 |
56 | # make sure that error are checked after a rows.Next()
57 | - rowserrcheck
58 |
59 | # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed.
60 | - sqlclosecheck
61 |
62 | # ensure that lint exceptions have explanations. Consider the case below:
63 | - nolintlint
64 |
65 | # detect duplicated words in code
66 | - dupword
67 |
68 | # detect the possibility to use variables/constants from the Go standard library.
69 | - usestdlibvars
70 |
71 | # mirror suggests rewrites to avoid unnecessary []byte/string conversion
72 | - mirror
73 |
74 | # testify checks good usage of github.com/stretchr/testify.
75 | - testifylint
76 |
77 | # Check whether the function uses a non-inherited context.
78 | - contextcheck
79 |
80 | # We already identified we don't want these ones
81 | # - gochecknoinit
82 | # - goerr113 # errorlint is better
83 | # - testpackage
84 |
85 | linters-settings:
86 | # configure the golang imports we don't want
87 | depguard:
88 | rules:
89 | # Name of a rule.
90 | main:
91 | # Packages that are not allowed where the value is a suggestion.
92 | deny:
93 | - pkg: "github.com/pkg/errors"
94 | desc: Should be replaced by standard lib errors package
95 |
96 | - pkg: "golang.org/x/net/context"
97 | desc: Should be replaced by standard lib context package
98 |
99 |
100 | loggercheck: # invalid key values count
101 | require-string-key: true
102 | # Require printf-like format specifier (%s, %d for example) not present.
103 | # Default: false
104 | no-printf-like: true
105 |
106 | nolintlint:
107 | # Disable to ensure that all nolint directives actually have an effect.
108 | # Default: false
109 | allow-unused: true
110 | # Enable to require an explanation of nonzero length
111 | # after each nolint directive.
112 | # Default: false
113 | require-explanation: true
114 | # Enable to require nolint directives to mention the specific
115 | # linter being suppressed.
116 | # Default: false
117 | require-specific: true
118 |
119 | # define the import orders
120 | gci:
121 | sections:
122 | # Standard section: captures all standard packages.
123 | - standard
124 | # Default section: catchall that is not standard or custom
125 | - default
126 | # linters that related to local tool, so they should be separated
127 | - localmodule
128 |
129 | staticcheck:
130 | # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks
131 | checks: ["all"]
132 |
133 | revive:
134 | enable-all-rules: true
135 | rules:
136 | # we must provide configuration for linter that requires them
137 | # enable-all-rules is OK, but many revive linters expect configuration
138 | # and cannot work without them
139 |
140 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity
141 | - name: cognitive-complexity
142 | severity: warning
143 | arguments: [7]
144 |
145 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument
146 | - name: context-as-argument
147 | arguments:
148 | - allowTypesBefore: "*testing.T"
149 |
150 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic
151 | - name: cyclomatic
152 | arguments: [5] # default 3
153 |
154 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported
155 | - name: exported
156 | arguments:
157 | # enables checking public methods of private types
158 | - "checkPrivateReceivers"
159 | # make error messages clearer
160 | - "sayRepetitiveInsteadOfStutters"
161 |
162 | # this linter completes errcheck linter, it will report method called without handling the error
163 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error
164 | - name: unhandled-error
165 | arguments: # here are the exceptions we don't want to be reported
166 | - "fmt.Print.*"
167 | - "fmt.Fprint.*"
168 | - "bytes.Buffer.Write"
169 | - "bytes.Buffer.WriteByte"
170 | - "bytes.Buffer.WriteString"
171 | - "strings.Builder.WriteString"
172 | - "strings.Builder.WriteRune"
173 |
174 | # boolean parameters that create a control coupling could be useful
175 | # but this one is way too noisy
176 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter
177 | - name: flag-parameter
178 | disabled: true
179 |
180 | # depguard linter is easier to configure and more powerful
181 | # than revive.imports-blocklist
182 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blocklist
183 | - name: imports-blocklist
184 | disabled: true
185 |
186 | # it's not really a problem for us in term of readability
187 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs
188 | - name: nested-structs
189 | disabled: true
190 |
191 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant
192 | # too noisy
193 | - name: add-constant
194 | disabled: true
195 |
196 | # too many false positive on jqp code
197 | - name: modifies-value-receiver
198 | disabled: true
199 |
200 | # disable everything we don't want
201 | - name: line-length-limit
202 | disabled: true
203 | - name: argument-limit
204 | disabled: true
205 | - name: banned-characters
206 | disabled: true
207 | - name: max-public-structs
208 | disabled: true
209 | - name: function-result-limit
210 | disabled: true
211 | - name: function-length
212 | disabled: true
213 | - name: file-header
214 | disabled: true
215 | - name: empty-lines
216 | disabled: true
217 |
218 | misspell:
219 | locale: "US" # Fix the colour => color, and co
220 |
221 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | project_name: "jqp"
3 |
4 | before:
5 | hooks:
6 | # You may remove this if you don't use go modules.
7 | - go mod tidy
8 | builds:
9 | - env:
10 | - CGO_ENABLED=0
11 | goos:
12 | - linux
13 | - windows
14 | - darwin
15 | archives:
16 | - id: default
17 | name_template: >-
18 | {{ .ProjectName }}_
19 | {{- title .Os }}_
20 | {{- if eq .Arch "amd64" }}x86_64
21 | {{- else if eq .Arch "386" }}i386
22 | {{- else }}{{ .Arch }}{{ end }}
23 | checksum:
24 | name_template: 'checksums.txt'
25 | snapshot:
26 | version_template: "{{ incpatch .Version }}-next"
27 | changelog:
28 | sort: asc
29 | filters:
30 | exclude:
31 | - '^docs:'
32 | - '^test:'
33 | brews:
34 | - homepage: https://github.com/noahgorstein/jqp
35 | description: "a TUI playground to experiment and play with jq"
36 | directory: Formula
37 | repository:
38 | owner: noahgorstein
39 | name: homebrew-tap
40 | branch: main
41 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
42 | commit_author:
43 | name: goreleaserbot
44 | email: bot@goreleaser.com
45 | license: "MIT"
46 | install: |
47 | bin.install "jqp"
48 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Pull requests are welcome for any changes.
4 |
5 | Consider opening an issue for larger changes to get feedback on the idea.
6 |
7 | For commit messages, please use conventional commits[^1] to make it easier to
8 | generate release notes.
9 |
10 | [^1]: https://www.conventionalcommits.org/en/v1.0.0
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Noah Gorstein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jqp
2 |
3 | a TUI playground for exploring jq.
4 |
5 | 
6 |
7 | This application utilizes [itchyny's](https://github.com/itchyny) implementation of `jq` written in Go, [`gojq`](https://github.com/itchyny/gojq).
8 |
9 | ## Installation
10 |
11 | ### homebrew
12 |
13 | ```bash
14 | brew install noahgorstein/tap/jqp
15 | ```
16 |
17 | ### macports
18 |
19 | ```bash
20 | sudo port install jqp
21 | ```
22 |
23 | ### Arch Linux
24 | Available through the Arch User Repository as [jqp-bin](https://aur.archlinux.org/packages/jqp-bin).
25 | ```bash
26 | yay -S jqp-bin
27 | ```
28 |
29 | ### Snap install
30 |
31 |
32 | ```
33 | sudo snap install jqp
34 | ```
35 |
36 | ### GitHub releases
37 |
38 | Download the relevant asset for your operating system from the latest GitHub release. Unpack it, then move the binary to somewhere accessible in your `PATH`, e.g. `mv ./jqp /usr/local/bin`.
39 |
40 | ### Build from source
41 |
42 | Clone this repository, build from source with `cd jqp && go build`, then move the binary to somewhere accessible in your `PATH`, e.g. `mv ./jqp /usr/local/bin`.
43 |
44 | ## Usage
45 |
46 | ```
47 | ➜ jqp --help
48 | jqp is a terminal user interface (TUI) for exploring the jq command line utility.
49 |
50 | You can use it to run jq queries interactively. If no query is provided, the interface will prompt you for one.
51 |
52 | The command accepts an optional query argument which will be executed against the input JSON or newline-delimited JSON (NDJSON).
53 | You can provide the input JSON or NDJSON either through a file or via standard input (stdin).
54 |
55 | Usage:
56 | jqp [query] [flags]
57 |
58 | Flags:
59 | --config string path to config file (default is $HOME/.jqp.yaml)
60 | -f, --file string path to the input JSON file
61 | -h, --help help for jqp
62 | -t, --theme string jqp theme
63 | -v, --version version for jqp
64 | ```
65 |
66 | `jqp` also supports input from STDIN. STDIN takes precedence over the command-line flag. Additionally, you can pass an optional query argument to jqp that it will execute upon loading.
67 |
68 | ```
69 | ➜ curl "https://api.github.com/repos/jqlang/jq/issues" | jqp '.[] | {"title": .title, "url": .url}'
70 | ```
71 |
72 | > [!NOTE]
73 | > Valid JSON or NDJSON [(newline-delimited JSON)](https://jsonlines.org/) can be provided as input to `jqp`.
74 |
75 | ## Keybindings
76 |
77 | | **Keybinding** | **Action** |
78 | |:---------------|:-----------|
79 | | `tab` | cycle through sections |
80 | | `shift-tab` | cycle through sections in reverse |
81 | | `ctrl-y` | copy query to system clipboard[^1] |
82 | | `ctrl-s` | save output to file (copy to clipboard if file not specified) |
83 | | `ctrl-c` | quit program / kill long-running query |
84 |
85 | ### Query Mode
86 |
87 | | **Keybinding** | **Action** |
88 | |:---------------|:-----------|
89 | | `enter` | execute query |
90 | | `↑`/`↓` | cycle through query history |
91 | | `ctrl-a` | go to beginning of line |
92 | | `ctrl-e` | go to end of line |
93 | | `←`/`ctrl-b` | move cursor one character to left |
94 | | `→`/`ctrl-f`| move cursor one character to right |
95 | | `ctrl-k` | delete text after cursor line |
96 | | `ctrl-u` | delete text before cursor |
97 | | `ctrl-w` | delete word to left |
98 | | `ctrl-d` | delete character under cursor |
99 |
100 | ### Input Preview and Output Mode
101 |
102 | | **Keybinding** | **Action** |
103 | |:---------------|:-----------|
104 | | `↑/k` | up |
105 | | `↓/j` | down |
106 | | `ctrl-u` | page up |
107 | | `ctrl-d` | page down |
108 |
109 | ## Configuration
110 |
111 | `jqp` can be configured with a configuration file. By default, `jqp` will search your home directory for a YAML file named `.jqp.yaml`. A path to a YAML configuration file can also be provided to the `--config` command-line flag.
112 |
113 | ```bash
114 | ➜ jqp --config ~/my_jqp_config.yaml < data.json
115 | ```
116 |
117 | If a configuration option is present in both the configuration file and the command-line, the command-line option takes precedence. For example, if a theme is specified in the configuration file and via `-t/--theme flag`, the command-line flag will take precedence.
118 |
119 | ### Available Configuration Options
120 |
121 | ```yaml
122 | theme:
123 | name: "nord" # controls the color scheme
124 | chromaStyleOverrides: # override parts of the chroma style
125 | kc: "#009900 underline" # keys use the chroma short names
126 | ```
127 |
128 | ## Themes
129 |
130 | Themes can be specified on the command-line via the `-t/--theme ` flag. You can also set a theme in your [configuration file](#configuration).
131 |
132 | ```yaml
133 | theme:
134 | name: "monokai"
135 | ```
136 |
137 |
138 |
139 | ### Chroma Style Overrides
140 |
141 | Overrides to the chroma styles used for a theme can be configured in your [configuration file](#configuration).
142 |
143 | For the list of short keys, see [`chroma.StandardTypes`](https://github.com/alecthomas/chroma/blob/d38b87110b078027006bc34aa27a065fa22295a1/types.go#L210-L308). To see which token to use for a value, see the [JSON lexer](https://github.com/alecthomas/chroma/blob/master/lexers/embedded/json.xml) (look for `` tags). To see the color and what's used in the style you're using, look for your style in the chroma [styles directory](https://github.com/alecthomas/chroma/tree/master/styles).
144 |
145 | ```yaml
146 | theme:
147 | name: "monokai" # name is required to know which theme to override
148 | chromaStyleOverrides:
149 | kc: "#009900 underline"
150 | ```
151 |
152 | You can change non-syntax colors using the `styleOverrides` key:
153 | ```yaml
154 | theme:
155 | styleOverrides:
156 | primary: "#c4b28a"
157 | secondary: "#8992a7"
158 | error: "#c4746e"
159 | inactive: "#a6a69c"
160 | success: "#87a987"
161 | ```
162 |
163 | Themes are broken up into [light](#light-themes) and [dark](#dark-themes) themes. Light themes work best in terminals with a light background and dark themes work best in a terminal with a dark background. If no theme is specified or a non-existent theme is provided, the default theme is used, which was created to work with both terminals with a light and dark background.
164 |
165 | ### Light Themes
166 |
167 | - `abap`
168 | - `algol`
169 | - `arduino`
170 | - `autumn`
171 | - `borland`
172 | - `catppuccin-latte`
173 | - `colorful`
174 | - `emacs`
175 | - `friendly`
176 | - `github`
177 | - `gruvbox-light`
178 | - `hrdark`
179 | - `igor`
180 | - `lovelace`
181 | - `manni`
182 | - `monokai-light`
183 | - `murphy`
184 | - `onesenterprise`
185 | - `paraiso-light`
186 | - `pastie`
187 | - `perldoc`
188 | - `pygments`
189 | - `solarized-light`
190 | - `tango`
191 | - `trac`
192 | - `visual_studio`
193 | - `vulcan`
194 | - `xcode`
195 |
196 | ### Dark Themes
197 |
198 | - `average`
199 | - `base16snazzy`
200 | - `catppuccin-frappe`
201 | - `catppuccin-macchiato`
202 | - `catppuccin-mocha`
203 | - `doom-one`
204 | - `doom-one2`
205 | - `dracula`
206 | - `fruity`
207 | - `github-dark`
208 | - `gruvbox`
209 | - `monokai`
210 | - `native`
211 | - `paraiso-dark`
212 | - `rrt`
213 | - `solarized-dark`
214 | - `solarized-dark256`
215 | - `swapoff`
216 | - `vim`
217 | - `witchhazel`
218 | - `xcode-dark`
219 |
220 | ## Built with:
221 |
222 | - [Bubbletea](https://github.com/charmbracelet/bubbletea)
223 | - [Bubbles](https://github.com/charmbracelet/bubbles)
224 | - [Lipgloss](https://github.com/charmbracelet/lipgloss)
225 | - [gojq](https://github.com/itchyny/gojq)
226 | - [chroma](https://github.com/alecthomas/chroma)
227 |
228 | ## Credits
229 |
230 | - [jqq](https://github.com/jcsalterego/jqq) for inspiration
231 |
232 | --------
233 |
234 | [^1]: `jqp` uses [https://github.com/atotto/clipboard](https://github.com/atotto/clipboard) for clipboard functionality. Things should work as expected with OSX and Windows. Linux, Unix require `xclip` or `xsel` to be installed.
235 |
--------------------------------------------------------------------------------
/_typos.toml:
--------------------------------------------------------------------------------
1 | [default]
2 | extend-ignore-identifiers-re = ["nd"]
3 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/alecthomas/chroma/v2"
10 | "github.com/charmbracelet/bubbletea"
11 | "github.com/spf13/cobra"
12 | "github.com/spf13/viper"
13 |
14 | "github.com/noahgorstein/jqp/tui/bubbles/jqplayground"
15 | "github.com/noahgorstein/jqp/tui/theme"
16 | )
17 |
18 | var rootCmd = &cobra.Command{
19 | Version: "0.7.0",
20 | Use: "jqp [query]",
21 | Short: "jqp is a TUI to explore jq",
22 | Long: `jqp is a terminal user interface (TUI) for exploring the jq command line utility.
23 |
24 | You can use it to run jq queries interactively. If no query is provided, the interface will prompt you for one.
25 |
26 | The command accepts an optional query argument which will be executed against the input JSON or newline-delimited JSON (NDJSON).
27 | You can provide the input JSON or NDJSON either through a file or via standard input (stdin).`,
28 | Args: cobra.MaximumNArgs(1),
29 | SilenceUsage: true,
30 | RunE: func(cmd *cobra.Command, args []string) error {
31 | query := ""
32 | if len(args) == 1 {
33 | query = strings.TrimSpace(args[0])
34 | }
35 |
36 | configTheme := viper.GetString(configKeysName.themeName)
37 | if !cmd.Flags().Changed(flagsName.theme) {
38 | flags.theme = configTheme
39 | }
40 | themeOverrides := viper.GetStringMapString(configKeysName.themeOverrides)
41 |
42 | styleOverrides := viper.GetStringMapString(configKeysName.styleOverrides)
43 | jqtheme, defaultTheme := theme.GetTheme(flags.theme, styleOverrides)
44 |
45 | // If not using the default theme,
46 | // and if theme specified is the same as in the config,
47 | // which happens if the theme flag was used,
48 | // apply chroma style overrides.
49 | if !defaultTheme && configTheme == flags.theme && len(themeOverrides) > 0 {
50 | // Reverse chroma.StandardTypes to be keyed by short string
51 | chromaTypes := make(map[string]chroma.TokenType)
52 | for tokenType, short := range chroma.StandardTypes {
53 | chromaTypes[short] = tokenType
54 | }
55 |
56 | builder := jqtheme.ChromaStyle.Builder()
57 | for k, v := range themeOverrides {
58 | builder.Add(chromaTypes[k], v)
59 | }
60 | style, err := builder.Build()
61 | if err == nil {
62 | jqtheme.ChromaStyle = style
63 | }
64 | }
65 |
66 | if isInputFromPipe() {
67 | stdin, err := streamToBytes(os.Stdin)
68 | if err != nil {
69 | return err
70 | }
71 | bubble, err := jqplayground.New(stdin, "STDIN", query, jqtheme)
72 | if err != nil {
73 | return err
74 | }
75 | p := tea.NewProgram(bubble, tea.WithAltScreen())
76 | m, err := p.Run()
77 | if err != nil {
78 | return err
79 | }
80 | if m, ok := m.(jqplayground.Bubble); ok && m.ExitMessage != "" {
81 | return errors.New(m.ExitMessage)
82 | }
83 | return nil
84 | }
85 |
86 | // get the file
87 | file, e := getFile()
88 | if e != nil {
89 | return e
90 | }
91 | defer file.Close()
92 |
93 | // read the file
94 | data, err := os.ReadFile(flags.filepath)
95 | if err != nil {
96 | return err
97 | }
98 |
99 | // get file info so we can get the filename
100 | fi, err := os.Stat(flags.filepath)
101 | if err != nil {
102 | return err
103 | }
104 |
105 | bubble, err := jqplayground.New(data, fi.Name(), query, jqtheme)
106 | if err != nil {
107 | return err
108 | }
109 | p := tea.NewProgram(bubble, tea.WithAltScreen())
110 |
111 | m, err := p.Run()
112 | if err != nil {
113 | return err
114 | }
115 | if m, ok := m.(jqplayground.Bubble); ok && m.ExitMessage != "" {
116 | return errors.New(m.ExitMessage)
117 | }
118 | return nil
119 | },
120 | }
121 |
122 | func initConfig() {
123 | if cfgFile != "" {
124 | // Use config file from the flag.
125 | viper.SetConfigFile(cfgFile)
126 | if err := viper.ReadInConfig(); err != nil {
127 | fmt.Fprintf(os.Stderr, "Config file %s was unable to be read: %v\n", viper.ConfigFileUsed(), err)
128 | }
129 | return
130 | }
131 | // Find home directory.
132 | home, err := os.UserHomeDir()
133 | cobra.CheckErr(err)
134 |
135 | // Search config in home directory
136 | viper.AddConfigPath(home)
137 |
138 | // register the config file
139 | viper.SetConfigName(".jqp")
140 |
141 | // only read from yaml files
142 | viper.SetConfigType("yaml")
143 |
144 | // Try to read the default config file
145 | if err := viper.ReadInConfig(); err != nil {
146 | // Check if the error is due to the file not existing
147 | var errFileNotFound viper.ConfigFileNotFoundError
148 | if !errors.As(err, &errFileNotFound) {
149 | // For errors other than file not found, print the error message
150 | fmt.Fprintf(os.Stderr, "Default config file %s was unable to be read: %v\n", viper.ConfigFileUsed(), err)
151 | }
152 | }
153 | }
154 |
155 | var flags struct {
156 | filepath, theme string
157 | }
158 |
159 | var flagsName = struct {
160 | file, fileShort, theme, themeShort string
161 | }{
162 | file: "file",
163 | fileShort: "f",
164 | theme: "theme",
165 | themeShort: "t",
166 | }
167 |
168 | var configKeysName = struct {
169 | themeName string
170 | themeOverrides string
171 | styleOverrides string
172 | }{
173 | themeName: "theme.name",
174 | themeOverrides: "theme.chromaStyleOverrides",
175 | styleOverrides: "theme.styleOverrides",
176 | }
177 |
178 | var cfgFile string
179 |
180 | func Execute() error {
181 | cobra.OnInitialize(initConfig)
182 |
183 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "path to config file (default is $HOME/.jqp.yaml)")
184 |
185 | rootCmd.Flags().StringVarP(
186 | &flags.filepath,
187 | flagsName.file,
188 | flagsName.fileShort,
189 | "", "path to the input JSON file")
190 |
191 | rootCmd.Flags().StringVarP(
192 | &flags.theme,
193 | flagsName.theme,
194 | flagsName.themeShort,
195 | "", "jqp theme")
196 |
197 | return rootCmd.Execute()
198 | }
199 |
--------------------------------------------------------------------------------
/cmd/utils.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | )
10 |
11 | func streamToBytes(stream io.Reader) ([]byte, error) {
12 | buf := new(bytes.Buffer)
13 | _, err := buf.ReadFrom(stream)
14 | if err != nil {
15 | return nil, err
16 | }
17 |
18 | return buf.Bytes(), nil
19 | }
20 |
21 | func isInputFromPipe() bool {
22 | fi, _ := os.Stdin.Stat()
23 | return (fi.Mode() & os.ModeCharDevice) == 0
24 | }
25 |
26 | func getFile() (*os.File, error) {
27 | if flags.filepath == "" {
28 | return nil, errors.New("please provide an input file")
29 | }
30 | if !fileExists(flags.filepath) {
31 | return nil, errors.New("the file provided does not exist")
32 | }
33 | file, e := os.Open(flags.filepath)
34 | if e != nil {
35 | return nil, fmt.Errorf("Unable to open file: %w", e)
36 | }
37 | return file, nil
38 | }
39 |
40 | func fileExists(filepath string) bool {
41 | info, e := os.Stat(filepath)
42 | if os.IsNotExist(e) {
43 | return false
44 | }
45 | return !info.IsDir()
46 | }
47 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/noahgorstein/jqp
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/alecthomas/chroma/v2 v2.14.0
7 | github.com/atotto/clipboard v0.1.4
8 | github.com/charmbracelet/bubbles v0.20.0
9 | github.com/charmbracelet/bubbletea v1.3.4
10 | github.com/charmbracelet/lipgloss v1.1.0
11 | github.com/itchyny/gojq v0.12.16
12 | github.com/muesli/termenv v0.16.0
13 | github.com/spf13/cobra v1.9.1
14 | github.com/spf13/viper v1.19.0
15 | )
16 |
17 | require (
18 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
20 | github.com/charmbracelet/x/ansi v0.8.0 // indirect
21 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
22 | github.com/charmbracelet/x/term v0.2.1 // indirect
23 | github.com/dlclark/regexp2 v1.11.0 // indirect
24 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
25 | github.com/fsnotify/fsnotify v1.7.0 // indirect
26 | github.com/hashicorp/hcl v1.0.0 // indirect
27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
28 | github.com/itchyny/timefmt-go v0.1.6 // indirect
29 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
30 | github.com/magiconair/properties v1.8.7 // indirect
31 | github.com/mattn/go-isatty v0.0.20 // indirect
32 | github.com/mattn/go-localereader v0.0.1 // indirect
33 | github.com/mattn/go-runewidth v0.0.16 // indirect
34 | github.com/mitchellh/mapstructure v1.5.0 // indirect
35 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
36 | github.com/muesli/cancelreader v0.2.2 // indirect
37 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
38 | github.com/rivo/uniseg v0.4.7 // indirect
39 | github.com/sagikazarmark/locafero v0.4.0 // indirect
40 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
41 | github.com/sourcegraph/conc v0.3.0 // indirect
42 | github.com/spf13/afero v1.11.0 // indirect
43 | github.com/spf13/cast v1.6.0 // indirect
44 | github.com/spf13/pflag v1.0.6 // indirect
45 | github.com/subosito/gotenv v1.6.0 // indirect
46 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
47 | go.uber.org/atomic v1.9.0 // indirect
48 | go.uber.org/multierr v1.9.0 // indirect
49 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
50 | golang.org/x/sync v0.11.0 // indirect
51 | golang.org/x/sys v0.30.0 // indirect
52 | golang.org/x/text v0.14.0 // indirect
53 | gopkg.in/ini.v1 v1.67.0 // indirect
54 | gopkg.in/yaml.v3 v3.0.1 // indirect
55 | )
56 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
2 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
3 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
4 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
5 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
6 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
7 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
8 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
9 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
10 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
11 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
12 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
13 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
14 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
15 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
16 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
17 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
18 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
19 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
20 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
21 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
22 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
23 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
24 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
25 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
31 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
32 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
33 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
34 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
35 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
36 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
37 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
38 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
39 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
40 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
41 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
42 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
43 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
44 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
45 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
46 | github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g=
47 | github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM=
48 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
49 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
50 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
51 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
54 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
55 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
56 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
57 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
58 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
59 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
60 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
61 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
62 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
63 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
64 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
65 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
66 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
67 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
68 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
69 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
70 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
71 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
72 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
73 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
75 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
77 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
78 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
79 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
80 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
81 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
82 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
83 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
84 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
85 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
86 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
87 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
88 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
89 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
90 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
91 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
92 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
93 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
94 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
95 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
96 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
97 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
98 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
99 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
100 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
101 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
102 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
103 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
104 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
105 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
106 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
107 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
108 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
109 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
110 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
111 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
112 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
113 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
114 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
115 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
116 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
117 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
118 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
119 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
120 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
121 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
123 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
124 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
125 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
126 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
128 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
129 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
130 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
131 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
132 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
133 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
134 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
135 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/noahgorstein/jqp/cmd"
7 | )
8 |
9 | func main() {
10 | err := cmd.Execute()
11 | if err != nil {
12 | // error is discarded as cobra already reported it
13 | os.Exit(1)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "definitions": {
4 | "override": {
5 | "type": "string",
6 | "examples": [
7 | "#000000",
8 | "#FF0000",
9 | "#00FF00",
10 | "#0000FF",
11 | "#FFFF00",
12 | "#FF00FF",
13 | "#00FFFF",
14 | "#FFFFFF",
15 | "bold",
16 | "underline",
17 | "italic",
18 | "bold #000000",
19 | "bold #FF0000",
20 | "bold #00FF00",
21 | "bold #0000FF",
22 | "bold #FFFF00",
23 | "bold #FF00FF",
24 | "bold #00FFFF",
25 | "bold #FFFFFF",
26 | "italic #000000",
27 | "italic #FF0000",
28 | "italic #00FF00",
29 | "italic #0000FF",
30 | "italic #FFFF00",
31 | "italic #FF00FF",
32 | "italic #00FFFF",
33 | "italic #FFFFFF",
34 | "underline #000000",
35 | "underline #FF0000",
36 | "underline #00FF00",
37 | "underline #0000FF",
38 | "underline #FFFF00",
39 | "underline #FF00FF",
40 | "underline #00FFFF",
41 | "underline #FFFFFF"
42 | ]
43 | }
44 | },
45 | "title": "jqp settings",
46 | "description": "jqp settings\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#configuration",
47 | "type": "object",
48 | "properties": {
49 | "theme": {
50 | "title": "theme",
51 | "description": "A theme\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#themes",
52 | "type": "object",
53 | "properties": {
54 | "name": {
55 | "title": "name",
56 | "description": "A theme name\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#themes",
57 | "type": "string",
58 | "minLength": 1,
59 | "enum": [
60 | "abap",
61 | "algol",
62 | "arduino",
63 | "autumn",
64 | "borland",
65 | "catppuccin-latte",
66 | "colorful",
67 | "emacs",
68 | "friendly",
69 | "github",
70 | "gruvbox-light",
71 | "hrdark",
72 | "igor",
73 | "lovelace",
74 | "manni",
75 | "monokai-light",
76 | "murphy",
77 | "onesenterprise",
78 | "paradaiso-light",
79 | "pastie",
80 | "perldoc",
81 | "pygments",
82 | "solarized-light",
83 | "tango",
84 | "trac",
85 | "visual_studio",
86 | "vulcan",
87 | "xcode",
88 | "average",
89 | "base16snazzy",
90 | "catppuccin-frappe",
91 | "catppuccin-macchiato",
92 | "catppuccin-mocha",
93 | "doom-one",
94 | "doom-one2",
95 | "dracula",
96 | "fruity",
97 | "github-dark",
98 | "gruvbox",
99 | "monokai",
100 | "native",
101 | "paradaiso-dark",
102 | "rrt",
103 | "solarized-dark",
104 | "solarized-dark256",
105 | "swapoff",
106 | "vim",
107 | "witchhazel",
108 | "xcode-dark"
109 | ]
110 | },
111 | "chromaStyleOverrides": {
112 | "title": "chroma style overrides",
113 | "description": "Chroma style overrides\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
114 | "type": "object",
115 | "properties": {
116 | "bg": {
117 | "title": "bg",
118 | "description": "A property which corresponds to Background\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
119 | "$ref": "#/definitions/override"
120 | },
121 | "chroma": {
122 | "title": "chroma",
123 | "description": "A property which corresponds to PreWrapper\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
124 | "$ref": "#/definitions/override"
125 | },
126 | "line": {
127 | "title": "line",
128 | "description": "A property which corresponds to Line\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
129 | "$ref": "#/definitions/override"
130 | },
131 | "ln": {
132 | "title": "ln",
133 | "description": "A property which corresponds to LineNumbers\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
134 | "$ref": "#/definitions/override"
135 | },
136 | "lnt": {
137 | "title": "lnt",
138 | "description": "A property which corresponds to LineNumbersTable\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
139 | "$ref": "#/definitions/override"
140 | },
141 | "hl": {
142 | "title": "hl",
143 | "description": "A property which corresponds to LineHighlight\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
144 | "$ref": "#/definitions/override"
145 | },
146 | "lntable": {
147 | "title": "lntable",
148 | "description": "A property which corresponds to LineTable\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
149 | "$ref": "#/definitions/override"
150 | },
151 | "lntd": {
152 | "title": "lntd",
153 | "description": "A property which corresponds to LineTableTD\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
154 | "$ref": "#/definitions/override"
155 | },
156 | "cl": {
157 | "title": "cl",
158 | "description": "A property which corresponds to CodeLine\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
159 | "$ref": "#/definitions/override"
160 | },
161 | "w": {
162 | "title": "w",
163 | "description": "A property which corresponds to Whitespace\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
164 | "$ref": "#/definitions/override"
165 | },
166 | "err": {
167 | "title": "err",
168 | "description": "A property which corresponds to Error\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
169 | "$ref": "#/definitions/override"
170 | },
171 | "x": {
172 | "title": "x",
173 | "description": "A property which corresponds to Other\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
174 | "$ref": "#/definitions/override"
175 | },
176 | "k": {
177 | "title": "k",
178 | "description": "A property which corresponds to Keyword\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
179 | "$ref": "#/definitions/override"
180 | },
181 | "kc": {
182 | "title": "kc",
183 | "description": "A property which corresponds to KeywordConstant\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
184 | "$ref": "#/definitions/override"
185 | },
186 | "kd": {
187 | "title": "kd",
188 | "description": "A property which corresponds to KeywordDeclaration\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
189 | "$ref": "#/definitions/override"
190 | },
191 | "kn": {
192 | "title": "kn",
193 | "description": "A property which corresponds to KeywordNamespace\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
194 | "$ref": "#/definitions/override"
195 | },
196 | "kp": {
197 | "title": "kp",
198 | "description": "A property which corresponds to KeywordPseudo\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
199 | "$ref": "#/definitions/override"
200 | },
201 | "kr": {
202 | "title": "kr",
203 | "description": "A property which corresponds to KeywordReserved\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
204 | "$ref": "#/definitions/override"
205 | },
206 | "kt": {
207 | "title": "kt",
208 | "description": "A property which corresponds to KeywordType\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
209 | "$ref": "#/definitions/override"
210 | },
211 | "n": {
212 | "title": "n",
213 | "description": "A property which corresponds to Name\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
214 | "$ref": "#/definitions/override"
215 | },
216 | "na": {
217 | "title": "na",
218 | "description": "A property which corresponds to NameAttribute\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
219 | "$ref": "#/definitions/override"
220 | },
221 | "nb": {
222 | "title": "nb",
223 | "description": "A property which corresponds to NameBuiltin\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
224 | "$ref": "#/definitions/override"
225 | },
226 | "bp": {
227 | "title": "bp",
228 | "description": "A property which corresponds to NameBuiltinPseudo\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
229 | "$ref": "#/definitions/override"
230 | },
231 | "nc": {
232 | "title": "nc",
233 | "description": "A property which corresponds to NameClass\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
234 | "$ref": "#/definitions/override"
235 | },
236 | "no": {
237 | "title": "no",
238 | "description": "A property which corresponds to NameConstant\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
239 | "$ref": "#/definitions/override"
240 | },
241 | "nd": {
242 | "title": "nd",
243 | "description": "A property which corresponds to NameDecorator\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
244 | "$ref": "#/definitions/override"
245 | },
246 | "ni": {
247 | "title": "ni",
248 | "description": "A property which corresponds to NameEntity\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
249 | "$ref": "#/definitions/override"
250 | },
251 | "ne": {
252 | "title": "ne",
253 | "description": "A property which corresponds to NameException\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
254 | "$ref": "#/definitions/override"
255 | },
256 | "nf": {
257 | "title": "nf",
258 | "description": "A property which corresponds to NameFunction\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
259 | "$ref": "#/definitions/override"
260 | },
261 | "fm": {
262 | "title": "fm",
263 | "description": "A property which corresponds to NameFunctionMagic\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
264 | "$ref": "#/definitions/override"
265 | },
266 | "py": {
267 | "title": "py",
268 | "description": "A property which corresponds to NameProperty\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
269 | "$ref": "#/definitions/override"
270 | },
271 | "nl": {
272 | "title": "nl",
273 | "description": "A property which corresponds to NameLabel\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
274 | "$ref": "#/definitions/override"
275 | },
276 | "nn": {
277 | "title": "nn",
278 | "description": "A property which corresponds to NameNamespace\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
279 | "$ref": "#/definitions/override"
280 | },
281 | "nx": {
282 | "title": "nx",
283 | "description": "A property which corresponds to NameOther\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
284 | "$ref": "#/definitions/override"
285 | },
286 | "nt": {
287 | "title": "nt",
288 | "description": "A property which corresponds to NameTag\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
289 | "$ref": "#/definitions/override"
290 | },
291 | "nv": {
292 | "title": "nv",
293 | "description": "A property which corresponds to NameVariable\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
294 | "$ref": "#/definitions/override"
295 | },
296 | "vc": {
297 | "title": "vc",
298 | "description": "A property which corresponds to NameVariableClass\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
299 | "$ref": "#/definitions/override"
300 | },
301 | "vg": {
302 | "title": "vg",
303 | "description": "A property which corresponds to NameVariableGlobal\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
304 | "$ref": "#/definitions/override"
305 | },
306 | "vi": {
307 | "title": "vi",
308 | "description": "A property which corresponds to NameVariableInstance\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
309 | "$ref": "#/definitions/override"
310 | },
311 | "vm": {
312 | "title": "vm",
313 | "description": "A property which corresponds to NameVariableMagic\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
314 | "$ref": "#/definitions/override"
315 | },
316 | "l": {
317 | "title": "l",
318 | "description": "A property which corresponds to Literal\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
319 | "$ref": "#/definitions/override"
320 | },
321 | "ld": {
322 | "title": "ld",
323 | "description": "A property which corresponds to LiteralDate\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
324 | "$ref": "#/definitions/override"
325 | },
326 | "s": {
327 | "title": "s",
328 | "description": "A property which corresponds to String\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
329 | "$ref": "#/definitions/override"
330 | },
331 | "sa": {
332 | "title": "sa",
333 | "description": "A property which corresponds to StringAffix\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
334 | "$ref": "#/definitions/override"
335 | },
336 | "sb": {
337 | "title": "sb",
338 | "description": "A property which corresponds to StringBacktick\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
339 | "$ref": "#/definitions/override"
340 | },
341 | "sc": {
342 | "title": "sc",
343 | "description": "A property which corresponds to StringChar\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
344 | "$ref": "#/definitions/override"
345 | },
346 | "dl": {
347 | "title": "dl",
348 | "description": "A property which corresponds to StringDelimiter\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
349 | "$ref": "#/definitions/override"
350 | },
351 | "sd": {
352 | "title": "sd",
353 | "description": "A property which corresponds to StringDoc\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
354 | "$ref": "#/definitions/override"
355 | },
356 | "s2": {
357 | "title": "s2",
358 | "description": "A property which corresponds to StringDouble\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
359 | "$ref": "#/definitions/override"
360 | },
361 | "se": {
362 | "title": "se",
363 | "description": "A property which corresponds to StringEscape\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
364 | "$ref": "#/definitions/override"
365 | },
366 | "sh": {
367 | "title": "sh",
368 | "description": "A property which corresponds to StringHeredoc\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
369 | "$ref": "#/definitions/override"
370 | },
371 | "si": {
372 | "title": "si",
373 | "description": "A property which corresponds to StringInterpol\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
374 | "$ref": "#/definitions/override"
375 | },
376 | "sx": {
377 | "title": "sx",
378 | "description": "A property which corresponds to StringOther\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
379 | "$ref": "#/definitions/override"
380 | },
381 | "sr": {
382 | "title": "sr",
383 | "description": "A property which corresponds to StringRegex\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
384 | "$ref": "#/definitions/override"
385 | },
386 | "s1": {
387 | "title": "s1",
388 | "description": "A property which corresponds to StringSingle\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
389 | "$ref": "#/definitions/override"
390 | },
391 | "ss": {
392 | "title": "ss",
393 | "description": "A property which corresponds to StringSymbol\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
394 | "$ref": "#/definitions/override"
395 | },
396 | "m": {
397 | "title": "m",
398 | "description": "A property which corresponds to Number\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
399 | "$ref": "#/definitions/override"
400 | },
401 | "mb": {
402 | "title": "mb",
403 | "description": "A property which corresponds to NumberBin\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
404 | "$ref": "#/definitions/override"
405 | },
406 | "mf": {
407 | "title": "mf",
408 | "description": "A property which corresponds to NumberFloat\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
409 | "$ref": "#/definitions/override"
410 | },
411 | "mh": {
412 | "title": "mh",
413 | "description": "A property which corresponds to NumberHex\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
414 | "$ref": "#/definitions/override"
415 | },
416 | "mi": {
417 | "title": "mi",
418 | "description": "A property which corresponds to NumberInteger\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
419 | "$ref": "#/definitions/override"
420 | },
421 | "il": {
422 | "title": "il",
423 | "description": "A property which corresponds to NumberIntegerLong\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
424 | "$ref": "#/definitions/override"
425 | },
426 | "mo": {
427 | "title": "mo",
428 | "description": "A property which corresponds to NumberOct\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
429 | "$ref": "#/definitions/override"
430 | },
431 | "o": {
432 | "title": "o",
433 | "description": "A property which corresponds to Operator\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
434 | "$ref": "#/definitions/override"
435 | },
436 | "ow": {
437 | "title": "ow",
438 | "description": "A property which corresponds to OperatorWord\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
439 | "$ref": "#/definitions/override"
440 | },
441 | "p": {
442 | "title": "p",
443 | "description": "A property which corresponds to Punctuation\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
444 | "$ref": "#/definitions/override"
445 | },
446 | "c": {
447 | "title": "c",
448 | "description": "A property which corresponds to Comment\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
449 | "$ref": "#/definitions/override"
450 | },
451 | "ch": {
452 | "title": "ch",
453 | "description": "A property which corresponds to CommentHashbang\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
454 | "$ref": "#/definitions/override"
455 | },
456 | "cm": {
457 | "title": "cm",
458 | "description": "A property which corresponds to CommentMultiline\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
459 | "$ref": "#/definitions/override"
460 | },
461 | "cp": {
462 | "title": "cp",
463 | "description": "A property which corresponds to CommentPreproc\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
464 | "$ref": "#/definitions/override"
465 | },
466 | "cpf": {
467 | "title": "cpf",
468 | "description": "A property which corresponds to CommentPreprocFile\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
469 | "$ref": "#/definitions/override"
470 | },
471 | "c1": {
472 | "title": "c1",
473 | "description": "A property which corresponds to CommentSingle\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
474 | "$ref": "#/definitions/override"
475 | },
476 | "cs": {
477 | "title": "cs",
478 | "description": "A property which corresponds to CommentSpecial\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
479 | "$ref": "#/definitions/override"
480 | },
481 | "g": {
482 | "title": "g",
483 | "description": "A property which corresponds to Generic\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
484 | "$ref": "#/definitions/override"
485 | },
486 | "gd": {
487 | "title": "gd",
488 | "description": "A property which corresponds to GenericDeleted\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
489 | "$ref": "#/definitions/override"
490 | },
491 | "ge": {
492 | "title": "ge",
493 | "description": "A property which corresponds to GenericEmph\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
494 | "$ref": "#/definitions/override"
495 | },
496 | "gr": {
497 | "title": "gr",
498 | "description": "A property which corresponds to GenericError\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
499 | "$ref": "#/definitions/override"
500 | },
501 | "gh": {
502 | "title": "gh",
503 | "description": "A property which corresponds to GenericHeading\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
504 | "$ref": "#/definitions/override"
505 | },
506 | "gi": {
507 | "title": "gi",
508 | "description": "A property which corresponds to GenericInserted\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
509 | "$ref": "#/definitions/override"
510 | },
511 | "go": {
512 | "title": "go",
513 | "description": "A property which corresponds to GenericOutput\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
514 | "$ref": "#/definitions/override"
515 | },
516 | "gp": {
517 | "title": "gp",
518 | "description": "A property which corresponds to GenericPrompt\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
519 | "$ref": "#/definitions/override"
520 | },
521 | "gs": {
522 | "title": "gs",
523 | "description": "A property which corresponds to GenericStrong\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
524 | "$ref": "#/definitions/override"
525 | },
526 | "gu": {
527 | "title": "gu",
528 | "description": "A property which corresponds to GenericSubheading\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
529 | "$ref": "#/definitions/override"
530 | },
531 | "gt": {
532 | "title": "gt",
533 | "description": "A property which corresponds to GenericTraceback\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
534 | "$ref": "#/definitions/override"
535 | },
536 | "gl": {
537 | "title": "gl",
538 | "description": "A property which corresponds to GenericUnderline\nhttps://github.com/noahgorstein/jqp?tab=readme-ov-file#chroma-style-overrides",
539 | "$ref": "#/definitions/override"
540 | }
541 | },
542 | "minProperties": 1,
543 | "additionalProperties": false
544 | }
545 | },
546 | "minProperties": 1,
547 | "additionalProperties": false
548 | }
549 | },
550 | "minProperties": 1,
551 | "additionalProperties": false
552 | }
553 |
--------------------------------------------------------------------------------
/snap/local/jqp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noahgorstein/jqp/eb2bb34433dd186ada53795ab822ab3bb5f5ba94/snap/local/jqp.png
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: jqp
2 | adopt-info: jqp
3 | summary: jqp is a TUI for exploring the jq command line utility
4 | description: |
5 | jqp is a terminal user interface (TUI) for exploring the jq command line utility.
6 |
7 | You can use it to run jq queries interactively. If no query is provided, the interface will prompt
8 | you for one.
9 |
10 | The command accepts an optional query argument which will be executed against the input JSON or
11 | newline-delimited JSON (NDJSON).
12 |
13 | You can provide the input JSON or NDJSON either through a file or via standard input (stdin).
14 |
15 | Usage:
16 | jqp [query] [flags]
17 |
18 | Flags:
19 | --config string path to config file (default is $HOME/.jqp.yaml)
20 | -f, --file string path to the input JSON file
21 | -h, --help help for jqp
22 | -t, --theme string jqp theme
23 | -v, --version version for jqp
24 |
25 | license: MIT
26 | issues: https://github.com/kz6fittycent/jqp
27 | contact: https://github.com/kz6fittycent/jqp
28 | source-code: https://github.com/noahgorstein/jqp
29 | icon: snap/local/jqp.png
30 |
31 | base: core24
32 | grade: stable
33 | confinement: strict
34 | compression: lzo
35 |
36 | platforms:
37 | amd64:
38 | build-on: [amd64]
39 | build-for: [amd64]
40 | arm64:
41 | build-on: [arm64]
42 | build-for: [arm64]
43 | armhf:
44 | build-on: [armhf]
45 | build-for: [armhf]
46 | ppc64el:
47 | build-on: [ppc64el]
48 | build-for: [ppc64el]
49 | s390x:
50 | build-on: [s390x]
51 | build-for: [s390x]
52 |
53 | apps:
54 | jqp:
55 | command: bin/jqp
56 | plugs:
57 | - home
58 |
59 | parts:
60 | jqp:
61 | source: https://github.com/noahgorstein/jqp
62 | source-type: git
63 | plugin: go
64 | build-snaps:
65 | - go
66 |
67 | override-pull: |
68 | craftctl default
69 | craftctl set version="$(git describe --tags | sed 's/^v//' | cut -d "-" -f1)"
70 |
--------------------------------------------------------------------------------
/tui/bubbles/fileselector/fileselector.go:
--------------------------------------------------------------------------------
1 | package fileselector
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/textinput"
5 | tea "github.com/charmbracelet/bubbletea"
6 | "github.com/charmbracelet/lipgloss"
7 |
8 | "github.com/noahgorstein/jqp/tui/theme"
9 | )
10 |
11 | type Bubble struct {
12 | Styles Styles
13 | textinput textinput.Model
14 | }
15 |
16 | func New(jqtheme theme.Theme) Bubble {
17 | s := DefaultStyles()
18 | ti := textinput.New()
19 | ti.Focus()
20 | ti.PromptStyle = s.promptStyle.Foreground(jqtheme.Secondary)
21 | s.inputLabelStyle.Foreground(jqtheme.Primary)
22 |
23 | return Bubble{
24 | Styles: s,
25 | textinput: ti,
26 | }
27 | }
28 |
29 | func (b Bubble) GetInput() string {
30 | return b.textinput.Value()
31 | }
32 |
33 | func (b *Bubble) SetInput(input string) {
34 | b.textinput.SetValue(input)
35 | }
36 |
37 | func (Bubble) Init() tea.Cmd {
38 | return nil
39 | }
40 |
41 | func (b Bubble) View() string {
42 | return b.Styles.containerStyle.Render(
43 | lipgloss.JoinVertical(
44 | lipgloss.Left,
45 | b.Styles.inputLabelStyle.Render("Save output to file (leave empty to copy to clipboard): "),
46 | b.textinput.View()))
47 | }
48 |
49 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
50 | var (
51 | cmd tea.Cmd
52 | cmds []tea.Cmd
53 | )
54 |
55 | b.textinput, cmd = b.textinput.Update(msg)
56 | cmds = append(cmds, cmd)
57 |
58 | return b, tea.Batch(cmds...)
59 | }
60 |
61 | func (b Bubble) SetSize(width int) {
62 | b.Styles.containerStyle = b.Styles.containerStyle.Width(width - b.Styles.containerStyle.GetHorizontalFrameSize())
63 | }
64 |
--------------------------------------------------------------------------------
/tui/bubbles/fileselector/styles.go:
--------------------------------------------------------------------------------
1 | package fileselector
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type Styles struct {
8 | containerStyle lipgloss.Style
9 | inputLabelStyle lipgloss.Style
10 | promptStyle lipgloss.Style
11 | }
12 |
13 | func DefaultStyles() (s Styles) {
14 | s.containerStyle = lipgloss.NewStyle().Align(lipgloss.Left).PaddingLeft(1)
15 | s.inputLabelStyle = lipgloss.NewStyle().Bold(true)
16 | s.promptStyle = lipgloss.NewStyle().Bold(true)
17 |
18 | return s
19 | }
20 |
--------------------------------------------------------------------------------
/tui/bubbles/help/help.go:
--------------------------------------------------------------------------------
1 | package help
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/help"
5 | "github.com/charmbracelet/bubbles/key"
6 | "github.com/charmbracelet/bubbletea"
7 |
8 | "github.com/noahgorstein/jqp/tui/bubbles/state"
9 | "github.com/noahgorstein/jqp/tui/theme"
10 | )
11 |
12 | type Bubble struct {
13 | state state.State
14 | help help.Model
15 | keys keyMap
16 | Styles Styles
17 | }
18 |
19 | func New(jqtheme theme.Theme) Bubble {
20 | styles := DefaultStyles()
21 | model := help.New()
22 | model.Styles.ShortKey = styles.helpKeyStyle.Foreground(jqtheme.Primary)
23 | model.Styles.ShortDesc = styles.helpTextStyle.Foreground(jqtheme.Secondary)
24 | model.Styles.ShortSeparator = styles.helpSeparatorStyle.Foreground(jqtheme.Inactive)
25 |
26 | return Bubble{
27 | state: state.Query,
28 | Styles: styles,
29 | help: model,
30 | keys: keys,
31 | }
32 | }
33 |
34 | func (b Bubble) collectHelpBindings() []key.Binding {
35 | k := b.keys
36 | bindings := []key.Binding{}
37 | switch b.state {
38 | case state.Query:
39 | bindings = append(bindings, k.submit, k.section, k.copyQuery, k.save)
40 | case state.Running:
41 | bindings = append(bindings, k.abort)
42 | case state.Input, state.Output:
43 | bindings = append(bindings, k.section, k.navigate, k.page, k.copyQuery, k.save)
44 | case state.Save:
45 | bindings = append(bindings, k.back)
46 | }
47 |
48 | return bindings
49 | }
50 |
51 | func (b *Bubble) SetWidth(width int) {
52 | b.Styles.helpbarStyle = b.Styles.helpbarStyle.Width(width - 1)
53 | }
54 |
55 | func (Bubble) Init() tea.Cmd {
56 | return nil
57 | }
58 |
59 | func (b Bubble) View() string {
60 | return b.Styles.helpbarStyle.Render(b.help.ShortHelpView(b.collectHelpBindings()))
61 | }
62 |
63 | func (b *Bubble) SetState(mode state.State) {
64 | b.state = mode
65 | }
66 |
67 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
68 | var cmd tea.Cmd
69 |
70 | if msg, ok := msg.(tea.WindowSizeMsg); ok {
71 | b.SetWidth(msg.Width)
72 | }
73 |
74 | return b, tea.Batch(cmd)
75 | }
76 |
--------------------------------------------------------------------------------
/tui/bubbles/help/keys.go:
--------------------------------------------------------------------------------
1 | package help
2 |
3 | import "github.com/charmbracelet/bubbles/key"
4 |
5 | type keyMap struct {
6 | section key.Binding
7 | back key.Binding
8 | submit key.Binding
9 | abort key.Binding
10 | navigate key.Binding
11 | page key.Binding
12 | save key.Binding
13 | copyQuery key.Binding
14 | }
15 |
16 | var keys = keyMap{
17 | section: key.NewBinding(
18 | key.WithKeys("tab"),
19 | key.WithHelp("tab", "section")),
20 | back: key.NewBinding(
21 | key.WithKeys("esc"),
22 | key.WithHelp("esc", "back")),
23 | submit: key.NewBinding(
24 | key.WithKeys("enter"),
25 | key.WithHelp("enter", "submit query")),
26 | abort: key.NewBinding(
27 | key.WithKeys("ctrl+c"),
28 | key.WithHelp("ctrl+c", "abort running query")),
29 | navigate: key.NewBinding(
30 | key.WithKeys("↑↓"),
31 | key.WithHelp("↑↓", "scroll")),
32 | page: key.NewBinding(
33 | key.WithKeys("ctrl+u/ctrl+d"),
34 | key.WithHelp("ctrl+u/ctrl+d", "page up/down")),
35 | save: key.NewBinding(
36 | key.WithKeys("ctrl+s"),
37 | key.WithHelp("ctrl+s", "save output")),
38 | copyQuery: key.NewBinding(
39 | key.WithKeys("ctrl+y"),
40 | key.WithHelp("ctrl+y", "copy query")),
41 | }
42 |
--------------------------------------------------------------------------------
/tui/bubbles/help/styles.go:
--------------------------------------------------------------------------------
1 | package help
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type Styles struct {
8 | helpbarStyle lipgloss.Style
9 | helpKeyStyle lipgloss.Style
10 | helpTextStyle lipgloss.Style
11 | helpSeparatorStyle lipgloss.Style
12 | }
13 |
14 | func DefaultStyles() (s Styles) {
15 | s.helpbarStyle = lipgloss.NewStyle().MarginLeft(1).MarginBottom(1)
16 |
17 | s.helpKeyStyle = lipgloss.NewStyle().Bold(true)
18 |
19 | s.helpSeparatorStyle = lipgloss.NewStyle().Bold(true)
20 |
21 | s.helpTextStyle = lipgloss.NewStyle()
22 |
23 | return s
24 | }
25 |
--------------------------------------------------------------------------------
/tui/bubbles/inputdata/inputdata.go:
--------------------------------------------------------------------------------
1 | package inputdata
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/charmbracelet/bubbles/viewport"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/charmbracelet/lipgloss"
11 |
12 | "github.com/noahgorstein/jqp/tui/theme"
13 | "github.com/noahgorstein/jqp/tui/utils"
14 | )
15 |
16 | type Bubble struct {
17 | styles Styles
18 | viewport viewport.Model
19 | height int
20 | width int
21 | inputJSON []byte
22 | highlightedJSON *bytes.Buffer
23 | filename string
24 | theme theme.Theme
25 | setInitialContentSub chan setPrettifiedContentMsg
26 | }
27 |
28 | func New(inputJSON []byte, filename string, jqtheme theme.Theme) (Bubble, error) {
29 | styles := DefaultStyles()
30 | styles.containerStyle = styles.containerStyle.BorderForeground(jqtheme.Inactive)
31 | styles.infoStyle = styles.infoStyle.BorderForeground(jqtheme.Inactive)
32 |
33 | v := viewport.New(0, 0)
34 | v.SetContent("Loading...")
35 |
36 | b := Bubble{
37 | styles: styles,
38 | viewport: v,
39 | inputJSON: inputJSON,
40 | filename: filename,
41 | theme: jqtheme,
42 | setInitialContentSub: make(chan setPrettifiedContentMsg),
43 | }
44 | return b, nil
45 | }
46 |
47 | func (b *Bubble) SetBorderColor(color lipgloss.TerminalColor) {
48 | b.styles.containerStyle = b.styles.containerStyle.BorderForeground(color)
49 | b.styles.infoStyle = b.styles.infoStyle.BorderForeground(color)
50 | }
51 |
52 | func (b Bubble) GetInputJSON() []byte {
53 | return b.inputJSON
54 | }
55 |
56 | func (b Bubble) GetHighlightedInputJSON() []byte {
57 | return b.highlightedJSON.Bytes()
58 | }
59 |
60 | func (b *Bubble) SetSize(width, height int) {
61 | b.width = width
62 | b.height = height
63 |
64 | b.styles.containerStyle = b.styles.containerStyle.
65 | Width(width - b.styles.containerStyle.GetHorizontalFrameSize()/2).
66 | Height(height - b.styles.containerStyle.GetVerticalFrameSize())
67 |
68 | b.viewport.Width = width - b.styles.containerStyle.GetHorizontalFrameSize()
69 | b.viewport.Height = height - b.styles.containerStyle.GetVerticalFrameSize() - 3
70 | }
71 |
72 | func (b Bubble) View() string {
73 | scrollPercent := fmt.Sprintf("%3.f%%", b.viewport.ScrollPercent()*100)
74 |
75 | info := b.styles.infoStyle.Render(fmt.Sprintf("%s | %s", lipgloss.NewStyle().Italic(true).Render(b.filename), scrollPercent))
76 | line := strings.Repeat(" ", max(0, b.viewport.Width-lipgloss.Width(info)))
77 |
78 | footer := lipgloss.JoinHorizontal(lipgloss.Center, line, info)
79 | content := lipgloss.JoinVertical(lipgloss.Left, b.viewport.View(), footer)
80 |
81 | return b.styles.containerStyle.Render(content)
82 | }
83 |
84 | func (b *Bubble) SetContent(content string) {
85 | formattedContent := lipgloss.NewStyle().Width(b.viewport.Width - 3).Render(content)
86 | b.viewport.SetContent(formattedContent)
87 | }
88 |
89 | // ReadyMsg signals that the inputdata Bubble has loaded the user's data
90 | // into the viewport
91 | type ReadyMsg struct{}
92 |
93 | // setPrettifiedContentMsg contains the input data prettified
94 | type setPrettifiedContentMsg struct {
95 | Content *bytes.Buffer
96 | }
97 |
98 | // prettifyContentCmd sends the initial prettified content to the provided channel.
99 | //
100 | // Prettifying the input data can be an expensive operation particularly for large inputs, so it is performed here and
101 | // sent through the channel to ensure the prettified data is available without blocking other operations.
102 | func (b Bubble) prettifyContentCmd(sub chan setPrettifiedContentMsg, isJSONLines bool) tea.Cmd {
103 | return func() tea.Msg {
104 | prettifiedData, _ := utils.Prettify(b.inputJSON, b.theme.ChromaStyle, isJSONLines)
105 | sub <- setPrettifiedContentMsg{Content: prettifiedData}
106 | return nil
107 | }
108 | }
109 |
110 | // A command that waits for a setPrettifiedContentMsg on a channel.
111 | func waitForPrettifiedContent(sub chan setPrettifiedContentMsg) tea.Cmd {
112 | return func() tea.Msg {
113 | return setPrettifiedContentMsg(<-sub)
114 | }
115 | }
116 |
117 | func (b Bubble) Init(isJSONLines bool) tea.Cmd {
118 | return tea.Batch(
119 | b.prettifyContentCmd(b.setInitialContentSub, isJSONLines),
120 | waitForPrettifiedContent(b.setInitialContentSub))
121 | }
122 |
123 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
124 | var (
125 | cmd tea.Cmd
126 | cmds []tea.Cmd
127 | )
128 |
129 | if msg, ok := msg.(setPrettifiedContentMsg); ok {
130 | b.highlightedJSON = msg.Content
131 | b.SetContent(msg.Content.String())
132 | return b, func() tea.Msg {
133 | return ReadyMsg{}
134 | }
135 | }
136 |
137 | b.viewport, cmd = b.viewport.Update(msg)
138 | cmds = append(cmds, cmd)
139 |
140 | return b, tea.Batch(cmds...)
141 | }
142 |
--------------------------------------------------------------------------------
/tui/bubbles/inputdata/styles.go:
--------------------------------------------------------------------------------
1 | package inputdata
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type Styles struct {
8 | infoStyle lipgloss.Style
9 | containerStyle lipgloss.Style
10 | }
11 |
12 | func DefaultStyles() (s Styles) {
13 | s.infoStyle = lipgloss.NewStyle().Bold(true).Border(lipgloss.RoundedBorder()).Padding(0, 1)
14 | s.containerStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1)
15 |
16 | return s
17 | }
18 |
--------------------------------------------------------------------------------
/tui/bubbles/jqplayground/commands.go:
--------------------------------------------------------------------------------
1 | package jqplayground
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "strings"
10 |
11 | "github.com/atotto/clipboard"
12 | tea "github.com/charmbracelet/bubbletea"
13 | "github.com/itchyny/gojq"
14 |
15 | "github.com/noahgorstein/jqp/tui/utils"
16 | )
17 |
18 | type errorMsg struct {
19 | error error
20 | }
21 |
22 | type queryResultMsg struct {
23 | rawResults string
24 | highlightedResults string
25 | }
26 |
27 | type writeToFileMsg struct{}
28 |
29 | type copyQueryToClipboardMsg struct{}
30 |
31 | type copyResultsToClipboardMsg struct{}
32 |
33 | // processQueryResults iterates through the results of a gojq query on the provided JSON object
34 | // and appends the formatted results to the provided string builder.
35 | func processQueryResults(ctx context.Context, results *strings.Builder, query *gojq.Query, obj any) error {
36 | iter := query.RunWithContext(ctx, obj)
37 | for {
38 | v, ok := iter.Next()
39 | if !ok {
40 | break
41 | }
42 |
43 | if err, ok := v.(error); ok {
44 | return err
45 | }
46 |
47 | if r, err := gojq.Marshal(v); err == nil {
48 | results.WriteString(fmt.Sprintf("%s\n", string(r)))
49 | }
50 | }
51 | return nil
52 | }
53 |
54 | func processJSONWithQuery(ctx context.Context, results *strings.Builder, query *gojq.Query, data []byte) error {
55 | var obj any
56 | d := json.NewDecoder(bytes.NewReader(data))
57 | d.UseNumber()
58 | if err := d.Decode(&obj); err != nil {
59 | return err
60 | }
61 | err := processQueryResults(ctx, results, query, obj)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | return nil
67 | }
68 |
69 | func processJSONLinesWithQuery(ctx context.Context, results *strings.Builder, query *gojq.Query, data []byte) error {
70 | const maxBufferSize = 100 * 1024 * 1024 // 100MB max buffer size
71 |
72 | processLine := func(line []byte) error {
73 | return processJSONWithQuery(ctx, results, query, line)
74 | }
75 |
76 | return utils.ScanLinesWithDynamicBufferSize(data, maxBufferSize, processLine)
77 | }
78 |
79 | func (b *Bubble) executeQueryOnInput(ctx context.Context) (string, error) {
80 | var results strings.Builder
81 | query, err := gojq.Parse(b.queryinput.GetInputValue())
82 | if err != nil {
83 | return "", err
84 | }
85 |
86 | processor := processJSONWithQuery
87 |
88 | if b.isJSONLines {
89 | processor = processJSONLinesWithQuery
90 | }
91 | if err := processor(ctx, &results, query, b.inputdata.GetInputJSON()); err != nil {
92 | return "", err
93 | }
94 | return results.String(), nil
95 | }
96 |
97 | func (b *Bubble) executeQueryCommand(ctx context.Context) tea.Cmd {
98 | return func() tea.Msg {
99 | results, err := b.executeQueryOnInput(ctx)
100 | if err != nil {
101 | return errorMsg{error: err}
102 | }
103 | highlightedOutput, err := utils.Prettify([]byte(results), b.theme.ChromaStyle, true)
104 | if err != nil {
105 | return errorMsg{error: err}
106 | }
107 | return queryResultMsg{
108 | rawResults: results,
109 | highlightedResults: highlightedOutput.String(),
110 | }
111 | }
112 | }
113 |
114 | func (b Bubble) saveOutput() tea.Cmd {
115 | if b.fileselector.GetInput() == "" {
116 | return b.copyOutputToClipboard()
117 | }
118 | return b.writeOutputToFile()
119 | }
120 |
121 | func (b Bubble) copyOutputToClipboard() tea.Cmd {
122 | return func() tea.Msg {
123 | err := clipboard.WriteAll(b.results)
124 | if err != nil {
125 | return errorMsg{
126 | error: err,
127 | }
128 | }
129 | return copyResultsToClipboardMsg{}
130 | }
131 | }
132 |
133 | func (b Bubble) writeOutputToFile() tea.Cmd {
134 | return func() tea.Msg {
135 | err := os.WriteFile(b.fileselector.GetInput(), []byte(b.results), 0o600)
136 | if err != nil {
137 | return errorMsg{
138 | error: err,
139 | }
140 | }
141 | return writeToFileMsg{}
142 | }
143 | }
144 |
145 | func (b Bubble) copyQueryToClipboard() tea.Cmd {
146 | return func() tea.Msg {
147 | err := clipboard.WriteAll(b.queryinput.GetInputValue())
148 | if err != nil {
149 | return errorMsg{
150 | error: err,
151 | }
152 | }
153 | return copyQueryToClipboardMsg{}
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/tui/bubbles/jqplayground/init.go:
--------------------------------------------------------------------------------
1 | package jqplayground
2 |
3 | import (
4 | tea "github.com/charmbracelet/bubbletea"
5 |
6 | "github.com/noahgorstein/jqp/tui/utils"
7 | )
8 |
9 | // invalidInputMsg signals that the user's data is not valid JSON or NDJSON
10 | type invalidInputMsg struct{}
11 |
12 | type setupMsg struct {
13 | isJSONLines bool
14 | }
15 |
16 | // initialQueryMsg represents a message containing an initial query string to execute when
17 | // the app is loaded.
18 | type initialQueryMsg struct {
19 | query string
20 | }
21 |
22 | func setupCmd(isJSONLines bool) tea.Cmd {
23 | return func() tea.Msg {
24 | return setupMsg{isJSONLines: isJSONLines}
25 | }
26 | }
27 |
28 | // initialQueryCmd creates a command that returns an initialQueryMsg with the provided query string.
29 | func initialQueryCmd(query string) tea.Cmd {
30 | return func() tea.Msg {
31 | return initialQueryMsg{query: query}
32 | }
33 | }
34 |
35 | func (b Bubble) Init() tea.Cmd {
36 | var cmds []tea.Cmd
37 |
38 | // validate input data
39 | _, isJSONLines, err := utils.IsValidInput(b.inputdata.GetInputJSON())
40 | if err != nil {
41 | return func() tea.Msg {
42 | return invalidInputMsg{}
43 | }
44 | }
45 |
46 | // initialize rest of app
47 | cmds = append(cmds, b.queryinput.Init(), b.inputdata.Init(isJSONLines), setupCmd(isJSONLines))
48 | if b.queryinput.GetInputValue() != "" {
49 | cmds = append(cmds, initialQueryCmd(b.queryinput.GetInputValue()))
50 | }
51 | return tea.Sequence(cmds...)
52 | }
53 |
--------------------------------------------------------------------------------
/tui/bubbles/jqplayground/model.go:
--------------------------------------------------------------------------------
1 | package jqplayground
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/noahgorstein/jqp/tui/bubbles/fileselector"
8 | "github.com/noahgorstein/jqp/tui/bubbles/help"
9 | "github.com/noahgorstein/jqp/tui/bubbles/inputdata"
10 | "github.com/noahgorstein/jqp/tui/bubbles/output"
11 | "github.com/noahgorstein/jqp/tui/bubbles/queryinput"
12 | "github.com/noahgorstein/jqp/tui/bubbles/state"
13 | "github.com/noahgorstein/jqp/tui/bubbles/statusbar"
14 | "github.com/noahgorstein/jqp/tui/theme"
15 | )
16 |
17 | func (b Bubble) GetState() state.State {
18 | return b.state
19 | }
20 |
21 | type Bubble struct {
22 | width int
23 | height int
24 | workingDirectory string
25 | state state.State
26 | queryinput queryinput.Bubble
27 | inputdata inputdata.Bubble
28 | output output.Bubble
29 | help help.Bubble
30 | statusbar statusbar.Bubble
31 | fileselector fileselector.Bubble
32 | results string
33 | cancel func()
34 | theme theme.Theme
35 | ExitMessage string
36 | isJSONLines bool
37 | }
38 |
39 | func New(inputJSON []byte, filename string, query string, jqtheme theme.Theme) (Bubble, error) {
40 | workingDirectory, err := os.Getwd()
41 | if err != nil {
42 | return Bubble{}, err
43 | }
44 |
45 | sb := statusbar.New(jqtheme)
46 | sb.StatusMessageLifetime = time.Second * 10
47 | fs := fileselector.New(jqtheme)
48 |
49 | fs.SetInput(workingDirectory)
50 |
51 | inputData, err := inputdata.New(inputJSON, filename, jqtheme)
52 | if err != nil {
53 | return Bubble{}, err
54 | }
55 | queryInput := queryinput.New(jqtheme)
56 | if query != "" {
57 | queryInput.SetQuery(query)
58 | }
59 |
60 | b := Bubble{
61 | workingDirectory: workingDirectory,
62 | state: state.Loading,
63 | queryinput: queryInput,
64 | inputdata: inputData,
65 | output: output.New(jqtheme),
66 | help: help.New(jqtheme),
67 | statusbar: sb,
68 | fileselector: fs,
69 | theme: jqtheme,
70 | }
71 | return b, nil
72 | }
73 |
--------------------------------------------------------------------------------
/tui/bubbles/jqplayground/update.go:
--------------------------------------------------------------------------------
1 | package jqplayground
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/charmbracelet/bubbles/textinput"
8 | tea "github.com/charmbracelet/bubbletea"
9 | "github.com/charmbracelet/lipgloss"
10 |
11 | "github.com/noahgorstein/jqp/tui/bubbles/inputdata"
12 | "github.com/noahgorstein/jqp/tui/bubbles/state"
13 | )
14 |
15 | func (b Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
16 | var cmds []tea.Cmd
17 |
18 | prevState := b.state
19 | b.handleMessage(msg, &cmds)
20 | b.updateState(prevState, &cmds)
21 | b.updateComponents(msg, &cmds)
22 |
23 | return b, tea.Batch(cmds...)
24 | }
25 |
26 | func totalHeight(bubbles ...interface{ View() string }) int {
27 | var height int
28 | for _, bubble := range bubbles {
29 | height += lipgloss.Height(bubble.View())
30 | }
31 | return height
32 | }
33 |
34 | func (b *Bubble) resizeBubbles() {
35 | b.queryinput.SetWidth(b.width)
36 | b.statusbar.SetSize(b.width)
37 | b.help.SetWidth(b.width)
38 | height := b.height
39 | if b.state == state.Save {
40 | b.fileselector.SetSize(b.width)
41 | height -= totalHeight(b.help, b.queryinput, b.statusbar, b.fileselector)
42 | } else {
43 | height -= totalHeight(b.help, b.queryinput, b.statusbar)
44 | }
45 | b.inputdata.SetSize(b.width/2, height)
46 | b.output.SetSize(b.width/2, height)
47 | }
48 |
49 | //nolint:revive // don't see a more elegant way to reduce complexity here since types can't be keys in a map
50 | func (b *Bubble) handleMessage(msg tea.Msg, cmds *[]tea.Cmd) {
51 | switch msg := msg.(type) {
52 | case setupMsg:
53 | b.isJSONLines = msg.isJSONLines
54 | case tea.WindowSizeMsg:
55 | b.handleWindowSizeMsg(msg)
56 | case tea.KeyMsg:
57 | b.handleKeyMsg(msg, cmds)
58 | case initialQueryMsg:
59 | b.executeQuery(cmds)
60 | case queryResultMsg:
61 | b.handleQueryResultMsg(msg, cmds)
62 | case writeToFileMsg:
63 | b.handleWriteToFileMsg(msg, cmds)
64 | case copyResultsToClipboardMsg:
65 | b.handleCopyResultsToClipboardMsg(cmds)
66 | case copyQueryToClipboardMsg:
67 | b.handleCopyQueryToClipboardMsg(cmds)
68 | case errorMsg:
69 | b.handleErrorMsg(msg, cmds)
70 | case invalidInputMsg:
71 | b.handleInvalidInput(cmds)
72 | case inputdata.ReadyMsg:
73 | b.state = state.Query
74 | }
75 | }
76 |
77 | func (b *Bubble) handleQueryResultMsg(msg queryResultMsg, cmds *[]tea.Cmd) {
78 | b.state = state.Query
79 | b.output.ScrollToTop()
80 | b.output.SetContent(msg.highlightedResults)
81 | b.results = msg.rawResults
82 | *cmds = append(*cmds, b.statusbar.NewStatusMessage("Successfully executed query.", true))
83 | }
84 |
85 | func (b *Bubble) handleWriteToFileMsg(_ writeToFileMsg, cmds *[]tea.Cmd) {
86 | b.state = state.Query
87 | *cmds = append(*cmds, b.statusbar.NewStatusMessage(fmt.Sprintf("Successfully wrote results to file: %s", b.fileselector.GetInput()), true))
88 | b.fileselector.SetInput(b.workingDirectory)
89 | }
90 |
91 | func (b *Bubble) handleCopyResultsToClipboardMsg(cmds *[]tea.Cmd) {
92 | b.state = state.Query
93 | *cmds = append(*cmds, b.statusbar.NewStatusMessage("Successfully copied results to system clipboard.", true))
94 | }
95 |
96 | func (b *Bubble) handleCopyQueryToClipboardMsg(cmds *[]tea.Cmd) {
97 | *cmds = append(*cmds, b.statusbar.NewStatusMessage("Successfully copied query to system clipboard.", true))
98 | }
99 |
100 | func (b *Bubble) handleErrorMsg(msg errorMsg, cmds *[]tea.Cmd) {
101 | if b.state == state.Running {
102 | b.state = state.Query
103 | }
104 | *cmds = append(*cmds, b.statusbar.NewStatusMessage(msg.error.Error(), false))
105 | }
106 |
107 | func (b *Bubble) handleWindowSizeMsg(msg tea.WindowSizeMsg) {
108 | b.width = msg.Width
109 | b.height = msg.Height
110 | b.resizeBubbles()
111 | }
112 |
113 | func (b *Bubble) handleKeyMsg(msg tea.KeyMsg, cmds *[]tea.Cmd) {
114 | keyHandlers := map[tea.KeyType]func(){
115 | tea.KeyCtrlC: func() { b.handleCtrlC(cmds) },
116 | tea.KeyTab: b.handleTab,
117 | tea.KeyShiftTab: b.handleShiftTab,
118 | tea.KeyEsc: b.handleEsc,
119 | tea.KeyEnter: func() { b.handleEnter(cmds) },
120 | tea.KeyCtrlS: b.handleCtrlS,
121 | tea.KeyCtrlY: func() { b.handleCtrlY(cmds) },
122 | }
123 | if handler, ok := keyHandlers[msg.Type]; ok {
124 | handler()
125 | }
126 | }
127 |
128 | func (b *Bubble) handleInvalidInput(cmds *[]tea.Cmd) {
129 | b.ExitMessage = "Data is not valid JSON or NDJSON"
130 | *cmds = append(*cmds, tea.Quit)
131 | }
132 |
133 | func (b *Bubble) handleCtrlC(cmds *[]tea.Cmd) {
134 | if b.state != state.Running {
135 | *cmds = append(*cmds, tea.Quit)
136 | }
137 | if b.cancel != nil {
138 | b.cancel()
139 | b.cancel = nil
140 | }
141 | b.state = state.Query
142 | }
143 |
144 | func (b *Bubble) handleTab() {
145 | if b.state != state.Save {
146 | switch b.state {
147 | case state.Query:
148 | b.state = state.Input
149 | case state.Input:
150 | b.state = state.Output
151 | case state.Output:
152 | b.state = state.Query
153 | }
154 | }
155 | }
156 |
157 | func (b *Bubble) handleShiftTab() {
158 | if b.state != state.Save {
159 | switch b.state {
160 | case state.Query:
161 | b.state = state.Output
162 | case state.Input:
163 | b.state = state.Query
164 | case state.Output:
165 | b.state = state.Input
166 | }
167 | }
168 | }
169 |
170 | func (b *Bubble) handleEsc() {
171 | if b.state == state.Save {
172 | b.state = state.Query
173 | }
174 | }
175 |
176 | func (b *Bubble) executeQuery(cmds *[]tea.Cmd) {
177 | b.queryinput.RotateHistory()
178 | b.state = state.Running
179 | var ctx context.Context
180 | ctx, b.cancel = context.WithCancel(context.Background())
181 | *cmds = append(*cmds, b.executeQueryCommand(ctx))
182 | }
183 |
184 | func (b *Bubble) handleEnter(cmds *[]tea.Cmd) {
185 | if b.state == state.Save {
186 | *cmds = append(*cmds, b.saveOutput())
187 | }
188 | if b.state == state.Query {
189 | b.executeQuery(cmds)
190 | }
191 | }
192 |
193 | func (b *Bubble) handleCtrlS() {
194 | b.state = state.Save
195 | }
196 |
197 | func (b *Bubble) handleCtrlY(cmds *[]tea.Cmd) {
198 | if b.state != state.Save {
199 | *cmds = append(*cmds, b.copyQueryToClipboard())
200 | }
201 | }
202 |
203 | func (b *Bubble) updateState(prevState state.State, cmds *[]tea.Cmd) {
204 | if b.state != prevState {
205 | b.updateActiveComponent(cmds)
206 | b.help.SetState(b.state)
207 |
208 | // Help menu may overflow when we switch sections
209 | // so we need resize when active section changed.
210 | // We also need to resize when file selector (dis)appears.
211 | b.resizeBubbles()
212 | }
213 | }
214 |
215 | func (b *Bubble) updateActiveComponent(cmds *[]tea.Cmd) {
216 | switch b.state {
217 | case state.Query:
218 | b.setComponentBorderColors(b.theme.Primary, b.theme.Inactive, b.theme.Inactive)
219 | *cmds = append(*cmds, textinput.Blink)
220 | case state.Input:
221 | b.setComponentBorderColors(b.theme.Inactive, b.theme.Primary, b.theme.Inactive)
222 | case state.Output:
223 | b.setComponentBorderColors(b.theme.Inactive, b.theme.Inactive, b.theme.Primary)
224 | case state.Save:
225 | b.setComponentBorderColors(b.theme.Inactive, b.theme.Inactive, b.theme.Inactive)
226 | }
227 | }
228 |
229 | func (b *Bubble) setComponentBorderColors(query, input, output lipgloss.Color) {
230 | b.queryinput.SetBorderColor(query)
231 | b.inputdata.SetBorderColor(input)
232 | b.output.SetBorderColor(output)
233 | }
234 |
235 | func (b *Bubble) updateComponents(msg tea.Msg, cmds *[]tea.Cmd) {
236 | var cmd tea.Cmd
237 | dispatch := map[state.State]func(msg tea.Msg, cmds *[]tea.Cmd){
238 | state.Query: func(msg tea.Msg, cmds *[]tea.Cmd) {
239 | b.queryinput, cmd = b.queryinput.Update(msg)
240 | *cmds = append(*cmds, cmd)
241 |
242 | },
243 | state.Input: func(msg tea.Msg, cmds *[]tea.Cmd) {
244 | b.inputdata, cmd = b.inputdata.Update(msg)
245 | *cmds = append(*cmds, cmd)
246 | },
247 | state.Output: func(msg tea.Msg, cmds *[]tea.Cmd) {
248 | b.output, cmd = b.output.Update(msg)
249 | *cmds = append(*cmds, cmd)
250 | },
251 | state.Save: func(msg tea.Msg, cmds *[]tea.Cmd) {
252 | b.fileselector, cmd = b.fileselector.Update(msg)
253 | *cmds = append(*cmds, cmd)
254 | },
255 | state.Loading: func(msg tea.Msg, cmds *[]tea.Cmd) {
256 | b.inputdata, cmd = b.inputdata.Update(msg)
257 | *cmds = append(*cmds, cmd)
258 | },
259 | }
260 |
261 | if updateFunc, ok := dispatch[b.state]; ok {
262 | updateFunc(msg, cmds)
263 | }
264 |
265 | b.statusbar, cmd = b.statusbar.Update(msg)
266 | *cmds = append(*cmds, cmd)
267 |
268 | b.help, cmd = b.help.Update(msg)
269 | *cmds = append(*cmds, cmd)
270 |
271 | }
272 |
--------------------------------------------------------------------------------
/tui/bubbles/jqplayground/view.go:
--------------------------------------------------------------------------------
1 | package jqplayground
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 |
6 | "github.com/noahgorstein/jqp/tui/bubbles/state"
7 | )
8 |
9 | func (b Bubble) View() string {
10 | inputoutput := []string{b.inputdata.View()}
11 | if b.width%2 != 0 {
12 | inputoutput = append(inputoutput, " ")
13 | }
14 | inputoutput = append(inputoutput, b.output.View())
15 |
16 | if b.state == state.Save {
17 | return lipgloss.JoinVertical(
18 | lipgloss.Left,
19 | b.queryinput.View(),
20 | lipgloss.JoinHorizontal(lipgloss.Top, inputoutput...),
21 | b.fileselector.View(),
22 | b.statusbar.View(),
23 | b.help.View())
24 | }
25 |
26 | return lipgloss.JoinVertical(
27 | lipgloss.Left,
28 | b.queryinput.View(),
29 | lipgloss.JoinHorizontal(lipgloss.Top, inputoutput...),
30 | b.statusbar.View(),
31 | b.help.View())
32 | }
33 |
--------------------------------------------------------------------------------
/tui/bubbles/output/output.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/bubbles/viewport"
8 | tea "github.com/charmbracelet/bubbletea"
9 | "github.com/charmbracelet/lipgloss"
10 |
11 | "github.com/noahgorstein/jqp/tui/theme"
12 | )
13 |
14 | type Bubble struct {
15 | Ready bool
16 | Styles Styles
17 | viewport viewport.Model
18 | content string
19 | height int
20 | width int
21 | }
22 |
23 | func New(jqtheme theme.Theme) Bubble {
24 | styles := DefaultStyles()
25 | styles.containerStyle = styles.containerStyle.BorderForeground(jqtheme.Inactive)
26 | styles.infoStyle = styles.infoStyle.BorderForeground(jqtheme.Inactive)
27 | v := viewport.New(1, 1)
28 | b := Bubble{
29 | Styles: styles,
30 | viewport: v,
31 | content: "",
32 | }
33 | return b
34 | }
35 |
36 | func (b *Bubble) SetBorderColor(color lipgloss.TerminalColor) {
37 | b.Styles.containerStyle = b.Styles.containerStyle.BorderForeground(color)
38 | b.Styles.infoStyle = b.Styles.infoStyle.BorderForeground(color)
39 | }
40 |
41 | func (b *Bubble) SetSize(width, height int) {
42 | b.width = width
43 | b.height = height
44 |
45 | b.Styles.containerStyle = b.Styles.containerStyle.
46 | Width(width - b.Styles.containerStyle.GetHorizontalFrameSize()/2).
47 | Height(height - b.Styles.containerStyle.GetVerticalFrameSize())
48 |
49 | b.viewport.Width = width - b.Styles.containerStyle.GetHorizontalFrameSize()
50 | b.viewport.Height = height - b.Styles.containerStyle.GetVerticalFrameSize() - 3
51 | }
52 |
53 | func (b *Bubble) GetContent() string {
54 | return b.content
55 | }
56 |
57 | func (b *Bubble) SetContent(content string) {
58 | b.content = content
59 | wrappedContent := lipgloss.NewStyle().Width(b.viewport.Width - 1).Render(content)
60 |
61 | b.viewport.SetContent(wrappedContent)
62 | }
63 |
64 | func (b *Bubble) ScrollToTop() {
65 | b.viewport.GotoTop()
66 | }
67 |
68 | func (b Bubble) View() string {
69 | scrollPercent := fmt.Sprintf("%3.f%%", b.viewport.ScrollPercent()*100)
70 |
71 | info := b.Styles.infoStyle.Render(fmt.Sprintf("%s | %s", lipgloss.NewStyle().Italic(true).Render("output"), scrollPercent))
72 | line := strings.Repeat(" ", max(0, b.viewport.Width-lipgloss.Width(info)))
73 |
74 | footer := lipgloss.JoinHorizontal(lipgloss.Center, line, info)
75 | content := lipgloss.JoinVertical(lipgloss.Left, b.viewport.View(), footer)
76 |
77 | return b.Styles.containerStyle.Render(content)
78 | }
79 |
80 | func (Bubble) Init() tea.Cmd {
81 | return nil
82 | }
83 |
84 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
85 | var (
86 | cmd tea.Cmd
87 | cmds []tea.Cmd
88 | )
89 |
90 | b.viewport, cmd = b.viewport.Update(msg)
91 | cmds = append(cmds, cmd)
92 |
93 | return b, tea.Batch(cmds...)
94 | }
95 |
--------------------------------------------------------------------------------
/tui/bubbles/output/styles.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type Styles struct {
8 | infoStyle lipgloss.Style
9 | containerStyle lipgloss.Style
10 | }
11 |
12 | func DefaultStyles() (s Styles) {
13 | s.infoStyle = lipgloss.NewStyle().Bold(true).Border(lipgloss.RoundedBorder()).Padding(0, 1)
14 | s.containerStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1)
15 |
16 | return s
17 | }
18 |
--------------------------------------------------------------------------------
/tui/bubbles/queryinput/queryinput.go:
--------------------------------------------------------------------------------
1 | package queryinput
2 |
3 | import (
4 | "container/list"
5 |
6 | "github.com/charmbracelet/bubbles/textinput"
7 | tea "github.com/charmbracelet/bubbletea"
8 | "github.com/charmbracelet/lipgloss"
9 |
10 | "github.com/noahgorstein/jqp/tui/theme"
11 | )
12 |
13 | type Bubble struct {
14 | Styles Styles
15 | textinput textinput.Model
16 |
17 | history *list.List
18 | historyMaxLen int
19 | historySelected *list.Element
20 | }
21 |
22 | func New(jqtheme theme.Theme) Bubble {
23 | s := DefaultStyles()
24 | s.containerStyle.BorderForeground(jqtheme.Primary)
25 | ti := textinput.New()
26 | ti.Focus()
27 | ti.PromptStyle.Height(1)
28 | ti.TextStyle.Height(1)
29 | ti.Prompt = lipgloss.NewStyle().Bold(true).Foreground(jqtheme.Secondary).Render("jq > ")
30 |
31 | return Bubble{
32 | Styles: s,
33 | textinput: ti,
34 |
35 | history: list.New(),
36 | historyMaxLen: 512,
37 | }
38 | }
39 |
40 | func (b *Bubble) SetBorderColor(color lipgloss.TerminalColor) {
41 | b.Styles.containerStyle = b.Styles.containerStyle.BorderForeground(color)
42 | }
43 |
44 | func (b Bubble) GetInputValue() string {
45 | return b.textinput.Value()
46 | }
47 |
48 | func (b *Bubble) RotateHistory() {
49 | b.history.PushFront(b.textinput.Value())
50 | b.historySelected = b.history.Front()
51 | if b.history.Len() > b.historyMaxLen {
52 | b.history.Remove(b.history.Back())
53 | }
54 | }
55 |
56 | func (Bubble) Init() tea.Cmd {
57 | return textinput.Blink
58 | }
59 |
60 | func (b *Bubble) SetWidth(width int) {
61 | b.Styles.containerStyle = b.Styles.containerStyle.Width(width - b.Styles.containerStyle.GetHorizontalFrameSize())
62 | b.textinput.Width = width - b.Styles.containerStyle.GetHorizontalFrameSize() - 1
63 | }
64 |
65 | func (b Bubble) View() string {
66 | return b.Styles.containerStyle.Render(b.textinput.View())
67 | }
68 |
69 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
70 | switch msg := msg.(type) {
71 | case tea.KeyMsg:
72 | return b.updateKeyMsg(msg)
73 | default:
74 | var cmd tea.Cmd
75 | b.textinput, cmd = b.textinput.Update(msg)
76 | return b, cmd
77 | }
78 | }
79 |
80 | func (b *Bubble) SetQuery(query string) {
81 | b.textinput.SetValue(query)
82 | }
83 |
84 | func (b Bubble) updateKeyMsg(msg tea.KeyMsg) (Bubble, tea.Cmd) {
85 | switch msg.Type {
86 | case tea.KeyUp:
87 | return b.handleKeyUp()
88 | case tea.KeyDown:
89 | return b.handleKeyDown()
90 | case tea.KeyEnter:
91 | b.RotateHistory()
92 | return b, nil
93 | default:
94 | var cmd tea.Cmd
95 | b.textinput, cmd = b.textinput.Update(msg)
96 | return b, cmd
97 | }
98 | }
99 |
100 | func (b Bubble) handleKeyUp() (Bubble, tea.Cmd) {
101 | if b.history.Len() == 0 {
102 | return b, nil
103 | }
104 | n := b.historySelected.Next()
105 | if n != nil {
106 | b.textinput.SetValue(n.Value.(string))
107 | b.textinput.CursorEnd()
108 | b.historySelected = n
109 | }
110 | return b, nil
111 | }
112 |
113 | func (b Bubble) handleKeyDown() (Bubble, tea.Cmd) {
114 | if b.history.Len() == 0 {
115 | return b, nil
116 | }
117 | p := b.historySelected.Prev()
118 | if p != nil {
119 | b.textinput.SetValue(p.Value.(string))
120 | b.textinput.CursorEnd()
121 | b.historySelected = p
122 | }
123 | return b, nil
124 | }
125 |
--------------------------------------------------------------------------------
/tui/bubbles/queryinput/styles.go:
--------------------------------------------------------------------------------
1 | package queryinput
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type Styles struct {
8 | containerStyle lipgloss.Style
9 | }
10 |
11 | func DefaultStyles() (s Styles) {
12 | s.containerStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
13 | return s
14 | }
15 |
--------------------------------------------------------------------------------
/tui/bubbles/state/state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | type State uint
4 |
5 | const (
6 | Query State = iota
7 | Running
8 | Input
9 | Output
10 | Save
11 | Loading
12 | )
13 |
--------------------------------------------------------------------------------
/tui/bubbles/statusbar/statusbar.go:
--------------------------------------------------------------------------------
1 | package statusbar
2 |
3 | import (
4 | "time"
5 |
6 | tea "github.com/charmbracelet/bubbletea"
7 |
8 | "github.com/noahgorstein/jqp/tui/theme"
9 | )
10 |
11 | type Bubble struct {
12 | styles styles
13 | StatusMessageLifetime time.Duration
14 | statusMessage string
15 | statusMessageTimer *time.Timer
16 | }
17 |
18 | func (Bubble) Init() tea.Cmd {
19 | return nil
20 | }
21 |
22 | func (b Bubble) View() string {
23 | return b.styles.containerStyle.Render(b.statusMessage)
24 | }
25 |
26 | func (b *Bubble) SetSize(width int) {
27 | b.styles.containerStyle = b.styles.containerStyle.Width(width)
28 | }
29 |
30 | func (b *Bubble) hideStatusMessage() {
31 | b.statusMessage = ""
32 | if b.statusMessageTimer != nil {
33 | b.statusMessageTimer.Stop()
34 | }
35 | }
36 |
37 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
38 | var cmd tea.Cmd
39 |
40 | switch msg := msg.(type) {
41 | case statusMessageTimeoutMsg:
42 | b.hideStatusMessage()
43 | case tea.WindowSizeMsg:
44 | b.SetSize(msg.Width)
45 | }
46 | return b, tea.Batch(cmd)
47 | }
48 |
49 | func New(jqtheme theme.Theme) Bubble {
50 | styles := defaultStyles()
51 | styles.successMessageStyle = styles.successMessageStyle.Foreground(jqtheme.Success)
52 | styles.errorMessageStyle = styles.errorMessageStyle.Foreground(jqtheme.Error)
53 | b := Bubble{
54 | styles: styles,
55 | }
56 | return b
57 | }
58 |
59 | type statusMessageTimeoutMsg struct{}
60 |
61 | func (b *Bubble) NewStatusMessage(s string, success bool) tea.Cmd {
62 | if success {
63 | b.statusMessage = b.styles.successMessageStyle.Render(s)
64 | } else {
65 | b.statusMessage = b.styles.errorMessageStyle.Render(s)
66 | }
67 |
68 | if b.statusMessageTimer != nil {
69 | b.statusMessageTimer.Stop()
70 | }
71 |
72 | b.statusMessageTimer = time.NewTimer(b.StatusMessageLifetime)
73 |
74 | // Wait for timeout
75 | return func() tea.Msg {
76 | <-b.statusMessageTimer.C
77 | return statusMessageTimeoutMsg{}
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tui/bubbles/statusbar/styles.go:
--------------------------------------------------------------------------------
1 | package statusbar
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | type styles struct {
8 | containerStyle lipgloss.Style
9 | errorMessageStyle lipgloss.Style
10 | successMessageStyle lipgloss.Style
11 | }
12 |
13 | func defaultStyles() (s styles) {
14 | s.containerStyle = lipgloss.NewStyle().PaddingLeft(1)
15 | s.errorMessageStyle = lipgloss.NewStyle()
16 | s.successMessageStyle = lipgloss.NewStyle()
17 |
18 | return s
19 | }
20 |
--------------------------------------------------------------------------------
/tui/theme/theme.go:
--------------------------------------------------------------------------------
1 | package theme
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/alecthomas/chroma/v2"
7 | "github.com/alecthomas/chroma/v2/styles"
8 | "github.com/charmbracelet/lipgloss"
9 | )
10 |
11 | type CustomTheme struct {
12 | Primary string
13 | Secondary string
14 | Inactive string
15 | Success string
16 | Error string
17 | }
18 |
19 | var CustomThemeKeys = CustomTheme{
20 | Primary: "primary",
21 | Secondary: "secondary",
22 | Success: "success",
23 | Inactive: "inactive",
24 | Error: "error",
25 | }
26 |
27 | const (
28 | BLUE = lipgloss.Color("69")
29 | PINK = lipgloss.Color("#F25D94")
30 | GREY = lipgloss.Color("240")
31 | GREEN = lipgloss.Color("76")
32 | RED = lipgloss.Color("9")
33 | )
34 |
35 | type Theme struct {
36 | Primary lipgloss.Color
37 | Secondary lipgloss.Color
38 | Inactive lipgloss.Color
39 | Success lipgloss.Color
40 | Error lipgloss.Color
41 | ChromaStyle *chroma.Style
42 | }
43 |
44 | func getDefaultTheme() Theme {
45 | theme := Theme{
46 | Primary: BLUE,
47 | Secondary: PINK,
48 | Inactive: GREY,
49 | Success: GREEN,
50 | Error: RED,
51 | ChromaStyle: styles.Get("paraiso-light"),
52 | }
53 | if lipgloss.HasDarkBackground() {
54 | theme.ChromaStyle = styles.Get("vim")
55 | }
56 | return theme
57 | }
58 |
59 | var (
60 | // from https://www.nordtheme.com/docs/colors-and-palettes
61 | nord7 = lipgloss.Color("#8FBCBB")
62 | nord9 = lipgloss.Color("#81A1C1")
63 | nord11 = lipgloss.Color("#BF616A")
64 | nord14 = lipgloss.Color("#A3BE8C")
65 | )
66 |
67 | var themeMap = map[string]Theme{
68 | "abap": {
69 | Primary: lipgloss.Color("#00f"),
70 | Secondary: lipgloss.Color("#3af"),
71 | Inactive: GREY,
72 | Success: lipgloss.Color("#5a2"),
73 | Error: lipgloss.Color("#F00"),
74 | ChromaStyle: styles.Get("abap"),
75 | },
76 | "algol": {
77 | Primary: lipgloss.Color("#5a2"),
78 | Secondary: lipgloss.Color("#666"),
79 | Inactive: GREY,
80 | Success: lipgloss.Color("#5a2"),
81 | Error: lipgloss.Color("#FF0000"),
82 | ChromaStyle: styles.Get("algol"),
83 | },
84 | "arduino": {
85 | Primary: lipgloss.Color("#1e90ff"),
86 | Secondary: lipgloss.Color("#aa5500"),
87 | Inactive: GREY,
88 | Success: lipgloss.Color("#5a2"),
89 | Error: lipgloss.Color("#F00"),
90 | ChromaStyle: styles.Get("arduino"),
91 | },
92 | "autumn": {
93 | Primary: lipgloss.Color("#aa5500"),
94 | Secondary: lipgloss.Color("#fcba03"),
95 | Inactive: GREY,
96 | Success: lipgloss.Color("#009999"),
97 | Error: lipgloss.Color("#ff0000"),
98 | ChromaStyle: styles.Get("autumn"),
99 | },
100 | "average": {
101 | Primary: lipgloss.Color("#ec0000"),
102 | Secondary: lipgloss.Color("#008900"),
103 | Inactive: GREY,
104 | Success: lipgloss.Color("#008900"),
105 | Error: lipgloss.Color("#ec0000"),
106 | ChromaStyle: styles.Get("average"),
107 | },
108 | "base16-snazzy": {
109 | Primary: lipgloss.Color("#ff6ac1"),
110 | Secondary: lipgloss.Color("#5af78e"),
111 | Inactive: GREY,
112 | Success: lipgloss.Color("#5af78e"),
113 | Error: lipgloss.Color("#ff5c57"),
114 | ChromaStyle: styles.Get("base16-snazzy"),
115 | },
116 | "borland": {
117 | Primary: lipgloss.Color("#00f"),
118 | Secondary: lipgloss.Color("#000080"),
119 | Inactive: GREY,
120 | Success: lipgloss.Color("#5a2"),
121 | Error: lipgloss.Color("#a61717"),
122 | ChromaStyle: styles.Get("borland"),
123 | },
124 | "catppuccin-latte": {
125 | Primary: lipgloss.Color("#179299"),
126 | Secondary: lipgloss.Color("#1e66f5"),
127 | Inactive: GREY,
128 | Success: lipgloss.Color("#40a02b"),
129 | Error: lipgloss.Color("#d20f39"),
130 | ChromaStyle: styles.Get("catppuccin-latte"),
131 | },
132 | "catppuccin-frappe": {
133 | Primary: lipgloss.Color("#81c8be"),
134 | Secondary: lipgloss.Color("#8caaee"),
135 | Inactive: GREY,
136 | Success: lipgloss.Color("#a6d189"),
137 | Error: lipgloss.Color("#e78284"),
138 | ChromaStyle: styles.Get("catppuccin-frappe"),
139 | },
140 | "catppuccin-macchiato": {
141 | Primary: lipgloss.Color("#8bd5ca"),
142 | Secondary: lipgloss.Color("#8aadf4"),
143 | Inactive: GREY,
144 | Success: lipgloss.Color("#a6da95"),
145 | Error: lipgloss.Color("#ed8796"),
146 | ChromaStyle: styles.Get("catppuccin-macchiato"),
147 | },
148 | "catppuccin-mocha": {
149 | Primary: lipgloss.Color("#94e2d5"),
150 | Secondary: lipgloss.Color("#89b4fa"),
151 | Inactive: GREY,
152 | Success: lipgloss.Color("#a6e3a1"),
153 | Error: lipgloss.Color("#f38ba8"),
154 | ChromaStyle: styles.Get("catppuccin-mocha"),
155 | },
156 | "colorful": {
157 | Primary: lipgloss.Color("#00d"),
158 | Secondary: lipgloss.Color("#070"),
159 | Inactive: GREY,
160 | Success: lipgloss.Color("#070"),
161 | Error: lipgloss.Color("#a61717"),
162 | ChromaStyle: styles.Get("colorful"),
163 | },
164 | "doom-one": {
165 | Primary: lipgloss.Color("#b756ff"),
166 | Secondary: lipgloss.Color("#63c381"),
167 | Inactive: GREY,
168 | Success: lipgloss.Color("#63c381"),
169 | Error: lipgloss.Color("#e06c75"),
170 | ChromaStyle: styles.Get("doom-one"),
171 | },
172 | "doom-one2": {
173 | Primary: lipgloss.Color("#76a9f9"),
174 | Secondary: lipgloss.Color("#63c381"),
175 | Inactive: GREY,
176 | Success: lipgloss.Color("#63c381"),
177 | Error: lipgloss.Color("#e06c75"),
178 | ChromaStyle: styles.Get("doom-one2"),
179 | },
180 | "dracula": {
181 | Primary: lipgloss.Color("#8be9fd"),
182 | Secondary: lipgloss.Color("#ffb86c"),
183 | Inactive: GREY,
184 | Success: lipgloss.Color("#50fa7b"),
185 | Error: lipgloss.Color("#f8f8f2"),
186 | ChromaStyle: styles.Get("dracula"),
187 | },
188 | "emacs": {
189 | Primary: lipgloss.Color("#008000"),
190 | Secondary: lipgloss.Color("#a2f"),
191 | Inactive: GREY,
192 | Success: lipgloss.Color("#008000"),
193 | Error: lipgloss.Color("#b44"),
194 | ChromaStyle: styles.Get("emacs"),
195 | },
196 | "friendly": {
197 | Primary: lipgloss.Color("#40a070"),
198 | Secondary: lipgloss.Color("#062873"),
199 | Inactive: GREY,
200 | Success: lipgloss.Color("#40a070"),
201 | Error: lipgloss.Color("#FF0000"),
202 | ChromaStyle: styles.Get("friendly"),
203 | },
204 | "fruity": {
205 | Primary: lipgloss.Color("#fb660a"),
206 | Secondary: lipgloss.Color("#0086f7"),
207 | Inactive: GREY,
208 | Success: lipgloss.Color("#40a070"),
209 | Error: lipgloss.Color("#FF0000"),
210 | ChromaStyle: styles.Get("fruity"),
211 | },
212 | "github": {
213 | Primary: lipgloss.Color("#d14"),
214 | Secondary: lipgloss.Color("#099"),
215 | Inactive: GREY,
216 | Success: lipgloss.Color("#099"),
217 | Error: lipgloss.Color("#d14"),
218 | ChromaStyle: styles.Get("github"),
219 | },
220 | "github-dark": {
221 | Primary: lipgloss.Color("#d2a8ff"),
222 | Secondary: lipgloss.Color("#f0883e"),
223 | Inactive: GREY,
224 | Success: lipgloss.Color("#56d364"),
225 | Error: lipgloss.Color("#ffa198"),
226 | ChromaStyle: styles.Get("github-dark"),
227 | },
228 | "gruvbox": {
229 | Primary: lipgloss.Color("#b8bb26"),
230 | Secondary: lipgloss.Color("#d3869b"),
231 | Inactive: GREY,
232 | Success: lipgloss.Color("#b8bb26"),
233 | Error: lipgloss.Color("#fb4934"),
234 | ChromaStyle: styles.Get("gruvbox"),
235 | },
236 | "gruvbox-light": {
237 | Primary: lipgloss.Color("#fb4934"),
238 | Secondary: lipgloss.Color("#b8bb26"),
239 | Inactive: GREY,
240 | Success: lipgloss.Color("#b8bb26"),
241 | Error: lipgloss.Color("#9D0006"),
242 | ChromaStyle: styles.Get("gruvbox-light"),
243 | },
244 | "hrdark": {
245 | Primary: lipgloss.Color("#58a1dd"),
246 | Secondary: lipgloss.Color("#ff636f"),
247 | Inactive: GREY,
248 | Success: lipgloss.Color("#a6be9d"),
249 | Error: lipgloss.Color("#FF0000"),
250 | ChromaStyle: styles.Get("hrdark"),
251 | },
252 | "igor": {
253 | Primary: lipgloss.Color("#009c00"),
254 | Secondary: lipgloss.Color("#00f"),
255 | Inactive: GREY,
256 | Success: lipgloss.Color("#009c00"),
257 | Error: lipgloss.Color("#FF0000"),
258 | ChromaStyle: styles.Get("igor"),
259 | },
260 | "lovelace": {
261 | Primary: lipgloss.Color("#b83838"),
262 | Secondary: lipgloss.Color("#2838b0"),
263 | Inactive: GREY,
264 | Success: lipgloss.Color("#009c00"),
265 | Error: lipgloss.Color("#b83838"),
266 | ChromaStyle: styles.Get("lovelace"),
267 | },
268 | "manni": {
269 | Primary: lipgloss.Color("#c30"),
270 | Secondary: lipgloss.Color("#309"),
271 | Inactive: GREY,
272 | Success: lipgloss.Color("#009c00"),
273 | Error: lipgloss.Color("#c30"),
274 | ChromaStyle: styles.Get("manni"),
275 | },
276 | "monokai": {
277 | Primary: lipgloss.Color("#a6e22e"),
278 | Secondary: lipgloss.Color("#f92672"),
279 | Inactive: GREY,
280 | Success: lipgloss.Color("#b4d273"),
281 | Error: lipgloss.Color("#960050"),
282 | ChromaStyle: styles.Get("monokai"),
283 | },
284 | "monokai-light": {
285 | Primary: lipgloss.Color("#00a8c8"),
286 | Secondary: lipgloss.Color("#f92672"),
287 | Inactive: GREY,
288 | Success: lipgloss.Color("#b4d273"),
289 | Error: lipgloss.Color("#960050"),
290 | ChromaStyle: styles.Get("monokai-light"),
291 | },
292 | "murphy": {
293 | Primary: lipgloss.Color("#070"),
294 | Secondary: lipgloss.Color("#66f"),
295 | Inactive: GREY,
296 | Success: lipgloss.Color("#070"),
297 | Error: lipgloss.Color("#F00"),
298 | ChromaStyle: styles.Get("murphy"),
299 | },
300 | "native": {
301 | Primary: lipgloss.Color("#6ab825"),
302 | Secondary: lipgloss.Color("#ed9d13"),
303 | Inactive: GREY,
304 | Success: lipgloss.Color("#6ab825"),
305 | Error: lipgloss.Color("#a61717"),
306 | ChromaStyle: styles.Get("native"),
307 | },
308 | "nord": {
309 | Primary: nord7,
310 | Secondary: nord9,
311 | Inactive: GREY,
312 | Success: nord14,
313 | Error: nord11,
314 | ChromaStyle: styles.Get("nord"),
315 | },
316 | "onesenterprise": {
317 | Primary: lipgloss.Color("#00f"),
318 | Secondary: lipgloss.Color("#f00"),
319 | Inactive: GREY,
320 | Success: lipgloss.Color("#6ab825"),
321 | Error: lipgloss.Color("#f00"),
322 | ChromaStyle: styles.Get("onesenterprise"),
323 | },
324 | "pastie": {
325 | Primary: lipgloss.Color("#b06"),
326 | Secondary: lipgloss.Color("#00d"),
327 | Inactive: GREY,
328 | Success: lipgloss.Color("#080"),
329 | Error: lipgloss.Color("#d20"),
330 | ChromaStyle: styles.Get("pastie"),
331 | },
332 | "perldoc": {
333 | Primary: lipgloss.Color("#8b008b"),
334 | Secondary: lipgloss.Color("#b452cd"),
335 | Inactive: GREY,
336 | Success: lipgloss.Color("#080"),
337 | Error: lipgloss.Color("#cd5555"),
338 | ChromaStyle: styles.Get("perldoc"),
339 | },
340 | "paraiso-dark": {
341 | Primary: lipgloss.Color("#48b685"),
342 | Secondary: lipgloss.Color("#5bc4bf"),
343 | Inactive: GREY,
344 | Success: lipgloss.Color("#48b685"),
345 | Error: lipgloss.Color("#ef6155"),
346 | ChromaStyle: styles.Get("paraiso-dark"),
347 | },
348 | "paraiso-light": {
349 | Primary: lipgloss.Color("#48b685"),
350 | Secondary: lipgloss.Color("#815ba4"),
351 | Inactive: GREY,
352 | Success: lipgloss.Color("#48b685"),
353 | Error: lipgloss.Color("#ef6155"),
354 | ChromaStyle: styles.Get("paraiso-light"),
355 | },
356 | "pygments": {
357 | Primary: lipgloss.Color("#008000"),
358 | Secondary: lipgloss.Color("#ba2121"),
359 | Inactive: GREY,
360 | Success: lipgloss.Color("#008000"),
361 | Error: lipgloss.Color("#ba2121"),
362 | ChromaStyle: styles.Get("pygments"),
363 | },
364 | "rainbow_dash": {
365 | Primary: lipgloss.Color("#0c6"),
366 | Secondary: lipgloss.Color("#5918bb"),
367 | Inactive: GREY,
368 | Success: lipgloss.Color("#0c6"),
369 | Error: lipgloss.Color("#ba2121"),
370 | ChromaStyle: styles.Get("rainbow_dash"),
371 | },
372 | "rrt": {
373 | Primary: lipgloss.Color("#f60"),
374 | Secondary: lipgloss.Color("#87ceeb"),
375 | Inactive: GREY,
376 | Success: lipgloss.Color("#0c6"),
377 | Error: lipgloss.Color("#f00"),
378 | ChromaStyle: styles.Get("rrt"),
379 | },
380 | "solarized-dark": {
381 | Primary: lipgloss.Color("#268bd2"),
382 | Secondary: lipgloss.Color("#2aa198"),
383 | Inactive: GREY,
384 | Success: lipgloss.Color("#0c6"),
385 | Error: lipgloss.Color("#cb4b16"),
386 | ChromaStyle: styles.Get("solarized-dark"),
387 | },
388 | "solarized-dark256": {
389 | Primary: lipgloss.Color("#0087ff"),
390 | Secondary: lipgloss.Color("#00afaf"),
391 | Inactive: GREY,
392 | Success: lipgloss.Color("#0c6"),
393 | Error: lipgloss.Color("#d75f00"),
394 | ChromaStyle: styles.Get("solarized-dark256"),
395 | },
396 | "solarized-light": {
397 | Primary: lipgloss.Color("#268bd2"),
398 | Secondary: lipgloss.Color("#2aa198"),
399 | Inactive: GREY,
400 | Success: lipgloss.Color("#859900"),
401 | Error: lipgloss.Color("#d75f00"),
402 | ChromaStyle: styles.Get("solarized-light"),
403 | },
404 | "swapoff": {
405 | Primary: lipgloss.Color("#0ff"),
406 | Secondary: lipgloss.Color("#ff0"),
407 | Inactive: GREY,
408 | Success: lipgloss.Color("#e5e5e5"),
409 | Error: lipgloss.Color("#e5e5e5"),
410 | ChromaStyle: styles.Get("swapoff"),
411 | },
412 | "tango": {
413 | Primary: lipgloss.Color("#204a87"),
414 | Secondary: lipgloss.Color("#0000cf"),
415 | Inactive: GREY,
416 | Success: lipgloss.Color("#4e9a06"),
417 | Error: lipgloss.Color("#a40000"),
418 | ChromaStyle: styles.Get("tango"),
419 | },
420 | "trac": {
421 | Primary: lipgloss.Color("#099"),
422 | Secondary: lipgloss.Color("#000080"),
423 | Inactive: GREY,
424 | Success: lipgloss.Color("#099"),
425 | Error: lipgloss.Color("#a61717"),
426 | ChromaStyle: styles.Get("trac"),
427 | },
428 | "vim": {
429 | Primary: lipgloss.Color("#cd00cd"),
430 | Secondary: lipgloss.Color("#cdcd00"),
431 | Inactive: GREY,
432 | Success: lipgloss.Color("#66FF00"),
433 | Error: lipgloss.Color("#cd0000"),
434 | ChromaStyle: styles.Get("vim"),
435 | },
436 | "visual_studio": {
437 | Primary: lipgloss.Color("#a31515"),
438 | Secondary: lipgloss.Color("#00f"),
439 | Inactive: GREY,
440 | Success: lipgloss.Color("#023020"),
441 | Error: lipgloss.Color("#a31515"),
442 | ChromaStyle: styles.Get("vs"),
443 | },
444 | "vulcan": {
445 | Primary: lipgloss.Color("#bc74c4"),
446 | Secondary: lipgloss.Color("#56b6c2"),
447 | Inactive: GREY,
448 | Success: lipgloss.Color("#82cc6a"),
449 | Error: lipgloss.Color("#cf5967"),
450 | ChromaStyle: styles.Get("vulcan"),
451 | },
452 | "witchhazel": {
453 | Primary: lipgloss.Color("#ffb8d1"),
454 | Secondary: lipgloss.Color("#56b6c2"),
455 | Inactive: GREY,
456 | Success: lipgloss.Color("#c2ffdf"),
457 | Error: lipgloss.Color("#ffb8d1"),
458 | ChromaStyle: styles.Get("witchhazel"),
459 | },
460 | "xcode": {
461 | Primary: lipgloss.Color("#c41a16"),
462 | Secondary: lipgloss.Color("#1c01ce"),
463 | Inactive: GREY,
464 | Success: lipgloss.Color("#023020"),
465 | Error: lipgloss.Color("#c41a16"),
466 | ChromaStyle: styles.Get("xcode"),
467 | },
468 | "xcode-dark": {
469 | Primary: lipgloss.Color("#fc6a5d"),
470 | Secondary: lipgloss.Color("#d0bf69"),
471 | Inactive: GREY,
472 | Success: lipgloss.Color("#90EE90"),
473 | Error: lipgloss.Color("#fc6a5d"),
474 | ChromaStyle: styles.Get("xcode-dark"),
475 | },
476 | }
477 |
478 | // returns a theme by name, and true if default theme was returned
479 | func GetTheme(themeName string, styleOverrides map[string]string) (Theme, bool) {
480 | lowercasedTheme := strings.ToLower(strings.TrimSpace(themeName))
481 |
482 | var isDefault bool
483 | var theme Theme
484 | if value, ok := themeMap[lowercasedTheme]; ok {
485 | theme = value
486 | isDefault = false
487 | } else {
488 | theme = getDefaultTheme()
489 | isDefault = true
490 | }
491 |
492 | theme.SetOverrides(styleOverrides)
493 |
494 | return theme, isDefault && len(styleOverrides) == 0
495 | }
496 |
497 | func (t *Theme) SetOverrides(overrides map[string]string) {
498 | t.Primary = customColorOrDefault(overrides[CustomThemeKeys.Primary], t.Primary)
499 | t.Secondary = customColorOrDefault(overrides[CustomThemeKeys.Secondary], t.Secondary)
500 | t.Inactive = customColorOrDefault(overrides[CustomThemeKeys.Inactive], t.Inactive)
501 | t.Success = customColorOrDefault(overrides[CustomThemeKeys.Success], t.Success)
502 | t.Error = customColorOrDefault(overrides[CustomThemeKeys.Error], t.Error)
503 | }
504 |
505 | func customColorOrDefault(color string, def lipgloss.Color) lipgloss.Color {
506 | if color == "" {
507 | return def
508 | }
509 |
510 | return lipgloss.Color(color)
511 | }
512 |
--------------------------------------------------------------------------------
/tui/utils/json.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 |
10 | "github.com/alecthomas/chroma/v2"
11 | "github.com/alecthomas/chroma/v2/formatters"
12 | "github.com/alecthomas/chroma/v2/lexers"
13 | "github.com/alecthomas/chroma/v2/styles"
14 | )
15 |
16 | const FourSpaces = " "
17 |
18 | // IsValidInput checks the validity of input data as JSON or JSON lines.
19 | // It takes a byte slice 'data' and returns two boolean values indicating
20 | // whether the data is valid JSON and valid JSON lines, along with an error
21 | // if the data is not valid in either format.
22 | func IsValidInput(data []byte) (isValidJSON bool, isValidJSONLines bool, err error) {
23 | if len(data) == 0 {
24 | err = errors.New("Data is not valid JSON or NDJSON")
25 | return false, false, err
26 | }
27 |
28 | isValidJSON = IsValidJSON(data) == nil
29 | isValidJSONLines = IsValidJSONLines(data) == nil
30 |
31 | if !isValidJSON && !isValidJSONLines {
32 | err = errors.New("Data is not valid JSON or NDJSON")
33 | return false, false, err
34 | }
35 |
36 | return isValidJSON, isValidJSONLines, nil
37 | }
38 |
39 | func highlightJSON(w io.Writer, source string, style *chroma.Style) error {
40 | l := lexers.Get("json")
41 | if l == nil {
42 | l = lexers.Fallback
43 | }
44 | l = chroma.Coalesce(l)
45 |
46 | f := formatters.Get(getTerminalColorSupport())
47 | if f == nil {
48 | f = formatters.Fallback
49 | }
50 |
51 | if style == nil {
52 | style = styles.Fallback
53 | }
54 |
55 | it, err := l.Tokenise(nil, source)
56 | if err != nil {
57 | return err
58 | }
59 | return f.Format(w, style, it)
60 | }
61 |
62 | func IsValidJSON(input []byte) error {
63 | var js json.RawMessage
64 | return json.Unmarshal(input, &js)
65 | }
66 |
67 | func IsValidJSONLines(input []byte) error {
68 | maxBufferSize := 100 * 1024 * 1024 // 100MB
69 | err := ScanLinesWithDynamicBufferSize(input, maxBufferSize, IsValidJSON)
70 | if err != nil {
71 | return err
72 | }
73 | return nil
74 | }
75 |
76 | func indentJSON(input *[]byte, output *bytes.Buffer) error {
77 | err := IsValidJSON(*input)
78 | if err != nil {
79 | return nil
80 | }
81 | err = json.Indent(output, []byte(*input), "", FourSpaces)
82 | if err != nil {
83 | return err
84 | }
85 | return nil
86 | }
87 |
88 | func prettifyJSON(input []byte, chromaStyle *chroma.Style) (*bytes.Buffer, error) {
89 | var indentedBuf bytes.Buffer
90 | err := indentJSON(&input, &indentedBuf)
91 | if err != nil {
92 | return nil, err
93 | }
94 | if indentedBuf.Len() == 0 {
95 | err := highlightJSON(&indentedBuf, string(input), chromaStyle)
96 | if err != nil {
97 | return nil, err
98 | }
99 | return &indentedBuf, nil
100 | }
101 | var highlightedBuf bytes.Buffer
102 | err = highlightJSON(&highlightedBuf, indentedBuf.String(), chromaStyle)
103 | if err != nil {
104 | return nil, err
105 | }
106 | return &highlightedBuf, nil
107 | }
108 |
109 | func Prettify(inputJSON []byte, chromaStyle *chroma.Style, isJSONLines bool) (*bytes.Buffer, error) {
110 | if !isJSONLines {
111 | return prettifyJSON(inputJSON, chromaStyle)
112 | }
113 |
114 | var buf bytes.Buffer
115 | processLine := func(line []byte) error {
116 | hightlighedLine, err := prettifyJSON(line, chromaStyle)
117 | if err != nil {
118 | return err
119 | }
120 | _, err = buf.WriteString(fmt.Sprintf("%v\n", hightlighedLine))
121 | if err != nil {
122 | return err
123 | }
124 | return nil
125 | }
126 |
127 | const maxBufferSize = 100 * 1024 * 1024 // 100MB max buffer size
128 | err := ScanLinesWithDynamicBufferSize(inputJSON, maxBufferSize, processLine)
129 | if err != nil {
130 | return nil, err
131 | }
132 | return &buf, nil
133 | }
134 |
--------------------------------------------------------------------------------
/tui/utils/scan.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "fmt"
8 | )
9 |
10 | // ScanLinesWithDynamicBufferSize scans the input byte slice line by line, using a dynamically
11 | // increasing buffer size. It starts with an initial buffer size of 64KB and doubles the buffer
12 | // size each time a line exceeds the current buffer size, up to the specified maximum buffer size.
13 | //
14 | // If a line exceeds the maximum buffer size, it returns an error.
15 | //
16 | // The processLine function is called for each line and should return an error if processing fails.
17 | //
18 | // The function returns an error if the input exceeds the maximum buffer size or if any other
19 | // error occurs during line processing. It returns nil if all lines are processed successfully.
20 | func ScanLinesWithDynamicBufferSize(input []byte, maxBufferSize int, processLine func([]byte) error) error {
21 | scanner := bufio.NewScanner(bytes.NewReader(input))
22 | initialBufferSize := 64 * 1024 // 64KB initial buffer size
23 |
24 | for bufferSize := initialBufferSize; bufferSize <= maxBufferSize; bufferSize *= 2 {
25 | if err := scanWithBufferSize(scanner, bufferSize, maxBufferSize, processLine); err != nil {
26 | if errors.Is(err, bufio.ErrTooLong) {
27 | // Buffer size is too small, retry with a larger buffer
28 | continue
29 | }
30 | return err
31 | }
32 | // All lines are processed successfully
33 | return nil
34 | }
35 |
36 | // Input exceeds maximum buffer size
37 | return fmt.Errorf("input exceeds maximum buffer size of %d bytes", maxBufferSize)
38 | }
39 |
40 | func scanWithBufferSize(scanner *bufio.Scanner, bufferSize, maxBufferSize int, processLine func([]byte) error) error {
41 | scanner.Buffer(make([]byte, bufferSize), maxBufferSize)
42 |
43 | for scanner.Scan() {
44 | if err := processLine(scanner.Bytes()); err != nil {
45 | return err
46 | }
47 | }
48 |
49 | return scanner.Err()
50 | }
51 |
--------------------------------------------------------------------------------
/tui/utils/terminal.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | "github.com/muesli/termenv"
6 | )
7 |
8 | var termenvChromaTerminal = map[termenv.Profile]string{
9 | termenv.Ascii: "terminal",
10 | termenv.ANSI: "terminal16",
11 | termenv.ANSI256: "terminal256",
12 | termenv.TrueColor: "terminal16m",
13 | }
14 |
15 | // returns a string used for chroma syntax highlighting
16 | func getTerminalColorSupport() string {
17 | if chroma, ok := termenvChromaTerminal[lipgloss.ColorProfile()]; ok {
18 | return chroma
19 | }
20 | return "terminal"
21 | }
22 |
--------------------------------------------------------------------------------