Skip to content

Latest commit

 

History

History
1068 lines (787 loc) · 30.5 KB

File metadata and controls

1068 lines (787 loc) · 30.5 KB

API Reference

Complete reference for all vui.el functions, macros, and variables.

1 Component Definition

1.1 defcomponent

(vui-defcomponent NAME (PROPS...) [DOCSTRING] BODY...)

Define a component named NAME.

1.1.1 Arguments

NAME
Symbol naming the component
PROPS
List of prop names the component accepts
DOCSTRING
Optional documentation string

1.1.2 Body Keywords

KeywordRequiredDescription
:stateNo(VAR INITIAL)... - local state variables
:on-mountNoForm run after first render
:on-updateNoForm run after re-render (not first)
:on-unmountNoForm run before removal
:should-updateNoReturn t to allow re-render, nil to skip
:renderYesThe render expression (must return a vnode)

1.1.3 Available Variables in Body

In all forms:

  • Props as named variables (e.g., title for :title prop)
  • State vars as named variables
  • children for nested content

In :on-update and :should-update additionally:

  • props - Current props plist
  • state - Current state plist
  • prev-props - Previous props plist
  • prev-state - Previous state plist

1.1.4 Example

(vui-defcomponent counter (initial-value)
  "A counter component with increment button."
  :state ((count initial-value))

  :on-mount
  (message "Counter mounted with %d" count)

  :on-update
  (unless (equal (plist-get prev-state :count)
                 (plist-get state :count))
    (message "Count changed to %d" count))

  :should-update
  (not (equal (plist-get prev-state :count)
              (plist-get state :count)))

  :on-unmount
  (message "Counter unmounted")

  :render
  (vui-fragment
   (vui-text (format "Count: %d" count))
   (vui-button "+" :on-click (lambda ()
                               (vui-set-state :count (1+ count))))))

2 Mounting

2.1 vui-mount

(vui-mount COMPONENT-VNODE &optional BUFFER-NAME) -> instance

Mount a component as root and render to a buffer.

COMPONENT-VNODE
Created with vui-component
BUFFER-NAME
String (default "*vui*")
Returns
The root instance
(vui-mount (vui-component 'my-app :title "Hello") "*my-app*")

2.2 vui-rerender

(vui-rerender INSTANCE) -> instance

Re-render an existing instance, preserving component state.

Triggers a re-render of the component tree rooted at INSTANCE. Component state (including collapsed sections, internal state) is preserved through reconciliation. Memoized values are also preserved and will only recompute if their dependencies change.

Returns INSTANCE for chaining.

;; Re-render without changing anything (useful for forcing a redraw)
(vui-rerender instance)

2.3 vui-update

(vui-update INSTANCE NEW-PROPS) -> instance

Update instance props, invalidate all memos, and re-render.

Use this when external data has changed and cached computations should be discarded (e.g., after a file save, database update, or user-triggered refresh).

NEW-PROPS completely replaces the instance’s current props. All memoized values in the instance tree are invalidated, forcing recomputation on the next render. Component state is preserved through reconciliation.

Returns INSTANCE for chaining.

;; After save - data on disk changed, force recomputation
(vui-update instance (list :note (fetch-note)))

2.4 vui-update-props

(vui-update-props INSTANCE NEW-PROPS) -> instance

Update instance props and re-render, preserving memos.

Use this for periodic or speculative refreshes where data may not have changed. Memoized values are preserved and only recompute if their dependencies change, avoiding unnecessary expensive work.

NEW-PROPS completely replaces the instance’s current props. Component state is preserved through reconciliation.

Returns INSTANCE for chaining.

;; Idle timer - data probably hasn't changed, let memos decide
(vui-update-props instance (list :note (fetch-note)))

2.5 When to use which

FunctionPropsMemosUse case
vui-rerenderKeepPreservedForce redraw, same props
vui-update-propsReplacePreservedPeriodic refresh, data may not have changed
vui-updateReplaceInvalidatedData changed, force recomputation

As a rule of thumb:

  • Save hooks, manual refreshvui-update (data changed, recompute everything)
  • Idle timers, window focusvui-update-props (data probably unchanged, let memos skip work)
  • Internal triggersvui-rerender (just redraw with current props and state)

3 Primitives

Interactive primitives (buttons, fields, checkboxes, selects) support TAB / S-TAB keyboard navigation.

3.1 vui-text

(vui-text CONTENT &rest PROPS) -> vnode

Create a text node.

PropertyTypeDescription
:facesymbolEmacs face for styling
:keyanyReconciliation key
(vui-text "Hello" :face 'bold)
(vui-text (format "Count: %d" count))

3.2 vui-newline

(vui-newline &optional KEY) -> vnode

Create a newline.

3.3 vui-space

(vui-space &optional WIDTH KEY) -> vnode

Create WIDTH spaces (default 1).

3.4 vui-fragment

(vui-fragment &rest CHILDREN) -> vnode

Group multiple vnodes without adding structure.

(vui-fragment
 (vui-text "Line 1")
 (vui-newline)
 (vui-text "Line 2"))

3.5 vui-button

(vui-button LABEL &rest PROPS) -> vnode

Create a clickable button. By default, buttons render with brackets: LABEL becomes [LABEL].

PropertyTypeDescription
:on-clickfunctionClick handler
:facesymbolButton face
:disabledbooleanDisable button
:max-widthintegerMax width including brackets (truncates)
:no-decorationbooleanRender without brackets
:help-echostring/nilTooltip (nil disables for performance)
:tab-orderintegerTab navigation order (-1 = skip)
:keymapkeymapCustom keymap active when on button
:keyanyReconciliation key
(vui-button "Click me"
            :on-click (lambda () (message "Clicked!"))
            :face 'custom-button)
;; Renders: [Click me]

;; Truncated button
(vui-button "Very long label" :max-width 12)
;; Renders: [Very lo...]

;; Plain button (no brackets)
(vui-button "Click here" :no-decoration t)
;; Renders: Click here

;; Disable tooltip for performance (useful with many buttons)
(vui-button "Item" :help-echo nil)

3.6 vui-field

(vui-field &key VALUE SIZE PLACEHOLDER ON-CHANGE ON-SUBMIT KEY FACE SECRET) -> vnode

Create a text input field. This is a simple string-based primitive. For typed fields with parsing and validation, use vui-typed-field from vui-components.el.

PropertyTypeDescription
:valuestringInitial content (default “”)
:sizeintegerWidth in characters
:placeholderstringHint text (not yet rendered)
:on-changefunctionCalled with value on change
:on-submitfunctionCalled with value on RET
:keyanyReconciliation key / lookup key
:facesymbolText face
:secretbooleanHide input (for passwords)
(vui-field :value name
           :size 20
           :on-change (lambda (v) (vui-set-state :name v)))

;; Password field
(vui-field :value password
           :size 20
           :secret t
           :on-change (lambda (v) (vui-set-state :password v)))

3.7 vui-field-value

(vui-field-value KEY) -> string or nil

Get current value of field with KEY, without triggering re-render.

(vui-field :key 'my-input :size 20)
(vui-button "Submit"
            :on-click (lambda ()
                        (process (vui-field-value 'my-input))))

3.8 vui-checkbox

(vui-checkbox &rest PROPS) -> vnode

Create a checkbox.

PropertyTypeDescription
:checkedbooleanWhether checked
:on-changefunctionCalled with boolean on toggle
:labelstringLabel after checkbox
:keyanyReconciliation key
(vui-checkbox :checked enabled
              :label "Enable feature"
              :on-change (lambda (v) (vui-set-state :enabled v)))

3.9 vui-select

(vui-select &rest ARGS) -> vnode

Create a selection button (minibuffer-based selection).

PropertyTypeDescription
:valuestringCurrent selection
:optionslistList of options (strings or alist)
:on-changefunctionCalled with selected value
:promptstringMinibuffer prompt (default “Select: “)
:keyanyReconciliation key
;; Simple list
(vui-select :value current-theme
            :options '("light" "dark" "system")
            :on-change (lambda (v) (vui-set-state :theme v)))

;; Alist with display values
(vui-select :value status
            :options '(("active" . "Active")
                       ("pending" . "Pending")
                       ("closed" . "Closed"))
            :on-change (lambda (v) (vui-set-state :status v)))

4 Layout

4.1 vui-hstack

(vui-hstack &rest ARGS) -> vnode

Horizontal layout. ARGS can start with options, then children.

OptionTypeDefaultDescription
:spacinginteger1Spaces between children
:keyanynilReconciliation key
(vui-hstack :spacing 2
            (vui-text "Label:")
            (vui-field :size 10))

4.2 vui-vstack

(vui-vstack &rest ARGS) -> vnode

Vertical layout. ARGS can start with options, then children.

OptionTypeDefaultDescription
:spacinginteger0Blank lines between children
:indentinteger0Left indent in spaces
:keyanynilReconciliation key
(vui-vstack :spacing 1 :indent 2
            (vui-text "Item 1")
            (vui-text "Item 2"))

4.3 vui-box

(vui-box CHILD &rest PROPS) -> vnode

Fixed-width container for CHILD.

PropertyTypeDefaultDescription
:widthinteger20Width in characters
:alignkeyword:left:left, :center, :right
:padding-leftinteger0Left padding
:padding-rightinteger0Right padding
:keyanynilReconciliation key
(vui-box (vui-text "Centered") :width 30 :align :center)

4.4 vui-table

(vui-table &rest ARGS) -> vnode

Create a table layout. Cell contents can be strings or vnodes (including interactive widgets like buttons, fields, and components).

PropertyTypeDescription
:columnslistList of column specs (see below)
:rowslistList of rows, each a list of cells
:bordersymbolnil, :ascii, or :unicode
:keyanyReconciliation key

When :border is set, cells are automatically padded with 1 space on each side for readability (e.g., | value | instead of |value|).

4.4.1 Column Spec Properties

PropertyTypeDescription
:headerstringHeader text
:widthintegerTarget width for cell content
:min-widthintegerMinimum width, expand as needed
:growbooleanPad short content, expand for long
:truncatebooleanTruncate long content with “…”
:alignkeyword:left (default), :center, :right

4.4.2 Width Behavior Table

:width:grow:truncateContent vs WidthResult
Wnilnilcontent < WColumn shrinks to content size
Wnilnilcontent > WOverflow with broken bar (¦)
Wtnilcontent < WColumn = W, content padded
Wtnilcontent > WColumn expands to fit content
Wniltcontent < WColumn shrinks to content size
Wniltcontent > WColumn = W, content truncated with “…”
Wttcontent < WColumn = W, content padded
Wttcontent > WColumn = W, content truncated with “…”
nil--anyColumn auto-sizes to content

4.4.3 Basic Example

(vui-table
 :columns '((:header "Name" :min-width 15)
            (:header "Age" :width 5 :align :right))
 :rows '(("Alice" "30")
         ("Bob" "25"))
 :border :ascii)

4.4.4 Interactive Cells

Cells can contain any vnode, including buttons and components:

(vui-table
 :columns '((:header "Item" :width 20)
            (:header "Action" :width 10))
 :rows `(("Apple" ,(vui-button "[Buy]" :on-click (lambda () (message "Bought!"))))
         ("Banana" ,(vui-button "[Buy]" :on-click (lambda () (message "Bought!")))))
 :border :ascii)

For interactive tables, embed components with callbacks:

(vui-table
 :columns '((:header "Name" :width 15)
            (:header "Score" :width 8))
 :rows (mapcar (lambda (item)
                 (list (plist-get item :name)
                       (vui-component 'score-input
                                      :key (plist-get item :id)
                                      :value (plist-get item :score)
                                      :on-change on-score-change)))
               items)
 :border :ascii)

4.4.5 Dynamic Column Widths

Calculate column widths at render time based on content:

(let* ((max-len (apply #'max (mapcar #'length names)))
       (col-width (max 18 (min 48 (+ max-len 2)))))
  (vui-table
   :columns `((:header "Name" :width ,col-width)
              (:header "Value" :width 10))
   :rows ...))

See docs/examples/05-wine-tasting.el for a complete example demonstrating interactive buttons, dynamic column widths, and real-time computed statistics.

4.5 vui-list

(vui-list ITEMS RENDER-FN &optional KEY-FN &key VERTICAL INDENT SPACING) -> vnode

Render a list of items with proper reconciliation.

ITEMS
List of items to render
RENDER-FN
(lambda (item) vnode) - renders each item
KEY-FN
(lambda (item) key) - extracts key (default: identity)
:vertical
If non-nil (default t), returns a vstack; otherwise returns an hstack
:indent
Left indentation in spaces (default 0)
:spacing
Blank lines between items for vertical (default 0), spaces for horizontal (default 1)

Returns a vstack for vertical lists, hstack for horizontal lists. This ensures proper indent propagation when nested inside other layout containers.

;; Vertical list (default) - each item on its own line
(vui-list todos
          (lambda (todo)
            (vui-text (plist-get todo :text)))
          (lambda (todo)
            (plist-get todo :id)))

;; With indentation
(vui-list items #'vui-text nil :indent 2)

;; Horizontal list - items on same line with spacing
(vui-list tags
          (lambda (tag) (vui-text (format "[%s]" tag)))
          nil :vertical nil)

5 Components

5.1 vui-component

(vui-component TYPE &rest PROPS-AND-CHILDREN) -> vnode

Create a component vnode.

TYPE
Symbol naming a defined component
PROPS-AND-CHILDREN
Plist of props, optionally ending with :children
(vui-component 'greeting :name "Alice")
(vui-component 'card :title "Hello" :children (list child1 child2))

6 State Management

6.1 vui-set-state

(vui-set-state KEY VALUE)

Set state KEY to VALUE and schedule re-render.

Must be called from within a component context (event handler, lifecycle hook).

If VALUE is a function, it is called with the current value and the result is used as the new value. This is essential for async callbacks where captured variables may be stale.

;; Direct value
(vui-button "Increment"
            :on-click (lambda ()
                        (vui-set-state :count (1+ count))))

;; Functional update (for async callbacks)
(run-with-timer 1 1
  (vui-with-async-context
    (vui-set-state :count #'1+)))  ; Gets current value, returns incremented

;; Lambda for complex updates
(vui-set-state :items (lambda (old) (cons new-item old)))

6.2 vui-batch

(vui-batch &rest BODY)

Batch multiple state updates into a single re-render.

(vui-batch
 (vui-set-state :name "Bob")
 (vui-set-state :age 30)
 (vui-set-state :active t))
;; Single re-render instead of three

6.3 vui-flush-sync

(vui-flush-sync)

Force immediate re-render, bypassing any pending deferred timers.

6.4 vui-with-async-context

(vui-with-async-context BODY...) -> function

Capture component context for use in async callbacks. Returns a function that, when called, restores the context and executes BODY.

Use this when you need vui-set-state in:

  • Timer callbacks (run-with-timer, run-with-idle-timer)
  • Emacs hook functions
  • Process sentinels (when not using vui-use-async)
  • Any callback that runs asynchronously
;; Timer example - use functional update for state-based values
(vui-use-effect ()
  (let ((timer (run-with-timer 1 1
                 (vui-with-async-context
                   (vui-set-state :seconds #'1+)))))
    (lambda () (cancel-timer timer))))

;; Hook example - (frame-width) is called fresh, no capture issue
(vui-use-effect ()
  (let ((handler (vui-with-async-context
                   (vui-set-state :width (frame-width)))))
    (add-hook 'window-size-change-functions handler)
    (lambda ()
      (remove-hook 'window-size-change-functions handler))))

Note: You do NOT need this for widget callbacks (buttons, fields) or vui-use-async loaders - those handle context automatically.

6.5 vui-async-callback

(vui-async-callback (ARGS...) BODY...) -> function

Create an async callback that captures component context and accepts arguments. Like vui-with-async-context, but the returned function accepts ARGS which are bound when executing BODY.

Use this when an async operation passes data to your callback:

  • API responses
  • Process output
  • Any async operation that returns a value
;; Fetch data and set state with the result
(vui-use-effect ()
  (fetch-data-async
    (vui-async-callback (result)
      (vui-set-state :data result))))

;; Multiple arguments
(vui-use-effect ()
  (my-api-call
    (vui-async-callback (data status)
      (vui-set-state :data data)
      (vui-set-state :status status))))

Compare with =vui-with-async-context=:

  • vui-with-async-context - fire-and-forget callbacks (timers, hooks)
  • vui-async-callback - callbacks that receive data from async operations

7 Hooks

7.1 use-effect

(vui-use-effect (DEPS...) BODY...)

Run side effect when DEPS change.

  • Runs after first render
  • Runs after re-render if any dep changed
  • If BODY returns a function, it’s called as cleanup
;; Run once on mount
(vui-use-effect ()
  (message "Mounted"))

;; Run when count changes
(vui-use-effect (count)
  (message "Count: %d" count))

;; With cleanup
(vui-use-effect (user-id)
  (let ((timer (run-with-timer 1 nil #'refresh)))
    (lambda () (cancel-timer timer))))

7.2 use-ref

(vui-use-ref INITIAL-VALUE) -> (VALUE . nil)

Create mutable ref that persists across renders.

Access via (car ref), set via (setcar ref new-value).

Modifying a ref does NOT trigger re-render.

(let ((timer-ref (vui-use-ref nil)))
  (vui-use-effect ()
    (setcar timer-ref (run-with-timer 1 1 #'tick))
    (lambda () (cancel-timer (car timer-ref)))))

7.3 use-callback

(vui-use-callback (DEPS...) BODY)

Create memoized callback that stays stable when DEPS unchanged.

(let ((handle-click (vui-use-callback (item-id)
                      (lambda () (delete-item item-id)))))
  (vui-button "Delete" :on-click handle-click))

7.4 use-callback*

(vui-use-callback* (DEPS...) :compare COMPARE BODY)

Like vui-use-callback with configurable comparison.

COMPARE options:

  • eq - identity comparison (fast)
  • equal - structural comparison (default)
  • function - custom (lambda (old-deps new-deps) bool)

7.5 use-memo

(vui-use-memo (DEPS...) BODY)

Cache computed value, recompute only when DEPS change.

(let ((filtered (vui-use-memo (items filter)
                  (seq-filter (lambda (i)
                                (string-match-p filter (plist-get i :name)))
                              items))))
  (vui-list filtered #'render-item))

7.6 use-memo*

(vui-use-memo* (DEPS...) :compare COMPARE BODY)

Like vui-use-memo with configurable comparison.

7.7 use-async

(vui-use-async KEY LOADER) -> plist

Asynchronously load data using LOADER, identified by KEY.

KEY
Cache key; when it changes, a new load is triggered
LOADER
(lambda (resolve reject) ...) - call resolve with data or reject with error

Returns a plist:

KeyDescription
:statusOne of: pending, ready, or error
:dataThe loaded data (when ready)
:errorError message (when error)
(let ((result (vui-use-async user-id
                (lambda (resolve _reject)
                  (funcall resolve (fetch-user user-id))))))
  (pcase (plist-get result :status)
    ('pending (vui-text "Loading..."))
    ('error (vui-text (format "Error: %s" (plist-get result :error))))
    ('ready (vui-text (format "User: %s" (plist-get result :data))))))

8 Context

8.1 defcontext

(vui-defcontext NAME &optional DEFAULT-VALUE DOCSTRING)

Define a context.

Creates:

  • NAME-context - The context object
  • NAME-provider - Function to provide value
  • use-NAME - Hook to consume value
(vui-defcontext theme 'light "Current UI theme.")

;; Provide
(theme-provider 'dark
  (vui-component 'my-app))

;; Consume (in component render)
(let ((theme (use-theme)))
  (vui-text (format "Theme: %s" theme)))

9 Error Handling

9.1 vui-error-boundary

(vui-error-boundary &key FALLBACK ON-ERROR ID CHILDREN) -> vnode

Catch render errors in CHILDREN and display FALLBACK.

PropertyTypeDescription
:fallbackvnodeDisplayed when error occurs
:on-errorfunctionCalled with error when caught
:idanyUnique identifier (auto-generated)
:childrenvnodeProtected content
(vui-error-boundary
 :id 'main-boundary
 :fallback (vui-text "Something went wrong!" :face 'error)
 :on-error (lambda (err) (log-error err))
 :children (vui-component 'risky-component))

9.2 vui-reset-error-boundary

(vui-reset-error-boundary ID)

Reset error state for boundary ID, allowing retry.

10 Configuration Variables

10.1 Error Handling

VariableDefaultDescription
vui-lifecycle-error-handlerwarnHandler for lifecycle errors
vui-event-error-handlerwarnHandler for event errors
vui-last-errornilLast error: (TYPE ERROR CONTEXT)

Handler values:

  • warn - Display warning (default)
  • message - Display in echo area
  • signal - Re-signal error
  • ignore - Silently ignore
  • function - Custom (lambda (hook-name err instance) ...)

10.2 Performance

VariableDefaultDescription
vui-render-delay0.01Seconds before deferred render (nil=immediate)
vui-timing-enablednilEnable timing collection

10.3 Debugging

VariableDefaultDescription
vui-debug-enablednilEnable debug logging
vui-debug-log-phases(render mount update unmount state-change)Phases to log

11 Developer Tools

11.1 vui-inspect

(vui-inspect &optional INSTANCE)

Display component tree in inspector buffer.

Shows hierarchical view with props and state for each component.

11.2 vui-inspect-state

(vui-inspect-state &optional INSTANCE)

Display only components with state.

11.3 vui-get-instance-by-id

(vui-get-instance-by-id ID &optional INSTANCE) -> instance or nil

Find component instance by ID.

11.4 vui-get-component-instances

(vui-get-component-instances COMPONENT-TYPE &optional INSTANCE) -> list

Find all instances of COMPONENT-TYPE.

11.5 vui-report-timing

(vui-report-timing &optional LAST-N)

Display timing report in buffer.

Groups by component, shows time per phase.

11.6 vui-get-timing

(vui-get-timing) -> list

Return raw timing data.

Each entry: (:phase PHASE :component NAME :duration SECONDS :timestamp TIME)

11.7 vui-clear-timing

(vui-clear-timing)

Clear all timing data.

11.8 vui-debug-show

(vui-debug-show)

Show the *vui-debug* buffer.

11.9 vui-debug-clear

(vui-debug-clear)

Clear debug log buffer.

12 Quick Reference

12.1 Creating Components

(vui-defcomponent NAME (PROPS...)
  :state ((VAR INIT) ...)
  :on-mount FORM
  :on-update FORM
  :on-unmount FORM
  :should-update FORM
  :render VNODE)

12.2 Primitives

FunctionDescription
vui-textText content
vui-newlineLine break
vui-spaceHorizontal space
vui-fragmentGroup without wrapper
vui-buttonClickable button
vui-fieldText input
vui-checkboxToggle checkbox
vui-selectSelection dropdown

12.3 Layout

FunctionDescription
vui-hstackHorizontal layout
vui-vstackVertical layout
vui-boxFixed-width box
vui-tableTable layout
vui-listList with keys

12.4 Hooks

HookDescription
vui-use-effectSide effects
vui-use-refMutable reference
vui-use-callbackMemoized callback
vui-use-memoMemoized value
vui-use-asyncAsync data loading

12.5 State

FunctionDescription
vui-set-stateUpdate component state
vui-batchBatch multiple updates
vui-flush-syncForce immediate render

12.6 Context

Macro/FunctionDescription
vui-defcontextDefine context
NAME-providerProvide value
use-NAMEConsume value

12.7 Debugging

FunctionDescription
vui-inspectShow component tree
vui-inspect-stateShow state only
vui-report-timingPerformance report
vui-debug-showShow debug log