- Round 1 — Core Request Engine
- Round 2 — Environment Variables (partly - more details here)
- Round 3 — Collections & Workspaces
- Round 4 — Authentication
- Round 5 — Request Headers & Query Params (Done Partly)
- Round 6 — Request Body Editor (Done Partly)
- Round 7 — Response Viewer
- Round 8 — History
- Round 9 — Scripting (Pre/Post Hooks)
- Round 10 — Streaming & SSE
- Round 11 — Import & Export
- Round 12 — Configuration & Theming
- Round 13 — Polish, Performance & Release
| Layer | Files |
|---|---|
| Entry | main.rs |
| App | app.rs |
| Events | event.rs |
| Terminal | terminal.rs |
| Errors | error.rs |
| State | state/app_state.rs, state/focus.rs, state/mode.rs, state/request_state.rs, state/response_state.rs |
| HTTP | http/client.rs, http/builder.rs, http/executor.rs |
| UI | ui/layout.rs, ui/sidebar.rs, ui/highlight.rs, ui/status_bar.rs |
| UI/Request | ui/request/url_bar.rs, ui/request/tab_bar.rs |
| UI/Response | ui/response/mod.rs, ui/response/tab_bar.rs, ui/response/body_viewer.rs |
- Event loop: background thread feeds
crosstermevents +Tickintompsc::UnboundedChannel<Event> - HTTP:
tokio::spawn+tokio::select!withCancellationTokenfor cancellation; result sent back asResponseevent - Rendering: all
ui/functions are pure — take&AppState+Frame, never mutate - Syntax highlighting:
syntectviaLazyLock-initializedSyntaxSet/ThemeSet
reqwest0.12:Response::cookies()removed — cookies parsed fromSet-Cookieheaders manuallytokio-util0.7:CancellationTokenlives under feature"rt", not"sync"(doesn't exist)- Mouse wheel scroll wired up in Normal mode via
MouseEventKind::ScrollUp/Down - Response scroll offset clamped to prevent over-scrolling past content
| Layer | Files |
|---|---|
| Env Core | src/env/mod.rs, src/env/resolver.rs, src/env/interpolator.rs |
| Storage | src/storage/environment.rs |
| State | src/state/environment.rs, src/state/app_state.rs (additions) |
| UI | src/ui/env_editor.rs, src/ui/request/url_bar.rs (additions) |
| App Logic | src/app.rs (additions), src/ui/layout.rs (additions) |
Data Structures (src/state/environment.rs):
Environment { id, name, color, variables }— each env stored as TOML by UUID filenameEnvVariable { key, value, var_type, enabled, description }— individually toggleable rowsVarType::Text | VarType::Secret— secrets masked as••••••••in UI, sent as real value
{{variable}} Parser (src/env/interpolator.rs):
parse_vars(input) → Vec<(start, end, name)>— byte-offset–aware, skips empty/unclosed braces- Handles multiple variables per string:
{{scheme}}://{{host}}/path
EnvResolver — Layered Resolution (src/env/resolver.rs):
resolver_from_state(state)builds resolver with priority layers: active env → OS env varsresolve(&str) → ResolvedString— for display: secrets masked, unresolved kept as{{name}},VarSpanlist for UI highlightingresolve_for_send(&str) → String— for HTTP: secrets sent as real value; unresolved kept as-is- UTF-8 byte-boundary aware throughout; unit-tested
Variable Highlighting in URL Bar (src/ui/request/url_bar.rs):
- Resolved vars highlighted cyan (
Rgb(42, 195, 222)), unresolved red (Rgb(247, 118, 142)) - Cursor mode:
build_highlighted_url_with_cursor()renders cursor block inside variable spans - Ghost text row 2: shows
→ resolved_url(muted) so user sees what will be sent
Variable Interpolation at Send-time (src/app.rs::send_request):
- URL and enabled request headers resolved via
resolve_for_send()before HTTP dispatch - Resolver built from current
AppStateat send-time → live env switching without re-typing
Environment Switcher Popup (src/ui/env_editor.rs::render_switcher):
Ctrl+Etoggles popup; centered, ~50%×40%,Clearoverlay darkens background- List with active env (green
●), search filter, selection withj/k Enteractivate,Alt+eopen editor,Alt+nnew env (naming mode),Alt+ddelete- Active env first-selected on startup (index 0 if any exist)
Environment Editor Popup (src/ui/env_editor.rs::render_editor):
- Full table: key/value/description/type columns with per-column widths
- Row + column cursor
(row, col), inline Insert mode per cell Tab→ next cell, auto-creates new row at end of last row;rrenames env nameSpacecontext-aware: col 0 = toggle enabled, col 1 = toggle show_secret, col 3 = toggle var_type- Secrets masked by default;
Spaceon value col reveals plaintext
TOML Persistence (src/storage/environment.rs):
save(env)writes{id}.toml;load_all()loads all*.tomlon startup;delete(id)removes file- Data dir:
%APPDATA%\forge\environments\(Windows) / XDG / macOS equivalents - Files named by UUID → rename-safe
| Feature | Status |
|---|---|
| Variable interpolation in request body & auth fields | Not started |
| Secret variable encryption (AES-256-GCM, machine-local key) | Not started (plaintext storage) |
| "Unresolved variable" persistent warning indicator | Not started (red highlight works; no badge/warning) |
- All cursor positions tracked in bytes (not chars);
prev_char_boundary_of()/next_char_boundary_of()helpers guard UTF-8 safety resolve()vsresolve_for_send()split keeps display masking decoupled from HTTP transmissionfiltered_env_count()prevents selection overflow when search narrows the list- Environment file IDs are UUIDs → files survive renames without path changes
| Layer | Files |
|---|---|
| State | state/collection.rs, state/workspace.rs, state/app_state.rs (major migration) |
| Storage | storage/workspace.rs, storage/collection.rs |
| UI | ui/sidebar.rs (full rewrite), ui/request_tabs.rs, ui/naming_popup.rs, ui/confirm_delete.rs, ui/workspace_switcher.rs |
| App Logic | app.rs (sidebar CRUD, tab management, workspace switching) |
- AppState migration:
state.request/state.response→state.workspace.open_tabs[active_tab_idx]; accessed viastate.active_tab()/state.active_tab_mut() - Environments moved:
state.environments→state.workspace.environments;state.active_env_idx→state.workspace.active_environment_idx - Storage path:
%APPDATA%/forge/workspaces/<ws-name>/(Windows) / XDG / macOS equivalents - Sidebar tree:
SidebarNodeenum flattened to a list viaflatten_collections(); collapsed node IDs tracked insidebar.collapsed_ids: HashSet<Uuid> - Tabs:
WorkspaceState.open_tabs: Vec<RequestTab>;active_tab_idxtracks focus; tabs persist toworkspace.toml
| Key | Action |
|---|---|
Ctrl+W |
Workspace switcher popup |
Ctrl+n (Sidebar) |
New collection |
n (Sidebar) |
New request |
f (Sidebar) |
New folder |
r (Sidebar) |
Rename selected item |
d (Sidebar) |
Delete selected item |
D (Sidebar) |
Duplicate selected item |
h / l (Sidebar) |
Collapse / expand node |
/ (Sidebar) |
Toggle search mode |
Alt+1–9 |
Switch to tab N |
Alt+w |
Close active tab |
[ / ] |
Cycle open tabs (non-UrlBar focus) |
- Sidebar search runs inline at the footer row (repurposed hint row);
NamingStatecarries the HTTP method for new requests so method persists through the naming popup flow active_tab()returnsOption<&RequestTab>— all render functions must handleNonegracefully- Workspace save triggered on every tab/request mutation via
dirtyflag; debounced via the Tick event flatten_collections()recurses into folders and respectscollapsed_idsto hide children
Key-Value Editor — generic reusable component built for the Headers tab:
- Add / remove key-value rows
- Toggle individual rows enabled/disabled
- Navigate rows with
j/k, edit cells in Insert mode - Used in
ui/request/headers_editor.rs(or equivalent); can be re-used for Query Params, form body, etc.
| Feature | Status |
|---|---|
| Query Params tab | Not started |
{{variable}} interpolation in header values |
Not started |
| Bidirectional URL ↔ Query Params sync | Blocked on Query Params |
Raw JSON Body Editor — basic body editing for JSON requests:
- Raw text editor for JSON body content
- Wired into the Body tab of the request panel
| Feature | Status |
|---|---|
| Body type selector (JSON / Form / Multipart / GraphQL / Raw / Binary) | Not started |
| Form URL-encoded body (key-value editor) | Not started |
| Multipart form body | Not started |
| GraphQL body (query + variables) | Not started |
| Raw body (plain text, XML, etc.) | Not started |
| Binary file upload | Not started |