├── .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 | ![jqp](https://user-images.githubusercontent.com/23270779/191256434-05aeda9d-9ee2-4b5e-a23f-6548dac08fdb.gif) 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 | Snap Status 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 | Screen Shot 2022-10-02 at 5 31 40 PM 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 | --------------------------------------------------------------------------------