Philpax icon

Philpax

Notes · Other Peoples' Talks · FOSDEM 2026 · Building a minimal cross-platform terminal UI library

https://fosdem.org/2026/schedule/event/ZPFNKB-lua-ui/

  • Needed a terminal UI library for a luarocks client (luarocket)
  • Goals:
    • Cross-platform (POSIX/Mac/Windows)
    • Non-blocking input, because of Lua's support for coroutines
    • Mechanisms over policies (don't enforce event mechanisms)
    • No external dependencies (sucks on Windows)
  • Non-goals:
    • No overlapping windows
    • No mouse support
  • Architecture: application atop terminal.lua (Lua) atop LuaSystem (C code) atop OSes
    • Preferred LuaSystem to be OS-independent, but terminal is vastly different between OSes
  • Easy part:
    • ANSI in Windows, but not for cmd.exe
    • Multi-byte encoding
    • Windows does not really have UTF-8 support, and calculating display width is generally a problem across OSes
  • UTF-8
    • Implemented width functions for UTF-8 chars/strings
    • string.sub replacements for UTF-8
  • Built a line editor: EditLine
    • Holds a string
    • Manipulated with a cursor
    • Tracks position/size in characters and columns
    • Used for general string manipulation
  • Non-blocking keyboard input
    • No non-blocking stdin on Windows
    • Fell back to conio.h (reading directly from the keyboard buffer)
    • On POSIX: have to read from stdin
    • On Windows: have to read from conio, convert to UTF-8, do this in a loop, strip scancodes, etc. Really involved in comparison.
  • Initialization:
    • POSIX:
      • Disable canonical mode
      • Disable echo
      • Detach FDs
      • Set stdin to non-blocking, but this can set stdout to non-blocking, especially on Macs. This is because FDs can point to the same file, which can be very confusing.
  • Non-blocking:
    • Three functions to read individual bytes: internal C, read single byte with timeout, read UTF8 character with timeout
    • Using non-default sleep makes it non-blocking
  • Querying is slow
    • Terminal takes time to responds to query, which can involve sleeps (2-15ms), which can stack with multiple queries
    • Faster to maintain your own declarative state (cursor shape, cursor visibility, scroll region, text attributes) and then query that instead
    • Implemented a stack abstraction to maintain this state and to sync to the terminal
  • Actually didn't need that many API functions to drive both POSIX and Windows
  • Conclusions:
    • Results were much better than expected, given the constraints of minimal primitives/no dependencies
    • Uniform keyboard reading was uniform and non-blocking
    • Display width is painful to deal with
    • Querying is slow