Lesson 17 bi format

The property format

A .bischema says what a Weapon is; a .bidata holds the weapons. Both are plain JSON, and bi opens them as a form — sliders, checkboxes, choices — edited with the keys you already know.

Two files, both JSON

The format is two file kinds and one rule: the schema defines types, the data holds instances of them, and everything is valid JSON, so jq, git diff and your own loader keep working.

KindHoldsExtension
Schemastruct and enum definitions.bischema
Datainstances with concrete values.bidata

Every file starts with "$dialect": "bi/1" — the extension picks the view, the dialect confirms it. A data file names its schema with $schema, a path relative to itself. Keys starting with $ are bi’s; your types and fields must not use them.

A schema is a types map. A struct’s fields are an array, because order is display order:

{
  "$dialect": "bi/1",
  "types": {
    "Rarity": { "kind": "enum", "values": ["common", "rare", "epic"] },
    "Weapon": {
      "kind": "struct",
      "fields": [
        { "name": "name",   "type": "string" },
        { "name": "damage", "type": "i32", "default": 10, "min": 0, "max": 999 },
        { "name": "rarity", "type": "Rarity", "default": "common" }
      ]
    }
  }
}

A data file is a list of instances, each naming its $type and $id:

{
  "$dialect": "bi/1",
  "$schema": "game.bischema",
  "instances": [
    { "$type": "Weapon", "$id": "rusty_sword", "name": "Rusty Sword", "damage": 12 }
  ]
}

Storage is sparse. rusty_sword omits rarity, so it is common from the schema — and changing that default changes every instance that did not override it. Writing a value equal to the default removes the key again on :w, so a diff shows real overrides and nothing else.

tip
bi gen sample writes all three — game.bischema, level1.bidata and game.bimapping — into the working directory, and :bi sample writes the one matching the buffer you are in. It is worth the one command: the sample uses every type and every layout attribute there is, and the demos below open it rather than a toy.

Starting from nothing

You do not have to write either file by hand. Open a name that does not exist yet, and the two commands that matter are :bi init — the skeleton — and :bi add, which grows the schema a type or a field at a time.

loot.bischema A schema and its data, from nothing 1 / 9
A schema and its data, from nothing: step 0 A schema and its data, from nothing: step 1 A schema and its data, from nothing: step 2 A schema and its data, from nothing: step 3 A schema and its data, from nothing: step 4 A schema and its data, from nothing: step 5 A schema and its data, from nothing: step 6 A schema and its data, from nothing: step 7 A schema and its data, from nothing: step 8

:bi init on the data file guesses $schema when exactly one .bischema sits beside it, so the second half of that session never named the first. From there :bi new <Type> <id> appends an instance and :bi set <path> <value> fills it in; both are covered in Editing data.

note
docs/specs/props.md says an empty buffer gets the skeleton written for it. It does not: an empty buffer is not valid JSON, so the file opens as text with invalid JSON: EOF while parsing a value at line 1 column 0 and waits for :bi init.

Types

A field’s type is a string, parsed by a small grammar with no whitespace in it:

ExpressionIsStored as
boola booleantrue / false
i8 … i64, u8 … u64an integer of that widtha JSON number
f32 f64a floata JSON number
stringUTF-8 texta JSON string
rgb rgbaa colour"#rrggbb", "#rrggbbaa"
curvea tuning curve over 0..1points, each [x, y, in, out]
gradientcolour stops over 0..1stops, each [t, "#rrggbbaa"]
Eone of an enum’s valuesthe value’s name
San embedded struct, owned by its parentan object
list<T>a sequencean array
optional<T>a value or nothingnull or the value
ref<S>a reference to another instancethe target’s $id

Embed a struct for small value-like data each parent owns — a Vec2, a stats block. Use ref<S> for something shared and identified, so one edit reaches every user. A ref without a default is required; make it optional<ref<S>> when absence is legitimate.

Ids are scoped per type, so a Weapon and an Enemy may both be boss. Enum values are stored by name, never by index, so reordering them is free.

The data view

Open a .bidata and you get a form: a section per instance, one row per field, each drawn as what it is.

level1.bidata A data file as a form 1 / 4
A data file as a form: step 0 A data file as a form: step 1 A data file as a form: step 2 A data file as a form: step 3

A number with both min and max is a slider, a bool a checkbox, an enum or a ref a choice, a rgb a brick painted its colour, a curve a sparkline, a gradient sixteen sampled bricks. A value the instance sets is drawn plainly, one inherited from the schema is dim, a read-only field is muted, and a warning rides on the row after ⚠. The field’s doc shows in the status row while the row is selected.

Rows that draw instead of spell

Four of the types have no useful text form, so the view gives them a picture on the row itself.

level1.bidata Colours, curves and gradients on a row 1 / 5
Colours, curves and gradients on a row: step 0 Colours, curves and gradients on a row: step 1 Colours, curves and gradients on a row: step 2 Colours, curves and gradients on a row: step 3 Colours, curves and gradients on a row: step 4
TypeThe row shows
rgbtwo cells painted the colour, and the hex
rgbathe same, blended over the pane by its alpha
curvethe curve sampled across sixteen cells, y clamped to 0..1, and its point count
gradientsixteen bricks sampled across the ramp, and its stop count

None of the four turns with l — there is no next colour — so dd puts the default back and Enter opens the value in its own editor: the colour picker or the gradient bar for a colour or a ramp, the curve editor for a curve, each split to the right of the view with its form down the edge. :set editor color, curve and gradient on the view do the same as Enter.

The keys in the tool rewrite the JSON in this buffer, and the view re-reads after every one, so the brick or the sparkline follows the edit as you make it. An inherited value is written into the instance first, as its own undo step, so there is a literal to edit; a readonly field refuses with read-only. Esc on the picture comes back to the view with the tool still open, :q closes it.

Turning values

l and h turn the value under the cursor; everywhere else they open and close rows. That one rule is the whole keymap.

level1.bidata Turning values 1 / 5
Turning values: step 0 Turning values: step 1 Turning values: step 2 Turning values: step 3 Turning values: step 4
KeyDoes
j k gg G Ctrl-D Ctrl-Upick a row, with a count
Tab Shift-Tabthe next, previous instance or type
l →turn a value up — a number by its step, an enum or ref along, a bool on; otherwise open the row
h ←turn it down; otherwise close the row, or go to the parent
H Lten steps at once
Backspacethe parent row
Spaceflip a bool, cycle an enum or ref, a number one step up — but see the note below
Ctrl-A Ctrl-Xthe same as l and h, for the vim hand
Enter iflip a bool; open a colour, curve or gradient in its editor; otherwise edit the value on the ex line
y pyank a typed value, put it on a row of the same type — in this file or another
J Kmove the row among its siblings; display order is the file’s order
aadd an item, an instance, a field, a value, a type
dda set value back to its default; a list item, an unknown key or an instance gone
gdon a ref, jump to its target
u Ctrl-Rundo, redo — the buffer’s

Every key that changes something is one edit of the buffer and one undo step. The view re-reads the buffer afterwards, so there is one history, not two: :w, :q, u and the modified marker are the file’s, exactly as in a text window.

vs vim
Space is bi’s leader by default, and a prefix has no meaning of its own — so in the view a bare Space waits for the rest of the sequence instead of flipping the row. Enter flips a checkbox, and l/Ctrl-A turn any value; rebind or unbind the leader if you want Space itself.
tip
y carries the value and its type. Yank an i32 and p only lands on another i32; yank an instance from its header row and p fills another instance of that type, keeping its id. A mismatch says yanked i32, this is string and writes nothing.

Refs across files

A ref<Weapon> stores an id. The editor keeps an index of every .bidata under the project root whose $schema resolves to the same schema, which is what l cycles through, what warns you about a dangling ref, and what gd jumps by — into another file when that is where the target lives.

level1.bidata Following a ref 1 / 3
Following a ref: step 0 Following a ref: step 1 Following a ref: step 2

The schema view

A .bischema opens as a tree instead: one row per type, its fields under it, and every layout attribute under the field.

game.bischema The schema as a tree 1 / 3
The schema as a tree: step 0 The schema as a tree: step 1 The schema as a tree: step 2

The attributes are the schema’s own, and they change how the editor draws a field, never what the data means — a reader that does not know them ignores them.

AttributeDoes
groupputs the field under a titled, foldable section; / nests
ordersorts fields, lowest first; the array order breaks ties
labelthe row says this instead of the field name
readonlyshown, never turned
show_if hide_ifthe row appears or goes while a condition on a sibling holds
widgettoggle draws an enum as all its values; inline opens a struct with no fold

A condition names a sibling: two_handed, !two_handed, rarity == epic, hp > 10. A struct may also carry groups with options per group, so a section can start collapsed.

Opening and mending

CommandDoes
:set editor bidata bischemathe property view over this buffer
:set editor textthe text back
:set editorsays which is up

The extension does this for you; the commands are for a file that has none. An empty buffer gets the skeleton written first, with $schema filled in when exactly one .bischema sits beside it. A file that cannot be read — no $schema, bad JSON — opens as text with the reason on the status line, and :set editor bidata on it keeps the view up with that error as its one row, so :bi init, :bi schema and :bi migrate can mend it — see Editing data.

Errors block the view and open the file as text with the error marked; warnings — a number outside its range, a dangling ref, an unknown key left by a rename — ride on the row and never block editing or saving. Unknown keys are preserved on write; bi does not drop data it does not understand.