Includes getting started guide, hiccup views reference, full API documentation, and annotated example walkthroughs with ASCII output examples. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
381 lines
9.3 KiB
Markdown
381 lines
9.3 KiB
Markdown
# 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`:
|
|
|
|
```clojure
|
|
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
|
|
```
|
|
|
|
### Requirements
|
|
|
|
- **Clojure 1.11+** for full async runtime (`tui.core`)
|
|
- **Babashka** for simple sync runtime (`tui.simple`)
|
|
- 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
|
|
|
|
```bash
|
|
mkdir my-tui-app
|
|
cd my-tui-app
|
|
```
|
|
|
|
Create a `deps.edn`:
|
|
|
|
```clojure
|
|
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
|
|
```
|
|
|
|
### Step 2: Write the Application
|
|
|
|
Create `src/myapp/core.clj`:
|
|
|
|
```clojure
|
|
(ns myapp.core
|
|
(:require [tui.simple :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
|
|
(defn view [{:keys [message]}]
|
|
[: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
|
|
|
|
```bash
|
|
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](https://guide.elm-lang.org/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 returns 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
|
|
|
|
```clojure
|
|
[: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:
|
|
|
|
```clojure
|
|
(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.
|
|
|
|
```clojure
|
|
(ns myapp.counter
|
|
(:require [tui.simple :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]
|
|
[: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/tick ms)` | Send `:tick` message 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:
|
|
|
|
```clojure
|
|
(defn update-fn [model msg]
|
|
(cond
|
|
;; Return quit command
|
|
(tui/key= msg "q")
|
|
[model tui/quit]
|
|
|
|
;; Return tick command (async runtime only)
|
|
(tui/key= msg "s")
|
|
[{:started true} (tui/tick 1000)]
|
|
|
|
;; No command
|
|
:else
|
|
[model nil]))
|
|
```
|
|
|
|
### Timer Example (Async Runtime)
|
|
|
|
```clojure
|
|
(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 tick message
|
|
(= msg :tick)
|
|
(let [new-count (dec (:count model))]
|
|
(if (pos? new-count)
|
|
[{:count new-count} (tui/tick 1000)]
|
|
[{:count 0 :done true} nil]))
|
|
|
|
:else
|
|
[model nil]))
|
|
|
|
(defn view [{:keys [count done]}]
|
|
[: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/tick 1000)})) ;; Start first tick
|
|
```
|
|
|
|
## Choosing a Runtime
|
|
|
|
### `tui.simple` - For Babashka and Simple Apps
|
|
|
|
- Works with Babashka
|
|
- Synchronous (blocking) input
|
|
- No async commands (tick, batch, etc.)
|
|
- Lower resource usage
|
|
|
|
### `tui.core` - For Complex Applications
|
|
|
|
- Requires full Clojure with core.async
|
|
- Async input handling
|
|
- Full command support (tick, batch, sequentially)
|
|
- Better for animations and background tasks
|
|
|
|
## Configuration Options
|
|
|
|
The `run` function accepts these options:
|
|
|
|
| Option | Description | Default |
|
|
|--------|-------------|---------|
|
|
| `:init` | Initial model (required) | - |
|
|
| `:update` | Update function (required) | - |
|
|
| `:view` | View function (required) | - |
|
|
| `:init-cmd` | Initial command to run | `nil` |
|
|
| `:fps` | Frames per second (core only) | `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:
|
|
|
|
```clojure
|
|
(tui/run {:init model
|
|
:update update-fn
|
|
:view view
|
|
:alt-screen false})
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
- [Hiccup Views](hiccup-views.md) - Learn about all view elements and styling
|
|
- [API Reference](api-reference.md) - Complete API documentation
|
|
- [Examples](examples.md) - Study annotated example applications
|