- Rebol 100%
| .forgejo/workflows | ||
| bench-gfx.r3 | ||
| bench-tui.r3 | ||
| completion.r3 | ||
| demo-tui-fields.r3 | ||
| gfx.r3 | ||
| README.md | ||
| rebol-key-dispatch.r3 | ||
| rebol-text.r3 | ||
| smoke-tui-events.r3 | ||
| test-gfx.r3 | ||
| test-tui.r3 | ||
| text-edit.r3 | ||
| tui.r3 | ||
Rebol terminal UI
This repo has two layers:
gfx.r3is the low-level terminal graphics dialect. It compiles drawing commands into ANSI escape strings and does not print.tui.r3is the retained TUI layer. It keeps a tree of widgets, renders into cell buffers, and emits only changed terminal cells on update.
Graphics layer
do %gfx.r3
print gfx [
bold red "Hello, " reset
fg 255.128.0 "world!" reset
newline
]
gfx.r3 still exports tui as a compatibility alias for older code, but new
graphics code should call gfx.
do %gfx.r3
The file also sets a top-level size (a pair! of cols by rows),
queried once from system/ports/output at load time. Re-load the file
to refresh it after a terminal resize.
Retained TUI layer
do %tui.r3
view: make-view 80x24 [
origin 2x2
title: text "Status" font-color yellow
return
box 30x3 [
status: text "Ready"
]
]
initial-output: render view
set-face view 'status [text "OK"]
delta-output: render view
The first render compares the empty front buffer with the rendered back
buffer and returns the ANSI output needed to draw the view. Later calls return
only dirty cell runs. Updating "Ready" to "OK" dirties the old and new face
bounds and emits only the cells needed to draw OK and clear the trailing
spaces.
Public retained APIs:
make-view <size> <layout-block>— build a retained widget treerender <view>— return changed ANSI output and clear dirty stateset-face <view> <id-or-face> <facet-block>— update a named faceget-face <view> <id>— return a named face objectresize-view <view> <size>— rebuild buffers and dirty the screeninvalidate <view> <id-or-face-or-rect>— manually mark dirty state
Initial widgets are text, base, panel, box, button, and field.
Layout words are
origin, space, pad, at, and return. Facets include size, offset,
color, font-color, visible?, enabled?, focusable?, flags, rate,
data, actors, and pane. In layout blocks, text content is usually the
bare string after text; in set-face, use text "...".
Event runtime
The retained renderer can also be driven by a minimal Red-like event runtime:
view <layout-block>— size from the terminal, render, and enter the loopview/tight <layout-block>— same, with compactmake-viewspacingshow <view-or-face>— invalidate and render after direct face mutationunview/unview/all— leave the current or all active view loopsdispatch-event <view> <event>— testable event entry point that returns render output instead of printing
Actor facets are on-key, on-time, on-click, on-change, and on-enter.
Actor blocks run with view, face, event, and named faces from the view in
scope. Keyboard dispatch sends on-key to the focused face first, then the
root actor. tab and shift-tab cycle visible, enabled, focusable faces in
tree order; button and field are focusable by default.
Keyboard events
dispatch-event accepts a word!, char!, none, or an event object. A
plain key value is normalized into a key event; none is normalized into a
time event.
dispatch-event view #"x"
dispatch-event view 'tab
dispatch-event view make object! [
type: 'key
key: 'enter
face: none
time: none
]
Common control characters are normalized before actor dispatch:
#"^-"->tab#"^M"and#"^/"->enter#"^["->escape#"^H"and#"^(7F)"->backspace
The interactive view loop also normalizes native read-key results and
common terminal CSI sequences before calling dispatch-event. In practice,
arrow keys, home, end, delete, backspace, tab, shift-tab, enter,
and escape arrive as the same logical words used by tests:
left right home end delete backspace tab shift-tab enter escape
Modified arrow keys are normalized as shift-<dir>, ctrl-<dir>, and
alt-<dir> (e.g. ctrl-left). They reach on-key actors unchanged; the
built-in field editing does not interpret them.
Unknown multi-character terminal sequences are ignored by the live loop.
Fields
field is a single-line editable text widget. It is focusable by default and
keeps two logical editing facets:
caret— 1-based insert position; defaults to the end oftextplus 1scroll— 1-based visible text start for the clipped field window
Fields render as a clipped, padded line. Padding is intentional: when text is deleted, the retained diff clears stale cells to the right of the shorter value.
When a field is focused inside the interactive view loop, the caret is shown
with inverse video on the active cell. Plain render calls keep the logical
caret hidden unless terminal mode is enabled on the view.
Focused, visible, enabled fields have built-in key handling:
- printable
char!inserts atcaret backspacedeletes beforecaretdeletedeletes atcaretleft,right,home, andendmovecaretenterdoes not edit text; it still dispatcheson-entertabandshift-tabkeep focus traversal behavior
For field keys, built-in editing runs before actor callbacks. If the text
changes, on-change is dispatched with event/type = 'change; after that,
normal focused on-key and root on-key dispatch continue. Caret movement
and failed deletions do not fire on-change.
view/tight [
on-key [if event/key = 'escape [unview]]
name: field "Edit me" size 24x1
on-change [
status/text: rejoin ["Editing: " face/text]
]
return
status: text "Ready" size 32x1
]
Graphics dialect reference
Tokens are consumed left-to-right. Whitespace and grouping in the source block are not significant.
Cursor positioning
<col>x<row>(anypair!) — move cursor to that row/column. Negative components count from the far edge:-1x-1is the bottom-right corner,-5x-3is 5 from the right and 3 from the bottom. Same rule applies to theboxsize pair.up/up N— move up by 1 (default) or N rowsdown/down Nleft/left Nright/right Ncol N— move to absolute column N on the current rowsave— save current cursor positionrestore— restore the saved position
Styling
reset— clear all attributesbolddimunderlineinvert— reverse video<color>orfg <color>— foreground (fgis optional)bg <color>— background (bgis required)
<color> may be:
- A named color:
black red green yellow blue magenta cyan white - An integer
0..255for the 256-color palette - A
tuple!like255.128.0for 24-bit truecolor
Boxes and lines
hline N— emit N-charactersvline N— draw a vertical run of N|charactersbox <refinements...> <pair>— draw a box of the given size; cursor returns to the top-left of the box and styles are reset
box accepts any number of optional refinements between the keyword
and the closing size pair, in any order:
- Style — one of
single(default),double,rounded,ascii:single ┌──┐ double ╔══╗ rounded ╭──╮ ascii +--+ │ │ ║ ║ │ │ | | └──┘ ╚══╝ ╰──╯ +--+ - Centering —
center(both axes),hcenter(horizontal only, preserves current row),vcenter(vertical only, preserves current column). Centering uses the file-levelsizepair. outline <color>— color the outline.<color>accepts the same forms asfg/bg.fill <color>— color the interior background. Outline is not tinted.
print gfx [
cls
box double outline red fill 17 center 30x10
]
Scrolling
scroll up/scroll up Nscroll down/scroll down N
Clearing
clsorclear screenclear lineclear to end— from cursor to end of line
Text
"..."— emit a literal string#"x"— emit a literal characternewline— emit^/
Panels
panel <start-pos> <end-pos> [<content>]— create a rectangular zone in which all coordinates are relative to the panel's top-left corner, and text strings are word-wrapped to the panel width. The cursor starts at the panel's top-left; content can use any dialect token.
print gfx [
cls
panel 5x3 30x10 [
bold "This long text is word-wrapped to the panel width."
newline
box outline blue center 20x6
]
]
Coordinates inside the panel are 1-based relative to its top-left. Negative
pairs resolve against the panel size. box centering uses panel bounds
instead of screen bounds. Panels nest: inner-panel coordinates are relative
to the outer panel.
Word wrapping inside a panel preserves repeated and leading whitespace when it fits on a line. When a break is needed, it prefers the last space within the panel width and only drops one leading continuation space after a hard word break.
Terminals provide a single cursor save/restore slot. box and panel
multi-line text both use it internally, so an explicit save … restore
straddling a box or a wrapped panel string will be clobbered. Save and
restore around contiguous code that doesn't itself draw boxes.
Tables
table <refinements...> <rows>— draw a bordered, column-aligned table.<rows>is a block of row blocks; each cell may be astring!,integer!, orchar!. Column widths auto-fit to the widest cell. The cursor returns to the top-left of the table and styles are reset, same asbox.
print gfx [
cls
table single center
headers ["Name" "Age" "City"]
align [left right left]
[
["Alice" 30 "Praha"]
["Bob" 25 "Brno" ]
["Carol" 100 "Plzen"]
]
]
Refinements (any order, like box):
- Style —
single(default),double,rounded,ascii. Picks both the outer border and the inner T/cross junctions. - Centering —
center/hcenter/vcenter. Resolved against the enclosing panel when inside one, otherwise against the file-levelsize. outline <color>— color the border characters.fill <color>— background color for cell interiors. The header row's bold reset clears the fill on header cells; data cells keep it.headers <block>— header row; rendered bold with a junction separator line below.widths <block>— explicit per-column widths. Cells wider than their column are truncated. Columns omitted from the block keep auto-fit.align <block>— per-column alignment:left(default),right,center. Columns omitted from the block default toleft.
Tables use the same single cursor save/restore slot as box and wrapped
panel text — the caveat above applies.
Errors
If the input contains anything the graphics dialect cannot match, gfx raises a
script/invalid-arg error rather than silently producing partial
output.
Tests
do %test-gfx.r3
do %test-tui.r3
For a manual runtime smoke test:
do %smoke-tui-events.r3
For a focused editable-field demo:
do %demo-tui-fields.r3