# 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 ```bash git clone 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 ```yaml 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 ```yaml 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 ```yaml 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:** ```lua 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:** ```lua 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. ```yaml 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 ```bash # 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 |