layoutz

Zero-dependency library for terminal animations, compositional ANSI strings, plots, and Elm-style TUIs. In Scala, Haskell, OCaml, and Clojure.

Scala Haskell OCaml Clojure

import layoutz._

val demo = layout(
  row(
    "Layoutz".style(Style.Bold),
    underline("^", Color.Cyan)("DEMO")
  ).center(),
  br,
  row(
    statusCard("Users", "1.2K"),
    statusCard("API", "UP").border(Border.Double),
    statusCard("CPU", "23%").border(Border.Thick).color(Color.Red),
    table(
      Seq("Name", "Role", "Skills"),
      Seq(
        Seq("Gegard", "Pugilist",
          ul("Armenian", ul("bad", ul("man")))),
        Seq("Eve", "QA", "Testing")
      )
    ).border(Border.Round).style(Style.Reverse)
  )
)

demo.putStrLn

Showcase

src: Scala | Haskell | OCaml | Clojure

Inline animations

Scala Haskell OCaml Clojure

import layoutz._

case class St(progress: Double, done: Int)
case object Tick

object Loader extends LayoutzApp[St, Tick.type] {
  def init = (St(0, 0), Cmd.none)
  def update(msg: Tick.type, s: St) = {
    if (s.done > 30) (s, Cmd.exit)
    else if (s.progress >= 1.0) (s.copy(done = s.done + 1), Cmd.none)
    else (s.copy(progress = math.min(1.0, s.progress + 0.008)), Cmd.none)
  }
  def subscriptions(s: St) = Sub.time.everyMs(16, Tick)
  def view(s: St) = {
    val w = 40; val filled = (s.progress * w).toInt
    val bar = (0 until w).map { i =>
      if (i < filled) { val r = i.toDouble / w
        "█".color(Color.True((r*180).toInt+50, ((1-r)*200).toInt+55, 255))
      } else "░".color(Color.BrightBlack)
    }
    layout(rowTight(bar: _*),
      s"Linking... ${(s.progress*100).toInt}%%".color(Color.BrightCyan))
  }
}

println("hello from a normal process")
println("doing some work...")
println("now watch this:")
Loader.run(clearOnStart = false, clearOnExit = false)
println("back to normal output")

Interactive TUIs

Scala Haskell OCaml Clojure

import layoutz._

object CounterApp extends LayoutzApp[Int, String] {
  def init = (0, Cmd.none)

  def update(msg: String, count: Int) = msg match {
    case "inc" => (count + 1, Cmd.none)
    case "dec" => (count - 1, Cmd.none)
    case _     => (count, Cmd.none)
  }

  def subscriptions(count: Int) =
    Sub.onKeyPress {
      case Key.Char('+') => Some("inc")
      case Key.Char('-') => Some("dec")
      case _             => None
    }

  def view(count: Int) = layout(
    section("Counter")(s"Count: $count"),
    br,
    ul("Press `+` or `-`")
  )

}

CounterApp.run

Links

GitHub


© 2025–2026 Matthieu Court