thebanri/limoni

A high-performance, immediate-mode TUI framework for Go. Built with zero-allocation double-buffering, rich canvas widgets, and native concurrency.

Go

51

201 commits

updated Sep 20, 2026

See the code
cli
go
golang
terminal
terminal-ui
tui
tui-framework

See what people are saying (2)

SourceMessageScoreDate

A rotating, searchable globe in the terminal — and an AI agent driving it from the next pane (r/SideProject)

go install [github.com/thebanri/limoni/apps/globe@latest](http://github.com/thebanri/limoni/apps/globe@latest) globe -at Türkiye / searches, arrows turn it, + and - zoom, b toggles country borders, click pins a point. The land is Natural Earth 1:110m; it's drawn in half-cells so the sphere is round…

2

Sep 21, 2026

A rotating, searchable globe in the terminal — and an mcp driving it from the next pane (r/commandline)

go install [github.com/thebanri/limoni/apps/globe@latest](http://github.com/thebanri/limoni/apps/globe@latest) globe -at Türkiye / searches, arrows turn it, + and - zoom, b toggles country borders, click pins a point. The land is Natural Earth 1:110m; it's drawn in half-cells so the sphere is round…

8

Sep 21, 2026

README

Limoni Logo

🍋 Limoni

A terminal UI engine for Go that tests can click, AI agents can drive,
and the garbage collector never sees.

Build Status Go Reference Go Version License Zero Allocations

EnglishTürkçe

▶ Try it in your browser — the same engine compiled to WebAssembly, no install.

Limoni rendering a shaded 3D model in the terminal


Quick start

go get github.com/thebanri/limoni
package main

import "github.com/thebanri/limoni"

func main() {
	limoni.Run(func(f *limoni.Frame, ev *limoni.Event) bool {
		if ev != nil && ev.Type == limoni.EventKey && ev.Key.Type == limoni.KeyEsc {
			return false // quit
		}
		f.RenderComponent(limoni.Border(
			limoni.Center(limoni.Label("Hello from Limoni 🍋  (Esc quits)", limoni.Bold())),
			limoni.SymbolsRounded,
			limoni.Fg(limoni.Hex("#FFCC00")),
		), f.Area())
		return true
	})
}

Or generate a project that runs straight away, with a test already written:

go run github.com/thebanri/limoni/cmd/limoni@latest new myapp    # -template counter|dashboard|form|ssh
cd myapp && go mod tidy && go run .
go test ./...                                                     # a uitest test comes with every template

Next: Getting started · Widget gallery · Examples


Why Limoni

1. Your TUI has a semantic tree, so tests and agents address widgets by name

Most terminal automation, such as termwright or mcp-tui-test, parses the rendered character grid, so a test breaks when the layout moves by one column. A Limoni app builds a semantic tree every frame (the same tree a screen reader uses), and the tools below work on that tree instead.

uitest: Playwright-style tests. Checks wait instead of sleeping, and a failure prints the whole tree of the last frame.

page := uitest.Run(t, 80, 24, app.draw)            // in process: no terminal needed

page.GetByRole("input", "New task").Type("Tag v1.0")
page.GetByRole("button", "Add task").Click()
page.Expect(page.GetByRole("list-item", "").Within(page.GetByRole("list", "Tasks"))).ToHaveCount(3)
page.Expect(page.GetByID("status")).ToContainLabel("Added")

limoni-mcp: let an AI agent drive the app. It gives Claude Code, Cursor or any MCP client eight tools (tree, click, type_text, wait_for…) that work on the same tree. In one recorded run, Claude Code completed a release checklist in 17 tool calls. That run included typing a deploy token the agent could not read back.

go run -tags limoni_debug ./examples/agent_checklist
claude mcp add limoni -- limoni-mcp -socket "$XDG_RUNTIME_DIR/limoni-checklist.sock"

The automation socket only exists in -tags limoni_debug builds. It is closed by default, only the same user can connect, and secret fields are never exposed. → Semantic automation, MCP and uitest

2. Fast where you can feel it

  • Zero heap allocations per frame for the renderer, every widget's drawing, and the click handling of the common input widgets, enforced in CI, so animations don't stutter from GC pauses. Widgets with drag or custom handlers (Table, Slider, Dialog, …) still allocate a closure each. The details.
  • Few bytes per frame. Blank runs become ECH/EL and repeats become REP: a full-screen redraw is 377 bytes, and an idle app sends nothing. Bytes, not CPU, are what you feel over SSH.
  • Virtual tables and lists. One million rows scroll at ~2.7 ms a frame, because only visible rows are touched.
Measured on a Ryzen 5 5600, Go 1.27.1LatencyAllocations
Full-screen diff, 120×40, every cell changed~50 µs0
10% of the screen changed~24 µs0
Nothing changed~2 ns0
100 layered blocks, drawn and diffed~68 µs0

Absolute numbers depend on the machine. The comparisons against Ratatui 0.30.2, Ultraviolet and Bubble Tea v1.3.10, including which ratios are not meaningful, are in docs/benchmarks.md and the methodology. One earlier run showed a 4,700× lead; it came from a bug in the benchmark harness, and the methodology explains how it was caught.

3. Batteries that other TUI libraries leave to you

🕶️ 3DA software rasteriser for OBJ/STL/PLY/GLB with Lambert and Gouraud shading, drawn in terminal cells
🖼️ ImagesKitty, Sixel, iTerm2 and half-block fallback
📊 ChartsBraille line charts, bar charts, pie charts, sparklines
📝 MarkdownGFM rendering with a scrollable reader
AccessibilityA semantic tree, a screen-reader line mode, NO_COLOR, high contrast, reduced motion
🧬 UnicodeUAX #29 grapheme clusters (Unicode 17.0, all 766 conformance tests), so flags and emoji families take one cell
🔗 HyperlinksOSC 8 links, in markdown or any style — and where the terminal cannot show them, the address is printed instead
📃 Inline modeRender in a band of the normal screen, like gum, with scrollback intact
⏺️ Session replayRecord a session, replay it as a regression test (docs)
🌐 EverywhereLinux, macOS, BSD, Windows, WebAssembly in the browser, SSH sessions

The only dependencies are golang.org/x/sys and golang.org/x/crypto.

TreeView with image previewCharts
go run ./examples/treeviewgo run ./examples/charts

Built with Limoni: zest

zest filtering a million-line log to billing errors, opening a line's details, then clearing the filter to show it in context

zest is a log viewer and Limoni's flagship app. It follows files and pipes, colours by level, and filters a million lines without stalling. A 67 MiB, 1,000,000-line log is on screen in about half a second.

go install github.com/thebanri/limoni/cmd/zest@latest
zest -demo 1000000        # or: zest app.log, kubectl logs -f pod | zest

Or try it in the browser: the "Logs · zest" scene.

And: globe

globe is a world you can turn, search, zoom into and pin — and what a Limoni application looks like from the outside, since it is a module of its own that depends on a published Limoni and uses nothing but its public API.

go install github.com/thebanri/limoni/apps/globe@latest
globe -at Türkiye

Everything on it is in the semantic tree, so "find Turkey on the world map" is something an agent can do over MCP: type into search, click the row, read image#globe back as value="39.3°N 34.5°E · zoom 2.6×". Nothing of it reaches you when you import Limoni — a directory with its own go.mod is not part of the module around it.


Two ways to write an app

Immediate modeDeclarative (Elm architecture)
Entry pointlimoni.Run(func(f, ev) bool)limoni.RunProgram(ctx, model)
State lives inyour closurea limoni.Model with Init / Update / View
Best fordashboards, 3D, games, animationforms, wizards, CRUD tools, async work
Runtime gives youa redraw on every eventcommands, cancellation, deterministic ordering, panic recovery, session recording

Both use the same renderer and widgets and are available from the root package. examples/counter is a complete declarative app in under 80 lines. Coming from Bubble Tea? See the migration guide.


Widgets

CategoryWidgets
LayoutVStack / HStack / ZStack, Flex, Border, grid layout (layout.GridLayout), Block (with border merging), Viewport, Dialog, Popup
DataTable (virtual), List (virtual), TreeView, Sparkline, ProgressBar, RichText
ChartsLineChart (Braille), BarChart, PieChart
InputTextInput, TextArea, Checkbox, RadioGroup, Select, Slider, ColorPicker
NavigationTabs, Scrollbar, CommandPalette, fuzzy search, keybinding manager
FeedbackSpinner, Toast
GraphicsCanvas (Braille / block), 3D meshes, Image
TextMarkdown, Label, Paragraph
ToolingDevTools HUD (F12), themes, validation

Widget gallery · Widget reference


Examples

ExampleWhat it shows
demoThe feature trailer: a 3D lemon in ASCII, Braille and half-blocks
showcaseTabs, forms, matrix rain, 3D, command palette, DevTools (F12)
3d_viewerOBJ/STL/PLY viewer with shading and orbit controls (-fps 240)
dashboardLive CPU and memory sparklines, a process table, streaming logs
table_virtualA one-million-row table
agent_checklistAn app built to be driven by an AI agent, and tested with uitest
todoA declarative todo app with tags, filters and fuzzy search
counterThe smallest declarative app
composableLayout with VStack, HStack, Border, Flex
forms · layer_demo · treeview · chartsInputs, modals, file tree, charts
ssh_server · wasmServing over SSH, running in the browser

Run any of them with go run ./examples/<name>, or without cloning: go run github.com/thebanri/limoni/examples/3d_viewer@latest. All examples: docs/examples.md.


Documentation

Getting startedInstall, first app, both application models
ArchitectureThe flat cell grid, the diff, why the draw path doesn't allocate
Layout · Widgets · Core APIReference
Semantic automationThe automation socket, limoni-mcp, uitest and the security model
Session recordingRecord and replay sessions as regression tests
Graphics · Animation · AccessibilityFeature guides
Drivers and platformsUnix, Windows, WebAssembly, SSH
How it comparesAgainst Bubble Tea v1/v2, Lip Gloss and Ratatui, with the caveats
BenchmarksEvery measured number and how to reproduce it
Rendering FAQHairline gaps, recommended terminals, emoji
Stability · ChangelogWhat may change before 1.0

Turkish documentation: docs/tr.


Status

Limoni is pre-1.0. Patch releases don't break the API; minor releases may, and every break is listed in the changelog. The core renderer, layout and widgets are settling; the automation, uitest and session packages are new and experimental. See docs/stability.md for what has to happen before v1.0.

Community and contributing

AI assistants (Claude, Gemini) were used during development for scaffolding, tests and documentation drafts. The architecture was designed, profiled and benchmarked by the author, and the benchmark harness exists to check claims, from people or tools.

Security policy · Code of Conduct · Apache License 2.0

Contributors

thebanri

191 commits

kekolar24

6 commits

Nishchal-ll

2 commits

team-humaki

1 commits

thebanri/limoni

A high-performance, immediate-mode TUI framework for Go. Built with zero-allocation double-buffering, rich canvas widgets, and native concurrency.

Go

51

201 commits

updated Sep 20, 2026

See the code
cli
go
golang
terminal
terminal-ui
tui
tui-framework

See what people are saying (2)

SourceMessageScoreDate

A rotating, searchable globe in the terminal — and an AI agent driving it from the next pane (r/SideProject)

go install [github.com/thebanri/limoni/apps/globe@latest](http://github.com/thebanri/limoni/apps/globe@latest) globe -at Türkiye / searches, arrows turn it, + and - zoom, b toggles country borders, click pins a point. The land is Natural Earth 1:110m; it's drawn in half-cells so the sphere is round…

2

Sep 21, 2026

A rotating, searchable globe in the terminal — and an mcp driving it from the next pane (r/commandline)

go install [github.com/thebanri/limoni/apps/globe@latest](http://github.com/thebanri/limoni/apps/globe@latest) globe -at Türkiye / searches, arrows turn it, + and - zoom, b toggles country borders, click pins a point. The land is Natural Earth 1:110m; it's drawn in half-cells so the sphere is round…

8

Sep 21, 2026

README

Limoni Logo

🍋 Limoni

A terminal UI engine for Go that tests can click, AI agents can drive,
and the garbage collector never sees.

Build Status Go Reference Go Version License Zero Allocations

EnglishTürkçe

▶ Try it in your browser — the same engine compiled to WebAssembly, no install.

Limoni rendering a shaded 3D model in the terminal


Quick start

go get github.com/thebanri/limoni
package main

import "github.com/thebanri/limoni"

func main() {
	limoni.Run(func(f *limoni.Frame, ev *limoni.Event) bool {
		if ev != nil && ev.Type == limoni.EventKey && ev.Key.Type == limoni.KeyEsc {
			return false // quit
		}
		f.RenderComponent(limoni.Border(
			limoni.Center(limoni.Label("Hello from Limoni 🍋  (Esc quits)", limoni.Bold())),
			limoni.SymbolsRounded,
			limoni.Fg(limoni.Hex("#FFCC00")),
		), f.Area())
		return true
	})
}

Or generate a project that runs straight away, with a test already written:

go run github.com/thebanri/limoni/cmd/limoni@latest new myapp    # -template counter|dashboard|form|ssh
cd myapp && go mod tidy && go run .
go test ./...                                                     # a uitest test comes with every template

Next: Getting started · Widget gallery · Examples


Why Limoni

1. Your TUI has a semantic tree, so tests and agents address widgets by name

Most terminal automation, such as termwright or mcp-tui-test, parses the rendered character grid, so a test breaks when the layout moves by one column. A Limoni app builds a semantic tree every frame (the same tree a screen reader uses), and the tools below work on that tree instead.

uitest: Playwright-style tests. Checks wait instead of sleeping, and a failure prints the whole tree of the last frame.

page := uitest.Run(t, 80, 24, app.draw)            // in process: no terminal needed

page.GetByRole("input", "New task").Type("Tag v1.0")
page.GetByRole("button", "Add task").Click()
page.Expect(page.GetByRole("list-item", "").Within(page.GetByRole("list", "Tasks"))).ToHaveCount(3)
page.Expect(page.GetByID("status")).ToContainLabel("Added")

limoni-mcp: let an AI agent drive the app. It gives Claude Code, Cursor or any MCP client eight tools (tree, click, type_text, wait_for…) that work on the same tree. In one recorded run, Claude Code completed a release checklist in 17 tool calls. That run included typing a deploy token the agent could not read back.

go run -tags limoni_debug ./examples/agent_checklist
claude mcp add limoni -- limoni-mcp -socket "$XDG_RUNTIME_DIR/limoni-checklist.sock"

The automation socket only exists in -tags limoni_debug builds. It is closed by default, only the same user can connect, and secret fields are never exposed. → Semantic automation, MCP and uitest

2. Fast where you can feel it

  • Zero heap allocations per frame for the renderer, every widget's drawing, and the click handling of the common input widgets, enforced in CI, so animations don't stutter from GC pauses. Widgets with drag or custom handlers (Table, Slider, Dialog, …) still allocate a closure each. The details.
  • Few bytes per frame. Blank runs become ECH/EL and repeats become REP: a full-screen redraw is 377 bytes, and an idle app sends nothing. Bytes, not CPU, are what you feel over SSH.
  • Virtual tables and lists. One million rows scroll at ~2.7 ms a frame, because only visible rows are touched.
Measured on a Ryzen 5 5600, Go 1.27.1LatencyAllocations
Full-screen diff, 120×40, every cell changed~50 µs0
10% of the screen changed~24 µs0
Nothing changed~2 ns0
100 layered blocks, drawn and diffed~68 µs0

Absolute numbers depend on the machine. The comparisons against Ratatui 0.30.2, Ultraviolet and Bubble Tea v1.3.10, including which ratios are not meaningful, are in docs/benchmarks.md and the methodology. One earlier run showed a 4,700× lead; it came from a bug in the benchmark harness, and the methodology explains how it was caught.

3. Batteries that other TUI libraries leave to you

🕶️ 3DA software rasteriser for OBJ/STL/PLY/GLB with Lambert and Gouraud shading, drawn in terminal cells
🖼️ ImagesKitty, Sixel, iTerm2 and half-block fallback
📊 ChartsBraille line charts, bar charts, pie charts, sparklines
📝 MarkdownGFM rendering with a scrollable reader
AccessibilityA semantic tree, a screen-reader line mode, NO_COLOR, high contrast, reduced motion
🧬 UnicodeUAX #29 grapheme clusters (Unicode 17.0, all 766 conformance tests), so flags and emoji families take one cell
🔗 HyperlinksOSC 8 links, in markdown or any style — and where the terminal cannot show them, the address is printed instead
📃 Inline modeRender in a band of the normal screen, like gum, with scrollback intact
⏺️ Session replayRecord a session, replay it as a regression test (docs)
🌐 EverywhereLinux, macOS, BSD, Windows, WebAssembly in the browser, SSH sessions

The only dependencies are golang.org/x/sys and golang.org/x/crypto.

TreeView with image previewCharts
go run ./examples/treeviewgo run ./examples/charts

Built with Limoni: zest

zest filtering a million-line log to billing errors, opening a line's details, then clearing the filter to show it in context

zest is a log viewer and Limoni's flagship app. It follows files and pipes, colours by level, and filters a million lines without stalling. A 67 MiB, 1,000,000-line log is on screen in about half a second.

go install github.com/thebanri/limoni/cmd/zest@latest
zest -demo 1000000        # or: zest app.log, kubectl logs -f pod | zest

Or try it in the browser: the "Logs · zest" scene.

And: globe

globe is a world you can turn, search, zoom into and pin — and what a Limoni application looks like from the outside, since it is a module of its own that depends on a published Limoni and uses nothing but its public API.

go install github.com/thebanri/limoni/apps/globe@latest
globe -at Türkiye

Everything on it is in the semantic tree, so "find Turkey on the world map" is something an agent can do over MCP: type into search, click the row, read image#globe back as value="39.3°N 34.5°E · zoom 2.6×". Nothing of it reaches you when you import Limoni — a directory with its own go.mod is not part of the module around it.


Two ways to write an app

Immediate modeDeclarative (Elm architecture)
Entry pointlimoni.Run(func(f, ev) bool)limoni.RunProgram(ctx, model)
State lives inyour closurea limoni.Model with Init / Update / View
Best fordashboards, 3D, games, animationforms, wizards, CRUD tools, async work
Runtime gives youa redraw on every eventcommands, cancellation, deterministic ordering, panic recovery, session recording

Both use the same renderer and widgets and are available from the root package. examples/counter is a complete declarative app in under 80 lines. Coming from Bubble Tea? See the migration guide.


Widgets

CategoryWidgets
LayoutVStack / HStack / ZStack, Flex, Border, grid layout (layout.GridLayout), Block (with border merging), Viewport, Dialog, Popup
DataTable (virtual), List (virtual), TreeView, Sparkline, ProgressBar, RichText
ChartsLineChart (Braille), BarChart, PieChart
InputTextInput, TextArea, Checkbox, RadioGroup, Select, Slider, ColorPicker
NavigationTabs, Scrollbar, CommandPalette, fuzzy search, keybinding manager
FeedbackSpinner, Toast
GraphicsCanvas (Braille / block), 3D meshes, Image
TextMarkdown, Label, Paragraph
ToolingDevTools HUD (F12), themes, validation

Widget gallery · Widget reference


Examples

ExampleWhat it shows
demoThe feature trailer: a 3D lemon in ASCII, Braille and half-blocks
showcaseTabs, forms, matrix rain, 3D, command palette, DevTools (F12)
3d_viewerOBJ/STL/PLY viewer with shading and orbit controls (-fps 240)
dashboardLive CPU and memory sparklines, a process table, streaming logs
table_virtualA one-million-row table
agent_checklistAn app built to be driven by an AI agent, and tested with uitest
todoA declarative todo app with tags, filters and fuzzy search
counterThe smallest declarative app
composableLayout with VStack, HStack, Border, Flex
forms · layer_demo · treeview · chartsInputs, modals, file tree, charts
ssh_server · wasmServing over SSH, running in the browser

Run any of them with go run ./examples/<name>, or without cloning: go run github.com/thebanri/limoni/examples/3d_viewer@latest. All examples: docs/examples.md.


Documentation

Getting startedInstall, first app, both application models
ArchitectureThe flat cell grid, the diff, why the draw path doesn't allocate
Layout · Widgets · Core APIReference
Semantic automationThe automation socket, limoni-mcp, uitest and the security model
Session recordingRecord and replay sessions as regression tests
Graphics · Animation · AccessibilityFeature guides
Drivers and platformsUnix, Windows, WebAssembly, SSH
How it comparesAgainst Bubble Tea v1/v2, Lip Gloss and Ratatui, with the caveats
BenchmarksEvery measured number and how to reproduce it
Rendering FAQHairline gaps, recommended terminals, emoji
Stability · ChangelogWhat may change before 1.0

Turkish documentation: docs/tr.


Status

Limoni is pre-1.0. Patch releases don't break the API; minor releases may, and every break is listed in the changelog. The core renderer, layout and widgets are settling; the automation, uitest and session packages are new and experimental. See docs/stability.md for what has to happen before v1.0.

Community and contributing

AI assistants (Claude, Gemini) were used during development for scaffolding, tests and documentation drafts. The architecture was designed, profiled and benchmarked by the author, and the benchmark harness exists to check claims, from people or tools.

Security policy · Code of Conduct · Apache License 2.0

Contributors

thebanri

191 commits

kekolar24

6 commits

Nishchal-ll

2 commits

team-humaki

1 commits

Languages

Go

97.5%

Rust

1.1%