A terminal HTTP client. Keep named request configs in your project or globally, then run them by name from the shell or open the TUI to browse, edit, and load-test them.
Via shell script (macOS/Linux):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/playsthisgame/ichigo/releases/latest/download/ichigo-installer.sh | shVia PowerShell (Windows):
powershell -ExecutionPolicy ByPass -c "irm https://github.com/playsthisgame/ichigo/releases/latest/download/ichigo-installer.ps1 | iex"Via Cargo:
cargo install ichigoConfigs live in one of two places:
.ichigo/— project-local (checked into git alongside your code)~/.config/ichigo/— global (available everywhere)
When a name exists in both, the local one wins.
Your own preferences are separate: ~/.config/ichigo/config.toml. It sits in
the same directory but is TOML, so it is never mistaken for a request. See
Configuring ichigo.
ichigo new <name> Create a new request config
ichigo run <name> Execute a request
ichigo list List all configs
ichigo show <name> Print a config file
ichigo delete <name> Delete a config
ichigo test <name> Run a load test
ichigo copy <name> <new-name> Duplicate a config under a new name
ichigo new config-server-prod --method GET --url https://api.example.com/health
ichigo new config-server-dev --method GET --url https://localhost:8080/health
ichigo new create-user --method POST --url https://api.example.com/usersFlags:
-m, --method— HTTP method (default:GET)-u, --url— target URL-g, --global— save to~/.config/ichigo/instead of.ichigo/--from-curl— build the request from a cURL command read from stdin
Anything that hands you a cURL command — a browser's "Copy as cURL", an API doc, a teammate's bug report — can become a config directly:
pbpaste | ichigo new api/login --from-curlichigo new api/login --from-curl <<'EOF'
curl -X POST 'https://api.example.com/login' \
-H 'Content-Type: application/json' \
--data-raw '{"user":"ada"}'
EOFThe command is read from stdin rather than taken as an argument, so you never
have to re-escape its quotes. --from-curl cannot be combined with --method
or --url, since the command already supplies both.
What the parser does with each flag:
| Mapped | --url, -X/--request, -H/--header, -d/--data/--data-raw/--data-ascii/--data-binary, --data-urlencode, --json, -G/--get, -I/--head, -u/--user, -b/--cookie, -A/--user-agent, -e/--referer |
| Ignored | -s, -S, -v, -i, -k, -L, -f, -o, -w, --compressed, --max-time, --connect-timeout, --retry, --limit-rate — these describe how curl behaves, not what it sends |
| Refused | -F/--form (multipart), @file data references, and any flag not listed above |
An unrecognized flag is an error naming the flag, not a silent skip: a config that quietly drops half of what you pasted is worse than one that refuses to be created. If ichigo refuses a flag you need, the command is unchanged in your clipboard.
Some details worth knowing:
- The method follows curl's own rules — an explicit
-Xwins,-Imeans HEAD, a body means POST, everything else is GET. - A query string on the URL is split into the
querymap. If a key repeats (?tag=a&tag=b), the query stays on the URL instead, since the map holds one value per key. - A
Content-Typeheader becomes the body'scontent_type. A body with no content type gets curl's default,application/x-www-form-urlencoded. --compressedis dropped rather than stored as anAccept-Encodingheader — ichigo's HTTP client negotiates encoding itself, so storing the header would make the config claim something it does not do.-u user:passbecomes theAuthorization: Basic …header curl would send.
An imported config holds the literal values from the command, including whatever token or cookie was in it. Before committing
.ichigo/to a repo, replace those with{{VAR}}placeholders and put the real values in a profile or an environment variable.
ichigo run config-server-prod
ichigo run create-user --var TOKEN=abc123 --var USER_ID=42Variables replace {{PLACEHOLDER}} tokens anywhere in the config (url, headers, query, body). They are resolved in this order: --var flags → environment variables.
Flags:
-v, --var KEY=VALUE— set a variable--verbose— print the full response: a200 OK · 143 ms · 4.2 KBsummary line, then the body-p, --profile— use a named profile (see Profiles)
A chain config runs multiple requests in sequence, passing extracted values from one step into the next. Only the final step's response is printed; use --verbose to see all steps.
ichigo run login-and-fetch --verboseChain configs are detected automatically by the presence of a steps: key — no separate command needed.
Duplicates an existing config under a new name. Useful for creating variants of a request (e.g. a prod and dev version of the same endpoint).
ichigo copy config-server-prod config-server-dev
ichigo copy config-server-prod config-server-staging --globalFlags:
-g, --global— look for the source in~/.config/ichigo/and save the copy there too
Without --global, both the source and the copy are in .ichigo/. With --global, both are in ~/.config/ichigo/. The copy command will error if the source does not exist or if a config with the new name already exists.
Runs the request N times and reports status code counts and timing stats.
ichigo test config-server-prod --iter 100
ichigo test create-user --iter 50 --profile stagingFlags:
-v, --var KEY=VALUE— set a variable-i, --iter— number of iterations-p, --profile— use a named profile
Opens an interactive terminal UI for browsing, running, and load-testing your configs.
ichigo tuiKeybindings:
| Key | Action |
|---|---|
? |
Show the full keymap |
j / k |
Navigate list |
gg / G |
Jump to top / bottom |
| Space | Expand / collapse a folder |
f |
Filter the list |
r / Enter |
Run selected request |
t |
Load-test selected request |
y |
Copy selected request as a cURL command |
i |
Import a pasted cURL command as a new request |
n / e / c |
New / edit / clone a request |
d |
Delete selected request |
R |
Refresh the config list from disk |
q |
Quit |
| Esc | Go back |
The hint line at the bottom of the screen carries only the handful of keys you
reach for most; press ? for the full keymap, grouped by what it does. Any key
dismisses it.
The TUI ships a dark and a light palette, and every color in it is configurable — see Themes.
Resizing the panes. Drag the divider between the list and the detail pane
with the mouse; it works in every mode, so you can widen the detail pane while a
response or a form is up. The width a session starts at is
layout.split_pct — a drag is not saved back to it.
The TUI is meant to be left open. Every run and load test re-reads the config file from disk first, so edits you make in another terminal — rotating a token in a profile, adding a header, changing a URL — take effect on the next run with no restart. Press R when you have created, renamed, or deleted config files on disk and want them to show up in the list.
Every form field in the TUI — a URL, a header value, a profile param — is a small vim-style editor rather than an append-only box.
Fields start in insert mode. Esc drops to normal mode, where the
usual motions work; a second Esc leaves the pane. The caret shows which mode
you are in: a thin bar between characters for insert, a block over a character
for normal.
Works in either mode:
| Key | Action |
|---|---|
| ← → , Home / End | Move the caret |
| Backspace / Delete | Delete a character |
| Tab / BackTab | Next / previous field |
Motions (normal mode). Anywhere below, "line" means the whole field — every field is a single line except the request body.
| Key | Action |
|---|---|
h / l |
Left / right, never past the ends of the line |
w / b / e |
Word forward / back / to word end |
W / B / E |
The same, treating punctuation as part of the word |
0 |
First character of the line |
^ / _ |
First non-blank of the line |
$ |
Last character of the line |
gg / G |
First / last line (the same place in a single-line field) |
j / k |
Next / previous field — or, in the body, next / previous line |
fx / Fx |
Jump forward / back to the next x on the line |
tx / Tx |
The same, stopping one character short of it |
; / , |
Repeat the last f/F/t/T forwards / backwards |
Entering insert mode:
| Key | Action |
|---|---|
i / a |
Insert before / after the caret |
I / A |
Insert at the first non-blank / end of the line |
s |
Delete the character under the caret and insert |
C / S |
Change to the end of the line / change the whole line |
Editing:
| Key | Action |
|---|---|
x |
Delete the character under the caret |
D |
Delete to the end of the line |
rx |
Replace the character under the caret with x |
~ |
Swap the case of the character under the caret |
u |
Undo the last change |
Operators. d (delete), c (change — delete and insert) and y (yank)
take any motion from the table above: dw, d$, db, ct/, y^. Doubling
the operator acts on the whole line (dd, cc, yy, and Y as a shorthand
for yy). As in vim, cw changes the word without swallowing the space after
it. A key that is not a motion cancels the operator rather than doing something
else, so a mistyped dq is harmless.
Putting. p puts the last yanked or deleted text after the caret, P
before it. x, s, d, c, D, C and S all fill the register, so x
then p transposes two characters exactly as in vim.
The register is shared across every field, which is what makes it useful:
yank a bearer token out of one header with y$ and put it into another with
p. It is always charwise, even for dd and yy — so yy then p inserts
the line's text at the caret rather than opening a new line below. That is
deliberate: every field but the body is a single line that must never gain a
newline.
u steps back one change at a time, not one keystroke: everything typed
between entering insert mode and leaving it undoes together, so one u reverses
the whole URL you just typed rather than its last character. Unlike the
register, the undo history belongs to the field, and moving focus starts a new
one — Tab away and back and there is nothing left to undo. There is no redo.
Long values stay readable. A value wider than the pane scrolls sideways to
keep the caret on screen, with ‹ and › marking text hidden either side — so
walking through a long bearer token with w or f never loses the cursor off
the edge. Widen the pane by dragging the divider if you want to see more of it
at once.
Every field here is a single line, with one exception: the request body, where
Enter types a newline, j/k move by line, and the line commands act on the
line under the cursor. See
Editing the request body.
If you map jk (or similar) to Esc in vim, see Configuring ichigo.
The response pane has a cursor line, so you can take just the lines you want without fighting your terminal's text selection:
| Key | Action |
|---|---|
j / k |
Move the cursor (the view scrolls to follow) |
y |
Copy the cursor line |
V |
Start a line selection; j/k extend it, y copies it |
| Esc | Cancel the selection (again to leave the pane) |
f |
Filter to matching lines |
c |
Copy everything the pane is showing |
H |
Show / hide the response headers |
The cursor line and the selection are drawn as bands whose colors — like every other color in the TUI — you can change; see Themes.
Copying this way beats dragging with the mouse: a terminal's selection is
linear, so a drag that spans more than one row also takes the request list and
the pane borders on the rows in between. y copies the lines themselves.
c copies what the pane shows. With a filter active that is the matching
lines only, which is often the quickest route to a couple of scattered
fields — filter to secret and c gives you both token lines with nothing
in between. Clearing the filter puts the cursor back at the top, since the
line it pointed at may no longer be on screen.
Pressing y in the request list turns the selected request into a cURL command and copies it to the clipboard. It runs the same profile picker and variable prompts as a normal run, so the command it produces carries the resolved values — the profile's real token, not {{TOKEN}}. The command is shown before it is copied, and c re-copies it. Chains cannot be copied this way: a chain feeds values extracted from one step into the next, which a single cURL command has no way to express.
The generated command contains your real credentials. That is what makes it useful, and also what makes it unsafe to paste into a public issue, a pull request, or a shared log. Redact before sharing.
Copying uses whichever clipboard tool is available, tried in order: pbcopy (macOS), wl-copy (Wayland), xclip, then xsel (X11). If none is installed, the TUI says so rather than failing silently.
Pressing i does the reverse: it opens a pane you paste a cURL command into.
Press Ctrl+s to import it, and the new-request form opens with the method,
URL, headers, query, and body already filled in — you supply the name, and
nothing is written until you save. Enter inserts a newline rather than
importing, so a multi-line paste works even in terminals that do not support
bracketed paste. If the command uses something ichigo cannot store, the pane
reports which flag and keeps your text so you can edit it. The
--from-curl section covers exactly
which flags are mapped, ignored, and refused.
If the selected request has profiles, a profile picker appears before the variable input screen. Use j/k to choose a profile (or (no profile) to skip), then press Enter. Any variables not covered by the profile can still be filled in manually.
Every run leads with one line saying what came back and how long it took:
200 OK · 143 ms · 4.2 KB
{
"id": 42
}
The time is measured around the request itself — sent to headers-in — which is
exactly what test averages, so a single run and a load test of the
same request are quoting the same number. Downloading the body is not in it.
The size is the body as the server sent it, not as the pane shows it:
ichigo pretty-prints JSON, and counting the newlines it added would be
reporting on ichigo rather than on the response.
An image response folds its media detail into the same line rather than stacking a second summary under it:
200 OK · 143 ms · image/jpeg · 2121×1414 · 1.2 MB
The summary is ordinary text like everything else in the pane, so c copies it
along with the body. Filter or use y if you want the body on its own.
ichigo run --verbose prints the same line. A bare ichigo run still prints
the body and nothing else, so piping into jq is unaffected.
A response's headers are hidden to start — the body is what a run is for — and
H folds them into the top of the pane:
content-type: application/json
etag: "v1-abc"
set-cookie: session=...
x-ratelimit-remaining: 41
{
"id": 42
}
The H headers marker in the pane's title says which way the toggle sits. It
appears only when there are headers to show, so H does nothing on an error, on
a generated cURL command, or on a chain — whose combined block has no single set
of headers to attach.
They are ordinary lines, so everything in Copying part of a
response works on them unchanged: f filters
across the headers and the body together, the cursor walks into them, and V/y
copies them like any other line — filter to ratelimit, press y, and you have
the header on its own. Names are lower-cased and sorted, and a header sent more
than once (Set-Cookie) gets one line per value, in the order it arrived.
Toggling resets the cursor and any selection, for the same reason changing the
filter does: the lines underneath them have moved.
A response whose content type is image/* is drawn in the response pane,
underneath its summary line:
200 OK · 143 ms · image/jpeg · 2121×1414 · 1.2 MB
The summary is always there, and it is real text — f, y, V and c work on
it exactly as they do on a JSON body. The picture is drawn below it, and if it
cannot be drawn you get the summary and a note saying why, never an error pane:
a 200 that could not be decoded is still a 200. SVG is shown as source
rather than rendered, since its markup is more use in the text pane than a
failed decode.
Drawing needs a terminal that speaks a graphics protocol — kitty (Ghostty, Kitty), sixel, or iTerm2's (iTerm2, WezTerm). ichigo probes for one at launch; where there is none, the pane says so instead of leaving a blank space.
Inside tmux it works with no configuration on your side. tmux draws no
images itself, it only forwards them to the terminal it is attached to, and
ichigo turns that forwarding on (allow-passthrough) for its own pane at
launch — nothing is written to your tmux.conf, and nothing changes for any
other pane. One thing does change: tmux refuses to forward more than 1 MiB at a
time, so a large image is drawn at lower resolution than the pane could show. It
is complete and readable, just softer than the same response outside tmux.
Inside herdr, add this to ~/.config/herdr/config.toml
and restart herdr:
[experimental]
# Local Kitty graphics rendering for attached clients (needed for image
# responses in ichigo, and any other TUI that draws images).
kitty_graphics = trueWithout it herdr does not render kitty graphics for its attached clients, so an image response shows only its summary line.
name: create-user
method: POST
url: https://api.example.com/users
description: "Create a new user"
headers:
Authorization: "Bearer {{TOKEN}}"
Accept: application/json
query:
version: "2"
body:
content_type: application/json
data: |
{
"name": "{{USER_NAME}}"
}Use steps: instead of a top-level request. Each step is a full request config, and extract: maps variable names to JSON paths in the response. Extracted values are available as {{VAR}} in all subsequent steps.
name: login-and-fetch
steps:
- name: login
method: POST
url: https://api.example.com/auth/login
body:
content_type: application/json
data: |
{
"username": "{{USERNAME}}",
"password": "{{PASSWORD}}"
}
extract:
TOKEN: $.token
- name: fetch-profile
method: GET
url: https://api.example.com/me
headers:
Authorization: "Bearer {{TOKEN}}"Profiles let you bundle a set of variable values under a name, so you can switch between environments (e.g. dev vs staging vs prod) without retyping vars each time.
name: create-user
method: POST
url: https://{{HOST}}/users
headers:
Authorization: "Bearer {{TOKEN}}"
body:
content_type: application/json
data: |
{
"name": "{{USER_NAME}}"
}
profiles:
- name: dev
params:
HOST: localhost:8080
TOKEN: dev-token-123
- name: staging
params:
HOST: staging.example.com
TOKEN: stg-token-456Run with a profile from the CLI:
ichigo run create-user --profile dev
ichigo test create-user --iter 50 --profile stagingAny {{PLACEHOLDER}} not covered by the chosen profile is still resolved from --var flags or environment variables as normal. In the TUI, a profile picker appears automatically when profiles are present — pick one (or skip) and fill in any remaining variables interactively.
n opens a blank request form and e opens the selected one. Tab and
Shift-Tab walk it, and the walk covers more than the four text fields:
| Row | Enter does |
|---|---|
name, method, url, description |
Save the request |
[ ] global |
Toggle between .ichigo/ and ~/.config/ichigo/ |
headers |
Open the headers pane |
query params |
Open the query params pane |
body |
Open the body pane |
profiles |
Open the profiles pane |
So every part of a request is reachable by tabbing through the form — there is
no chord to know in advance. Ctrl+g, Ctrl+e, Ctrl+q, Ctrl+b, and
Ctrl+p still jump straight to those five from anywhere in the form.
The four text fields are editors (see Editing text fields), which is why Esc takes two presses there — the first leaves insert mode, the second leaves the form. On the five rows below them there is no text to edit, so one Esc leaves.
Nothing reaches disk until you save, and the headers, query, body and profile panes all apply their edits into the form rather than to the file. So every one of those panes says ● unsaved in its title the moment the request holds something a save would write and a save has not written yet:
┌ Headers ● unsaved ───────────────────────────────┐
A sub-pane counts its own rows too, before you apply them — a header you have typed but not yet confirmed with Enter is a change the request does not have.
Leaving the form with changes outstanding puts up a prompt rather than dropping
them. It is a list: j/k or the arrow keys walk it, Enter picks.
| Row | What it does |
|---|---|
| Save and close | Write the request and leave the form |
| Discard changes | Leave the form and lose the changes |
| Keep editing | Back to the form, everything intact |
It opens on Save and close, so a reflexive Enter never loses anything, and Esc means keep editing. Any other key is ignored while the prompt is up.
A form you have only walked through is not unsaved, and neither is an edit the save would normalize away — a trailing space typed into a URL is not a change the file would record. A cloned or imported request is unsaved from the moment it opens: there is no file behind it yet.
Headers and query params are edited in the same pane. Tab to headers or
query params in the request form and press Enter, or use Ctrl+e and
Ctrl+q from anywhere in the form:
| Key | Action |
|---|---|
| Tab / Shift-Tab | Move between name and value fields |
Ctrl+a |
Add a row |
Ctrl+d |
Remove the row under the cursor |
| Enter | Apply to the request |
| Esc | Discard the edits |
Ctrl+halso opens headers, but only in terminals that send it. Many terminals, tmux configs, and shells bind Ctrl+H to backward-delete-char and send a plain Backspace instead, in which case the key just deletes a character. Tab to the row and press Enter if you want the path nothing can intercept.
Applying only updates the request in memory — Enter on the request form is what writes the file, so Esc out of that form drops the changes too.
Blank-named rows are dropped rather than treated as an error, so a row you add and then think better of costs nothing. Names and values are trimmed.
The two panes differ in one rule, and it follows the protocol rather than the
UI. Two headers with the same name are refused without regard to case: HTTP
treats Accept and accept as one header, so keeping both would mean sending
whichever won a coin toss. Two query params are compared exactly, so page
and Page are different params and both are kept — a query key means whatever
the server says it means, and refusing that pair would reject a request that is
perfectly legal to send. An exact repeat is still refused, since only one of the
two could survive.
If the request has a body, a Content-Type header is stored as the body's
content_type rather than as a header — ichigo derives the header from the body
when it sends, and holding both would put it on the wire twice. That rule is
headers-only: a query param that happens to be named Content-Type is just a
param.
Tab to body in the request form and press Enter, or press Ctrl+b:
| Key | Action |
|---|---|
| Tab / Shift-Tab | Move between the content type and the body |
| Enter | A newline in the body; a step down from the content type |
Ctrl+s |
Apply to the request |
| Esc | Discard the edits |
The body is the one field in ichigo that holds more than one line, so Enter
types a newline there and Ctrl+s is what applies the pane — the same pair of
keys the cURL import buffer uses, and for the same reason: a terminal without
bracketed paste delivers a pasted body as characters with Enters between the
lines, so an Enter that applied would keep the first line and throw the rest
away. Inside the body, j and k move by line and only move between the
pane's two rows at the top and bottom; 0, $, D and S act on the line
under the cursor rather than on the whole body.
Ctrl+bis tmux's default prefix key, so under tmux it never reaches ichigo. Tab to thebodyrow and press Enter instead — as with every other pane, that is the path nothing can intercept.
Emptying the body removes it, along with its content type: a request with
an empty body is a request that sends none, and this is the way to take a body
back off a request that has one. Going the other way, giving a body to a
request that had none takes over its Content-Type header — ichigo derives that
header from the body when it sends, so holding both would put it on the wire
twice. The content type field opens on that header's value when there is one,
on the existing body's when there is one, and on application/json otherwise.
A body with the content type cleared is refused rather than guessed at.
The body itself is stored exactly as typed. Unlike header and param values it is not trimmed: leading whitespace and a trailing newline are bytes the server may well be counting.
Applying only updates the request in memory, as the other panes do — Enter on the request form is what writes the file.
Tab to profiles in the request form and press Enter, or press Ctrl+p from
anywhere in the form:
| Key | Action |
|---|---|
j / k |
Move between profiles |
| Enter | Edit the selected profile (or + new profile to add one) |
n |
Add a profile |
d |
Delete the selected profile |
| Esc | Back to the request form |
Params appear under whichever profile is selected, so a request with several
environments does not print every token at once. Inside a profile, Tab moves
between fields and Ctrl+a adds a param.
Profile changes are held with the rest of the request until you save it: Esc
returns you to the request form, and Enter there writes the file. Escaping
out of that form discards the profile edits along with everything else, so you
can back out of a change you started by mistake. Two profiles cannot share a
name — the second is refused rather than saved, since a duplicate would be
unreachable from both the picker and --profile.
Profile values are re-read from the file every time you run, so a token you rotate in the YAML is picked up on the next run of a long-lived TUI session. Values resolved from environment variables are not: a process cannot see exports its parent shell makes after launch, so an env-sourced token stays stale until you restart ichigo. Put values that rotate in a profile.
Your preferences live at ~/.config/ichigo/config.toml — TOML, unlike the
requests themselves, so the two can share a directory without ever being
confused for one another. The file is optional, as is every key in it, and it is
global only: there is no project-local override, since these are preferences
about how you type rather than about a project.
Every option ichigo currently understands.
| Option | Type | Default | What it does |
|---|---|---|---|
keys.insert_escape |
string, exactly two characters | unset — Esc only |
A two-key sequence that leaves insert mode in a TUI text field, the equivalent of vim's inoremap jk <Esc>. See Editing text fields. |
layout.split_pct |
integer, 15–85 | 35 |
The request list's share of the TUI width, as a percentage. The rest goes to the detail pane. |
theme.name |
"dark" or "light" |
"dark" |
Which built-in palette to start from. See Themes. |
theme.colors.* |
color spec | from theme.name |
Overrides one role of that palette. Twenty-two roles; see Themes. |
A complete file, with every option set:
[keys]
insert_escape = "jk"
[layout]
split_pct = 35
[theme]
name = "dark"
[theme.colors]
accent = "#fabd2f" # override as many or as few roles as you likeAny option you leave out keeps its default, and an empty file — or no file at all — means every default. A section header whose options you have all omitted can be left out too.
The sequence must be exactly two characters; ichigo refuses anything else rather
than guessing. The first character is typed into the field as normal and
un-typed when the second completes the sequence within a second — so j, a
pause to think, then k leaves you with a literal jk, the same as vim's
timeoutlen. Typing jk quickly does escape, which is why the usual advice
is to pick a digraph you never type.
The TUI is two panes: the request list on the left, everything else on the
right. split_pct is the list's share of the width, so a smaller number gives
more room to responses and forms:
[layout]
split_pct = 25 # a narrow list, a wide detail paneIt is a percentage rather than a column count, so the same file means the same layout on a laptop and on a wide monitor, and the ratio holds when you resize the terminal. Values outside 15–85 are refused rather than quietly clamped — below that either pane has room for little more than its own borders.
You can also drag the divider with the mouse at any time, in any mode. That
is a per-session adjustment: it starts from split_pct and is not written back
to the file, so widening the detail pane to read one long response does not
silently become your permanent setting. Set the option for the layout you want
every session to open with, and drag when a particular response needs the room.
Because the TUI captures the mouse to do this, your terminal's own click-drag
text selection is unavailable while ichigo is running. Most terminals still give
it to you with Shift held down (Option on macOS), and the response
pane's own V/y copy works regardless.
ichigo ships two palettes:
[theme]
name = "dark" # the default: what ichigo has always looked like
# name = "light" # for a light terminal backgrounddark is mostly ANSI color names on purpose. It asks the terminal for
"yellow" rather than for #fabd2f, so it inherits your scheme — gruvbox,
nord, solarized, whatever you switch to next — without ichigo shipping a
palette for each. light cannot work that way (see below) and so is written in
exact colors throughout.
Nothing in ichigo names a color; it names what the color is for. Those roles
are what you override, on top of whichever name you picked:
[theme]
name = "dark"
[theme.colors]
accent = "#fabd2f" # exact color, immune to your terminal's palette
json_string = "green" # an ANSI name, which follows your terminal's theme
cursor_line = "237" # a 0-255 palette indexThree forms of color spec, and the choice matters: #rrggbb is exact and
never remapped, an ANSI name ("green", "bright green") follows your
terminal so ichigo keeps matching when you change schemes, and an index
0–255 reaches the grayscale ramp and the color cube.
Every role, and what it paints:
UI
| Role | What it colors |
|---|---|
text |
Primary text: field values, list rows, the filter you are typing |
dim |
Everything secondary — labels, hints, tree prefixes, descriptions, disabled rows |
accent |
The focused thing: a focused field's label, the key names in the hint line, the active filter border |
border |
Pane borders at rest |
border_focus |
The border of the pane that has focus |
badge_text |
Ink drawn on a colored badge, such as the status code beside " Response " |
cursor_line |
The band under the cursor line in the response pane |
selection |
The band under a V selection, a step louder than cursor_line |
row_selected |
The highlighted row in the request list and the profile pickers, and the VISUAL badge |
row_selected_text |
Ink on row_selected |
variable |
{{VAR}} placeholders, profile names, the CHAIN badge, and the profile panes' borders |
folder |
Folder rows and their borders, and the ? overlay's column headings |
Status — each also colors the matching HTTP method in the request list:
| Role | What it colors |
|---|---|
success |
2xx, and POST |
info |
3xx, and GET |
warning |
4xx, and PUT / PATCH |
error |
5xx, DELETE, and error panes |
JSON — the response body's syntax coloring:
| Role | What it colors |
|---|---|
json_key |
"name" |
json_string |
"strawberry" |
json_number |
42 |
json_bool |
true, false |
json_null |
null |
json_punct |
Braces, brackets, colons, commas |
Status and JSON are separate on purpose even where they start out the same
color: success and json_string are both green in the dark theme, and
splitting them is what lets you recolor a 2xx without recoloring every string
in every response body.
row_selected_text is a separate role rather than reusing text because it has
to contrast with the selection rather than with the page — a distinction a
light theme makes obvious and a dark one hides.
| Form | Example | Notes |
|---|---|---|
| Hex | "#fabd2f" |
Exact. No terminal theme can remap it. Six digits, # required. |
| ANSI name | "green", "bright green" |
Follows your terminal, so ichigo keeps matching when you change schemes. |
| Index | "237" |
0–255: the 16 ANSI colors, the color cube, and the grayscale ramp (232–255). |
The accepted names are black, red, green, yellow, blue, magenta,
cyan, white, and gray (grey also works), each with a bright form —
bright red, bright green, and so on. dark gray and bright black are the
same color. Spaces, dashes and underscores are ignored and case does not
matter, so "bright green", "bright-green" and "BrightGreen" are one thing.
Matching ichigo to gruvbox exactly, rather than letting it inherit the ANSI approximations:
[theme]
name = "dark"
[theme.colors]
text = "#ebdbb2"
dim = "#928374"
accent = "#fabd2f"
border = "#504945"
border_focus = "#8ec07c"
badge_text = "#282828"
cursor_line = "#3c3836" # gruvbox bg1
selection = "#665c54" # gruvbox bg3
row_selected = "#458588"
row_selected_text = "#fbf1c7"
variable = "#d3869b"
folder = "#83a598"
success = "#b8bb26"
info = "#83a598"
warning = "#fabd2f"
error = "#fb4934"
json_key = "#fabd2f"
json_string = "#b8bb26"
json_number = "#d3869b"
json_bool = "#fe8019"
json_null = "#fb4934"
json_punct = "#928374"You rarely need all of it. Setting name and two or three roles is the common
case — everything you leave out keeps the value from the theme you named.
A misspelled role is refused by name rather than silently ignored, the same as any other key in this file.
On a highlighted line ichigo steps every color away from the band behind it, or
the line you are reading would be the least legible one on screen. On a dark
theme "away" means brighter, and asking the terminal for the bright variant of
a color name does it. On a light theme the ANSI bright variants are usually
darker than their base, so the same move pushes the wrong way — that is a
wrong assumption rather than a bad color, and no palette of names can fix it.
light is written in #rrggbb throughout precisely so ichigo can compute the
direction from the band's own lightness instead of guessing.
The practical upshot: if you are building a light palette of your own, use hex rather than color names, or highlighted lines will fight you.
Unknown keys are an error, not a shrug — insert_esc will not silently do
nothing while you wonder why your keymap is dead:
Config: Invalid TOML in ~/.config/ichigo/config.toml: unknown field `insert_esc`,
expected `insert_escape` for key `keys` at line 1 column 1
The same goes for a value ichigo cannot use, such as a sequence of the wrong length. Either way the TUI opens on the message and runs on defaults; press Esc to carry on with them. One consequence worth knowing: because unknown keys are refused, a file written for a newer ichigo will not load on an older one.
Nerd Font icons in the TUI are detected from your terminal, and overridden with an environment variable rather than a config key:
ICHIGO_ICONS=1 ichigo tui # force icons on
ICHIGO_ICONS=0 ichigo tui # force them offUnset, ichigo turns them on for terminals known to ship a Nerd Font (Kitty, WezTerm, Ghostty, iTerm2), including through tmux.
ichigo can generate a completion script so that pressing Tab autocompletes config names:
ichigo run config<TAB>
# → config-server-dev config-server-prodThe completions resolve config names live from .ichigo/ and ~/.config/ichigo/, so new configs appear automatically without any extra setup.
Add this line to your ~/.zshrc:
eval "$(ichigo completions zsh)"Powerlevel10k users: place this line after the instant prompt block at the top of your
.zshrc.
Add this line to your ~/.bashrc:
eval "$(ichigo completions bash)"Add this line to ~/.config/fish/config.fish:
ichigo completions fish | sourceAfter adding the line, open a new terminal (or source the file) and tab completion will be active.
