You're reading docs for unreleased changes —  switch to v0.10.0.
Skip to content

Configuration

Options, key bindings, the statusline, plugins, and language servers are all configured in one language, in one file. There's no separate config format to learn — init.scm is a Scheme program, so anything you can compute you can configure.

HUME can be configured two ways: the :set command for runtime changes during a session, or an init.scm file for persistent configuration loaded at startup.

HUME reads persistent configuration from:

  • macOS / Linux: $XDG_CONFIG_HOME/hume/init.scm (defaults to ~/.config/hume/init.scm)
  • Windows: %APPDATA%\hume\init.scm

If the file does not exist, HUME starts with defaults. If it fails partway through, the error is reported in :messages and everything up to that point stays applied — so a broken line late in the file leaves you half-configured rather than back at defaults. Fix it and run :reload-config to re-run the file without restarting.

:reload-config starts from a clean slate: every option, key binding, hook, command, and plugin goes back to its default first, then the file runs again — so removing a line from init.scm and reloading does undo what it did. Any :set global/:set buffer/:theme change you made during the session is discarded too, not just what init.scm set, with two exceptions: a pane-scoped :set pane override, which stays as you left it (panes are editing state, not config), and an explicit :set buffer language=<name>, which is restored after the reload rather than discarded — detection can't reconstruct it on its own (that's exactly why you had to set it explicitly), so losing it on every reload would be more surprising than keeping it. If the file fails partway through this time, you're left with defaults plus whatever ran before the error, same as at startup.

Buffers stay open and language servers stay attached across a reload — it behaves as if every open file were closed and reopened. Completion triggers, inline diagnostics, and any per-language setup your config applies (e.g. from on-language-set) come back too, without restarting the language server or losing your place in the file.

A reference config ships at runtime/init.scm.example (see File locations); copy it to the path above if you want a starting point, or see Example init.scm below.

Setting options

There are two ways to set an option: the :set command for runtime changes, or set-option! in init.scm for persistent defaults.

:set command

The :set command takes a scope and a key=value pair. The scope is required:

:set global <option>=<value>     set the global default
:set buffer <option>=<value>     override for the current buffer only (takes precedence over global)
:set pane <option>=<value>       override for the current pane only (view-scoped settings)

For a buffer option (the Buffer options table below), :set global takes effect immediately in every buffer that has no override of its own — not just newly opened ones. wrap-mode additionally accepts :set pane, which pins one pane's wrap style above both the buffer and global setting (see Text wrap).

Changes apply to the current session and are not persisted — for persistent configuration, use init.scm (below).

From init.scm

scheme
(set-option! "option-name" value)

Sets the global default. The value is a string, boolean, or integer. Callable from init.scm, a plugin body, or a command/hook body — anywhere Steel code runs.

scheme
(set-option! "line-number-style" "absolute")
(set-option! "tab-width" 2)

init.scm is a real Scheme program, not a flat list of settings, so you can react to what's being opened rather than only set fixed defaults. The most common case is configuring an option per file type: register an on-language-set handler and call (set-buffer-option! bid "option" value) to override just that buffer (see Hooks for the full hook API):

scheme
; 2-space indentation for Markdown buffers
(register-hook! 'on-language-set
  (lambda (bid lang)
    (when (equal? lang "markdown")
      (set-buffer-option! bid "tab-width" 2))))

; word-wrap Markdown buffers, leave source code unwrapped
(register-hook! 'on-language-set
  (lambda (bid lang)
    (when (equal? lang "markdown")
      (set-buffer-option! bid "wrap-mode" "word"))))

Global options

Set with :set global <option>=<value> or (set-option! "option" value). All of these are global-only.

For a bool option, :set accepts true/false, on/off, yes/no, or 1/0; from Scheme, pass #t/#f.

OptionTypeDefaultDescription
themestring"" (built-in sand)Active color theme name
scrolloffinteger3Minimum lines kept above/below cursor
mouse-enabledbool#tEnable mouse support
mouse-scroll-linesinteger3Lines per mouse scroll tick
mouse-selectbool#fMouse drag creates selections
jump-list-capacityinteger ≥ 1100Max jump list entries
jump-line-thresholdinteger5Line distance to record a jump
history-capacityinteger ≥ 1100Max entries per :///? prompt history
undo-levelsinteger0Max undo states kept per buffer; 0 means unlimited. Once the limit is reached, the oldest states — including whole abandoned branches — are dropped as new edits are made
steel-init-budget-msinteger ≥ 110000Max evaluation time (ms) for init.scm and each plugin activation. Setting it from init.scm has no effect on that same run — the budget is read before each file/plugin evaluation starts, so a change only takes effect for evaluations after it, i.e. the next plugin activation or the next session
steel-command-budget-msinteger ≥ 11000Max Steel command evaluation time (ms)
popup-borderbool#tShow popup borders
syntax-highlight-max-bytesinteger ≥ 11048576Max bytes for syntax highlighting
pane-dividersbool#tDraw a 1-cell divider between sibling panes
statuslineleft | center | rightsee StatuslineThree |-separated sections, each a comma-separated list of element names (empty sections allowed), e.g. Mode,FileName||Position
statusline.mode-colorsbool#tTint the whole statusline with the current mode's color; off shows the theme's base ui.statusline color in every mode
lsp.inlay-hintsbool#fShow inlay hints from the language server
lsp.diagnostics-severity-floorerror | warning | info | hinthintLowest diagnostic severity to display
lsp.request-timeout-msinteger ≥ 110000How long to wait for a language-server reply
lsp.viewport-debounce-msinteger ≥ 1150Delay before re-requesting hints after scrolling

Buffer options

These options have a global default that every buffer without its own override resolves to — including buffers already open when you change it, not just ones opened afterward — and a per-buffer override that takes precedence when present. Set the global default with :set global <option>=<value> or (set-option! "option" value); override the current buffer with :set buffer <option>=<value>, or from a script with (set-buffer-option! buffer-id "option" value) — see Plugins for setting per-language overrides from the on-language-set hook.

language is an exception, it has no global default — it is auto-detected per buffer and can only be set with :set buffer language=<name>.

OptionTypeDefaultDescription
wrap-modenone | soft[:N] | word[:N] | indent[:N]indentLine wrapping. N is the wrap column (0 or omitted = pane content width). Also accepts :set pane wrap-mode=<value> to pin one pane above the buffer/global setting — see Text wrap
tab-widthinteger, 1–2554Spaces per indent level
indent-guidesbool#tDraw vertical guides at each indentation level
tab-stylehard | softhardWhat Tab inserts: hard = literal \t; soft = spaces to next tab stop
line-number-styleabsolute | relative | hybridhybridLine number display in the gutter
auto-pairs-enabledbool#tEnable auto-pair insertion
select-changed-textbool#tAfter c (change), keeps the selection on the text you changed
word-selects-whitespacebool#tw/W/b/B and mm/MM cover the whitespace before the destination word (trailing instead, for the first word of a line); #f selects the bare word instead
signcolumnalways[:N] | auto[:N]always:1Gutter column for plugin-supplied signs (diagnostics, etc). N is the number of sign slots (1–127, default 1); auto collapses the column to zero width when no signs are visible
autoreadbool#tPrompt to reload when the current buffer's file changes on disk. #f only warns — reload manually with :e!
whitespace-spacenone | all | trailingnoneWhen to render space indicators. Also reveals invisible Unicode spaces (non-breaking and ideographic) with a distinct marker
whitespace-tabnone | all | trailingnoneWhen to render tab indicators
whitespace-newlinenone | allnoneWhen to render newline indicators
languagestring(auto-detected)Language for syntax highlighting

Text wrap

Text wrap is controlled by three layers — a global default, a per-buffer override, and a per-pane pin — plus a per-pane toggle command.

  • wrap-mode is a buffer option (see Buffer options): set a global default with :set global wrap-mode=<value> or set-option!, or override one buffer with :set buffer wrap-mode=<value> or (set-buffer-option! bid "wrap-mode" value). To wrap by file type — markdown but not source code, say — set it per language from an on-language-set hook (see Plugins).
  • :set pane wrap-mode=<value> pins the style for the pane you're currently in and the buffer it's currently showing, live, above both the buffer and global setting, without affecting other panes on the same buffer. Switching that pane to a different buffer resolves the new buffer's own setting instead; switching back returns the pin. There's no command to clear a pin back to following the buffer/global setting — pin it to a different value, or close the buffer, to move on from it.
  • :wrap (alias of :toggle-soft-wrap) toggles wrapping on or off for the current pane and buffer. Turning it off pins the pane to no wrap; turning it back on restores whatever it was doing before — the buffer/global setting, if the pane wasn't pinned, or the exact style you pinned it to with :set pane wrap-mode=…. If that restores a setting that doesn't actually wrap, it pins the configured global style instead (or indent, if the global itself is none) — :wrap always visibly wraps.

Two panes showing the same buffer can still wrap independently once one of them is pinned with :set pane or :wrap — that's what the per-pane layer is for.

Accepted values:

  • none — no wrapping; long lines scroll horizontally.
  • soft — break at the pane width, splitting at any character (may split a word in the middle).
  • word — break at the pane width but prefer whitespace, so words aren't split.
  • indent — like word, but continuation rows are indented to match the line's leading whitespace, so nested code stays visually nested (this is the default).
  • :N suffix — wrap at column N instead of the pane's content width (e.g. word:80). 0 or omitted means content width.

Themes

scheme
(set-option! "theme" "sand")

To see which themes are available, type :theme and press Tab.

Custom themes are TOML files placed in the themes/ subdirectory of your HUME config directory. HUME uses the Helix theme format, so any theme written for Helix works in HUME too.

HUME reads these Helix statusline scopes:

  • ui.statusline — fallback style for the statusline row, and the style shown in every mode when statusline.mode-colors is off
  • ui.statusline.normal — row style in Normal mode
  • ui.statusline.insert — row style in Insert mode
  • ui.statusline.separator — separator glyph between statusline elements; when a theme doesn't define it, the separator matches whatever the row itself is currently tinted, rather than the untinted base ui.statusline

The whole statusline row is tinted with the current mode's color (see statusline.mode-colors above); a theme that omits a mode scope falls back to ui.statusline. HUME adds four more mode scopes for modes Helix doesn't have:

  • ui.statusline.extend
  • ui.statusline.search
  • ui.statusline.command
  • ui.statusline.select

Popups and menus (LSP hover, completion, the fuzzy picker) read their own scopes:

  • ui.popup / ui.popup.info — hover and info popup background
  • ui.popup.scroll — scrollbar thumb on a scrolled hover popup
  • ui.menu / ui.menu.selected — completion and picker rows / the selected row
  • ui.menu.scroll — scrollbar thumb on a scrolled menu

HUME ships a theme editor — a single-file HTML tool you can open in a browser to edit themes visually and export them as TOML. You can download it from https://github.com/cvlmtg/HUME/blob/main/tools/theme-editor/index.html

Key bindings

scheme
(bind-key! 'normal "ctrl-j" "move-down")
(bind-key! 'normal "g e" "goto-last-line")
(unbind-key! 'normal "ctrl-j")

bind-key! — binds a key in the given mode ('normal, 'insert, 'extend). unbind-key! — removes a binding. bind-key-extend! — binds a key so it always extends the selection, as the one-shot Ctrl+ motions do.

To set several bindings at once, use the plural forms:

scheme
(bind-keys! 'normal
  ("ctrl-h" "select-prev-word")
  ("ctrl-l" "select-next-word"))

(unbind-keys! 'normal "ctrl-j" "ctrl-k")

bind-keys-extend! is the bulk form of bind-key-extend!.

Binding a key that waits for a character

Some commands need a character typed right after the key (find/till motions, surround). bind-wait-char! binds a key sequence so the next keypress is captured and passed to the target command instead of being looked up in the keymap:

scheme
(bind-wait-char! 'normal "m s" "surround-add")

Inside the target command, read the captured character with (pending-char) — see Plugins for the full command-writing API, including the related (request-wait-char! cmd-name), which waits for a character from inside an already-running command rather than from a key binding.

Key-string grammar

A key string is a whitespace-separated list of tokens. Each token is [modifier-]*key where the modifier separator is a dash - (not +):

ComponentValues
Modifiersctrl-/c-, shift-/s-, alt-/a- (case-insensitive, repeatable, any order, short and long forms may be mixed)
Named keysspace, tab, enter / return / cr / ret, esc / escape, lt (<), backspace / bs, delete / del, insert / ins, home, end, pageup, pagedown, up, down, left, right, f1f12
Single charAny single Unicode character; case is preserved ("G" and "g" are distinct)

Multi-key sequences are space-separated: "g e", "m i w", "ctrl-p h". Examples: "ctrl-j", "shift-tab" (becomes BackTab), "ctrl-shift-left", "g e".

Binding the backslash key

In Scheme string literals \ is the escape character, so to bind the \ key write it escaped — "\\", not "\":

scheme
(bind-key! 'normal "\\" "my-command")

The same applies to the double quote: bind " as "\"".

Statusline

The statusline is fully configurable from Steel:

scheme
(configure-statusline! '("Mode" "Separator" "FileName") '() '("Position"))

Each argument is a list of element name strings: left, center, right.

Available elements:

ElementDescription
"Mode"Current mode label (NOR/INS/EXT/CMD/SRC/SEL)
"Separator"Divider between sections
"FileName"Current buffer filename (basename)
"FilePath"Full path of current buffer
"Cwd"Working directory
"Position"Line and column position
"Selections"Number of active selections
"KittyProtocol"Kitty keyboard protocol indicator
"DirtyIndicator"[+] when buffer has unsaved changes
"LineEnding"Line ending type (LF/CRLF)
"SearchMatches"Current search match count
"MiniBuf"Pending key sequence hint
"MacroRecording"Macro recording indicator
"Language"Buffer language
"ReadOnly"[RO] indicator
"Diagnostics"Error and warning counts from the language server

The default is equivalent to:

scheme
(configure-statusline!
  '("Position" "FilePath" "Language" "ReadOnly" "DirtyIndicator")
  '()
  '("MacroRecording" "SearchMatches" "Diagnostics" "KittyProtocol" "Separator" "Mode"))

Language detection

HUME detects file languages from extension, glob pattern, or shebang line. See Teach HUME a new language for defining custom languages with define-language! and, for grammars outside the catalog, register-grammar!.

Example init.scm

A complete starting config — copy it to ~/.config/hume/init.scm and edit:

scheme
;; Bundled plugins
(load-plugin "core:stdlib")           ; helper toolkit other plugins depend on
(load-plugin "core:pickers")          ; fuzzy file/buffer finders
(declare-plugin "core:lsp")           ; language server features
(declare-plugin "core:plum")          ; plugin/grammar manager

Before your init.scm runs, HUME loads its own prelude (which defines bind-keys!, define-language! and friends) and its built-in language definitions — so those are always available to you.

File locations

HUME resolves its directories per OS:

PathmacOS / LinuxWindows
Config dir (init.scm, user themes/)$XDG_CONFIG_HOME/hume/ (default ~/.config/hume/)%APPDATA%\hume\
Data dir (plugin clones, tree-sitter grammars)$XDG_DATA_HOME/hume/ (default ~/.local/share/hume/)%LOCALAPPDATA%\hume\ (fallback %APPDATA%\hume\)
Runtime dir (bundled runtime/: tutor.rst, themes/, scheme/, init.scm.example, core plugins)see belowsee below

HUME looks for its runtime directory in this order, taking the first that exists:

  1. $HUME_RUNTIME, if set
  2. ../share/hume/ relative to the binary (macOS and Linux only — this is the layout you get from the release archive)
  3. runtime/ next to the binary (the Windows archive layout)
  4. runtime/ in the current working directory (handy when running from a source checkout)

Notable subpaths inside the data dir: data/plugins/ (PLUM-managed plugin clones), data/grammars/ and data/grammars/sources/ (compiled and source tree-sitter grammars).

Plugins are trusted code

Plugins run with the same privileges as HUME itself — they can read and write any file your user account can, and run other programs. There is no sandbox. Install third-party plugins only from sources you trust.

INFO

On macOS HUME follows the XDG convention (~/.config/hume/, ~/.local/share/hume/) rather than ~/Library/Application Support/.

Released under the MIT License.