nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

termeの操作関数を作る

以前、readline / editlineの依存から脱出しようと、自前のプロンプトを作成しました。
その機能に、termeモジュールという名前を付けました。
何も考えずにnptをコンパイルすると標準で使えるようになっています。

どうやって実現したのかというと、端末をraw modeにしてエスケープシーケンスをまともに扱っただけです。
あまりnptに機能を盛り込みたくなかったのですが、せっかく作ったので、端末の機能が使えるような関数を用意したら面白いのでは?
ということでやってみました。

関数はnpt-system:termeというものに集約されています。
たった一つの関数にまとめています。
nptはプロセスの起動を早くしたいため、極力関数を作りたくないのです。

あとでちゃんとしたドキュメントを用意しますが、適当な使い方を書いておきます。
書いておかないと自分が忘れますので。

★用意しました! Lisp関数仕様 - terme

まずはtermeを使えるようにします。
面倒なのでnpt-systemパッケージをuseします。

(use-package 'npt-system)

端末をraw modeにします。

(terme 'terme-begin)  ;; ★注意
-> #<PAPER ...>

返却値は変更前の端末情報を格納したPaperオブジェクトです。
「★注意」と書かれているように結構危険な命令であり、 raw modeに移行すると以降の表示がめちゃくちゃになってしまします。
終わるときは必ずterme-endを実行するようにしてください。

(terme 'terme-end paper)

典型的な使用例は下記のようになります。

(let ((paper (terme 'terme-begin)))
  (unwind-protect
    (progn
      ...)
    (terme 'terme-end paper)))

(progn ...)内では、入力、出力、文字種の変更、色の変更などができます。
format関数などは使えますが、出力はめちゃくちゃになりますのでお勧めしません。

最初にやることは、まずどうやって終了させるかです。
raw modeなので、Ctrl+CCtrl+Zなどを押しても入力として扱われるため反応しません。

キーの入力は、terme-inputによって行われます。
まずはCtrl + Cを補足する方法を説明します。

(multiple-value-bind (type value) (terme 'terme-input)
  (when (and (eq type 'terme-code)
             (eql value 3))
    ;; ★Ctrl + Cが押された
    ))

出力は次のようになります。

(terme 'terme-output x)

xは、文字、文字列、数値、nilのどれかです。
文字はそのまま出力。
文字列もそのまま出力。
数値はUnicodeとして出力。
そしてnilは、いわゆるfinish-outputです。
xは省略可能であり、そのときはnilになります。

覚えておかなければならないことがあり、 文字の出力の他にも、カーソル移動だったり、画面全消去などの操作があるのですが、 機能は全てエスケープシーケンスを文字として出力して実現しています。
よって、finish-outputである、(terme 'terme-output)を実行しない限り、 おそらくは画面に反映されません。

例をあげます。
カーソルを座標(3, 5)に移動する例は次のようになります。

(terme 'terme-move 3 5 :absolute)

もし即座に画面に反応させたいのであれば、次のように実行してください。

(terme 'terme-move 3 5 :absolute)
(terme 'terme-output)

ここまでわかれば使えると思います。

実行例を示します。

(use-package 'npt-system)

(defvar *x*)
(defvar *y*)
(defvar *cx*)
(defvar *cy*)
(defvar *rx*)
(defvar *ry*)
(defvar *i*)
(defvar *char*)

(defconstant pi2 (* 2.0d0 pi))

(defun set-position ()
  (let* ((i (mod *i* 360))
         (theta (float (/ (* pi2 i) 360.0d0)))
         (x (* *rx* (sin theta)))
         (y (* *ry* (cos theta))))
    (setq *x* (truncate (+ *cx* x)))
    (setq *y* (truncate (+ *cy* y)))
    (when (zerop i)
      (setq *char* (not *char*)))))

(defun main-output ()
  (let ((c (if *char* #\+ #\Space)))
    (set-position)
    (terme 'terme-font 'palback (mod *i* #xFF))
    (terme 'terme-move *x* *y* :absolute)
    (terme 'terme-output c)
    (terme 'terme-move 0 0 :absolute)
    (terme 'terme-output)))

(defun main-input ()
  (multiple-value-bind (type value) (terme 'terme-input 0.01)
    (when (and (eq type 'terme-code)
               (eql value 3))
      (return-from main-input nil))
    (main-output)
    (incf *i* 1))
  t)

(defun main-loop ()
  (terme 'terme-clear)
  (terme 'terme-output)
  (multiple-value-setq (*x* *y*) (terme 'terme-size))
  (setq *i* 1)
  (setq *cx* (* *x* 0.5d0))
  (setq *cy* (* *y* 0.5d0))
  (setq *rx* (* *cx* 0.9d0))
  (setq *ry* (* *cy* 0.9d0))
  (setq *char* t)
  (loop while (main-input)))

(defun main-clean (x)
  (terme 'terme-font nil)
  (terme 'terme-end x))

(defun main ()
  (let ((x (terme 'terme-begin)))
    (unwind-protect
      (main-loop)
      (main-clean x))))

(main)

終了はCtrl + Cを押してください。
実行画面は次の通り。

f:id:nptcl:20220214135517p:plain
terme操作関数・実行例

色がついてると綺麗ですね。

大昔、狂ったようにBASICをやっていた時代がありました。
家になぜか当時でも時代遅れの8bitのパソコンがあって、 何を作るわけでもなく、画面に文字を出して、 キーボードで移動させたりして遊んでいたのですが、 そんな時代を思い出してしまいました。

A$=INKEY$

だったかな。
不思議なことにあまり覚えてません。