ncurses TUI bindings for Spinel, an ahead-of-time Ruby compiler.
Uses Spinel's built-in FFI DSL (ffi_lib, ffi_func, ffi_const) — no external gems required.
- Spinel (
spin) - ncurses, statically linked from source (see
spin.toml's[[build]]/[native]sections) — nobrew installneeded at build or run time, the compiled binaries have no ncurses dylib dependency
The first build clones a pinned ncurses tag from ThomasDickey/ncurses-snapshots (the upstream maintainer's own GitHub mirror) and compiles it, then compiles a small C shim (mouse support) against it. Spin will prompt you to allow this native build step — answer always to permanently trust it, or run spin trust ncurses beforehand to skip the prompt entirely. Requires network access on first build; the result is cached afterward.
Create a new project and add ncurses as a dependency:
mkdir myapp && cd myapp
spin init
spin add ncurses --git https://github.com/jockofcode/ncursesPut your program in bin/hello.rb:
require "ncurses"
Ncurses::Screen.open do |screen|
screen.root.write_centered(screen.rows / 2, "Hello, world!")
screen.root.refresh
Ncurses::Input.get_char(screen.root)
endBuild and run:
spin run hello # build + run in one stepOr build first and run the binary directly:
spin build
./build/bin/helloThe entry point. Always use the block form — it handles initscr/endwin and window cleanup.
Ncurses::Screen.open do |screen|
screen.rows # terminal height
screen.cols # terminal width
screen.root # the root Window (stdscr)
screen.sleep(ms) # non-blocking millisecond sleep (napms)
endWraps an ncurses WINDOW*. The root window is screen.root; subwindows are created with subwindow.
# Creating subwindows — block form, window is a typed parameter
screen.root.subwindow(rows: h, cols: w, top: r, left: c) do |win|
win.write_at(row, col, str) # move cursor and write
win.write_centered(row, str) # horizontally centered write
win.fill(char) # fill all cells with char
win.dimensions # => [rows, cols]
win.clear
win.refresh
win.border
endAttributes are applied for the duration of the block, then restored.
win.bold { win.write_at(r, c, "text") }
win.underline { win.write_at(r, c, "text") }
win.reverse { win.write_at(r, c, "text") }
win.standout { win.write_at(r, c, "text") }
win.dim { win.write_at(r, c, "text") }
win.blink { win.write_at(r, c, "text") }
win.italic { win.write_at(r, c, "text") } # ncurses 6+ onlyNcurses::Color.start
pair = Ncurses::Color.pair(fg: Ncurses::Color::RED, bg: Ncurses::Color::BLACK)
win.color(pair) { win.write_at(r, c, "text") }win.scrollable(true) # enable ncurses scroll mode
win.scroll(1) # scroll content up one line
win.scroll(-1) # scroll content down one lineNcurses::Color.start # call once after initscr
pair = Ncurses::Color.pair(fg: FG, bg: BG) # register a color pair, returns int idColor constants: BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE
# Blocking — waits for a keypress, returns key code
Ncurses::Input.get_char(window)
# Non-blocking — returns -1 if no key waiting, key code otherwise
Ncurses::Input.nodelay(window, true)
Ncurses::Input.get_char_nowait(window)
# Buffered string input with backspace support
Ncurses::Input.get_string(window, max_length: 80)Both get_char and get_char_nowait return LibNCurses::KEY_RESIZE when the terminal is resized. Re-read screen.rows / screen.cols and redraw on that code.
Key constants: KEY_UP KEY_DOWN KEY_LEFT KEY_RIGHT KEY_BACKSPACE KEY_RESIZE KEY_MOUSE
Mouse support is implemented via a C shim (ncurses/mouse_shim.c) that wraps getmouse() and exposes the MEVENT fields, since Spinel's FFI DSL has no struct type.
Ncurses::Mouse.enable # call once after Screen.open — enables all events + position reporting
# In the input loop:
ch = Ncurses::Input.get_char(window)
if ch == LibNCurses::KEY_MOUSE && Ncurses::Mouse.read
x = Ncurses::Mouse.x # column
y = Ncurses::Mouse.y # row
Ncurses::Mouse.button1_clicked? # left click
Ncurses::Mouse.button1_pressed? # left button down
Ncurses::Mouse.button1_released? # left button up
Ncurses::Mouse.button2_clicked? # middle click
Ncurses::Mouse.button3_clicked? # right click
Ncurses::Mouse.button3_pressed? # right button down
Ncurses::Mouse.bstate # raw mmask_t for custom checks
endOptional file logger, useful when stdout is taken by ncurses.
Ncurses::Log.open("/tmp/ncurses.log") # call before Screen.open
Ncurses::Log.write("message")
Ncurses::Log.close # called automatically by Screen.open| Constant | Value |
|---|---|
A_STANDOUT |
0x010000 |
A_UNDERLINE |
0x020000 |
A_REVERSE |
0x040000 |
A_BLINK |
0x080000 |
A_DIM |
0x100000 |
A_BOLD |
0x200000 |
A_ITALIC |
0x80000000 |
| Program | Description |
|---|---|
bin/demo.rb |
Bouncing * animation (DVD screensaver style) |
bin/layout.rb |
Multi-pane layout with header, sidebar, and status bar |
bin/input_demo.rb |
Buffered text input demonstration |
bin/scroll.rb |
Scrollable list with arrow key navigation |
bin/life.rb |
Conway's Game of Life |
bin/snake.rb |
Snake game |
bin/mouse_demo.rb |
Mouse click and movement tracking |
Build all examples:
spin buildRun an example:
spin run snake
spin run lifeOr after building:
./build/bin/snake
./build/bin/lifespin testSnapshot tests print to stdout and diff against .expected files in test/.