6.0 KiB
6.0 KiB
OpenCortex TUI Client (Standalone)
Overview
The OpenCortex TUI Client is a standalone Common Lisp application built on Croatoan.
Implementation
Package Context
(in-package :cl-user)
(defpackage :opencortex.tui
(:use :cl :croatoan :usocket :bordeaux-threads)
(:export :main))
(in-package :opencortex.tui)
Global State
(defvar *daemon-host* "127.0.0.1")
(defvar *daemon-port* 9105)
(defvar *socket* nil)
(defvar *stream* nil)
(defvar *chat-history* nil)
(defvar *scroll-index* 0)
(defvar *input-buffer* (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t))
(defvar *is-running* t)
(defvar *queue-lock* (bt:make-lock))
(defvar *incoming-msgs* nil)
Utilities
(defun enqueue-msg (msg)
"Thread-safe addition to incoming message queue."
(bt:with-lock-held (*queue-lock*)
(setf *incoming-msgs* (append *incoming-msgs* (list msg)))))
(defun dequeue-msgs ()
"Thread-safe retrieval of incoming messages."
(bt:with-lock-held (*queue-lock*)
(let ((msgs *incoming-msgs*))
(setf *incoming-msgs* nil)
msgs)))
Rendering
(defun render-chat (win h)
(when (and win (integerp h))
(clear win)
(box win 0 0)
(let* ((view-height (- h 2))
(history (reverse *chat-history*))
(len (length history))
(num-to-draw (min len view-height)))
(loop for i from 0 below num-to-draw
for msg in history
do (when (and msg (< (1+ i) (1- h)))
(add-string win (format nil "~a" msg) :y (1+ i) :x 2))))
(refresh win)))
Input Handling
(defun handle-backspace ()
(when (> (fill-pointer *input-buffer*) 0)
(decf (fill-pointer *input-buffer*))))
(defun handle-return (stream)
(let ((cmd (coerce *input-buffer* 'string)))
(setf (fill-pointer *input-buffer*) 0)
(when (> (length cmd) 0)
(enqueue-msg (format nil "⬆ ~a" cmd))
(handler-case
(progn
(when (and stream (open-stream-p stream))
(let* ((msg (list :TYPE :EVENT
:META (list :SOURCE :tui)
:PAYLOAD (list :SENSOR :user-input :TEXT cmd)))
(payload (format nil "~s" msg))
(len (length payload)))
(format stream "~6,'0x~a" len payload)
(finish-output stream)))
(enqueue-msg "✓ Sent"))
(error (c)
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
(setf *is-running* nil))))
(when (string= cmd "/exit") (setf *is-running* nil))
(when (string= cmd "/clear") (setf *chat-history* nil))))
Background Reader
(defun start-background-reader (stream)
(bt:make-thread
(lambda ()
(loop while *is-running* do
(handler-case
(let* ((len-buf (make-string 6))
(count (read-sequence len-buf stream)))
(when (= count 6)
(let* ((msg-len (parse-integer len-buf :radix 16))
(msg-buf (make-string msg-len)))
(read-sequence msg-buf stream)
(let ((msg (read-from-string msg-buf)))
(let ((payload (getf msg :payload)))
(cond
((eq (getf payload :action) :handshake)
(enqueue-msg "* Connected to daemon *"))
(t
(let ((text (or (getf payload :text) (format nil "~a" payload))))
(enqueue-msg (format nil "⬇ ~a" text))))))))))
(error (c)
(when *is-running*
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
(setf *is-running* nil))))))
:name "opencortex-tui-reader"))
Main Entry Point
(defun main ()
(setf (uiop:getenv "PROVIDER_CASCADE") "openrouter,openai")
(handler-case
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
(setf *stream* (usocket:socket-stream *socket*))
(unless (uiop:getenv "TERM")
(format t "TUI requires a terminal. Set TERM environment variable.~%")
(return-from main))
(unwind-protect
(handler-case
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
(let* ((h (or (height scr) 24))
(w (or (width scr) 80))
(chat-h (- h 4))
(input-y (- h 2)))
(let ((chat-win (make-instance 'window :height chat-h :width (- w 2) :y 1 :x 1))
(input-win (make-instance 'window :height 1 :width (- w 2) :y input-y :x 1)))
(setf (input-blocking input-win) nil)
(start-background-reader *stream*)
(loop :while *is-running* :do
(let ((msgs (dequeue-msgs)))
(when msgs
(dolist (m msgs) (push m *chat-history*))
(render-chat chat-win chat-h)))
(let* ((ev (get-event input-win))
(ch (when (and ev (typep ev 'event)) (event-key ev))))
(when ch
(cond
((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*))
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
((characterp ch) (vector-push-extend ch *input-buffer*))))
(clear input-win)
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
(refresh input-win))
(sleep 0.01)))))
(error (c)
(format t "TUI Error: ~a~%" c)))
(setf *is-running* nil)
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))