A cell grid rendering library for Odin. Write roguelike and TUI rendering code once — run it in a terminal or a graphical window with no code changes.
ocurses handles the screen: cell grids, layers, colors, input events. Your game handles everything else.
This project started as a proof of concept. NoteYe is a graphical frontend for terminal roguelikes — it intercepts terminal output and replaces characters with tiles, which is how ADOM and others got a graphical mode. The architecture works, but the interception layer introduces lag that is hard to tune away.
The question ocurses answers is: what if the graphical layer was not a middleware shim, but a first-class rendering backend — one the game targets directly, the same way it targets the terminal?
With ocurses, a game written for terminal output can switch to a pixel-perfect GPU-rendered graphical window by changing one line at init. No tile replacement, no escape code interception, no middleware. The same cell grid API produces either a raw ANSI terminal or a Bezier-rendered SDL3 window with batched GPU draw calls. The performance ceiling is the GPU, not a parsing loop.
The practical use case: a terminal roguelike that wants an optional graphical mode — the kind of thing that would make a compact, coffee-break game like TGGW viable with tiles for players who want them, without forking the rendering code.
- Layer compositing — named cell grids with z-ordering and transparency (null rune = see-through)
- Unified input — keyboard, mouse, resize, and quit events through one event queue
- Color palettes — named colors with runtime palette swapping (default, gruvbox, dracula, or define your own)
- Signal handling — SIGWINCH (resize), SIGINT (Ctrl+C), SIGTSTP/SIGCONT (Ctrl+Z suspend/resume)
- Pluggable backends — terminal mode works out of the box; SDL3 graphical backend with GPU Bezier text rendering via odin-slug
- Pixel-perfect text — SDL3 backend renders glyphs from actual Bezier curves on the GPU, crisp at any size or DPI
- Live backend swapping — switch from terminal to graphical window at runtime, layers and state intact
As a git submodule (recommended):
git submodule add https://github.com/Naughtyusername/ocurses ocursesManual clone:
git clone https://github.com/Naughtyusername/ocursesCopy or clone the ocurses/ directory into your project so that import oc "ocurses" resolves.
No external dependencies are required for terminal mode.
package main
import oc "ocurses"
main :: proc() {
ctx := oc.init()
defer oc.destroy(&ctx)
layer := oc.create_layer(&ctx, "main", 0, 0, 40, 20)
for !oc.should_quit(&ctx) {
oc.layer_clear(layer)
oc.layer_write(layer, 1, 1, "Hello, roguelike!", oc.COLOR_WHITE, oc.COLOR_BLACK)
oc.blit(&ctx)
event := oc.wait_event(&ctx)
switch ev in event {
case oc.Key_Event:
if ev.key == .Q || ev.key == .Escape do oc.quit(&ctx)
case oc.Quit_Event:
oc.quit(&ctx)
case oc.Resize_Event, oc.Mouse_Event:
// handle as needed
}
}
}| Proc | Description |
|---|---|
init(Config) -> Context |
Initialize ocurses (default: terminal backend) |
destroy(^Context) |
Shut down, restore terminal state |
blit(^Context) |
Composite all layers and render to screen |
quit(^Context) |
Signal the application to quit |
should_quit(^Context) -> bool |
Check if quit has been signaled |
get_size(^Context) -> (w, h) |
Get current terminal grid dimensions |
| Proc | Description |
|---|---|
poll_event(^Context) -> (Event, bool) |
Non-blocking: return next event if available |
wait_event(^Context) -> Event |
Blocking: wait until an event arrives |
Event types: Key_Event, Mouse_Event, Resize_Event, Quit_Event
| Proc | Description |
|---|---|
create_layer(^Context, name, x, y, w, h, z_order) -> ^Layer |
Create a new layer |
destroy_layer(^Context, ^Layer) |
Remove and free a layer |
layer_set_cell(^Layer, x, y, rune, fg, bg) |
Set a single cell |
layer_get_cell(^Layer, x, y) -> Cell |
Read a cell |
layer_write(^Layer, x, y, string, fg, bg) |
Write a string starting at position |
layer_clear(^Layer) |
Clear all cells (fill with null rune) |
layer_set_visible(^Layer, bool) |
Show/hide a layer |
layer_set_z(^Layer, int) |
Change z-order |
Null rune (0) = transparent. Higher z-order layers render on top.
| Proc | Description |
|---|---|
palette_set(^Context, Palette) |
Set the active color palette |
resolve_color(^Context, Color) -> Any_Color |
Resolve named or RGB color |
default_palette() -> Palette |
Standard 8-color palette |
gruvbox_palette() -> Palette |
Gruvbox dark palette |
dracula_palette() -> Palette |
Dracula theme palette |
Named color constants: COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN, COLOR_WHITE
| Proc | Description |
|---|---|
draw_box(^Layer, x, y, w, h, fg, bg, style) |
Draw a bordered rectangle (Single/Double/Rounded) |
draw_box_filled(^Layer, x, y, w, h, fg, bg, style) |
Bordered rectangle with space-filled interior |
write_centered(^Layer, y, string, fg, bg) |
Write a string centered on a layer row |
write_wrapped(^Layer, x, y, w, string, fg, bg) -> uint |
Word-wrap text into bounded width, returns lines written |
draw_bar(^Layer, x, y, w, current, max, fg, bg) |
Horizontal progress/health bar |
writef(^Layer, x, y, fg, bg, format, ..args) |
Printf-style formatted text |
Box styles: .Single (┌─┐│), .Double (╔═╗║), .Rounded (╭─╮│)
A panel wraps a layer with a border and optional title.
| Proc | Description |
|---|---|
create_panel(^Context, name, x, y, w, h, z, title, style, fg, bg) -> Panel |
Create a bordered layer with title |
panel_draw_border(^Panel) |
Redraw border and title (call after clearing) |
panel_clear_content(^Panel) |
Clear interior only, preserving the border |
panel_write(^Panel, x, y, string, fg, bg) |
Write at content-relative coordinates |
destroy_panel(^Context, ^Panel) |
Destroy the underlying layer |
Content coordinates are relative to the interior — panel_write(p, 0, 0, ...) writes to the first cell inside the border.
A ring buffer of lines with a viewport. Tracks content and renders the visible slice.
| Proc | Description |
|---|---|
scroll_init(view_w, view_h) -> Scroll_Buffer |
Create a scroll buffer for a viewport size |
scroll_append(^Scroll_Buffer, text, fg, bg) |
Add a line (auto-scrolls to bottom) |
scroll_up(^Scroll_Buffer, n) |
Scroll toward older messages |
scroll_down(^Scroll_Buffer, n) |
Scroll toward newer messages |
scroll_to_top(^Scroll_Buffer) |
Jump to oldest message |
scroll_to_bottom(^Scroll_Buffer) |
Jump to newest message |
scroll_render(^Scroll_Buffer, ^Layer, x, y) |
Render visible lines into a layer |
scroll_count(^Scroll_Buffer) -> uint |
Get total line count |
Newest messages appear at the bottom. Max capacity: 512 lines (ring buffer, oldest overwritten).
Render the same cell grid in a graphical SDL3 window instead of the terminal. Same API, same layers, same input — different output surface.
Text rendering uses odin-slug — GPU Bezier curve font rendering via SDL3's GPU API. Glyphs are evaluated per-pixel in the fragment shader from actual curve data, so text stays crisp at any size or DPI. No texture atlases, no bitmap scaling artifacts.
sdl3system package (for windowing and GPU API)- odin-slug cloned as a sibling directory (or anywhere, referenced via
-collection:libs=) - A monospace TTF font (Liberation Mono bundled in
fonts/)
# Arch Linux
sudo pacman -S sdl3
# Clone odin-slug next to ocurses
git clone https://github.com/Naughtyusername/odin-slug ../odin-slug
# Build shaders (requires glslc from shaderc/vulkan-devel)
cd ../odin-slug && ./build.sh shaders && cd ../ocursesimport oc "ocurses"
import sdl3 "ocurses/sdl3_backend"
main :: proc() {
ctx := oc.init({
backend = .Custom,
custom_vtable = sdl3.VTABLE,
})
defer oc.destroy(&ctx)
// Everything else is identical to terminal mode
layer := oc.create_layer(&ctx, "main", 0, 0, 40, 20)
// ...
}Build with the collection flag pointing to odin-slug:
odin build examples/demo_sdl3 -out:demo_sdl3 -collection:libs=../odin-slugOverride the font path or size with -define:
odin build examples/demo_sdl3 -out:demo_sdl3 \
-collection:libs=../odin-slug \
-define:FONT_PATH=/usr/share/fonts/noto/NotoSansMono-Regular.ttf \
-define:FONT_SIZE=20| Config | Default | Description |
|---|---|---|
FONT_PATH |
/usr/share/fonts/liberation/LiberationMono-Regular.ttf |
Path to a monospace TTF font |
FONT_SIZE |
16 |
Font point size |
- GPU Bezier rendering: Text is rendered by odin-slug's fragment shader evaluating quadratic Bezier curves per-pixel. No rasterization, no texture atlases — glyphs are mathematically crisp at every size.
- SDL3 GPU API: The backend uses SDL3's low-level GPU abstraction (Vulkan/D3D12/Metal underneath, selected automatically by SDL3) rather than the simpler SDL_Renderer 2D API.
- Batched draw calls: All cell backgrounds are batched into a single rect draw call, and all glyphs into a single text draw call. A full 80x24 grid renders in 2 GPU draw calls total.
- Cell dimensions computed from font metrics (
mono_widthandline_height) - Window opens at
80x24cells, resizable — grid recalculates on resize - Input:
SDL_PollEvent/SDL_WaitEventtranslated to oterm'sKeyboard_Input/Mouse_Input - Window close (
SDL_QUIT) maps to Ctrl+C, triggering the existing quit flow
| Example | Description | Build |
|---|---|---|
demo |
Walkable @ with palette cycling and mouse teleport |
odin build examples/demo -out:demo |
panels |
Multi-panel roguelike layout (map + sidebar + log) | odin build examples/panels -out:panels |
scroll |
Scrolling message log with keyboard navigation | odin build examples/scroll -out:scroll |
popup |
Modal dialog overlay with layer visibility toggling | odin build examples/popup -out:popup |
demo_sdl3 |
Same as demo but in a graphical SDL3 window |
odin build examples/demo_sdl3 -out:demo_sdl3 -collection:libs=../odin-slug |
demo_swap |
Live backend swap from terminal to SDL3 at runtime | odin build examples/demo_swap -out:demo_swap -collection:libs=../odin-slug |
demo_tiles |
Tile rendering with a spritesheet in the SDL3 backend | odin build examples/demo_tiles -out:demo_tiles -collection:libs=../odin-slug |
mini_rogue |
Small playable roguelike — dungeon, combat, items | odin build examples/mini_rogue -out:mini_rogue |
# Terminal examples (no external dependencies)
odin build examples/demo -out:demo
odin build examples/panels -out:panels
odin build examples/scroll -out:scroll
odin build examples/popup -out:popup
odin build examples/mini_rogue -out:mini_rogue
# SDL3 graphical examples (requires sdl3 + odin-slug)
odin build examples/demo_sdl3 -out:demo_sdl3 -collection:libs=../odin-slug
odin build examples/demo_swap -out:demo_swap -collection:libs=../odin-slug
odin build examples/demo_tiles -out:demo_tiles -collection:libs=../odin-slug
# Run
./demo
./mini_rogue
./demo_sdl3| Platform | Terminal | SDL3 (graphical) |
|---|---|---|
| Linux | Yes | Yes |
| macOS | Yes | Untested |
| Windows | Yes | Untested |
Terminal backend design inspired by TermCL by Raph (BSD 3-Clause).
BSD 3-Clause. See LICENSE.