(in-package #:cl-tui.input) ;;; --------------------------------------------------------------------------- ;;; Utility: split-string (avoids external dependency) ;;; --------------------------------------------------------------------------- (defun %split-string (string separator) "Split STRING at each occurrence of SEPARATOR. Returns list of strings." (loop with start = 0 for pos = (position separator string :start start) collect (subseq string start pos) while pos do (setf start (1+ pos)))) ;;; --------------------------------------------------------------------------- ;;; Global variables for rendering pipeline (set by application) ;;; --------------------------------------------------------------------------- (defvar *current-backend* nil "The active backend used for rendering.") (defvar *current-theme* nil "The active theme used for semantic color resolution.") ;;; --------------------------------------------------------------------------- ;;; Key event struct ;;; --------------------------------------------------------------------------- (defstruct key-event (key nil :type (or keyword null)) (ctrl nil :type boolean) (alt nil :type boolean) (shift nil :type boolean) (code nil :type (or fixnum null)) (raw nil :type (or string null)) (text nil :type (or string null))) ;;; --------------------------------------------------------------------------- ;;; Mouse event struct ;;; --------------------------------------------------------------------------- (defstruct mouse-event (type nil :type (or keyword null)) (button nil :type (or keyword nil)) (x 0 :type fixnum) (y 0 :type fixnum) (raw nil :type (or string null))) ;;; --------------------------------------------------------------------------- ;;; Terminal raw mode ;;; --------------------------------------------------------------------------- (defun save-terminal-state () (sb-posix:tcgetattr 0)) (defun make-raw-termios (termios) (flet ((clear-flag (flags mask) (logand flags (lognot mask)))) (setf (sb-posix:termios-iflag termios) (clear-flag (sb-posix:termios-iflag termios) (logior sb-posix:brkint sb-posix:ignpar sb-posix:istrip sb-posix:inlcr sb-posix:igncr sb-posix:icrnl sb-posix:ixon))) (setf (sb-posix:termios-oflag termios) (clear-flag (sb-posix:termios-oflag termios) sb-posix:opost)) (setf (sb-posix:termios-lflag termios) (clear-flag (sb-posix:termios-lflag termios) (logior sb-posix:icanon sb-posix:echo sb-posix:isig sb-posix:iexten))) (setf (sb-posix:termios-cc termios sb-posix:vmin) 1) (setf (sb-posix:termios-cc termios sb-posix:vtime) 0) termios)) (defun set-raw-mode () (let ((raw (make-raw-termios (save-terminal-state)))) (sb-posix:tcsetattr 0 sb-posix:tcsanow raw) raw)) (defun restore-terminal-state (termios) (sb-posix:tcsetattr 0 sb-posix:tcsanow termios)) (defmacro with-raw-terminal (&body body) (let ((saved (gensym "SAVED"))) `(let ((,saved (save-terminal-state))) (set-raw-mode) (unwind-protect (progn ,@body) (restore-terminal-state ,saved))))) ;;; --------------------------------------------------------------------------- ;;; Low-level byte reading ;;; --------------------------------------------------------------------------- (defun read-raw-byte (&key timeout) (if timeout (let ((deadline (+ (get-universal-time) timeout))) (loop while (< (get-universal-time) deadline) do (handler-case (let ((buf (make-array 1 :element-type '(unsigned-byte 8)))) (let ((n (sb-posix:read 0 buf 1))) (when (plusp n) (return-from read-raw-byte (aref buf 0))))) (sb-posix:syscall-error () (return-from read-raw-byte nil))) (sleep 0.01)) nil) (let ((buf (make-array 1 :element-type '(unsigned-byte 8)))) (multiple-value-bind (n err) (ignore-errors (sb-posix:read 0 buf 1)) (if (and (integerp n) (plusp n)) (aref buf 0) (progn (when err (format *error-output* "read error: ~A~%" err)) nil)))))) ;;; --------------------------------------------------------------------------- ;;; CSI parameter parser ;;; --------------------------------------------------------------------------- (defun parse-csi-params () (let ((params '()) (raw (make-array 0 :element-type '(unsigned-byte 8) :fill-pointer 0 :adjustable t)) (current 0)) (loop (let ((b (read-raw-byte))) (unless b (return (values nil nil nil))) (vector-push-extend b raw) (cond ((and (>= b #x30) (<= b #x3f)) (if (char= (code-char b) #\;) (progn (push current params) (setf current 0)) (setf current (+ (* current 10) (- b #x30))))) ((and (>= b #x20) (<= b #x2f)) nil) ((and (>= b #x40) (<= b #x7e)) (push current params) (return (values (nreverse params) b (map 'string #'code-char raw)))) (t (return (values nil nil nil)))))))) ;;; --------------------------------------------------------------------------- ;;; Key event tables ;;; --------------------------------------------------------------------------- (defparameter *csi-key-table* '((#\A . :up) (#\B . :down) (#\C . :right) (#\D . :left) (#\F . :end) (#\H . :home) (#\P . :f1) (#\Q . :f2) (#\R . :f3) (#\S . :f4) (#\Z . :tab))) (defparameter *csi-tilde-table* '((1 . :home) (2 . :insert) (3 . :delete) (4 . :end) (5 . :page-up) (6 . :page-down) (7 . :home) (8 . :end) (11 . :f1) (12 . :f2) (13 . :f3) (14 . :f4) (15 . :f5) (17 . :f6) (18 . :f7) (19 . :f8) (20 . :f9) (21 . :f10) (23 . :f11) (24 . :f12))) ;;; --------------------------------------------------------------------------- ;;; SGR mouse parser ;;; --------------------------------------------------------------------------- (defun parse-sgr-mouse (raw) (let* ((start (position #\< raw)) (end (position #\m raw :from-end t)) (end2 (position #\M raw :from-end t)) (final (if end end end2)) (releasep (char= (char raw (1- (length raw))) #\m))) (when (and start final (> final start)) (let* ((nums (mapcar #'parse-integer (%split-string (subseq raw (1+ start) final) #\;))) (code (first nums)) (x (or (second nums) 0)) (y (or (third nums) 0)) (button (logand code #x03)) (mod (logand code #x1c)) (motion (logand code #x20)) (wheel (logand code #x40))) (declare (ignore mod)) (make-mouse-event :type (cond (releasep :release) (motion :drag) (t :press)) :button (cond (wheel (if (zerop (logand code #x01)) :wheel-up :wheel-down)) ((= button 0) :left) ((= button 1) :middle) ((= button 2) :right) (t :none)) :x x :y y :raw raw))))) ;;; --------------------------------------------------------------------------- ;;; Escape sequence reader ;;; --------------------------------------------------------------------------- (defun %read-escape-sequence () (let ((b (read-raw-byte))) (unless b (return-from %read-escape-sequence (make-key-event :key :escape :raw (string #\Esc)))) (case b ;; SS3: ESC O X (#x4f (let ((b2 (read-raw-byte))) (if b2 (let ((key (cdr (assoc (code-char b2) '((#\P . :f1) (#\Q . :f2) (#\R . :f3) (#\S . :f4)))))) (make-key-event :key (or key :unknown) :raw (format nil "~C~C~C" #\Esc #\O (code-char b2)))) (make-key-event :key :escape :raw (string #\Esc))))) ;; CSI: ESC [ ... (#x5b (multiple-value-bind (params final-byte) (parse-csi-params) (if (null final-byte) (make-key-event :key :escape :raw (string #\Esc)) (if (and (char= (code-char final-byte) #\M) (>= (length params) 3)) (let* ((p0 (first params))) (if (zerop (logand p0 #x40)) (let* ((x (second params)) (y (third params)) (button (logand p0 #x03)) (motion (logand p0 #x20)) (wheel (logand p0 #x40))) (make-mouse-event :type (if motion :drag :press) :button (cond (wheel (if (zerop (logand p0 #x01)) :wheel-up :wheel-down)) ((= button 0) :left) ((= button 1) :middle) ((= button 2) :right) (t :none)) :x x :y y :raw (format nil "~C[<~d;~d;~d~C" #\Esc p0 x y (code-char final-byte)))) (let* ((tilde-p (char= (code-char final-byte) #\~)) (param (or p0 0)) (key (if tilde-p (cdr (assoc param *csi-tilde-table*)) (cdr (assoc (code-char final-byte) *csi-key-table*)))) (modifier (when (> (length params) 1) (second params)))) (let ((ctrl nil) (alt nil) (shift nil)) (when modifier (setf shift (logtest modifier 1) alt (logtest modifier 2) ctrl (logtest modifier 4))) (make-key-event :key (or key :unknown) :ctrl ctrl :alt alt :shift shift :raw (format nil "~C[~d~C" #\Esc param (code-char final-byte)))))) (let* ((tilde-p (char= (code-char final-byte) #\~)) (param (or (first params) 0)) (key (if tilde-p (cdr (assoc param *csi-tilde-table*)) (cdr (assoc (code-char final-byte) *csi-key-table*)))) (modifier (when (> (length params) 1) (second params)))) (let ((ctrl nil) (alt nil) (shift nil)) (when modifier (setf shift (logtest modifier 1) alt (logtest modifier 2) ctrl (logtest modifier 4))) (make-key-event :key (or key :unknown) :ctrl ctrl :alt alt :shift shift :raw (format nil "~C[~d~C" #\Esc param (code-char final-byte)))))))))) ;; ESC ESC (#x1b (make-key-event :key :escape :alt t :raw "\\e\\e")) ;; ESC + printable = Alt+key (t (let ((ch (code-char b))) (if (and (>= b #x20) (<= b #x7e)) (make-key-event :key (intern (string (string-upcase ch)) :keyword) :alt t :raw (format nil "~C~C" #\Esc ch)) (make-key-event :key :unknown :raw (format nil "~C~C" #\Esc ch)))))))) ;;; --------------------------------------------------------------------------- ;;; Top-level event reader ;;; --------------------------------------------------------------------------- (defun %read-event (&key timeout) (let ((b (read-raw-byte :timeout timeout))) (unless b (return-from %read-event nil)) (case b (#x1b (%read-escape-sequence)) (#x09 (make-key-event :key :tab :code #x09)) (#x0a (make-key-event :key :enter :code #x0a)) (#x0d (make-key-event :key :enter :code #x0d)) ((#x7f #x08) (make-key-event :key :backspace :code b)) ((and (>= b #x01) (<= b #x1a)) (let ((key (intern (string-upcase (string (code-char (+ b #x60)))) :keyword))) (make-key-event :key key :ctrl t :code b))) (#x1c (make-key-event :key :backslash :ctrl t :code b)) (#x1d (make-key-event :key :rbracket :ctrl t :code b)) (#x1e (make-key-event :key :caret :ctrl t :code b)) (#x1f (make-key-event :key :underscore :ctrl t :code b)) ((and (>= b #x20) (<= b #x7e)) (let ((ch (code-char b))) (make-key-event :key (intern (string (string-upcase ch)) :keyword) :code b))) (t (make-key-event :key :unknown :code b :raw (string (code-char b))))))) ;;; --------------------------------------------------------------------------- ;;; Backend integration ;;; --------------------------------------------------------------------------- (defmethod read-event ((b cl-tui.backend:backend) &key timeout) (declare (ignore b)) (when (probe-file "/dev/stdin") (%read-event :timeout timeout)))