Skip to content

Repository files navigation

🍓 ichigo 🍓

CI crates.io downloads license MSRV

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.

ichigo TUI

Installation

Via shell script (macOS/Linux):

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/playsthisgame/ichigo/releases/latest/download/ichigo-installer.sh | sh

Via PowerShell (Windows):

powershell -ExecutionPolicy ByPass -c "irm https://github.com/playsthisgame/ichigo/releases/latest/download/ichigo-installer.ps1 | iex"

Via Cargo:

cargo install ichigo

Config files

Configs 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.

Commands

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

new

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/users

Flags:

  • -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

Creating a request from a cURL command

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-curl
ichigo new api/login --from-curl <<'EOF'
curl -X POST 'https://api.example.com/login' \
  -H 'Content-Type: application/json' \
  --data-raw '{"user":"ada"}'
EOF

The 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 -X wins, -I means HEAD, a body means POST, everything else is GET.
  • A query string on the URL is split into the query map. 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-Type header becomes the body's content_type. A body with no content type gets curl's default, application/x-www-form-urlencoded.
  • --compressed is dropped rather than stored as an Accept-Encoding header — ichigo's HTTP client negotiates encoding itself, so storing the header would make the config claim something it does not do.
  • -u user:pass becomes the Authorization: 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.

run

ichigo run config-server-prod
ichigo run create-user --var TOKEN=abc123 --var USER_ID=42

Variables 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: a 200 OK · 143 ms · 4.2 KB summary line, then the body
  • -p, --profile — use a named profile (see Profiles)

Chaining requests

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 --verbose

Chain configs are detected automatically by the presence of a steps: key — no separate command needed.

copy

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 --global

Flags:

  • -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.

test

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 staging

Flags:

  • -v, --var KEY=VALUE — set a variable
  • -i, --iter — number of iterations
  • -p, --profile — use a named profile

tui

Opens an interactive terminal UI for browsing, running, and load-testing your configs.

ichigo tui

Keybindings:

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.

Editing text fields

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.

Copying part of a response

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.

The run summary

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.

Response headers

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.

Image responses

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 = true

Without it herdr does not render kitty graphics for its attached clients, so an image response shows only its summary line.

Config format

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}}"
    }

Chain config format

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

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-456

Run with a profile from the CLI:

ichigo run create-user --profile dev
ichigo test create-user --iter 50 --profile staging

Any {{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.

The request form

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.

Editing headers and query params in the TUI

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+h also 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.

Editing the request body in the TUI

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+b is tmux's default prefix key, so under tmux it never reaches ichigo. Tab to the body row 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.

Editing profiles in the TUI

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.

Configuring ichigo

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.

All options

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 like

Any 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.

keys.insert_escape

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.

layout.split_pct

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 pane

It 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.

Themes

ichigo ships two palettes:

[theme]
name = "dark"    # the default: what ichigo has always looked like
# name = "light" # for a light terminal background

dark 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.

Overriding roles

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 index

Three 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 0255 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.

Color specs

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" 0255: 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.

A worked example

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.

Why light is not just dark with different values

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.

When the file is wrong

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.

Preferences that are not in this file

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 off

Unset, ichigo turns them on for terminals known to ship a Nerd Font (Kitty, WezTerm, Ghostty, iTerm2), including through tmux.

Shell completions

ichigo can generate a completion script so that pressing Tab autocompletes config names:

ichigo run config<TAB>
# → config-server-dev  config-server-prod

The completions resolve config names live from .ichigo/ and ~/.config/ichigo/, so new configs appear automatically without any extra setup.

Zsh

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.

Bash

Add this line to your ~/.bashrc:

eval "$(ichigo completions bash)"

Fish

Add this line to ~/.config/fish/config.fish:

ichigo completions fish | source

After adding the line, open a new terminal (or source the file) and tab completion will be active.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages