Files
clojure-tui/src/tui/render.clj
2026-01-21 01:16:37 -05:00

186 lines
5.7 KiB
Clojure

(ns tui.render
"Render hiccup to ANSI strings."
(:require [tui.ansi :as ansi]
[clojure.string :as str]))
;; === Hiccup Parsing ===
(defn- parse-element
"Parse hiccup element into [tag attrs children]."
[elem]
(cond
(string? elem) [:text {} [elem]]
(number? elem) [:text {} [(str elem)]]
(nil? elem) [:text {} [""]]
(vector? elem)
(let [[tag & rest] elem
[attrs children] (if (map? (first rest))
[(first rest) (vec (next rest))]
[{} (vec rest)])]
[tag attrs children])
:else [:text {} [(str elem)]]))
;; === Text Rendering ===
(defn- apply-style
"Apply style attributes to text."
[text {:keys [fg bg bold dim italic underline inverse strike]}]
(if (or fg bg bold dim italic underline inverse strike)
(ansi/style text
:fg fg :bg bg
:bold bold :dim dim :italic italic
:underline underline :inverse inverse :strike strike)
text))
(defn- render-text
"Render :text element."
[attrs children]
(let [content (apply str (flatten children))]
(apply-style content attrs)))
;; === Layout Primitives ===
(declare render-element)
(defn- render-children
"Render all children and return list of rendered strings."
[children ctx]
(mapv #(render-element % ctx) children))
(defn- render-row
"Render :row - horizontal layout."
[{:keys [gap justify align] :or {gap 0}} children ctx]
(let [rendered (render-children children ctx)
separator (apply str (repeat gap " "))]
(str/join separator rendered)))
(defn- render-col
"Render :col - vertical layout."
[{:keys [gap] :or {gap 0}} children ctx]
(let [rendered (render-children children ctx)
separator (str/join (repeat gap "\n"))]
(str/join (str "\n" separator) rendered)))
(defn- render-box
"Render :box - bordered container."
[{:keys [border title padding width]
:or {border :rounded padding 0}}
children ctx]
(let [chars (get ansi/box-chars border (:rounded ansi/box-chars))
content (str/join "\n" (render-children children ctx))
lines (str/split content #"\n" -1)
;; Calculate padding
[pad-top pad-right pad-bottom pad-left]
(cond
(number? padding) [padding padding padding padding]
(vector? padding)
(case (count padding)
1 (let [p (first padding)] [p p p p])
2 (let [[v h] padding] [v h v h])
4 padding
[0 0 0 0])
:else [0 0 0 0])
;; Calculate content width
max-content-width (apply max 0 (map ansi/visible-length lines))
inner-width (+ max-content-width pad-left pad-right)
box-width (or width (+ inner-width 2))
content-width (- box-width 2)
;; Pad lines
padded-lines (for [line lines]
(str (apply str (repeat pad-left " "))
(ansi/pad-right line (- content-width pad-left pad-right))
(apply str (repeat pad-right " "))))
;; Add vertical padding
empty-line (apply str (repeat content-width " "))
all-lines (concat
(repeat pad-top empty-line)
padded-lines
(repeat pad-bottom empty-line))
;; Build box
top-line (str (:tl chars)
(if title
(str " " title " "
(apply str (repeat (- content-width (count title) 3) (:h chars))))
(apply str (repeat content-width (:h chars))))
(:tr chars))
bottom-line (str (:bl chars)
(apply str (repeat content-width (:h chars)))
(:br chars))
body-lines (for [line all-lines]
(str (:v chars)
(ansi/pad-right line content-width)
(:v chars)))]
(str/join "\n" (concat [top-line] body-lines [bottom-line]))))
(defn- render-space
"Render :space - empty space."
[{:keys [width height] :or {width 1 height 1}} _ _]
(let [line (apply str (repeat width " "))]
(str/join "\n" (repeat height line))))
;; === Main Render Function ===
(defn render-element
"Render a hiccup element to ANSI string."
[elem ctx]
(cond
;; Raw string - just return it
(string? elem) elem
;; Number - convert to string
(number? elem) (str elem)
;; Nil - empty string
(nil? elem) ""
;; Vector - hiccup element
(vector? elem)
(let [[tag attrs children] (parse-element elem)]
(case tag
:text (render-text attrs children)
:row (render-row attrs children ctx)
:col (render-col attrs children ctx)
:box (render-box attrs children ctx)
:space (render-space attrs children ctx)
;; Default: just render children
(apply str (render-children children ctx))))
;; Anything else - convert to string
:else (str elem)))
(defn render
"Render hiccup to ANSI string."
([hiccup] (render hiccup {}))
([hiccup ctx]
(render-element hiccup ctx)))
;; === Convenience Components ===
(defn text
"Create a text element."
[& args]
(if (map? (first args))
(into [:text (first args)] (rest args))
(into [:text {}] args)))
(defn row
"Create a row (horizontal) layout."
[& args]
(if (map? (first args))
(into [:row (first args)] (rest args))
(into [:row {}] args)))
(defn col
"Create a col (vertical) layout."
[& args]
(if (map? (first args))
(into [:col (first args)] (rest args))
(into [:col {}] args)))
(defn box
"Create a bordered box."
[& args]
(if (map? (first args))
(into [:box (first args)] (rest args))
(into [:box {}] args)))