Simple, beautiful CLI output 🪶
Build declarative and composable sections, trees, tables and dashboards for your consoles. Part of d4
- Zero dependencies, use Layoutz.scala like a header-file
- Effortless composition of elements
- Thread-safe, purely functional rendering
layoutz is on MavenCentral and cross-built for Scala, 2.12, 2.13, 3.x
"xyz.matthieucourt" %% "layoutz" % "0.1.0"Or try in REPL:
scala-cli repl --scala 3 --dep xyz.matthieucourt:layoutz_3:0.1.0All you need:
import layoutz._import layoutz._
val dashboard = layout(
section("System Status")(
row(
statusCard("CPU", "45%"),
statusCard("Memory", "78%"),
statusCard("Disk", "23%")
)
),
box("Recent Activity")(
bullets(
"User alice logged in",
"Database backup completed",
"3 new deployments"
)
)
)
println(dashboard.render)yields:
=== System Status ===
┌───────┐ ┌──────────┐ ┌────────┐
│ CPU │ │ Memory │ │ Disk │
│ 45% │ │ 78% │ │ 23% │
└───────┘ └──────────┘ └────────┘
┌───────Recent Activity───────┐
│ • User alice logged in │
│ • Database backup completed │
│ • 3 new deployments │
└─────────────────────────────┘
- We have
s"...", and full-blown TUI libraries - but there is a gap in-between. - With LLM's, boilerplate code that formats & "pretty-prints" is cheaper than ever...
- Thus, more than ever, "string formatting code" is spawning, and polluting domain logic
- Utlimately, layoutz is just a tiny, declarative DSL to combat this
- Every piece of content is an
Element - Elements are immutable and composable - you build complex layouts by combining simple elements.
- A
layoutis just a special element that arranges other elements vertically with consistent spacing:
layout(elem1, elem2, elem3) /* Joins with "\n\n" */Call .render on an element to get a String
The power comes from uniform composition, since everything is an Element, everything can be combined with everything else.
All components implementing the Element interface you can use in your layouts...
layoutz implicitly converts Strings to Text element
"Simple text" // <- valid Element
Text("Simple text") // <- you don't need to do thisthis lets you splice strings into layouts as you build them with var-arg shorthand
Add extra line-break "\n" with br:
layout("Line 1", br, "Line 2")section("Config")(kv("env" -> "prod"))=== Config ===
env : prod
layout("First", "Second", "Third")First
Second
Third
row("Left", "Middle", "Right")Left Middle Right
hr
hr("~", 10)──────────────────────────────────────────────────
~~~~~~~~~
kv("name" -> "Alice", "role" -> "admin")name : Alice
role : admin
table(
headers = Seq("Name", "Status"),
rows = Seq(Seq("Alice", "Online"), Seq("Bob", "Away"))
)┌───────┬────────┐
│ Name │ Status │
├───────┼────────┤
│ Alice │ Online │
│ Bob │ Away │
└───────┴────────┘
Simple bullet list
bullets("Task 1", "Task 2", "Task 3")• Task 1
• Task 2
• Task 3
Single bullet with nested children
bullet("Backend",
bullet("API"),
bullet("Database")
)• Backend
• API
• Database
Complex nesting
bullets(
bullet("Frontend",
bullet("Components",
bullet("Header"),
bullet("Footer")
),
bullet("Styles")
),
bullet("Backend",
bullet("API"),
bullet("Database")
)
)• Frontend
• Components
• Header
• Footer
• Styles
• Backend
• API
• Database
Mix bullets with other elements
bullet("Status",
"System online",
inlineBar("Health", 0.95),
"All services running"
)• Status
• System online
• Health [███████████████████─] 95%
• All services running
box("Summary")(kv("total" -> "42"))┌──Summary───┐
│ total : 42 │
└────────────┘
statusCard("CPU", "45%")┌───────┐
│ CPU │
│ 45% │
└───────┘
inlineBar("Download", 0.75)Download [███████████████─────] 75%
diffBlock(
added = Seq("new feature"),
removed = Seq("old code")
)Changes:
- old code
+ new feature
tree("Project")(
branch("src",
branch("main", leaf("App.scala")),
branch("test", leaf("AppSpec.scala"))
)
)Project
└── src/
├── main/
│ └── App.scala
└── test/
└── AppSpec.scala
The full power of Scala functional collections is at your fingertips to render your strings with layoutz
case class User(name: String, role: String)
val users = Seq(User("Alice", "Admin"), User("Bob", "User"), User("Tom", "User"))
val usersByRole = users.groupBy(_.role)
section("Users by Role")(
layout(
usersByRole.map { case (role, roleUsers) =>
box(role)(
bullets(roleUsers.map(_.name): _*)
)
}.toSeq: _*
)
)=== Users by Role ===
┌──Admin──┐
│ • Alice │
└─────────┘
┌──User──┐
│ • Bob │
│ • Tom │
└────────┘
- ScalaTags by Li Haoyi
- Countless templating libraries via osmosis ...