288 lines
11 KiB
Common Lisp
288 lines
11 KiB
Common Lisp
(defpackage #:herecy-term
|
|
(:use #:cl #:ettram #:trivial-gray-streams)
|
|
(:export #:terminal #:move-cursor-relative #:clear-line
|
|
#:make-terminal))
|
|
|
|
(in-package #:herecy-term)
|
|
|
|
(defun row-col-to-point (row col)
|
|
(make-point (* (1- col) 8) (* (1- row) 16)))
|
|
|
|
(defclass cursor-state ()
|
|
((%row :accessor row :initform 1)
|
|
(%col :accessor col :initform 1)
|
|
(%blinkingp :accessor blinkingp :initform t)
|
|
(%blink-phase :accessor blink-phase :initform 0)
|
|
(%blink-state :accessor blink-state :initform t)
|
|
(%visiblep :accessor visiblep :initform t)))
|
|
|
|
(defmethod cursor-picture-position ((cursor cursor-state))
|
|
(with-accessors ((row row) (col col)) cursor
|
|
(row-col-to-point row col)))
|
|
|
|
(defmethod set-cursor ((cursor cursor-state) row col)
|
|
(setf (col cursor) col
|
|
(row cursor) row))
|
|
|
|
(defclass input-buffer ()
|
|
((%array :accessor internal-array :initform (make-array 80 :element-type 'character :adjustable t :fill-pointer 0))
|
|
(%write-index :accessor write-index :initform 0)
|
|
(%readout-index :accessor readout-index :initform 0)
|
|
(%finishedp :accessor finishedp :initform nil)))
|
|
|
|
#+(or)
|
|
(defmethod push-to-buffer ((buffer input-buffer) element)
|
|
(vector-push-extend element (internal-array buffer)))
|
|
|
|
(defmethod insert-into-buffer ((buffer input-buffer) element)
|
|
(let ((ia (internal-array buffer))
|
|
(index (write-index buffer)))
|
|
(loop :initially (vector-push-extend #\Null ia)
|
|
:for index :from (- (length ia) 1) :downto index
|
|
:do (setf (aref ia (1+ index)) (aref ia index)))
|
|
(setf (aref ia index) element)
|
|
(incf (write-index buffer))))
|
|
|
|
(defmethod rubout-from-buffer ((buffer input-buffer))
|
|
(unless (zerop (write-index buffer))
|
|
(let ((ia (internal-array buffer)))
|
|
(decf (write-index buffer))
|
|
(loop :for index :from (write-index buffer) :below (1- (length ia))
|
|
:do (setf (aref ia index) (aref ia (1+ index)))
|
|
:finally (pop-from-buffer buffer)))))
|
|
|
|
(defmethod consume-from-buffer ((buffer input-buffer))
|
|
(cond ((< (readout-index buffer) (length (internal-array buffer)))
|
|
(prog1
|
|
(aref (internal-array buffer) (readout-index buffer))
|
|
(incf (readout-index buffer))
|
|
(when (>= (readout-index buffer) (length (internal-array buffer)))
|
|
(reset-buffer buffer))))
|
|
(t
|
|
(prog1
|
|
:eof
|
|
(reset-buffer buffer)))))
|
|
|
|
(defmethod pop-from-buffer ((buffer input-buffer))
|
|
(unless (buffer-empty-p buffer)
|
|
(vector-pop (internal-array buffer))))
|
|
|
|
(defmethod finish-buffer ((buffer input-buffer))
|
|
(setf (finishedp buffer) t))
|
|
|
|
(defmethod reset-buffer ((buffer input-buffer))
|
|
(setf (finishedp buffer) nil
|
|
(fill-pointer (internal-array buffer)) 0
|
|
(readout-index buffer) 0
|
|
(write-index buffer) 0))
|
|
|
|
(defmethod buffer-empty-p ((buffer input-buffer))
|
|
(zerop (fill-pointer (internal-array buffer))))
|
|
|
|
(defclass terminal (fundamental-character-stream)
|
|
((%cursor-state :accessor cursor-state :initform (make-instance 'cursor-state))
|
|
(%width :accessor width :initarg :columns :initarg :width)
|
|
(%height :accessor height :initarg :rows :initarg :height)
|
|
(%screen-grid :accessor screen-grid :initarg :grid)
|
|
(%picture :accessor picture :initarg :picture)
|
|
(%input-buffer :accessor input-buffer :initarg :input-buffer)
|
|
(%overflowedp :accessor overflowedp :initform nil)
|
|
(%command-state :accessor command-state :initform :normal)))
|
|
|
|
(defun make-terminal (rows columns picture)
|
|
(make-instance 'terminal :picture picture :width columns :height rows
|
|
:grid (make-array (list rows columns) :element-type 'character
|
|
:initial-element #\Space)
|
|
:input-buffer (make-instance 'input-buffer)))
|
|
|
|
(defmethod draw-screen-grid ((terminal terminal))
|
|
(let ((original-row (row (cursor-state terminal)))
|
|
(original-col (col (cursor-state terminal))))
|
|
(loop :for col :from 1 :to (width terminal)
|
|
:with cursor := (cursor-state terminal)
|
|
:do (loop :for row :from 1 :to (height terminal)
|
|
:do (progn
|
|
(set-cursor cursor row col)
|
|
(print-to-picture (string (char-under-cursor terminal))
|
|
(cursor-picture-position cursor)
|
|
(picture terminal)))))
|
|
(set-cursor (cursor-state terminal) original-row original-col)))
|
|
|
|
(defmethod update-cursor ((terminal terminal))
|
|
(with-accessors ((cursor cursor-state)
|
|
(picture picture))
|
|
terminal
|
|
(incf (blink-phase cursor))
|
|
(when (>= (blink-phase cursor) *blink-duration*)
|
|
(setf (blink-phase cursor) 0
|
|
(blink-state cursor) (not (blink-state cursor)))
|
|
(let ((*fg-text-color* *fg-text-color*)
|
|
(*bg-text-color* *bg-text-color*))
|
|
(unless (blink-state cursor)
|
|
(rotatef *fg-text-color* *bg-text-color*))
|
|
(print-to-picture (string (char-under-cursor terminal))
|
|
(cursor-picture-position cursor) picture)))))
|
|
|
|
(defmethod reset-cursor-blinking ((terminal terminal))
|
|
(let ((cursor (cursor-state terminal)))
|
|
(setf (blink-state cursor) nil
|
|
(blink-phase cursor) *blink-duration*)
|
|
(update-cursor terminal)))
|
|
|
|
(defmethod scroll-down ((terminal terminal))
|
|
;; Scroll the screen grid
|
|
(let ((grid (screen-grid terminal)))
|
|
(loop :for y :from 0 :below (height terminal)
|
|
:unless (= y 0)
|
|
:do (dotimes (x (width terminal))
|
|
(setf (aref grid (1- y) x)
|
|
(aref grid y x))))
|
|
(dotimes (x (width terminal))
|
|
(setf (aref grid (1- (height terminal)) x)
|
|
#\Space)))
|
|
;; Move cursor up a step
|
|
(move-cursor-relative terminal -1 0)
|
|
;; Redisplay
|
|
(draw-screen-grid terminal))
|
|
|
|
(defmethod move-cursor-absolute ((terminal terminal) row col)
|
|
(let ((cursor (cursor-state terminal)))
|
|
(setf row (max 1 row)
|
|
col (max 1 col))
|
|
(when (> col (width terminal))
|
|
(setf col 1
|
|
row (1+ row)))
|
|
(cond ((> row (height terminal))
|
|
(setf row (height terminal))
|
|
(scroll-down terminal)
|
|
(setf (overflowedp terminal) t))
|
|
(t (setf (overflowedp terminal) nil)))
|
|
;; Ensure cursor blink is reset
|
|
(reset-cursor-blinking terminal)
|
|
;; Perform the move
|
|
(set-cursor cursor row col)
|
|
(setf (blink-state cursor) t
|
|
(blink-phase cursor) *blink-duration*)
|
|
(update-cursor terminal)))
|
|
|
|
(defmethod move-cursor-relative ((terminal terminal) row col)
|
|
(let ((cursor (cursor-state terminal)))
|
|
(move-cursor-absolute terminal
|
|
(+ (row cursor) row)
|
|
(+ (col cursor) col))))
|
|
|
|
(defmethod char-under-cursor ((terminal terminal))
|
|
(aref (screen-grid terminal)
|
|
(1- (row (cursor-state terminal)))
|
|
(1- (col (cursor-state terminal)))))
|
|
|
|
(defmethod put-character-at ((terminal terminal) row col char)
|
|
(ettram:print-to-picture (string char)
|
|
(row-col-to-point row col)
|
|
(picture terminal))
|
|
(setf (aref (screen-grid terminal) (1- row) (1- col)) char))
|
|
|
|
(defmethod put-character ((terminal terminal) char)
|
|
(with-accessors ((row row) (col col))
|
|
(cursor-state terminal)
|
|
(put-character-at terminal row col char)
|
|
(move-cursor-relative terminal
|
|
0
|
|
(if (= (character-width char) 8)
|
|
1
|
|
2))))
|
|
|
|
(defmethod clear-line ((terminal terminal))
|
|
(dotimes (col (width terminal))
|
|
(put-character-at terminal (row (cursor-state terminal)) (1+ col) #\Space)))
|
|
|
|
(defmethod stream-terpri ((stream terminal))
|
|
(move-cursor-relative stream 1 -1000))
|
|
|
|
(defmethod stream-write-char ((stream terminal) character)
|
|
(case (command-state stream)
|
|
(:normal
|
|
(case character
|
|
(#\Newline (unless (overflowedp stream)
|
|
(terpri stream)))
|
|
(#\Return (unless (overflowedp stream)
|
|
(terpri stream)))
|
|
(#\Escape (setf (command-state stream) :escape))
|
|
(t (put-character stream character))))
|
|
(:escape
|
|
(cerror "I also like to live dangerously." "Escape codes not implemented.")
|
|
(case character
|
|
(#\[ (setf (command-state stream) :csi))))
|
|
(:csi
|
|
(cerror "I also like to live dangerously." "CSI codes not implemented."))))
|
|
|
|
(defmethod stream-write-string ((stream terminal) string &optional start end)
|
|
(let ((str string))
|
|
(unless (null start)
|
|
(setf str (subseq string start end)))
|
|
(loop :for char :across str :do (write-char char stream))))
|
|
|
|
(defmethod stream-read-line ((stream terminal))
|
|
(let ((buffer (input-buffer stream)))
|
|
(loop :until (finishedp buffer)
|
|
:do (sleep 1/30))
|
|
(prog1
|
|
(subseq (internal-array buffer)
|
|
0 (1- (length (internal-array buffer))))
|
|
(reset-buffer buffer))))
|
|
|
|
(defmethod stream-read-char ((stream terminal))
|
|
(loop :until (finishedp (input-buffer stream))
|
|
:do (sleep 1/30))
|
|
(let ((popped (consume-from-buffer (input-buffer stream))))
|
|
(when (buffer-empty-p (input-buffer stream))
|
|
(reset-buffer (input-buffer stream)))
|
|
popped))
|
|
|
|
(defmethod stream-force-output ((stream terminal)))
|
|
|
|
(defparameter *blink-duration* 30)
|
|
|
|
(defmethod update ((obj terminal) delta)
|
|
;; Get keyboard input
|
|
(let ((events (copy-events)))
|
|
(dolist (event events)
|
|
(typecase event
|
|
(char-type-event
|
|
(let ((char (codepoint event))
|
|
(ib (input-buffer obj))
|
|
(cursor (cursor-state obj)))
|
|
(insert-into-buffer ib char)
|
|
(put-character obj char)
|
|
(when (< (write-index ib) (length (internal-array ib)))
|
|
(let ((row (row cursor))
|
|
(col (col cursor)))
|
|
(loop :for char :across (subseq (internal-array ib) (write-index ib))
|
|
:do (put-character obj char))
|
|
(move-cursor-absolute obj row col)))))
|
|
(key-press-event
|
|
(when (press-p event)
|
|
(case (key-symbol event)
|
|
(:enter
|
|
(setf (write-index (input-buffer obj))
|
|
(length (internal-array (input-buffer obj))))
|
|
(write-char #\Newline obj)
|
|
(insert-into-buffer (input-buffer obj) #\Newline)
|
|
(finish-buffer (input-buffer obj)))
|
|
(:backspace
|
|
(unless (zerop (write-index (input-buffer obj)))
|
|
(move-cursor-relative obj 0 -1)
|
|
(put-character obj #\Space)
|
|
(move-cursor-relative obj 0 -1)
|
|
(rubout-from-buffer (input-buffer obj))))
|
|
(:left
|
|
(unless (zerop (write-index (input-buffer obj)))
|
|
(move-cursor-relative obj 0 -1)
|
|
(decf (write-index (input-buffer obj)))))
|
|
(:right
|
|
(unless (>= (write-index (input-buffer obj))
|
|
(length (internal-array (input-buffer obj))))
|
|
(move-cursor-relative obj 0 1)
|
|
(incf (write-index (input-buffer obj)))))))))))
|
|
;; Update cursor
|
|
(update-cursor obj))
|