Reviewed-on: #1
greq
greq — a git-native, TUI-based API client for the terminal. Like Postman, but it lives in your .greq/ folder, is version-controllable, and never opens a browser.
╭──────────────────────────╮╭──────────────────────────────────────────────────╮
│ GREQ [1:default] ││ → POST https://api.example.com/auth/login │
├──────────────────────────┤│──────────────────────────────────────────────────│
│ ▶ GET Health Check ││ { │
│ ▸ AUTH ││ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ ▶ POST Login ││ "user_id": 42 │
│ GET Refresh ││ } │
│ ▸ USERS ││ │
│ GET List Users ││──────────────────────────────────────────────────│
│ GET Get User ││ ● 200 OK │ ⚡ 213ms │ ◎ 1.2 KB │ app/json │
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
Enter:run s:stress c:curl i:import d:diff S:snapshot h:history n:new e:edit
Installation
git clone <repo>
cd greq
go build -o greq .
./greq # looks for .greq/ in the current directory
Dependencies: Go 1.21+. For clipboard support on Linux you may need
xclip(X11) orwl-clipboard(Wayland).
Project structure
greq/
├── main.go # entry point
├── internal/
│ ├── api/
│ │ ├── client.go # HTTP client, variable resolution, token extraction
│ │ ├── chain.go # request chaining ({{last_FIELD}})
│ │ ├── curl.go # cURL import / export
│ │ ├── lua_test_runner.go # Lua test script runner (gopher-lua)
│ │ ├── script.go # pre_script shell execution
│ │ └── stress.go # load test runner (100×, 10 workers)
│ ├── storage/
│ │ ├── types.go # RequestFile, RequestItem structs
│ │ ├── loader.go # YAML loading, env list
│ │ ├── writer.go # YAML saving
│ │ └── config.go # .greq/config.yaml (e.g. editor setting)
│ └── tui/
│ ├── model.go # Bubble Tea Model, states, item types
│ ├── update.go # Update logic, key handlers, commands
│ ├── view.go # View rendering (header, panels, overlays)
│ ├── styles.go # Lipgloss styles, colours, env colour
│ ├── delegate.go # Custom list delegate (folder headers)
│ ├── history.go # HistoryEntry, stats bar, history table
│ ├── diff.go # LCS line diff, snapshot save/load
│ └── editor_picker.go # Available editor detection
└── .greq/ # project-level data (can be committed to git!)
├── config.yaml # user settings
├── env.yaml # default environment
├── env.local.yaml # optional: local env
├── env.staging.yaml # optional: staging env
├── env.prod.yaml # optional: prod env
├── requests/
│ ├── health.yaml # root-level request
│ ├── auth/
│ │ └── login.yaml # auth folder
│ └── users/
│ ├── list.yaml
│ └── get_user.yaml
└── snapshots/ # response baselines (d/S keys)
└── login.json
.greq/ folder layout
env.yaml — variables
base_url: https://api.example.com
user_id: "42"
api_key: secret123
Reference them in any request with {{base_url}}, {{user_id}}, etc.
Request YAML schema
name: Login # display name in the list
method: POST # GET / POST / PUT / DELETE / PATCH
url: "{{base_url}}/auth/login" # variables allowed
headers:
Content-Type: application/json
X-Api-Key: "{{api_key}}"
body: |
{
"username": "admin",
"password": "secret"
}
# Shell script — runs BEFORE the request (to generate extra headers)
pre_script: |
TS=$(date +%s)
echo "X-Timestamp: $TS"
# Lua script — runs AFTER the response (for assertions)
test_script: |
assert_status(200)
assert_eq(json_body.success, true, "login successful")
if json_body.token then pass("token present") else fail("no token") end
config.yaml — settings
editor: nvim # default editor for the 'e' key
Keybindings
Main view
| Key | Action |
|---|---|
Enter |
Run the selected request |
s |
Load test (100 requests, Min/Max/Avg/error rate) |
e |
Edit request in $EDITOR (or configured editor) |
n |
New request wizard |
i |
Import a cURL command |
c |
Copy selected request as cURL |
d |
Diff: current response vs. saved baseline |
S |
Save current response as baseline |
h |
History overlay (all requests in the session) |
r |
Reload request list and env |
/ |
Fuzzy search across requests |
Tab |
Toggle focus: list ↔ response panel |
1–9 |
Switch environment (env.local.yaml, env.staging.yaml, etc.) |
q / Ctrl+C |
Quit |
Response panel (after Tab)
| Key | Action |
|---|---|
↑ / ↓ |
Scroll |
PgUp / PgDn |
Page scroll |
Tab |
Return to list |
Overlays (History, Diff)
| Key | Action |
|---|---|
↑ / ↓ |
Scroll |
Esc / h / d |
Close |
Features
Variables and Request Chaining
Values from env.yaml can be used as {{key}} placeholders in URLs, headers, and bodies.
If a response contains JSON, every top-level field is automatically available in the next request as {{last_FIELD}}:
Login response: { "id": 42, "token": "abc" }
→ {{last_id}} = "42"
→ {{last_token}} = "abc"
Next request URL: {{base_url}}/users/{{last_id}}
The token, access_token, and jwt fields are also automatically injected as Authorization: Bearer … headers in subsequent requests.
Lua test scripts
test_script runs in Lua after the response arrives.
Available variables:
| Variable | Type | Description |
|---|---|---|
status |
number | HTTP status code (e.g. 200) |
status_text |
string | Full status string (e.g. "200 OK") |
body |
string | Raw response body |
json_body |
table / nil | Parsed JSON, or nil |
headers |
table | Response headers (lowercase keys) |
duration_ms |
number | Response time in milliseconds |
size |
number | Body size in bytes |
Helper functions:
pass("message") -- green assertion
fail("message") -- red assertion
assert_eq(a, b, "message") -- fail if a ~= b
assert_status(200) -- fail if status does not match
json_decode("...") -- JSON string → Lua table
Example:
assert_status(200)
if json_body == nil then
fail("response is not JSON")
return
end
assert_eq(json_body.role, "admin", "admin permission")
if duration_ms > 500 then
fail("too slow: " .. duration_ms .. "ms")
else
pass("response time OK: " .. duration_ms .. "ms")
end
Results appear in the stats bar badge: ✓ 3 tests or ✗ 1/3 failed.
Shell pre-script
pre_script runs before the request, in a shell. Lines printed to stdout matching Key: Value are injected as extra request headers.
pre_script: |
TS=$(date +%s)
SIG=$(echo -n "POST${TS}" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -binary | base64)
echo "X-Timestamp: $TS"
echo "X-Signature: $SIG"
If the script exits with a non-zero code, the request is aborted.
cURL Import / Export
Export (c): Copies the selected request as a ready-to-use curl command to the clipboard (variables resolved). If no clipboard tool is available, the command is displayed in the right panel.
Import (i): Paste a curl command (e.g. from Chrome DevTools → Copy as cURL) and greq parses it automatically, opening the wizard pre-filled.
Load Testing ⚡
Press s on any selected request and greq fires it 100 times concurrently (10 workers at a time).
The response panel shows the aggregated stats:
⚡ LOAD TEST RESULTS
Requests 100
Concurrency 10 workers
Min 43ms
Avg 97ms
Max 312ms
Errors 0 (0.0%)
If any requests failed, the Errors line is highlighted in red. Press Enter afterwards to go back to normal request mode.
Response Diff
S → saves the current response as a baseline (.greq/snapshots/)
d → shows what changed since the baseline (red/green per line)
Useful when hitting the same endpoint at different times and wanting to see exactly what changed in the response.
Environments (1–9)
Multiple env files can coexist:
.greq/env.yaml → 1 (default, purple header)
.greq/env.local.yaml → 2 (green header)
.greq/env.staging.yaml → 3 (yellow header)
.greq/env.prod.yaml → 4 (RED header — warning)
The header colour instantly signals which environment is active, so you don't accidentally delete production data.
Quick start
# 1. Create the .greq folder at your project root
mkdir -p .greq/requests/auth
# 2. Write an env file
cat > .greq/env.yaml << EOF
base_url: https://api.example.com
EOF
# 3. Create a request
cat > .greq/requests/auth/login.yaml << EOF
name: Login
method: POST
url: "{{base_url}}/auth/login"
headers:
Content-Type: application/json
body: |
{"username": "admin", "password": "secret"}
test_script: |
assert_status(200)
if json_body.token then pass("token OK") else fail("no token") end
EOF
# 4. Run it
./greq
greq always looks for the .greq/ folder in the current directory, so each git repo can have its own request set.
Dependencies
| Package | Purpose |
|---|---|
charmbracelet/bubbletea |
TUI framework |
charmbracelet/bubbles |
List, textinput, viewport components |
charmbracelet/lipgloss |
Styles, colours, layout |
alecthomas/chroma/v2 |
JSON syntax highlighting |
yuin/gopher-lua |
Lua interpreter for test scripts |
atotto/clipboard |
Clipboard support |
gopkg.in/yaml.v3 |
YAML read/write |