Build a GUI app
A step-by-step tutorial: build an interactive terminal app with serez-ui— a React-style UI library with components, state, and a render loop. We'll build a keyboard-driven task tracker.
Window / Component model, reusable components with props, rendering lists with .map(), handling input with onKey, and driving the screen with EventLoop.Step 1 — Set up
mkdir task-tracker
cd task-tracker
sz init --y
sz install serez-uiserez-ui uses a React-like model: components extend Window and return a tree from render(). You can write that tree as JSX in a .szx file, which the bundled translator turns into .sz.
Step 2 — A reusable row component
serez-ui has two base classes: Window is the root interface (your screen), and Component is a reusable piece you can drop in anywhere. A component receives props (data passed to it) and children (content between its tags) — just like React.
Create app.szx and define a row component. It reads its data from this.props with plain dot access:
import "serez-ui"
// A reusable row. Renders one task line based on its props.
class TaskRow:Component {
public TaskRow() { super() }
public render() {
let mark = "[ ] "
if (this.props.done) { mark = "[x] " }
let pointer = " "
if (this.props.selected) { pointer = "> " }
return (
<li>{pointer + mark + this.props.text}</li>
)
}
}this.props.textwith a dot — serez-ui's translator turns it into the dict access serez-code needs, so you never see the plumbing.Step 3 — Render the list with .map()
Now the screen. TaskTracker holds the state and renders one <TaskRow> per task with .map() — no manual loops, no h(...)calls. The list is just an expression in the JSX:
class TaskTracker:Window {
public TaskTracker() {
super()
this.tasks = ["buy milk", "write docs", "ship release"]
this.done = [false, false, false]
this.cursor = 0
}
public render() {
// capture state in locals so the map callback can see it
let tasks = this.tasks
let done = this.done
let cursor = this.cursor
return (
<div>
<h1>Task Tracker</h1>
<p>j/k move · space toggle · q quit</p>
<hr />
<ul>
{
tasks.map(fn (t, i) {
return (
<TaskRow text={t} done={done[i]} selected={i == cursor} />
)
})
}
</ul>
</div>
)
}
}That's the whole list — one .map() returning a component per item. Adding a column or changing the look means editing TaskRow, nothing else.
Step 4 — Handle the keyboard
EventLoop calls onKey on every keypress with an event whose code is the key. Move the cursor and toggle completion:
public void onKey(any evt) {
let code = evt.code
if (code == "j") {
if (this.cursor < this.tasks.length() - 1) { this.cursor = this.cursor + 1 }
}
if (code == "k") {
if (this.cursor > 0) { this.cursor = this.cursor - 1 }
}
if (code == " ") {
this.done[this.cursor] = !this.done[this.cursor]
}
}q exits by default — the EventLoop handles it for you. Override quitKey on the loop to change it.Step 5 — Start the loop
At the bottom of the file, mount the component and start rendering:
let app = new TaskTracker()
let loop = new EventLoop(app)
loop.start() // blocks, renders, dispatches keys until you press qRun it
.szxfiles are JSX — translate & run them with the bundled wrapper:
# Windows
.\packages\serez-ui\tools\szx.ps1 app.szx
# macOS / Linux
./packages/serez-ui/tools/szx.sh app.szxPrefer plain .sz? Write the tree with h(tag, props, children) instead of JSX and run it with sz run dev — no translation step.
Next steps
- Use
Inputfor text fields anduseState/useEffecthooks. - See the serez-ui reference.
- Ship it: turn this into a double-click
.exeanyone can run with the packaging tutorial.