Files
greq/README.md
T
Mester Gábor 2dd6519168 Initial commit
2026-03-19 07:12:03 +01:00

11 KiB
Raw Permalink Blame History

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) or wl-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
19 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 (19)

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