Files
clojure-tui/docs/getting-started.md
2026-01-22 10:50:26 -05:00

364 lines
8.9 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
- **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
```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.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
```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 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
```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.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/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]} _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/tick 1000)})) ;; Start first tick
```
## 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:
```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