Skip to content
Oleaflyv0.4.8

Give your own Typst functions a form, so people who fill in a report see labelled fields in Visual mode instead of function-call syntax.

A Typst template often ships its own functions: a cover page, an info box, a sign-off block. Whoever writes the report calls them, as in #info-box(title: "Supply risk", tone: "risk")[...]. If you maintain the template, you can describe each function’s arguments once. Visual mode then shows every call to that function as a form: labelled fields, a list for fixed choices, a switch for yes or no. Editing a field rewrites only that one argument in the .typ file, so the report stays ordinary Typst source that compiles the same way.

A call on its own lines becomes a card. The card’s header shows the form’s name and, if you choose one, a short summary such as the box’s title. Each argument gets a row with its label and a control.

A call inside a sentence, such as #badge("review"), becomes a chip in the line of text. Clicking the chip opens the same fields in a small popover.

Text fields and markup fields save when you press Enter (⌘ Enter or Ctrl Enter in a multi-line field) or leave the field. Esc puts the old value back. Lists and switches save at once. Every change is one undo step.

Moving the cursor into a call shows its source, like any other Visual mode element. Edit in source on the card does the same.

There are two places, with the same format:

  • In a package. Add a [tool.oleafly] section to the package’s typst.toml. Typst keeps [tool.*] sections for other tools, so the package still installs and compiles everywhere. This works for @local packages and for @preview packages from Typst Universe.
  • In the project. For templates kept as plain .typ files inside the project, create oleafly-forms.toml at the project root. Each form also names the file that defines the function, with module.

A package form:

[tool.oleafly]
schema = 1
[[tool.oleafly.forms]]
function = "info-box"
label = "Info box"
summary = "title"
[[tool.oleafly.forms.fields]]
arg = "title"
kind = "text"
label = "Heading"
placeholder = "Short heading"
required = true
[[tool.oleafly.forms.fields]]
arg = "tone"
kind = "choice"
label = "Tone"
default = "note"
options = [{ value = "note", label = "Note" }, { value = "risk", label = "Risk" }, "neutral"]
[[tool.oleafly.forms.fields]]
arg = 0
kind = "content"
label = "Text"

A project form, in oleafly-forms.toml:

[tool.oleafly]
schema = 1
[[tool.oleafly.forms]]
module = "template/memo.typ"
function = "memo-header"
label = "Memo header"
fields = [
{ arg = "to", kind = "text", label = "To" },
{ arg = "subject", kind = "text", label = "Subject" },
]

schema = 1 is required. A file without it, or with another number, is ignored.

Each [[tool.oleafly.forms]] entry has:

KeyRequiredMeaning
functionYesThe function’s name, as one Typst identifier. Hyphens are fine.
moduleIn oleafly-forms.toml onlyThe project file that defines the function, relative to the project root, with / between folders.
labelNoThe form’s name on cards and chips. Defaults to the function’s name.
summaryNoThe argument whose value appears next to the label, written like a field’s arg.
fieldsYesOne or more fields.

Each field has:

KeyRequiredMeaning
argYesA name for a named argument ("title"), or a number for a positional one (0 is the first). A trailing [...] block counts as a positional argument after the ones in parentheses.
kindYestext, content, choice, number or toggle.
labelYesThe field’s label.
helpNoA short note under the field.
placeholderNoGrey text in an empty field.
requiredNotrue marks an empty field as missing and hides its Clear button. Oleafly never adds the argument by itself.
defaultNoThe value your function uses when the argument is left out. It is shown, never written.

Kinds and their extra keys:

  • text writes a Typst string. Add multiline = true for a larger box.
  • content edits Typst markup and writes it as a [...] block. A string argument stays a string.
  • choice needs options: a list of strings, or { value, label } tables when the label differs from the value. Values are written as strings.
  • number takes integer, min, max and step. Numbers have no units.
  • toggle writes true or false.

Oleafly follows the report’s own #import and #let lines at the top level of the file:

  • #import "@local/acme-report:0.1.0" as acme makes #acme.info-box(...) a form.
  • #import "@local/acme-report:0.1.0": info-box, status-badge as badge makes #info-box(...) and #badge(...) forms.
  • #import "@local/acme-report:0.1.0": * makes every function the package declares a form for.
  • #import "template/memo.typ": memo-header matches a project form whose module is template/memo.typ. Paths are read from the report’s own folder, or from the project root when they start with /.

The package version must match exactly. A #let with the same name, later in the file, replaces the form from that point on. Calls reached through another file that re-imports the package keep their source text.

A function named like a Typst element that Visual mode already draws, such as table or figure, keeps the built-in look when called by its bare name. #acme.table(...) or a renamed import still gets the form.

Some arguments can’t be edited safely from a form. Their row shows the source with the reason, and Edit in source jumps to it:

  • Computed in the source: the value is an expression, such as title: data.title.
  • Written as a different kind of value: for example a number where the form expects text.
  • Not one of the choices: a value your options don’t list.
  • Set by a spread argument: a call with ..args passes values the form can’t see, so every field is read-only.

When a field can’t be added where it would go, for example a positional argument after a missing one, the form says so and offers Add in source.

Open oleafly-forms.toml, or a vendored package’s typst.toml, in Oleafly and each problem shows as a marker on its line: an unknown kind, a missing label, a duplicate form, a module outside the project. A broken form is left out and the others keep working. A file larger than 256 KB is not read.

Oleafly reads package forms from the same folders the compiler uses. A collaborator who doesn’t have the @local package sees plain source and no errors. To share forms with everyone on a project, open the Typst packages dialog, turn on Use packages from the project folder and choose Vendor packages into project. The package, and its forms, then travel in the project’s typst-packages folder (see Typst projects).

Image preview

AI answers couldn’t load

Try again, or search the documentation.