186 lines
5.7 KiB
Clojure
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)))
|