Lesson 11 setup

Configuration

bi works with no config at all. When you do write one, it is a patch over the defaults: say what you want changed and nothing else.

Where the config lives

~/.config/bi/config.toml. bi looks in $BI_CONFIG first — which names the directory, not the file — then $XDG_CONFIG_HOME/bi, then ~/.config/bi.

Nothing creates it behind your back. Ask for one:

bi config init   # writes config.toml with every default, commented out
bi config edit   # opens the config directory as a file tree

init never overwrites a file that already exists. Inside bi, :reload re-reads the config through the same path startup uses, without restarting.

The file is a patch, never a replacement. An option you do not mention keeps doing what bi already does, including whatever a later version adds. Mistakes do not stop bi from starting: an unknown option or a wrong type drops that one line and says so on the status line — 1 config problem: unknown option: nmber. A failed :reload changes nothing.

[options] is :set

One key per option, spelled the same in both places. :set number 5 and number = 5 reach one setting.

[options]
number     = 5      # 0 off, -1 relative, N every Nth
hlsearch   = false
tab_width  = 4      # how wide a \t is drawn
shiftwidth = 0      # how far `>` moves; 0 means "the same as tab_width"
expandtab  = true   # write an indent as spaces
autoindent = true
textwidth  = 80     # where `gq` reflows
theme      = "main"

number shows the idea well: vim needs two booleans, number and relativenumber, and still cannot say “every fifth line”. bi uses one option that takes a value.

lstring.c One option, three gutters 1 / 5
One option, three gutters: step 0 One option, three gutters: step 1 One option, three gutters: step 2 One option, three gutters: step 3 One option, three gutters: step 4
vs vim
:set nu and :set rnu do not exist. Bare :set number reports the current value instead of turning numbering on.

Per file, in layers

Options resolve per buffer, so a Makefile and a Lua file open side by side can each have what they need. From weakest to strongest:

Layer Where it comes from
defaults compiled into bi
[options] your config.toml
filetype bi’s small built-in table — a Makefile gets tabs, Go gets tabs — then your [filetype.<name>]
.editorconfig the project’s, nearest file wins, root = true stops the walk
:set what you typed this session
[filetype.go]
tab_width = 4      # gofmt writes tabs; how wide they look is yours

[filetype.python]
shiftwidth = 4

The filetype name is the one bi picks the grammar by: rust, c, lua, make, markdown.

.editorconfig needs no switch. indent_style, indent_size, tab_width, trim_trailing_whitespace and insert_final_newline become options, and charset and end_of_line apply when the file is opened.

Themes

Sixteen are built in: main (the default), ansi (your terminal’s own palette), bonsai, ferra, gb, github, gruvbox, gruvbox-light, kanagawa, lighthaus, monokai, nordark, pascal, purple, vesper and xcode. :set theme swaps one live.

lstring.c Switch themes 1 / 5
Switch themes: step 0 Switch themes: step 1 Switch themes: step 2 Switch themes: step 3 Switch themes: step 4
[options]
theme = "kanagawa"
ssh_theme = "gruvbox-light"   # what an SSH session gets instead

A file at <config dir>/themes/<name>.toml is a patch over the built-in of the same name, so changing one colour does not mean copying the rest of the theme.

Keys

[keys.normal], [keys.visual] and [keys.tree] rebind keys. A binding names a command, and false unbinds a key. <leader> is whatever [keys] leader says — Space unless you change it.

[keys]
leader = " "

[keys.normal]
"h" = false            # unbound
"j" = "left"           # hjkl shifted one key right
"k" = "down"
"l" = "up"
";" = "right"
"<leader>e" = "window_tree"
"<leader>a" = ":alt<CR>"          # a : line runs...
"<leader>o" = ":e "               # ...or, without <CR>, waits for you to finish it

Rebinding a motion rebinds every use of it: with the map above, d2k deletes two lines down. [keys.visual] falls back to [keys.normal] for anything it does not name. Nothing is remapped while you type text — insert mode, the command line, search and the picker take keys literally.

A prefix has no meaning of its own. Once something binds <leader>e, Space stops moving right. bi has no timeout to decide between the two; that is also why it never pauses before j moves.

An unknown command name is reported with a suggestion: unknown command: tree_expnd — did you mean tree_expand?

bi ships exactly one leader binding, <leader><leader> for :actions, and even that one gives way to a binding of your own.

Project config: .bi.toml

A repository can have a say too. bi takes the first .bi.toml found in the working directory or the nearest ancestor, and lays it over your config the same way your config lays over the defaults.

# .bi.toml
[options]
textwidth = 100

[filetype.c]
shiftwidth = 2

[alternate]
"src/*.c" = ["include/*.h"]

Some keys are refused, with a diagnostic, because a cloned repository must not be able to run code on your machine:

Refused Why
[keys] a binding can carry a : line, and your keys are yours
[lsp.servers.*].command names a binary bi spawns when you open a file
[fmt.tools.*].command names a binary your buffer is piped through

Everything else is read: [options], [filetype.*], [alternate].

The other file: [alternate]

ga (:alt) opens the file paired with this one, and gA (:valt) opens it in a vertical split. The pairs are rules:

[alternate]
"*_test.go" = ["*.go"]
"*.go"      = ["*_test.go"]
"*.c"       = ["*.h"]
"*.h"       = ["*.c"]

* matches anything, separators included, and means the same text on the right. The first rule whose pattern matches decides — which is why *_test.go comes before *.go — and the first of its paths that exists is opened. A rule you write replaces bi’s rule for that pattern. Go, C and C++ pairs are built in.

Saving: trimming, encodings, line endings

:w tidies the file on the way out: trailing whitespace goes, blank lines at either end go, and a missing final newline is added. The trim is its own undo step, so u after :w brings the whitespace back and nothing else. Markdown keeps its trailing spaces, since two of them are a line break there.

[options]
trim_on_write      = true    # the master switch
trim_trailing      = true
trim_first_line    = true
trim_last_line     = true
trim_final_newline = true

Inside bi, text is always UTF-8 with \n. Encoding, BOM and line endings are detected when a file opens and restored when it is saved, so a latin1 header or a CRLF .csv comes back byte-identical. The status row shows a [windows-1250], [bom] or [crlf] badge only when a file is not plain UTF-8.

fileencodings = ["utf-8", "cp1250"]   # tried in order; the first clean decode wins
Command Does
:set fileencoding cp1250 this buffer’s encoding on disk; the next :w converts
:set fileformat dos \r\n on disk — unix for \n
:set bom true write a byte-order mark
:e ++enc=latin1 reopen the file as the encoding you name
:retab rewrite leading indentation to match the options in force
vs vim
:retab only touches leading whitespace. A tab inside a string or before a trailing comment is content, and stays.