EUI applications without a browser
Samples§07

Eight applications, and one client that drew all of them.

Every picture below is a real render, taken off the wire: a Soli server answering an EUI session, the Rust client laying it out and painting it on the GPU, captured at 2× by the repository's own snapshot tool. No mockups, and no browser anywhere in the chain.

They all live in one Soli application — examples/counter-app — and each is a route of four words: a handler that takes an event and returns state, and a view that takes state and returns a hash. Clone the repository and you are running these in about a minute.

run any of them
soli serve examples/counter-app --port 5092
cargo run -p eui-client -- ws://127.0.0.1:5092/_eui/session/tracker
Tracker§07.1

A tracker, mixed by the server

FastTracker 2's shape: sixty-four rows, eight channels, five instrument shapes, and a cursor that lives under the keyboard. The client has no synthesiser and no note scheduler — on Play the server walks the pattern into PCM, writes a wav, and the window fetches it by hash like any picture. The milliseconds the window reports back are what move the row cursor: the pattern scrolls because the sound says where it is.

The tracker: a nameplate, transport and tempo panels, eight scopes, an instrument list, a channel heading strip and a 64-row pattern with the playing row lit.
examples/counter-app · app/controllers/tracker_controller.sl router_eui("tracker", …)
169 344
samples mixed for one pattern at 22 kHz, 16-bit 0.9 s of interpreted Soli — eight times faster than it plays
2
handlers on the whole grid, one click and one key_down a pattern is a character grid, so a click's point is two divisions
2 skins
theme roles, or FastTracker 2's own bevels the modern one follows the viewer's desktop palette; the retro one is painted in hex
Editor§07.2

A code editor, and the highlighting is a function

The buffer is an array of lines in the session's state; the keyboard arrives as key_down on one box, and every keystroke is a round trip that comes back as a patch — the line that changed and the cursor that moved. There is no text widget involved: the client has a field whose caret belongs to the window, and that is the wrong shape for an editor, which wants the gutter, the highlighting and the cursor to be one thing.

The lexer is thirty lines of Soli. It cuts a line into keywords, strings, comments and numbers, and each token gets a text node with a role for its colour — so the same buffer is olive on one desktop and blue on another, and the client is never asked to know what a keyword is. What it opens is its own source. Nothing is ever written back: there is no route that could.

The editor: a header with the file name, its state and the cursor position, a line-numbered gutter, the controller's own source with comments dimmed and strings and numbers coloured, and a status line.
examples/counter-app · app/controllers/editor_controller.sl router_eui("editor", …)
27 of 475
lines sent for a window of a 475-line file the buffer is windowed around the cursor, like the tracker's pattern
5 kinds
the lexer answers with: keyword, string, comment, number, the rest each one a theme role, resolved on the client (05 §1)
1 handler
for the whole keyboard a click on a line moves the cursor; the box itself only takes keys
Needle§07.3

A music player that ships as one binary

Search, a record, a track list, a transport, and a disc that turns for exactly as long as something plays. Its sleeves are drawn from each record's own name, so nothing is fetched; point it at a Spotify client and the server fetches the real artwork instead, crops it and re-encodes it, and the window still sees an ordinary picture from its own origin. Packaged with soli desktop build it is one file in the application menu.

Needle: a search bar, five generated record sleeves, and a transport bar across the foot of the window.
examples/counter-app · app/controllers/music_controller.sl router_eui("music", …)
0 pictures
fetched by the window, ever assets come from the session's own origin, by BLAKE3 hash (01 §2.2)
MPRIS
plays on the machine it runs on, or on a Spotify Connect device the server talks to the desktop bus; the client only draws
Catalogue§07.4

Every widget, on one page

The conformance page: navbar, tabs, stat tiles, banner, progress, badges, chips, segmented control, pager, breadcrumb, stepper, select, slider, sound, moving picture, three calendars, a data grid with inline editing, four charts, an accordion, a tree, a menu, a tooltip, an empty state, a form and a sheet. Every one of them is a Soli function that returns a hash — there is no widget runtime in the client to update.

The catalogue page: stat tiles, an info banner, a progress bar with badges, a segmented control, a pager, a stepper and the controls card.
examples/counter-app · app/controllers/live_controller.sl router_eui("gallery", …)
107
functions in the catalogue, none of them in the client the component reference lists all of them
1 breakpoint
per layout decision, evaluated on the server the tiles wrap themselves; the client has no media queries
Feed§07.5

Forty thousand cards, and only the ones you can see

A social feed, windowed: the client sends the row range it can show, the server builds those cards and no others. A like is one round trip that patches one card. Given an X account it fills the same cards from a real timeline instead, and says why in the header when the API refuses.

The feed: a header with a post count and a load button, and a card with an avatar, text and a moving picture with a scrubber.
examples/counter-app · app/controllers/live_controller.sl router_eui("feed", …)
40 000
rows the client knows the height of row heights are arithmetic, not measurement (04 §7.1)
1 window
of cards built per scroll, not forty thousand the server is told the range; the rest are never encoded
Table§07.6

Ten thousand rows, sorted without rebuilding one

The performance check. Sorting sends a permutation of keys, not a new table: the client matches keyed children by key and moves them. Mounting the whole thing and sorting it are both measured in the repository's budget suite, and the numbers on the front page come from that run.

The table: a header row and ten thousand virtualised rows of reference, client, status and amount.
examples/counter-app · app/controllers/live_controller.sl router_eui("table", …)
50 014
nodes in the session, five per row only the rows in view are measured or painted
< 2 ms
of CPU per scrolled frame, the budget this holds to spec/10 §2 — measured by cargo xtask bench
Todo§07.7

The list everyone writes first

Keyed rows, checkboxes that carry the id of what they toggle, and a text field whose value belongs to the client: what a person has typed survives every re-render the application does for other reasons, because the caret, the selection and the clipboard never leave the window (03 §3).

The todo list: an avatar and title, a text field with an add button, and four rows with checkboxes.
examples/counter-app · app/controllers/live_controller.sl router_eui("todo", …)
Counter§07.8

Nine nodes, and the whole protocol

A title, a number, two buttons and a hint. It is here because it is the smallest thing that exercises every part of the round trip — and because one of its buttons never asks the server at all: a declared local handler updates the number in the window and reports afterwards. The other is 25 bytes out and 9 bytes back.

The counter: a title, the number 0, a minus and a plus button, and a line of help text.
examples/counter-app · app/controllers/live_controller.sl router_eui("counter", …)
25 B
a click, on the wire and 9 bytes for the answer that changes the number
0 B
for the button that runs locally a declared handler, not code from the network (07 §1)
Yours§07.9

What it takes to add the ninth

One line in config/routes.sl, a handler, a view. No client release, no widget registration, no build step: the client already knows the sixteen kinds, and everything above composes them.

config/routes.sl
router_eui("tracker", "tracker#tracker", "tracker#tracker_view")

The handler is (event, params, state) -> state. The view is state -> hash. Everything else — diffing the tree, encoding the patch, resolving the styles, fetching the assets — belongs to the two ends and not to you.