init
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
(ns tui.terminal
|
||||
"Terminal management: raw mode, size, input/output."
|
||||
(:require [tui.ansi :as ansi]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.java.shell :refer [sh]])
|
||||
(:import [java.io BufferedReader InputStreamReader]))
|
||||
|
||||
;; === Terminal State ===
|
||||
(def ^:private original-stty (atom nil))
|
||||
|
||||
(defn- stty [& args]
|
||||
(let [result (apply sh "stty" (concat args [:in (io/file "/dev/tty")]))]
|
||||
(when (zero? (:exit result))
|
||||
(clojure.string/trim (:out result)))))
|
||||
|
||||
(defn get-terminal-size
|
||||
"Get terminal dimensions as [width height]."
|
||||
[]
|
||||
(try
|
||||
(let [result (stty "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 "-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)
|
||||
(print s)
|
||||
(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 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)))))))
|
||||
Reference in New Issue
Block a user