qki-cli
v1.1.0
Published
Visual jq alternative with Terminal UI
Readme
The Problem
Every backend developer and DevOps engineer faces the same thing daily:
how to filter nested json jq
jq select where key equals
jq get array element by index
jq extract nested object fieldjq is a powerful tool. But its syntax is impossible to memorize.
You work with an API, receive a massive JSON response, and need to extract a single field. You open a browser, google jq syntax, copy an example, adapt it to your structure, make a mistake, google again, try once more...
QKI solves this problem once and for all.
The Solution
cat response.json | qkiAn interactive interface opens right in your terminal. You see the JSON tree.
Navigate with arrow keys. Expand and collapse nodes. Find the field you need.
Press Enter. The correct jq path is automatically copied to your clipboard
and printed to stdout.
No more googling. No more memorizing syntax. Visual, fast, precise.
Features
| Feature | Description |
|---------|-------------|
| Tree navigation | JSON is displayed as an interactive tree with indentation |
| Expand/collapse | Objects and arrays can be expanded and collapsed |
| Auto jq path generation | A correct jq path is calculated in real time for every node |
| Clipboard copy | Press Enter and the path is copied to the system clipboard |
| Stdout output | The path is also printed to the terminal for use in pipelines |
| Pipe support | Works via cat file.json \| qki |
| File support | Works via qki file.json |
| Vim keybindings | Navigate with h/j/k/l in addition to arrow keys |
| Special chars in keys | Keys with spaces/special characters are automatically quoted |
| Cross-platform | Linux, macOS, Windows |
Demo
Input data
{
"users": [
{
"id": 1,
"name": "Alice",
"address": {
"city": "Moscow",
"zip": "101000"
},
"tags": ["admin", "active"]
},
{
"id": 2,
"name": "Bob",
"address": {
"city": "Berlin",
"zip": "10115"
},
"tags": ["user"]
}
],
"meta": {
"total": 2,
"page": 1
}
}QKI interface
┌─QKI──────────────────────────────────────────────────────────┐
│>> ▼ root │
│ ▼ users: [...] │
│ ▶ 0: {...} │
│ ▶ 1: {...} │
│ ▼ meta: {...} │
│ total: 2 │
│ page: 1 │
│ │
│ │
│ │
├──────────────────────────────────────────────────────────────┤
│ JQ Path: . | Enter: copy & exit | q/Esc: exit │
└──────────────────────────────────────────────────────────────┘Navigating to a nested field
Press ↓ a few times, then → to expand users, then → on element 0,
then → on address, move to city and press Enter:
┌─QKI──────────────────────────────────────────────────────────┐
│ ▼ root │
│ ▼ users: [...] │
│ ▼ 0: {...} │
│ id: 1 │
│ name: "Alice" │
│ ▼ address: {...} │
│>> city: "Moscow" │
│ zip: "101000" │
│ ▶ tags: [...] │
│ ▶ 1: {...} │
│ ▶ meta: {...} │
├──────────────────────────────────────────────────────────────┤
│ JQ Path: .users[0].address.city | Enter: copy & exit │
└──────────────────────────────────────────────────────────────┘Result in clipboard and stdout:
.users[0].address.cityNow you can use it immediately:
cat response.json | jq '.users[0].address.city'"Moscow"Installation
From source (recommended)
Requirements: Rust 1.75+ and cargo
git clone https://github.com/filip-mitish/QKI.git
cd QKI
cargo build --releaseThe binary will be at target/release/qki. Copy it to your PATH:
sudo cp target/release/qki /usr/local/bin/Or to a local directory:
mkdir -p ~/.local/bin
cp target/release/qki ~/.local/bin/Make sure ~/.local/bin is in your $PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcVia cargo install
cargo install --git https://github.com/filip-mitish/QKI.gitVia NPM
npm install -g qki-cliThe NPM package will automatically compile the binary via cargo build --release
during installation. A Rust toolchain is required.
Usage
Basic usage
Read from a file:
qki data.jsonRead from stdin (pipe):
cat data.json | qkiWork with APIs:
curl -s https://api.github.com/repos/torvalds/linux | qkiReal-world API examples
GitHub API
curl -s https://api.github.com/users/torvalds | qkiNavigate to public_repos, press Enter, you get:
.public_reposDocker API
docker inspect nginx | qkiNavigate to the container configuration field you need.
Kubernetes
kubectl get pod my-pod -o json | qkiVisually find the field you need in the pod spec.
AWS CLI
aws ec2 describe-instances | qkiNo more remembering the path to Reservations[0].Instances[0].InstanceId.
Terraform
terraform show -json | qkiNavigate through your infrastructure state visually.
Controls
Navigation keys
| Key | Action |
|-----|--------|
| ↑ or k | Move up in the tree |
| ↓ or j | Move down in the tree |
| → or l | Expand a node (object/array) |
| ← or h | Collapse a node or jump to parent |
| Enter | Copy jq path to clipboard and exit |
| q | Exit without copying |
| Esc | Exit without copying |
Node icons
| Icon | Meaning |
|------|---------|
| ▼ | Expanded object or array |
| ▶ | Collapsed object or array |
| (space) | Leaf value (string, number, bool, null) |
Current position indicator
The >> symbol to the left of a line shows the currently selected node.
The bottom panel displays the full jq path for the current node.
How jq paths are generated
QKI automatically builds a correct jq path for any node regardless of nesting depth and key types.
Regular keys
Keys consisting of letters, digits, and underscores are written with dot notation:
.users
.address.city
.meta.totalKeys with special characters
Keys containing spaces, hyphens, and other special characters are automatically wrapped in quotes:
{
"my field": "value",
"some-key": 42,
"123start": true
}Generated paths:
."my field"
."some-key"
."123start"Array elements
Array elements are addressed with square brackets and an index:
.users[0]
.users[1].name
.tags[0]Nested structures
For deeply nested structures the path is built recursively:
{
"a": {
"b": {
"c": {
"d": [1, 2, {"e": "deep"}]
}
}
}
}Path to "deep":
.a.b.c.d[2].eMixed structures
{
"data": [
{
"items": [
{"name": "first"},
{"name": "second"}
]
}
]
}Path to "second":
.data[0].items[1].nameArchitecture
Project structure
QKI/
├── src/
│ └── main.rs # Main application code
├── bin/
│ └── qki # NPM wrapper (Node.js)
├── Cargo.toml # Rust configuration
├── Cargo.lock # Locked dependencies
├── package.json # NPM configuration
└── README.md # DocumentationDependencies
| Crate | Version | Purpose |
|-------|---------|---------|
| ratatui | 0.30.0 | Terminal UI framework |
| crossterm | 0.29.0 | Cross-platform terminal handling |
| serde_json | 1.0.149 | JSON parsing |
| arboard | 3.6.1 | System clipboard access |
Internal design
1. Input parsing
QKI accepts JSON from two sources:
- stdin (via pipe) — detected using
io::stdin().is_terminal() - file (command line argument) — via
fs::read_to_string
Input data is parsed through serde_json into the Value type.
2. Tree construction
The JSON value is recursively converted into a tree of Node structs.
Each node contains:
- Key (
k) — field name or array index - Value (
v) — displayed value - Children (
c) — vector of child nodes - Expansion state (
ex) — expanded or collapsed - jq path (
p) — full jq path to this node - Leaf flag (
is_leaf) — whether the node is a terminal value
3. Rendering
On each frame the tree is "flattened" into a linear list of visible items
(the flatten function). Collapsed nodes skip their children.
The list is rendered through the List widget from ratatui with
highlighting of the current element.
4. Input handling
Keyboard events are read through crossterm::event. Non-blocking polling
is used with a 16ms timeout (60 FPS). Navigation key presses update the
current element index. Pressing Enter saves the path and the loop exits.
5. Clipboard copy
On exit via Enter, the arboard library copies the jq path to the system
clipboard. If the clipboard is unavailable (headless server, SSH without X11),
the path is still printed to stdout.
Comparison with alternatives
QKI vs jq
| Criterion | jq | QKI | |-----------|------|------| | Interactive | No | Yes | | Learning curve | Steep | Zero | | Visual representation | No | Tree-based TUI | | Auto path generation | No | Yes | | Clipboard copy | No | Yes | | Speed for simple tasks | Slow (googling) | Instant | | Filtering & transformation | Yes | No (different purpose) | | Scripting | Yes | Minimal |
QKI does not fully replace jq. QKI helps you find the correct path, which you then use with jq. It is a complementary tool.
QKI vs fx
| Criterion | fx | QKI | |-----------|------|------| | Language | Go/JS | Rust | | jq path generation | No | Yes | | Path copy | No | Automatic | | Binary size | ~10MB | ~3MB | | Startup speed | Medium | Fast | | Dependencies | Node.js (for some features) | None |
QKI vs jless
| Criterion | jless | QKI | |-----------|-------|------| | Language | Rust | Rust | | jq path generation | Partial | Full | | Path copy | No | Automatic | | Search | Yes | No (planned) | | Vim navigation | Yes | Yes | | Focus | Viewing | Path generation |
Pipelines and integration
Usage in scripts
When you press Enter, QKI outputs the path to stdout, which allows it to be used in pipelines:
PATH_EXPR=$(cat data.json | qki)
cat data.json | jq "$PATH_EXPR"Integration with fzf
cat data.json | jq 'paths | map(tostring) | join(".")' | fzfQKI is more convenient for this task as it shows context and values.
Shell alias integration
Add to your .bashrc or .zshrc:
alias jqi='qki'
alias jsonpath='qki'Or a more advanced alias with auto-apply:
jqauto() {
local path
path=$(cat "$1" | qki)
if [ -n "$path" ]; then
echo "Applying: jq '$path'"
cat "$1" | jq "$path"
fi
}Usage:
jqauto response.jsonIntegration with curl
apiq() {
local path
path=$(curl -s "$1" | qki)
if [ -n "$path" ]; then
curl -s "$1" | jq "$path"
fi
}Usage:
apiq https://api.github.com/users/torvaldsBuilding from source
Requirements
- Rust 1.75 or higher
- cargo (installed alongside Rust)
Installing Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shDebug build
cargo buildBinary: target/debug/qki
Release build
cargo build --releaseBinary: target/release/qki
Run tests
cargo testCheck code
cargo clippyFormat code
cargo fmtNPM package configuration
The NPM package uses a postinstall hook to automatically compile the Rust code:
{
"scripts": {
"postinstall": "cargo build --release"
}
}When installing via npm install -g qki-cli:
- NPM downloads the package
cargo build --releaseis executed- The binary
target/release/qkiis created - NPM registers the wrapper
bin/qkias a global command - The wrapper launches the compiled Rust binary when invoked
Error handling
QKI implements strict error handling at every stage:
| Situation | Behavior |
|-----------|----------|
| No data on stdin and no file argument | Prints no data, exit code 1 |
| Error reading stdin | Prints err_stdin, exit code 1 |
| Error reading file | Prints err_file, exit code 1 |
| Invalid JSON | Prints bad json, exit code 1 |
| Terminal initialization error | Prints raw err, exit code 1 |
| Alternate screen error | Prints altscreen err, exit code 1 |
| Clipboard unavailable | Path is printed to stdout only |
| Rendering error | Clean TUI exit |
All errors are printed to stderr, keeping stdout clean.
Limitations
- Does not support filtering or data transformation (that is what jq is for)
- Does not support search within the tree (planned)
- Does not support JSON editing
- Does not support YAML, TOML, or other formats (planned)
- On very large JSON files (100MB+) parsing delay is possible
- Clipboard may not work in headless environments without X11/Wayland
Roadmap
v1.1
- [ ] Search by keys and values (
/to start search) - [ ] Syntax highlighting for values (strings, numbers, bools, null in different colors)
- [ ] Configurable colors via configuration file
v1.2
- [ ] YAML input support
- [ ] TOML input support
- [ ] Multiple path selection
- [ ] Selected data export
v2.0
- [ ] Built-in real-time jq filter
- [ ] Split view: tree on the left, jq result on the right
- [ ] Path selection history
- [ ] Bookmarks for frequently used paths
FAQ
Why yet another JSON viewer?
QKI is not a viewer. It is a tool for generating jq syntax. The core value is automatic creation of the correct path and instant clipboard copy.
Why Rust?
- Fast startup (critical for CLI tools)
- Minimal memory usage
- No runtime dependencies
- Single static binary
Does it work over SSH?
Yes, but the clipboard will not function without X11 forwarding. The path is still output to stdout, so you can use it manually.
Does it work with large files?
QKI loads the entire JSON into memory. For files up to 50MB it works well.
For files above 100MB there may be a parsing delay. For gigabyte-sized files
it is recommended to pre-filter with jq.
Can I use it together with jq?
That is exactly the intended workflow. QKI finds the path, jq applies it:
# Step 1: find the path visually
cat data.json | qki
# Copied: .users[0].address.city
# Step 2: apply with jq
cat data.json | jq '.users[0].address.city'Why the name QKI?
Quick Key Inspector — a fast key inspector for JSON structures.
Contributing
Contributions of any kind are welcome. Here are a few areas where help is needed:
- Tree search — implementing
/for searching by keys and values - Color highlighting — different colors for JSON value types
- YAML/TOML support — automatic format detection
- Tests — unit tests for path generation and parsing
How to contribute
git clone https://github.com/filip-mitish/QKI.git
cd QKI
cargo build
cargo run -- test.jsonCreate a branch, make your changes, open a Pull Request.
License
MIT License
Copyright (c) 2026 tripock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
