wolfadex/elm-layoutz

A package for simple, beautiful CLI output.


License
BSD-3-Clause
Install
elm-package install wolfadex/elm-layoutz 1.0.0

Documentation

elm-layoutz

Simple, beautiful CLI output for Elm ๐Ÿชถ

Build declarative and composable sections, trees, tables, dashboards, and interactive Elm-style TUIs. Easily create new primitives (no component-library limitations).

Features

  • Rich text formatting: alignment, underlines, padding, margins
  • Lists, trees, tables, charts, spinners...
  • ANSI colors and wide character support
  • Easily create new primitives (no component-library limitations)

TaskList SimpleGame

Table of Contents

Quickstart

(1/2) Static rendering - Beautiful, compositional strings:

import Ansi.Color -- from wolfadex/elm-ansi
import Layoutz

demo =
    Layoutz.layout
        [ Layoutz.center <|
            Layoutz.row 
                [ Layoutz.withStyle Layoutz.StyleBold <| Layoutz.text "Layoutz"
                , Layoutz.withColor Ansi.Color.Cyan <| Layoutz.underline "ห†" <| Layoutz.text "DEMO"
                ]
        , Layoutz.br
        , Layoutz.row
            [ Layoutz.statusCard "Users" "1.2K"
            , Layoutz.withBorder Layoutz.BorderDouble <| Layoutz.statusCard "API" "UP"
            , Layoutz.withColor Ansi.Color.Red <| Layoutz.withBorder Layoutz.BorderThick <| Layoutz.statusCard "CPU" "23%"
            , Layoutz.withStyle Layoutz.StyleReverse <|
                Layoutz.withBorder Layoutz.BorderRound <|
                    Layoutz.table
                        ["Name", "Role", "Skills"] 
               	        [ [ Layoutz.text "Gegard"
                          , Layoutz.text "Pugilist"
                          , Layoutz.ul
                              [ Layoutz.text "Armenian"
                              , Layoutz.ul [ Layoutz.text "bad", Layoutz.ul [ Layoutz.text"man" ] ]
                              ]
                          ]
                        , [ Layoutz.text "Eve", Layoutz.text "QA", Layoutz.text "Testing"]
                        ]
            ]
        ]

render demo

Readme

(2/2) Interactive apps - Build Elm-style TUI's:

import Ansi.Cursor
import Layoutz
import Ports -- user defined, see examples

type Msg = Increment | Decrement

init : () -> ( Model, Cmd Msg )
init () =
    render ( { count = 0 }, Cmd.none )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Increment ->
            render ( { model | count = model.count + 1 }, Cmd.none )

        Decrement ->
            render ( { model | count = model.count - 1 }, Cmd.none )

render : ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
render ( model, cmd ) =
    ( model
    , Cmd.batch
        [ Layoutz.layout
            [ Layoutz.section "Counter"
                [ Layoutz.text <| "Count: " ++ String.fromInt model.count
                ]
            , Layoutz.br
            , Layoutz.ul
                [ Layoutz.text "Press '+' or '-'"
                , Layoutz.text "Press ESC to quit"
                ]
            ]
            |> Layoutz.render
            |> (\rendered -> Ansi.Cursor.hide ++ Ansi.clearScreen ++ rendered)
            |> Ports.stdout
        , cmd
        ]
    )

Core concepts

  • Every piece of content is an Element
  • Elements are immutable and composable - build complex layouts by combining simple elements
  • A layout arranges elements vertically:
Layoutz.layout [elem1, elem2, elem3]  -- Joins with "\n"

Call Layoutz.render on any element to get a string

The power comes from uniform composition - since everything is an Element, everything can be combined.

Elements

Text

text "Simple text"
Simple text

Line Break

Add line breaks with br:

layout [text "Line 1", br, text "Line 2"]
Line 1

Line 2

Section: section

section "Config" [keyValue [("env", "prod")]]
section' "-" "Status" [keyValue [("health", "ok")]]
section'' "#" "Report" 5 [keyValue [("items", "42")]]
=== Config ===
env: prod

--- Status ---
health: ok

##### Report #####
items: 42

Layout (vertical): layout

layout [text "First",text "Second",text "Third"]
First
Second
Third

Row (horizontal): row

Arrange elements side-by-side horizontally:

row [text "Left",text "Middle",text "Right"]
Left Middle Right

Multi-line elements are aligned at the top:

row 
  [ layout [text "Left",text "Column"]
  , layout [text "Middle",text "Column"]
  , layout [text "Right",text "Column"]
  ]

Tight Row: tightRow

Like row, but with no spacing between elements (useful for gradients and progress bars):

tightRow [withColor Ansi.Color.Red <| text "โ–ˆ", withColor Ansi.Color.Green $<| text "โ–ˆ", withColor Ansi.Color.Blue <| text "โ–ˆ"]
โ–ˆโ–ˆโ–ˆ

Text alignment: alignLeft, alignRight, alignCenter, justify

Align text within a specified width:

layout
  [ alignLeft 40 "Left aligned"
  , alignCenter 40 "Centered"
  , alignRight 40 "Right aligned"
  , justify 40 "This text is justified evenly"
  ]
Left aligned                            
               Centered                 
                           Right aligned
This  text  is  justified         evenly

Horizontal rule: hr

hr
hr' "~"
hr'' "-" 10
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------

Vertical rule: vr

row [vr, vr' "โ•‘", vr'' "x" 5]
โ”‚ โ•‘ x
โ”‚ โ•‘ x
โ”‚ โ•‘ x
โ”‚ โ•‘ x
โ”‚ โ•‘ x
โ”‚ โ•‘
โ”‚ โ•‘
โ”‚ โ•‘
โ”‚ โ•‘
โ”‚ โ•‘

Key-value pairs: keyValue

keyValue [("name", "Alice"), ("role", "admin")]
name: Alice
role: admin

Table: table

Tables automatically handle alignment and borders:

table ["Name", "Age", "City"] 
  [ [text "Alice", text "30", text "New York"]
  , [text "Bob", text "25", text ""]
  , [text "Charlie", text "35", text "London"]
  ]
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Name    โ”‚ Age โ”‚ City    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Alice   โ”‚ 30  โ”‚ New Yorkโ”‚
โ”‚ Bob     โ”‚ 25  โ”‚         โ”‚
โ”‚ Charlie โ”‚ 35  โ”‚ London  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Unordered Lists: ul

Clean unordered lists with automatic nesting:

ul [text "Feature A", text "Feature B", text "Feature C"]
โ€ข Feature A
โ€ข Feature B
โ€ข Feature C

Nested lists with auto-styling:

ul [ text "Backend"
   , ul [text "API", text "Database"]
   , text "Frontend"
   , ul [text "Components", ul [text "Header", ul [text "Footer"]]]
   ]
โ€ข Backend
  โ—ฆ API
  โ—ฆ Database
โ€ข Frontend
  โ—ฆ Components
    โ–ช Header
      โ€ข Footer

Ordered Lists: ol

Numbered lists with automatic nesting:

ol [text "First step", text "Second step", text "Third step"]
1. First step
2. Second step
3. Third step

Nested ordered lists with automatic style cycling (numbers โ†’ letters โ†’ roman numerals):

ol [ text "Setup"
   , ol [text "Install dependencies", text "Configure", ol [text "Check version"]]
   , text "Build"
   , text "Deploy"
   ]
1. Setup
  a. Install dependencies
  b. Configure
    i. Check version
2. Build
3. Deploy

Underline: underline

Add underlines to any element:

underline <| text "Important Title"
underline' "=" <| text "Custom"  -- Use text for custom underline char
Important Title
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Custom
โ•โ•โ•โ•โ•โ•

Box: box

With title:

box "Summary" [keyValue [("total", "42")]]
โ”Œโ”€โ”€Summaryโ”€โ”€โ”€โ”
โ”‚ total: 42  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Without title:

box "" [keyValue [("total", "42")]]
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ total: 42  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Status card: statusCard

statusCard "CPU" "45%"
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CPU   โ”‚
โ”‚ 45%   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Progress bar: inlineBar

inlineBar "Download" 0.75
Download [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”€โ”€โ”€โ”€โ”€] 75%

Tree: tree

tree "Project" 
  [ branch "src" 
      [ leaf "main.hs"
      , leaf "test.hs"
      ]
  , branch "docs"
      [ leaf "README.md"
      ]
  ]
Project
โ”œโ”€โ”€ src
โ”‚   โ”œโ”€โ”€ main.hs
โ”‚   โ””โ”€โ”€ test.hs
โ””โ”€โ”€ docs
    โ””โ”€โ”€ README.md

Chart: chart

chart [("Web", 10), ("Mobile", 20), ("API", 15)]
Web    โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 10
Mobile โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 20
API    โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 15

Padding: pad

Add uniform padding around any element:

pad 2 <| text "content"
        
        
  content  
        
        

Spinners: spinner

Animated loading spinners for TUI apps:

spinner "Loading..." frameNum SpinnerDots
spinner "Processing" frameNum SpinnerLine
spinner "Working" frameNum SpinnerClock
spinner "Thinking" frameNum SpinnerBounce

Styles:

  • SpinnerDots - Braille dot spinner: โ ‹ โ ™ โ น โ ธ โ ผ โ ด โ ฆ โ ง โ ‡ โ 
  • SpinnerLine - Classic line spinner: | / - \
  • SpinnerClock - Clock face spinner: ๐Ÿ• ๐Ÿ•‘ ๐Ÿ•’ ...
  • SpinnerBounce - Bouncing dots: โ  โ ‚ โ „ โ ‚

Increment the frame number on each render to animate:

-- In your app state, track a frame counter
type alias Model = { spinnerFrame : Int, ... }

-- In your view function
spinner "Loading" model.spinnerFrame SpinnerDots

-- In your update function (triggered by a tick or key press)
{ model | spinnerFrame = model.spinnerFrame + 1 }

With colors:

withColor Ansi.Color.Green <| spinner "Success!" frame SpinnerDots
withColor Ansi.Color.Yellow <| spinner "Warning" frame SpinnerLine

Centering: center

Smart auto-centering and manual width:

center <| text "Auto-centered"     -- Uses layout context
center' 20 <| text "Manual width"  -- Fixed width
        Auto-centered        

    Manual width    

Margin: margin

Add prefix margins to elements for compiler-style error messages:

margin "[error]"
  [ text "Ooops"
  , text ""
  , row [ text "result :: Int = "
        , underline' "^" <| text "getString"
        ]
  , text "Expected Int, found String"
  ]
[error] Ooops
[error]
[error] result :: Int =  getString
[error]                  ^^^^^^^^^
[error] Expected Int, found String

Border Styles

Elements like box, table, and statusCard support different border styles:

BorderNormal (default):

box "Title" [text "content"]
โ”Œโ”€โ”€Titleโ”€โ”€โ”
โ”‚ content โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

BorderDouble:

withBorder BorderDouble <| statusCard "API" "UP"
โ•”โ•โ•โ•โ•โ•โ•โ•โ•—
โ•‘ API   โ•‘
โ•‘ UP    โ•‘
โ•šโ•โ•โ•โ•โ•โ•โ•โ•

BorderThick:

withBorder BorderThick <| table ["Name"] [["Alice"]]
โ”โ”โ”โ”โ”โ”โ”โ”โ”“
โ”ƒ Name  โ”ƒ
โ”ฃโ”โ”โ”โ”โ”โ”โ”โ”ซ
โ”ƒ Alice โ”ƒ
โ”—โ”โ”โ”โ”โ”โ”โ”โ”›

BorderRound:

withBorder BorderRound <| box "Info" ["content"]
โ•ญโ”€โ”€Infoโ”€โ”€โ”€โ•ฎ
โ”‚ content โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

BorderNone (invisible borders):

withBorder BorderNone <| box "Info" ["content"]
  Info   
 content 
         

Colors (ANSI Support)

Add ANSI colors to any element using wolfadex/elm-ansi:

layout[
  withColor Ansi.Color.Red <| text "The quick brown fox...",
  withColor Ansi.Color.BrightCyan <| text "The quick brown fox...",
  underlineColored "~" Ansi.Color.Red <| text "The quick brown fox...",
  margin "[INFO]" [withColor Ansi.Color.Cyan <| text "The quick brown fox..."]
]

Styles

Add ANSI styles to any element:

layout[
  withStyle StyleBold <| text "The quick brown fox...",
  withColor ColorRed <| withStyle StyleBold <| text "The quick brown fox...",
  withBackgroundColor Ansi.Color.White <| withStyle StyleItalic <| text "The quick brown fox..."
]

Styles:

  • StyleBold StyleDim StyleItalic StyleUnderline
  • StyleBlink StyleHidden StyleStrikethrough
  • StyleNoStyle (for conditional formatting)
layout[
  withStyle (StyleCombined [StyleBold, StyleItalic, StyleUnderline]) <| text "The quick brown fox..."
]

You can also combine colors and styles:

withColor Ansi.Color.BrightYellow <| withStyle (StyleCombined [StyleBold, StyleItalic]) <| text "The quick brown fox..."

Interactive Apps

Build terminal applications with the example TUI runtime.

Inspiration