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:
{:deps {charm.clj {:local/root "/path/to/charm.clj"}}}Or if published:
{:deps {io.github.yourname/charm.clj {:git/tag "v0.1.0" :git/sha "..."}}}(ns myapp.core
(:require [charm.core :as charm]))
(defn update-fn [state msg]
(cond
(charm/key-match? msg "q") [state charm/quit-cmd]
(charm/key-match? msg "k") [(update state :count inc) nil]
(charm/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"))
(charm/run {:init {:count 0}
:update update-fn
:view view})(charm/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
:mouse :cell ; Mouse mode: nil, :normal, :cell, :all
:focus-reporting false ; Report focus in/out events
:fps 60}) ; Frames per secondMessages are maps with a :type key. Built-in message types:
;; Check message types
(charm/key-press? msg) ; Keyboard input
(charm/mouse? msg) ; Mouse event
(charm/window-size? msg) ; Terminal resized
(charm/quit? msg) ; Quit signal
;; Match specific keys
(charm/key-match? msg "q") ; Letter q
(charm/key-match? msg "ctrl+c") ; Ctrl+C
(charm/key-match? msg "enter") ; Enter key
(charm/key-match? msg :up) ; Arrow up
;; Check modifiers
(charm/ctrl? msg)
(charm/alt? msg)
(charm/shift? msg)Commands are async functions that produce messages:
;; Quit the program
charm/quit-cmd
;; Create a custom command
(charm/cmd (fn [] (charm/key-press :custom)))
;; Run multiple commands in parallel
(charm/batch cmd1 cmd2 cmd3)
;; Run commands in sequence
(charm/sequence-cmds cmd1 cmd2 cmd3)(require '[charm.core :as charm])
;; Create a style
(def my-style
(charm/style :fg charm/red
:bold true
:padding [1 2]))
;; Apply style to text
(charm/render my-style "Hello!")
;; Shorthand
(charm/styled "Hello!" :fg charm/green :italic true)
;; Colors
(charm/rgb 255 100 50) ; True color
(charm/hex "#ff6432") ; Hex color
(charm/ansi :red) ; ANSI 16 colors
(charm/ansi256 196) ; 256 palette
;; Borders
(charm/render (charm/style :border charm/rounded-border) "boxed")
;; Layout
(charm/join-horizontal :top block1 block2)
(charm/join-vertical :center block1 block2)(ns counter.core
(:require [charm.core :as charm]))
(def title-style
(charm/style :fg charm/magenta :bold true))
(def count-style
(charm/style :fg charm/cyan
:padding [0 1]
:border charm/rounded-border))
(defn update-fn [state msg]
(cond
;; Quit on q or Ctrl+C
(or (charm/key-match? msg "q")
(charm/key-match? msg "ctrl+c"))
[state charm/quit-cmd]
;; Increment on k or up arrow
(or (charm/key-match? msg "k")
(charm/key-match? msg :up))
[(update state :count inc) nil]
;; Decrement on j or down arrow
(or (charm/key-match? msg "j")
(charm/key-match? msg :down))
[(update state :count dec) nil]
;; Ignore other messages
:else
[state nil]))
(defn view [state]
(str (charm/render title-style "Counter App") "\n\n"
(charm/render count-style (str (:count state))) "\n\n"
"j/k or arrows to change\n"
"q to quit"))
(defn -main [& args]
(charm/run {:init {:count 0}
:update update-fn
:view view
:alt-screen true}))charm.clj/
├── src/charm/
│ ├── core.clj ; Public API
│ ├── program.clj ; Event loop
│ ├── 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
│ │ └── layout.clj ; Padding, margin, alignment
│ └── 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