Shared UI functions for my Helix plug-ins (juju,
nrepl.hx): overlay geometry, frame and
row drawing, key-event predicates, scroll math, a scrollable overlay-view
shell, a transient menu, and a filterable list picker, built on top of Steel’s
helix/components.
There are some /examples, but for real world use check the plug-ins above.
Probably vendor this if you want to use it in a plug-in of your own: high likelihood of breaking changes, at least until Steel is in a stable Helix release.
Plugins declare it as a forge dependency in their cog.scm:
(define dependencies
'((#:name "ui-utils.hx"
#:git-url "https://github.com/waddie/ui-utils.hx")))Forge installs it to ~/.steel/cogs/ui-utils.hx/ alongside the plugin. For a
manual install from a checkout:
./install.sh(require "ui-utils.hx/strings.scm")
(require "ui-utils.hx/keys.scm")Modules are documented in their headers. The pure modules (strings, scroll,
nav, selection, line-edit, menu-model, picker-model, geometry-model,
component) have no Helix dependencies and load under the bare steel CLI;
the rest (geometry, keys, style, draw, overlay-view, menu,
picker) require helix/components and only load inside Helix.
The -model modules hold the logic their component shell would otherwise bury
behind helix/components: geometry-model is the overlay and pane arithmetic
that geometry wraps in area structs, and picker-model owns make-picker
and the picker spec so that spec construction stays testable.
There is no library-level mutable state: styles and overlay scale are passed per
call (a tag->style function, an #:overlay-scale thunk).
A picker's #:preview closure returns one entry per line. A line is either a
(text . style) pair, styled as a whole, or a list of such pairs drawn left to
right as spans:
;; one style for the line
(cons "plain text" (style))
;; several styles across the line
(list (cons ":deps " (style-fg (style) Color/Magenta))
(cons "{}" (style)))'() draws a blank line. Span lines are clipped to the pane width without an
ellipsis, since the cut falls inside one span's styling. See
examples/picker-preview.scm.
A row given to draw-rows (and so to an overlay-view) is normally one string
under one style tag:
(hash 'text " modified src/main.rs" 'tag 'file)A row needing several styles on one line declares columns instead:
(hash 'spans
(list (hash 'text " 12 " 'tag 'diff-context 'width 5)
(hash 'text old-line 'tag 'diff-del 'flex 1)
(hash 'text "│" 'tag 'diff-meta 'width 1)
(hash 'text new-line 'tag 'diff-add 'flex 1)))'width fixes a column; 'flex takes a weighted share of what is left (a span
with neither flexes with weight 1). layout-spans (in geometry-model)
resolves the positions at draw time, where the width is known, so a caller can
build rows without it. Fixed columns that overflow the row clamp the rest to
zero width. Cursor and marked styling apply per span, so the highlight still
covers the whole line. The two row shapes can be mixed in one list.
Under #:filter? #t the line is editable, same keys as the builtin pickers:
| Key | Action |
|---|---|
Left, C-b |
back a character |
Right, C-f |
forward a character |
Alt-b, C-Left |
back a word |
Alt-f, C-Right |
forward a word |
C-a / C-e |
line start / line end |
Backspace |
delete before the caret |
Delete |
delete at the caret |
C-w, Alt-Backspace |
delete the previous word |
Alt-d, Alt-Delete |
delete the next word |
C-k |
kill to end of line |
The caret arithmetic is line-edit.scm, which is pure and reusable by any
other one-line input.
A columned picker can opt into the query syntax Helix's own pickers use, where
%name sends the text after it to one column (in the builtin command palette,
%b ? searches the bindings column for ?):
(make-picker #:columns columns
#:filter? #t
#:filter-columns? #t
#:primary-column 0 ; the default
...)Bare text filters the primary column. %name picks the column whose label
name prefixes, ignoring case, shortest label first when several match, so
%t fn narrows a "Type" column and %ns core a "Namespace" one. A space ends
the name, which leaves multi-word labels reachable only by a prefix. Text under
an unrecognised name falls back to the primary column, \% is a literal
percent, and naming a column twice concatenates its patterns.
Only the primary column ranks the result: the other columns narrow it while
keeping the order the items were given in. #:filter-fn still overrides the
whole thing. Off (the default), % is ordinary filter text. See
examples/picker-columns.scm.
sh tests/run-all.shRuns the headless unit tests for the pure modules. Each tests/test-*.scm file
is a steel-test suite covering one
module; run-all.sh runs them all in file mode and aggregates the exit code.
Requires steel on PATH and steel-test installed in ~/.steel/cogs (via
forge pkg install --git https://github.com/waddie/steel-test, or copy the
package there by hand). Run individual suites with steel tests/test-nav.scm.
Drawing and component modules cannot load outside Helix and are verified live.
Copyright © 2026 Tom Waddington
Distributed under the MIT License. See LICENSE file for details.