本文へ移動
Hikari 仕様

std:term — 端末制御

標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。

import term := "std:term"raw_mode / read_key / peek_key / size の 4 slot を持つ namespace object を term に束縛する。
対象はプロセスの標準入力 (raw_mode / read_key / peek_key) と標準出力 (size)。

本モジュールは 1 バイト単位の生 I/O を担う。バイト列を矢印・マウス・修飾キー等の高水準イベントへデコードする層は term/event.md (std:term/event)、代替画面・カーソル・色などの出力制御列は term/screen.md (std:term/screen) を参照。

wasm ビルドでは、Playground (web/play.html) が実行対象ソースの std:termimport を自動判定し、対話端末 (xterm.js + Web Worker) 上で実行する。この TUI 実行では JS 側 (xterm.js) が供給する入力キューと固定の端末サイズにより raw_mode / read_key / peek_key / size のすべてが動作する。ブラウザー内で完結する擬似端末という位置づけで、本書のその他の意味論 (戻り値・エラーモデル・タイムアウト挙動など) は通常の実端末実行と同一。

一方 wasm の REPL / Playground (hikari.eval / hikari.run。ホストが入力を供給しない) には実端末が無い。raw_mode は端末モードを切り替えず block をそのまま実行して成功する (no-op。§6 のヘッドレスモードと同じ挙動)。read_key は入力キューが空なら即 Err({kind := "io_error" …}) を返し (ブロックしない)、peek_key は先読みバッファーが常に空のため Err({kind := "empty" …}) を返す。size はホストが端末サイズを指定していないため Err({kind := "not_a_terminal" …}) を返す。

import { raw_mode, read_key, peek_key, size } := "std:term"
名前 効果 意味
raw_mode raw_mode(block) Io(ブロックを透過) 端末を raw mode にして block を実行し、終了時に必ず元の設定へ復元する (同期)
read_key read_key(timeout_ms) Io 標準入力から 1 バイト読み、Future を返す。timeout_ms省略できない (index.md)
peek_key peek_key() Io 先読み済みバッファーの先頭 1 バイトを消費せず返す (非ブロッキング・同期)。無ければ empty
size size() Io 端末の行数・列数を返す (同期)

1. term.raw_mode

  • 引数: block (評価可能なブロック) 1 個
  • 端末を raw mode に切り替え、block を呼び出しフロー上で同期実行する。block 終了時 (正常・異常を問わず) に元の端末設定へ復元する
  • 戻り値は Result(T):
    • 成功時: Ok(result) — result は block の評価値
    • 失敗時: Err(e) — 標準入力が端末でない等で raw mode に切り替えられないとき。e は {kind, message} (§5)
  • block の評価中に panic 型エラー (language-spec.md §16) が生じた場合は、端末を復元したうえでそのエラーをそのまま伝播する (Result に包まない)
import { raw_mode, read_key } := "std:term"

(raw_mode {
  # ここは raw mode。read_key で 1 キーずつ読める
  read_key(0)!
}).unwrap_or_else { e | print e.message }

2. term.read_key

  • 引数: タイムアウト timeout_ms (Int, ミリ秒) を 1 個。省略できない (index.md) — 0 でブロック
  • 戻り値: Future! で resolve 値を待つ (prelude.md §9.4 のブロッキング I/O 規則に乗る)
  • resolve 値は Result(Int):
    • 成功時: Ok(byte) — byte は読み取った 1 バイトの Int (0〜255)
    • 失敗時: Err(e) — 入力終端や I/O 失敗。e は {kind, message} (§5)
  • タイムアウト (timeout_ms > 0 のとき):
    • timeout_ms 以内に入力が来なければ Err({kind := "timeout" …}) で resolve する
    • タイムアウト時は 1 バイトも消費しない (次の read_key がそのバイトを読める)。ESC 単押しと矢印/マウス等の多バイト列の判別に使う
  • raw_mode ブロックの内側で使うこと。raw mode 外でも 1 バイト読めるが、行バッファー越しになり TUI 用途では意図通り動かない
  • 矢印キー等は複数バイトのエスケープ列 (例: ESC [ A) として届く。複数回 read_key して解釈するのは利用側の責務 (本モジュールはバイト列のデコードを行わない)
match read_key(0)! {
  Ok(key) => if (key == 113) { print("quit") } { print("byte: ${key}") }  # 113 = "q"
  Err(e) => print(e.message)
}

read_key は実装上、その時点で利用可能な入力をまとめて先読みしバッファーすることがある。読み込んだバイトは後続の read_key が順に返し、未消費のバイトは peek_key で消費せず覗ける。外部から見た「1 回の read_key = 1 バイト」という契約は変わらない。タイムアウト時 (timeout_ms>0 で間に合わないとき) は従来どおり 1 バイトも消費しない。

3. term.peek_key

  • 引数: なし
  • 同期 (Future ではない)。read_key が先読みしたバッファーの先頭を消費せずに返す
  • 戻り値は Result(Int):
    • バッファーに未消費バイトがあるとき: Ok(byte) — byte は先頭 1 バイトの Int (0〜255)。消費しない (続けて read_key すれば同じバイトが返る)
    • バッファーが空のとき: Err(e) — e は {kind, message} (§5)。kind は emptyread せず即座に返す (入力を待たない)
  • 用途: エスケープ列の判別。ESC を読んだ後 peek_key()empty なら後続は無く単独 ESC[ 等の既知 prefix なら列。バイトを消費しないので「列でなければ次の read_key にそのまま流す」ができる
import { read_key, peek_key } := "std:term"

read_key(0)!.unwrap!  # 27 (ESC) を読む
when peek_key().is_err! {  # empty → 単独 ESC
  print("lone ESC")
}

4. term.size

  • 引数: なし
  • 戻り値は Result:
    • 成功時: Ok(dim) — dim は {rows, cols} の 2 slot を持つデータオブジェクト (ともに Int)。dim.rows が行数、dim.cols が列数
    • 失敗時: Err(e) — 標準出力が端末でないとき。e は {kind, message} (§5)
match size() {
  Ok(dim) => print("${dim.rows} x ${dim.cols}")
  Err(e) => print(e.message)
}

5. エラーモデル

失敗時の err は回復可能エラーオブジェクト {kind, message} (language-spec.md §16)。std:term が返す正規 kind (閉じた一覧):

kind 意味
not_a_terminal 対象 fd が端末でない (raw mode 化・サイズ取得ができない)
eof read_key で入力が終端に達した
timeout read_key(timeout_ms)timeout_ms 内に入力を得られなかった (バイトは未消費)
empty peek_key 時点で先読みバッファーに未消費入力が無い
io_error その他の I/O 失敗

messageterm.read_key: ... のように関数名を含む人間可読文字列。分岐は kind で行うこと。

6. ヘッドレス(テスト)モード

全画面 TUI は size/raw_mode が端末 (TTY) を要求するため、TTY の無い環境 (CI・パイプ駆動の自動テスト) ではループを回せない。ヘッドレスモードは標準入出力をパイプにしたまま TUI を駆動できるようにする。実端末を仮想化して入力を注入し出力を捕捉する、という TUI ライブラリのテスト方針 (tcell の simulation screen 等) と同型。

活性化: 環境変数 HIKARI_TERM_HEADLESS非空かつ 0 以外のとき有効。未設定なら挙動は通常モードと完全に同一。

端末サイズ: HIKARI_TERM_SIZE"<rows>x<cols>" 形式で与える (例 24x80 = 24 行 80 列)。未設定・パース不能時の既定は 24x80

ヘッドレス時の各関数:

関数 挙動
size() TTY を問い合わせず、設定サイズの Ok(dim) を返す (dim.rows/dim.cols)。失敗しない
raw_mode(block) 端末モードを切り替えず block をそのまま実行し Ok(block評価値) を返す。block が panic 型エラーを返した場合は通常モードと同じくそのまま伝播する。失敗しない
read_key(timeout_ms) 通常モードと同一 (標準入力から 1 バイト)。標準入力がパイプなら流し込んだバイトを順に読み、尽きたら Err({kind := "eof" …})
peek_key() 通常モードと同一 (先読みバッファーの先頭を消費せず覗く)。バッファー参照のみで TTY 非依存
出力 (print 等) 通常モードと同一 (標準出力)。テスト側で捕捉する

これにより、スクリプト化した入力バイト列を標準入力へ流し、標準出力に現れる ESC 列・描画を検証することで、size → raw_mode → render → read_key → decode → update のループ全体を TTY 無しでテストできる。

# Cherry 行(画面行4)への左クリック SGR を流してデモを駆動する例
printf '\x1b[<0;5;4M' | HIKARI_TERM_HEADLESS=1 HIKARI_TERM_SIZE=24x80 \
  hikari examples/tui_app/main.hika

入力が尽きると read_keyeof を返すので、TUI ループは (eof でループ終了する作りなら) 自然に終了する。