This guide walks you through building your first terminal UI application with charm.clj.
Add charm.clj to your deps.edn:
{:deps {io.github.yourname/charm.clj {:git/tag "v0.1.0" :git/sha "..."}}}
Or for local development, add the path:
{:deps {}
:paths ["src" "path/to/charm.clj/src"]}
Let's build a simple counter application.
Create src/counter/core.clj:
(ns counter.core
(:require [charm.core :as charm]))
(defn init []
[{:count 0} nil])
(defn update-fn [state msg]
[state nil])
(defn view [state]
(str "Count: " (:count state)))
(defn -main [& _args]
(charm/run {:init init
:update update-fn
:view view}))
Run it:
clj -M -m counter.core
You'll see "Count: 0" but can't interact with it yet.
Add key handling to the update function:
(defn update-fn [state msg]
(cond
;; Quit on 'q'
(charm/key-match? msg "q")
[state charm/quit-cmd]
;; Increment on up arrow or 'k'
(or (charm/key-match? msg :up)
(charm/key-match? msg "k"))
[(update state :count inc) nil]
;; Decrement on down arrow or 'j'
(or (charm/key-match? msg :down)
(charm/key-match? msg "j"))
[(update state :count dec) nil]
;; Ignore other input
:else
[state nil]))
Add instructions and styling:
(defn view [state]
(str "Counter: " (:count state) "\n\n"
"Controls:\n"
" Up/k - Increment\n"
" Down/j - Decrement\n"
" q - Quit"))
Make it visually appealing:
(def title-style
(charm/style :fg charm/cyan :bold true))
(def count-style
(charm/style :fg charm/yellow
:bold true
:padding [1 3]
:border charm/rounded-border))
(def help-style
(charm/style :fg 240)) ; Gray
(defn view [state]
(str (charm/render title-style "Counter App") "\n\n"
(charm/render count-style (str (:count state))) "\n\n"
(charm/render help-style "Up/k: +1 Down/j: -1 q: quit")))
For a cleaner experience, use the alternate screen buffer:
(defn -main [& _args]
(charm/run {:init init
:update update-fn
:view view
:alt-screen true}))
(ns counter.core
(:require [charm.core :as charm]))
(def title-style
(charm/style :fg charm/cyan :bold true))
(def count-style
(charm/style :fg charm/yellow
:bold true
:padding [1 3]
:border charm/rounded-border))
(def help-style
(charm/style :fg 240))
(defn init []
[{:count 0} nil])
(defn update-fn [state msg]
(cond
(charm/key-match? msg "q")
[state charm/quit-cmd]
(or (charm/key-match? msg :up) (charm/key-match? msg "k"))
[(update state :count inc) nil]
(or (charm/key-match? msg :down) (charm/key-match? msg "j"))
[(update state :count dec) nil]
:else
[state nil]))
(defn view [state]
(str (charm/render title-style "Counter App") "\n\n"
(charm/render count-style (str (:count state))) "\n\n"
(charm/render help-style "Up/k: +1 Down/j: -1 q: quit")))
(defn -main [& _args]
(charm/run {:init init
:update update-fn
:view view
:alt-screen true}))
charm.clj uses the Elm Architecture pattern:
┌─────────────────────────────────────────────────┐
│ │
│ ┌─────┐ ┌────────┐ ┌──────┐ ┌─────┐ │
│ │ Msg │───▶│ Update │───▶│ View │───▶│ UI │ │
│ └─────┘ └────────┘ └──────┘ └─────┘ │
│ ▲ │ │ │
│ │ ▼ │ │
│ │ ┌────────┐ │ │
│ │ │ State │ │ │
│ │ └────────┘ │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ User Input │
└─────────────────────────────────────────────────┘
(state, msg) -> [new-state, cmd]state -> stringLet's add a spinner to show loading state.
(ns loader.core
(:require [charm.core :as charm]))
(defn init []
(let [[spinner cmd] (charm/spinner-init (charm/spinner :dots))]
[{:spinner spinner
:loading true}
cmd]))
(defn update-fn [state msg]
(cond
(charm/key-match? msg "q")
[state charm/quit-cmd]
;; Pass spinner ticks to the spinner
:else
(let [[spinner cmd] (charm/spinner-update (:spinner state) msg)]
[(assoc state :spinner spinner) cmd])))
(defn view [state]
(str (charm/spinner-view (:spinner state))
" Loading..."))
(defn -main [& _args]
(charm/run {:init init
:update update-fn
:view view
:alt-screen true}))
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |