Files
clojure-tui/docs/getting-started.md
2026-01-23 07:56:25 -05:00

8.9 KiB

Getting Started

This guide will walk you through creating your first TUI application with Clojure TUI.

Installation

Using deps.edn

Add the library to your deps.edn:

{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}

Requirements

  • Babashka (recommended) or Clojure 1.11+
  • A terminal that supports ANSI escape codes (most modern terminals)

Your First Application

Let's build a simple "Hello World" TUI application.

Step 1: Create the Project

mkdir my-tui-app
cd my-tui-app

Create a deps.edn:

{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}

Step 2: Write the Application

Create src/myapp/core.clj:

(ns myapp.core
  (:require [tui.core :as tui]))

;; 1. Model - the application state
(def initial-model
  {:message "Hello, TUI!"})

;; 2. Update - handle messages and return [new-model command]
(defn update-fn [model msg]
  (cond
    ;; Quit on 'q' key
    (tui/key= msg "q")
    [model tui/quit]

    ;; Default: no change
    :else
    [model nil]))

;; 3. View - render the model as hiccup (receives model and terminal size)
(defn view [{:keys [message]} _size]
  [:col
   [:text {:fg :cyan :bold true} message]
   [:space {:height 1}]
   [:text {:fg :gray} "Press q to quit"]])

;; Run the application
(defn -main [& args]
  (tui/run {:init initial-model
            :update update-fn
            :view view}))

Step 3: Run It

clojure -M -m myapp.core

You'll see:

Hello, TUI!

Press q to quit

Press q to exit.

Understanding the Elm Architecture

Clojure TUI uses the Elm Architecture, a pattern for building interactive applications.

The Three Parts

                    ┌─────────────┐
                    │    Model    │
                    │  (state)    │
                    └──────┬──────┘
                           │
              ┌────────────┴────────────┐
              │                         │
              v                         │
       ┌─────────────┐           ┌──────┴──────┐
       │    View     │           │   Update    │
       │ (render UI) │           │(handle msg) │
       └──────┬──────┘           └──────┬──────┘
              │                         ^
              │                         │
              v                         │
       ┌─────────────┐           ┌──────┴──────┐
       │   Screen    │           │   Message   │
       │  (output)   │           │  (input)    │
       └─────────────┘           └─────────────┘
  1. Model: Your application state. Can be any Clojure data structure.

  2. Update: A pure function that takes the current model and a message, returning a vector of [new-model command].

  3. View: A pure function that takes the model and terminal size, returning a hiccup data structure representing the UI.

The Flow

  1. The runtime renders the initial view
  2. User presses a key
  3. The key is parsed into a message like [:key {:char \a}]
  4. The update function receives the model and message
  5. update returns a new model and optional command
  6. The view is re-rendered with the new model
  7. If a command was returned, it's executed (may produce more messages)
  8. Repeat until tui/quit is returned

Handling Input

Keys are represented as messages in the format [:key ...].

Key Message Examples

[:key {:char \a}]              ;; Regular character
[:key {:char \Z}]              ;; Uppercase character
[:key :enter]                  ;; Enter key
[:key :up]                     ;; Arrow up
[:key {:ctrl true :char \c}]   ;; Ctrl+C
[:key {:alt true :char \x}]    ;; Alt+X

Matching Keys

Use tui/key= to match keys in your update function:

(defn update-fn [model msg]
  (cond
    ;; Match character 'q'
    (tui/key= msg "q") [model tui/quit]

    ;; Match Enter key
    (tui/key= msg :enter) [(handle-enter model) nil]

    ;; Match arrow keys
    (tui/key= msg :up) [(move-up model) nil]
    (tui/key= msg :down) [(move-down model) nil]

    ;; Match Ctrl+C
    (tui/key= msg [:ctrl \c]) [model tui/quit]

    ;; Default
    :else [model nil]))

Special Keys

Key Pattern
Enter :enter
Escape :escape
Tab :tab
Backspace :backspace
Arrow Up :up
Arrow Down :down
Arrow Left :left
Arrow Right :right
Home :home
End :end
Page Up :page-up
Page Down :page-down
Delete :delete
Insert :insert
F1-F12 :f1 through :f12

Building a Counter

Let's build something more interactive: a counter.

(ns myapp.counter
  (:require [tui.core :as tui]))

(defn update-fn [model msg]
  (cond
    ;; Quit
    (tui/key= msg "q") [model tui/quit]

    ;; Increment with 'k' or up arrow
    (or (tui/key= msg "k")
        (tui/key= msg :up))
    [(inc model) nil]

    ;; Decrement with 'j' or down arrow
    (or (tui/key= msg "j")
        (tui/key= msg :down))
    [(dec model) nil]

    ;; Reset with 'r'
    (tui/key= msg "r") [0 nil]

    :else [model nil]))

(defn view [count _size]
  [:col
   [:box {:border :rounded :padding [0 2]}
    [:row
     [:text "Count: "]
     [:text {:fg (cond
                   (pos? count) :green
                   (neg? count) :red
                   :else :yellow)
             :bold true}
      (str count)]]]
   [:space {:height 1}]
   [:text {:fg :gray} "j/k or arrows: change value"]
   [:text {:fg :gray} "r: reset  q: quit"]])

(defn -main [& args]
  (tui/run {:init 0
            :update update-fn
            :view view}))

Output:

╭────────────────╮
│  Count: 0      │
╰────────────────╯

j/k or arrows: change value
r: reset  q: quit

After pressing k a few times:

╭────────────────╮
│  Count: 3      │
╰────────────────╯

j/k or arrows: change value
r: reset  q: quit

Commands

Commands are how your application performs side effects.

Available Commands

Command Description
tui/quit Exit the application
(tui/after ms msg) Send msg after delay
(tui/batch cmd1 cmd2 ...) Run commands in parallel
(tui/sequentially cmd1 cmd2 ...) Run commands in sequence

Returning Commands

Commands are returned as the second element of the update function's return vector:

(defn update-fn [model msg]
  (cond
    ;; Return quit command
    (tui/key= msg "q")
    [model tui/quit]

    ;; Return after command (async runtime only)
    (tui/key= msg "s")
    [{:started true} (tui/after 1000 :timer-tick)]

    ;; No command
    :else
    [model nil]))

Timer Example (Async Runtime)

(ns myapp.timer
  (:require [tui.core :as tui]))  ;; Note: tui.core for async

(defn update-fn [model msg]
  (cond
    (tui/key= msg "q")
    [model tui/quit]

    ;; Handle timer tick message
    (= msg :timer-tick)
    (let [new-count (dec (:count model))]
      (if (pos? new-count)
        [{:count new-count} (tui/after 1000 :timer-tick)]
        [{:count 0 :done true} nil]))

    :else
    [model nil]))

(defn view [{:keys [count done]} _size]
  [:col
   (if done
     [:text {:fg :green :bold true} "Time's up!"]
     [:text {:bold true} (str "Countdown: " count)])
   [:text {:fg :gray} "q: quit"]])

(defn -main [& args]
  (tui/run {:init {:count 10 :done false}
            :update update-fn
            :view view
            :init-cmd (tui/after 1000 :timer-tick)}))  ;; Start first timer

Configuration Options

The run function accepts these options:

Option Description Default
:init Initial model (required) -
:update Update function (required) -
:view View function (fn [model size] hiccup) (required) -
:init-cmd Initial command to run nil
:fps Frames per second 60
:alt-screen Use alternate screen buffer true

Alternate Screen

By default, applications use the alternate screen buffer. This means:

  • Your application gets a clean screen
  • When you quit, the original terminal content is restored

To disable this:

(tui/run {:init model
          :update update-fn
          :view view
          :alt-screen false})

Next Steps