A Clojure TUI (Terminal User Interface) library inspired by Bubble Tea.
Build terminal applications using the Elm Architecture (Model-Update-View pattern) with a simple, functional API. Run them on the JVM, as a native-image binary or on babashka.
This library is vibecoded and very early. It works for the examples but please let me know if you encounter any issues. I am planning to use it for something more sophisticated. Please expect breaking changes.
- Elm Architecture - Simple init/update/view pattern for predictable state management
- UI Components - Spinner, text-input, list, paginator, timer, progress, help
- Styling - Colors (ANSI, 256, true color), borders, padding, alignment
- Input handling - Keyboard and mouse events with modifier support
- Efficient rendering - Line diffing for minimal terminal updates
- core.async - Asynchronous command execution
- Getting Started - Build your first app
- Components - UI component reference
- API Reference
- Guides
- Examples - Runnable demo applications
Add to your deps.edn:
via github
{:deps {io.github.timokramer/charm.clj {:git/tag "v0.1.64" :git/sha "e5120a9"}}}or via clojars maven repository
{:deps {de.timokramer/charm.clj {:mvn/version "0.1.64"}}}(ns myapp.core
(:require
[charm.message :as msg]
[charm.program :as program]
[charm.style.core :as style]))
(defn update-fn [state msg]
(cond
(msg/key-match? msg "q") [state program/quit-cmd]
(msg/key-match? msg "k") [(update state :count inc) nil]
(msg/key-match? msg "j") [(update state :count dec) nil]
:else [state nil]))
(defn view [state]
(str "Count: " (:count state) "\n\n"
"j/k to change, q to quit"))
(program/run {:init {:count 0}
:update update-fn
:view view})(program/run {:init initial-state-or-fn
:update (fn [state msg] [new-state cmd])
:view (fn [state] "string to render")
;; Options
:alt-screen false ; Use [alternate screen buffer](#alternate-screen-buffer)
:mouse :cell ; Mouse mode: nil, :normal, :cell, :all
:focus-reporting false ; Report focus in/out events
:fps 60}) ; Frames per secondTerminals have two screen buffers: the normal buffer (where your shell history lives) and the alternate buffer (a separate, clean screen).
When you set :alt-screen true, your program switches to the alternate buffer on startup and switches back when it exits. This is the same mechanism used by vim, less, and htop — your previous terminal content disappears while the program runs and reappears when it quits, as if nothing happened.
Use :alt-screen true for full-screen applications like file browsers, editors, or dashboards. Leave it false (the default) for inline programs like prompts or spinners that should leave their output visible in the scrollback after they finish.
Messages are maps with a :type key. Built-in message types:
;; Check message types
(msg/key-press? msg) ; Keyboard input
(msg/mouse? msg) ; Mouse event
(msg/window-size? msg) ; Terminal resized
(msg/quit? msg) ; Quit signal
;; Match specific keys
(msg/key-match? msg "q") ; Letter q
(msg/key-match? msg "ctrl+c") ; Ctrl+C
(msg/key-match? msg "enter") ; Enter key
(msg/key-match? msg :up) ; Arrow up
;; Check modifiers
(msg/ctrl? msg)
(msg/alt? msg)
(msg/shift? msg)Commands are async functions that produce messages:
;; Quit the program
program/quit-cmd
;; Create a custom command
(program/cmd (fn [] (msg/key-press :custom)))
;; Run multiple commands in parallel
(program/batch cmd1 cmd2 cmd3)
;; Run commands in sequence
(program/sequence-cmds cmd1 cmd2 cmd3)(require '[charm.style.core :as style])
;; Create a style
(def my-style
(style/style :fg style/red
:bold true
:padding [1 2]))
;; Apply style to text
(style/render my-style "Hello!")
;; Shorthand
(style/styled "Hello!" :fg style/green :italic true)
;; Colors
(style/rgb 255 100 50) ; True color
(style/hex "#ff6432") ; Hex color
(style/ansi :red) ; ANSI 16 colors
(style/ansi256 196) ; 256 palette
;; Borders (require '[charm.style.border :as border])
(style/render (style/style :border border/rounded) "boxed")
;; Layout
(style/join-horizontal :top block1 block2)
(style/join-vertical :center block1 block2)Please take a look at the examples in the docs folder and don't hesitate to contribute your examples please.
charm.clj/
├── src/charm/
│ ├── program.clj ; Event loop & program runner
│ ├── terminal.clj ; JLine wrapper
│ ├── message.clj ; Message types
│ ├── ansi/
│ │ ├── parser.clj ; ANSI sequence parsing
│ │ └── width.clj ; Text width calculation
│ ├── input/
│ │ ├── keys.clj ; Key sequence mapping
│ │ ├── mouse.clj ; Mouse event parsing
│ │ └── handler.clj ; Input reading
│ ├── style/
│ │ ├── core.clj ; Style API
│ │ ├── color.clj ; Color definitions
│ │ ├── border.clj ; Border styles
│ │ ├── overlay.clj ; Overlay placement
│ │ └── layout.clj ; Padding, margin, alignment
│ ├── components/
│ │ ├── spinner.clj ; Spinner animations
│ │ ├── text_input.clj; Text input field
│ │ ├── list.clj ; Scrollable list
│ │ ├── paginator.clj ; Page navigation
│ │ ├── timer.clj ; Countdown timer
│ │ ├── progress.clj ; Progress bar
│ │ ├── viewport.clj ; Scrollable content
│ │ ├── table.clj ; Table display
│ │ └── help.clj ; Help key bindings
│ └── render/
│ ├── core.clj ; Renderer
│ └── screen.clj ; ANSI sequences
└── test/charm/ ; Tests
clojure -M:test- JDK 22+
- Clojure 1.12+
- JLine 3 - Terminal I/O
- core.async - Async message handling
MIT