150 lines
3.7 KiB
Clojure
150 lines
3.7 KiB
Clojure
(ns tui.terminal
|
|
"Terminal management: raw mode, size, input/output."
|
|
(:require [tui.ansi :as ansi]
|
|
[clojure.java.io :as io])
|
|
(:import [java.io BufferedReader InputStreamReader]))
|
|
|
|
;; === Terminal State ===
|
|
(def ^:private original-stty (atom nil))
|
|
|
|
(defn- stty [& args]
|
|
(let [cmd (concat ["sh" "-c" (str "stty " (clojure.string/join " " args) " </dev/tty")]
|
|
(when (empty? args) ["sh" "-c" "stty </dev/tty"]))
|
|
pb (ProcessBuilder. ^java.util.List (vec cmd))
|
|
_ (.inheritIO pb)
|
|
proc (.start pb)
|
|
exit (.waitFor proc)]
|
|
(when (zero? exit)
|
|
"")))
|
|
|
|
(defn- stty-get [& args]
|
|
(let [cmd ["sh" "-c" (str "stty " (clojure.string/join " " args) " </dev/tty")]
|
|
pb (ProcessBuilder. ^java.util.List (vec cmd))
|
|
_ (.redirectInput pb java.lang.ProcessBuilder$Redirect/INHERIT)
|
|
proc (.start pb)
|
|
output (slurp (.getInputStream proc))
|
|
exit (.waitFor proc)]
|
|
(when (zero? exit)
|
|
(clojure.string/trim output))))
|
|
|
|
(defn get-terminal-size
|
|
"Get terminal dimensions as [width height]."
|
|
[]
|
|
(try
|
|
(let [result (stty-get "size")]
|
|
(when result
|
|
(let [[rows cols] (map parse-long (clojure.string/split result #"\s+"))]
|
|
{:width cols :height rows})))
|
|
(catch Exception _
|
|
{:width 80 :height 24})))
|
|
|
|
(defn raw-mode!
|
|
"Enter raw terminal mode (no echo, no line buffering)."
|
|
[]
|
|
(reset! original-stty (stty-get "-g"))
|
|
(stty "raw" "-echo" "-icanon" "min" "1")
|
|
(print ansi/hide-cursor)
|
|
(flush))
|
|
|
|
(defn restore!
|
|
"Restore terminal to original state."
|
|
[]
|
|
(when @original-stty
|
|
(stty @original-stty)
|
|
(reset! original-stty nil))
|
|
(print ansi/show-cursor)
|
|
(print ansi/reset)
|
|
(flush))
|
|
|
|
(defn alt-screen!
|
|
"Enter alternate screen buffer."
|
|
[]
|
|
(print ansi/enter-alt-screen)
|
|
(flush))
|
|
|
|
(defn exit-alt-screen!
|
|
"Exit alternate screen buffer."
|
|
[]
|
|
(print ansi/exit-alt-screen)
|
|
(flush))
|
|
|
|
(defn clear!
|
|
"Clear screen and move cursor home."
|
|
[]
|
|
(print ansi/clear-screen)
|
|
(print ansi/cursor-home)
|
|
(flush))
|
|
|
|
(defn render!
|
|
"Render string to terminal."
|
|
[s]
|
|
(print ansi/cursor-home)
|
|
(print ansi/clear-to-end)
|
|
;; In raw mode, \n only moves down without returning to column 0
|
|
;; Replace \n with \r\n to get proper line breaks
|
|
(print (clojure.string/replace s "\n" "\r\n"))
|
|
(flush))
|
|
|
|
;; === Input Handling ===
|
|
(def ^:private tty-reader (atom nil))
|
|
|
|
(defn init-input!
|
|
"Initialize input reader from /dev/tty."
|
|
[]
|
|
(reset! tty-reader
|
|
(BufferedReader.
|
|
(InputStreamReader.
|
|
(java.io.FileInputStream. "/dev/tty")))))
|
|
|
|
(defn close-input!
|
|
"Close input reader."
|
|
[]
|
|
(when-let [r @tty-reader]
|
|
(.close r)
|
|
(reset! tty-reader nil)))
|
|
|
|
(defn input-ready?
|
|
"Check if input is available without blocking."
|
|
[]
|
|
(when-let [r @tty-reader]
|
|
(.ready r)))
|
|
|
|
(defn read-char
|
|
"Read a single character. Blocking."
|
|
[]
|
|
(when-let [r @tty-reader]
|
|
(let [c (.read r)]
|
|
(when (>= c 0)
|
|
(char c)))))
|
|
|
|
(defn read-available
|
|
"Read all available characters without blocking."
|
|
[]
|
|
(when-let [r @tty-reader]
|
|
(loop [chars []]
|
|
(if (.ready r)
|
|
(let [c (.read r)]
|
|
(if (>= c 0)
|
|
(recur (conj chars (char c)))
|
|
chars))
|
|
chars))))
|
|
|
|
(defn read-char-timeout
|
|
"Read char with timeout in ms. Returns nil on timeout."
|
|
[timeout-ms]
|
|
(when-let [r @tty-reader]
|
|
(let [deadline (+ (System/currentTimeMillis) timeout-ms)]
|
|
(loop []
|
|
(cond
|
|
(.ready r)
|
|
(let [c (.read r)]
|
|
(when (>= c 0) (char c)))
|
|
|
|
(> (System/currentTimeMillis) deadline)
|
|
nil
|
|
|
|
:else
|
|
(do
|
|
(Thread/sleep 1)
|
|
(recur)))))))
|