Old Norse Terminal Toolkit

December 18, 2025 ยท View on GitHub

Build retro ASCII games in Common Lisp. Data dashboards & business apps, too. If you are into that type of thing.

Old Norse is a low-latency, grid-based terminal graphics engine with an integrated event loop.

Features: Mouse support, 60fps rendering, deploy anywhere via SSH or TTYD

Core libraries

Old Norse is implementation-dependent on SBCL.

Quick start

Run these examples in the terminal, not SLIME/EMACS

Simple example: styling and alignment

(bifrost:with-bifrost                       ; Enter raw terminal mode
  (skald:skald-init :fg :black :bg :white)  ; Initialize buffers & clear screen
  (skald:skald                              ; Open a draw transaction
    (skald:span ((- skald:*screen-center-row* 4) skald:*screen-center-col*
                 :fg :magenta :align :center)
      "Welcome to Old Norse")
    (skald:sprite ((- skald:*screen-center-row* 2) skald:*screen-center-col*
                   :align :center)
      "  __"
      "<(o )___"
      " ( ._> /"
      "  `---'")
    (dotimes (i 10)
      (skald:span ((+ i 2 skald:*screen-center-row*) skald:*screen-center-col*
                   :bg :blue :fg :white :align :center)
        (make-string (- 40 (* i 2)) :initial-element #\.)))))

Example with styling and layout

Simple example: fast animation that follows mouse movement

(bifrost:with-bifrost                               ; Enter raw terminal mode
  (skald:skald-init :bg :white :fg :blue)           ; Initialize buffers & clear screen
  (bifrost:with-mouse-tracking (1003)               ; start tracking mouse movement
    (flokkr:flokkr                                  ; enter flokkr loop
      (:input
        (:mouse-click-left (return-from flokkr:flokkr)) ; on left click, exit
        (:mouse-move                                    ; on mouse move, reposition sprite
         (skald:skald                                   ; draw it
           (skald:sprite ((first bifrost:*rune-payload*) 
                          (second bifrost:*rune-payload*) 
                          :align :center)
                "โ•”โ•โ•โ•โ•โ•โ•โ•โ•—"
                "โ•‘  ๐ŸŒ   โ•‘"
                "โ•‘ hello โ•‘"
                "โ•‘ world โ•‘"
                "โ•šโ•โ•โ•โ•โ•โ•โ•โ•")))))))

Example with mouse tracking

Fancy example: simultaneous timing loop & user interaction

(bifrost:with-bifrost
  (skald:skald-init :bg :black :fg :white)
  (bifrost:with-mouse-tracking (1000)
    (let ((particles nil)
          (colors '(:red :yellow :cyan :magenta :green :white)))
      (flokkr:flokkr
        (:after 0.033 
         :do (setf particles
                  (remove-if (lambda (life) (<= life 0))
                             (mapcar (lambda (p)
                                       (destructuring-bind (r c vr vc life color) p
                                       (list (+ r vr)
                                             (+ c vc)
                                             (+ vr 0.15)  ; gravity
                                             (* vc 0.98)  ; drag
                                             (1- life)
                                             color)))
                                      particles)
                             :key #'fifth))
         :repeat)
        (:input
          ((#\q #\Q #\esc) (return-from flokkr:flokkr))
          (:mouse-click-left
           (let ((click-r (first bifrost:*rune-payload*))
                 (click-c (second bifrost:*rune-payload*))
                 (color (nth (random (length colors)) colors)))
             (dotimes (i 12)
               (let ((angle (* i (/ 3.14159 6))))
                 (push (list (float click-r)
                             (float click-c)
                             (* -1.5 (cos angle))  ; upward bias
                             (* 3.0 (sin angle))
                             (+ 15 (random 10))    ; life
                             color)
                       particles))))))
        (:also (skald:skald
                 (dolist (p particles)
                   (destructuring-bind (r c vr vc life color) p
                     (declare (ignore vr vc))
                     (when (and (> r 0) (<= r skald:*screen-height*)
                                (> c 0) (<= c skald:*screen-width*))
                       (skald:span ((round r) (round c) :fg color)
                                    (if (> life 10)
                                        "*"
                                        ".")))))
                 (skald:span (1 2 :fg :yellow) "Click anywhere to launch fireworks! Q to exit.")))))))

Fireworks show

Design pillars

(1) Low-level grid-based terminal graphics engine

Old Norse doesn't provide you with a widget for making a status bar. It provides you with tools to draw sprites to the screen & animate them according to precise timing logic, so that you can make your own custom status bar. The goal of the library is to make it easy to prototype a wide array of experimental game mechanics and interfaces, so long as they can be represented on a chunky terminal grid, within the constraints of a Unix-like terminal emulator.

Our roadmap plan is to add support for sixel graphics (terminal-friendly dot matrix printer graphic image format). This will allow us to animate graphic images. However, the terminal grid will still remain the only coordinate system. Sixel sprites will snap to the same terminal grid as ASCII characters.

(2) UX speed & precision timing

Timing is critical to games. Even klunky prototypes. Speed & responsiveness are important to any kind of user application.

The Old Norse design goals are:

  1. 60fps animation (assumes normal screen size)
  2. Update screen in under 16ms after user input (local, before factoring in network latency from remote deployment)
  3. High-precision control of timings & interactive behavior

Based on our observation, TUI applications can achieve this by managing the following bottlenecks:

  1. Efficient diff-based screen updates - provided by SKALD
  2. Immediate response to user input - provided by FLOKKR
  3. Get slow DB queries & cloud API calls out of main loop - Currently must be managed by the user. (But plan to add a new flokkr form to support async io.)
  4. Strategic scheduling of GC pauses - Must be managed by the user.
  5. When deploying remotely, deploy in-region - must be managed by the user.

(3) Develop in an hour. Deploy anywhere.

The goal is to think of an idea, implement it quickly, then get it in front of other people for feedback right away.

Focus on rapid development

  • 1:1 mapping between back-end SBCL program & client session
  • DSL-based approach

Easy remote deployment

  • SSH
  • Browser-based via TTYD.

(4) Old Norse "high locality" coding style

If not structured correctly, even the simplest Terminal UI application can grow into a complicated mess of spaghetti code. The Old Norse way to deal with this is by prioritizing locality. In other words, the TUI application code structure should put related logic close together.

  1. JUST ONE FUNCTION TO RENDER THE ENTIRE SCREEN - Skald makes efficient diff-based screen updates, only updating sections of the that have recently changed. This allows you to define a single function to draw the entire screen, and then call it however frequently you want, relying on SKALD's low-level optimization to eliminate unecessay redrawing.

  2. JUST ONE CONTROL STRUCTURE - Flokkr makes it possible to put all timing logic & input reaction logic in one place, making it easier to reason about timing & interactive behaviors. Modular composability is still possible, but within rigid constraints (chaining via :SUBFLOKKR) to help prevent hidden scheduling issues.

In practice, we have found this design pattern to DRASTICALLY simplify & shorten TUI application codebases.

The Old Norse terminal toolkit libraries

Flokkr

Flokkr is a concurrency library purpose-built for building interactive terminal applications with skald/bifrost.

  • Manage complex timing & behaviors loops via mini-DSL (inspired by the LOOP macro)
  • Respond instantly to terminal input from user (without relying on polling)
  • Define widget/object timing behaviors seperately, then compose via :SUBFLOCKKR

Skald

Skald is a high-level terminal display and animation framework

  • Treat blocks of ASCII/unicode text as sprites (transparant char enables composite layering)
  • Efficient diff-based screen updates for fast redrawing with minimal flicker
  • Grid-based positioning/layout/alignment, foreground/background colors, cropping/fill, emojis, etc

Bifrost ๐ŸŒˆ

Bifrost is a low-level utility for reading from & controlling the terminal. Used by skald & flokkr

  • Two-way mapping between ASCII escape sequences & s-expressions
  • Mouse event tracking logic (click/hover)
  • Raw I/O to bypass terminal read buffer (but also debugging modes to troubleshoot TUI applications within SLIME/EMACS)

Meadhorn

Meadhorn is a simple debugging utility.

  • Print statements are a simple, powerful debugging tool. But when developing terminal applications, they mess up the display. MEADHORN:MH is just like FORMAT except that it broadcasts output to a Unix socket. Read it with netcat for simple print-statement based debuging without disrupting the terminal UI.

Old Norse

You can load all of these libraries via the umbrella package (require :old-norse)

Requirements

  1. Requires a Unix-like terminal emulator that implements standard TTY interface. It must support SGR mode (required for larger grid size). Examples: xterm, gnome-terminal, iTerm2, Mac OS X Terminal, TTYD, etc
  2. Old Norse is implementation-dependent on SBCL
  3. Old Norse requires quicklisp installable TRIVIAL-RAW-IO library
  4. If you want to use mouse tracking features, the terminal must support XTERM mouse tracking protocol. (Most modern terminal emulators do.)
  5. In order for ASCII sprites to line up correctly, your terminal window needs to be using monospaced font.
  • For example, here is how to do that with TTYD:

    ttyd -t fontFamily="'Courier','Lucinda Console','Roboto Mono','Courier New','Monospace'" -p 8080 --writable sbcl
    

We have found the following prefix naming conventions to be useful when structuring our own TUIs:

;;   <r>-row        row coordinate on the terminal grid (sometimes called Y)
;;   <c>-column     column coordinate on the terminal grid (sometimes called X)
;;   <rc>-point     a cons of (ROW . COLUMN). Sometimes called a "point"
;;   [d]-duration   duration of seconds elapsed or for a timer to wait
;;   [%]-progress   for tracking transition from 0.0 to 1.0 (state machine or animation)
;;   [n]-ticks      number of ticks/frames for something to run (eg for an animation)

Status

v0.1.0 - Core API subject to change

This is our development roadmap

Lispy alternatives (terminal UI)

  • cl-tuition - "Common Lisp library for building rich, responsive terminal user interfaces (TUIs). It blends the simplicity of TEA with the power of CLOS so you can model state clearly, react to events via generic methods, and render your UI as pure strings."
  • uncursed - "A cross-platform library for writing terminal interfaces with minimal dependencies (terminfo on unix, recent conhost with VT support on windows). A higher-level buffered drawing abstraction and low-level utilities are provided. Supported implementations will include sbcl, ccl and ecl."
  • text-draw - Common Lisp functions to draw graphics using pure Unicode text. Drawing only.

License

MIT