Accepted
charm.clj follows the Elm architecture where:
update function processes events one at a timeview function renders the entire screen on each updateJLine provides several APIs for terminal input handling:
Use NonBlockingReader for raw input and KeyMap for escape sequence lookup. Do not use BindingReader or LineReader.
| JLine API | Usage |
|---|---|
Terminal | Terminal creation, raw mode, size detection |
NonBlockingReader | Character-by-character input with timeout |
KeyMap | Escape sequence → key event mapping |
InfoCmp$Capability | Terminal-aware key sequences |
Display | Efficient screen diffing and rendering |
AttributedString | Unicode width calculation, ANSI parsing |
| JLine API | Reason |
|---|---|
BindingReader | Blocks until complete sequence; incompatible with async event loop |
LineReader | Manages its own display; conflicts with Elm view function |
| JLine mouse API | Requires detecting mouse prefix first; custom parsing is cleaner |
BindingReader.readBinding() blocks until it recognizes a complete key sequence or times out. This is problematic for the Elm architecture:
// BindingReader blocks here until complete sequence
Object binding = bindingReader.readBinding(keyMap);
In contrast, the async event loop needs to:
The custom approach reads characters individually via NonBlockingReader:
(defn read-event [terminal & {:keys [timeout-ms]}]
(let [reader (.reader terminal)
c (.read reader timeout-ms)] ; Returns immediately on timeout
(when (pos? c)
(parse-input c))))
LineReader provides rich line editing (history, completion, syntax highlighting) but:
Displayview renders the entire screencharm.clj's text-input component provides similar functionality within the Elm architecture, where each keystroke is an event that updates state and triggers a full re-render.
We use KeyMap for escape sequence lookup while handling input ourselves:
(defn create-keymap [terminal]
(let [keymap (KeyMap.)]
;; Terminal-aware: uses actual sequences from terminfo
(when terminal
(.bind keymap {:type :up} (KeyMap/key terminal Capability/key_up)))
;; Fallback: standard sequences for terminals without capabilities
(.bind keymap {:type :up} "[A")
(.bind keymap {:type :up} "OA")
keymap))
;; O(1) lookup via trie
(defn lookup [keymap sequence]
(.getBound keymap sequence))
Benefits:
JLine's BindingReader can parse mouse sequences, but:
[M or [<) firstOur custom parser handles X10 and SGR mouse formats directly:
(defn parse-sgr-mouse [s]
(when-let [[_ code x y final] (re-find #"\x1b\[<(\d+);(\d+);(\d+)([Mm])" s)]
{:type :mouse
:button (parse-button code)
:x (parse-long x)
:y (parse-long y)
:action (if (= final "m") :release :press)}))
This decision can be revisited if:
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 |