Skip to content

Naughtyusername/ocurses

Repository files navigation

ocurses

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.

Background

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.

Features

  • 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

Install

As a git submodule (recommended):

git submodule add https://github.com/Naughtyusername/ocurses ocurses

Manual clone:

git clone https://github.com/Naughtyusername/ocurses

Copy or clone the ocurses/ directory into your project so that import oc "ocurses" resolves.

No external dependencies are required for terminal mode.

Quick Start

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
        }
    }
}

API Overview

Lifecycle

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

Input

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

Layers

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.

Colors

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

Drawing Utilities

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 (╭─╮│)

Panels

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.

Scroll Buffer

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).

SDL3 Backend

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.

Requirements

  • sdl3 system 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 ../ocurses

Usage

import 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-slug

Build-time Configuration

Override 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

How It Works

  • 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_width and line_height)
  • Window opens at 80x24 cells, resizable — grid recalculates on resize
  • Input: SDL_PollEvent / SDL_WaitEvent translated to oterm's Keyboard_Input / Mouse_Input
  • Window close (SDL_QUIT) maps to Ctrl+C, triggering the existing quit flow

Examples

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

Build

# 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 Support

Platform Terminal SDL3 (graphical)
Linux Yes Yes
macOS Yes Untested
Windows Yes Untested

Credits

Terminal backend design inspired by TermCL by Raph (BSD 3-Clause).

License

BSD 3-Clause. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors