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:term の import を自動判定し、対話端末 (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 はempty。read せず即座に返す (入力を待たない) - 用途: エスケープ列の判別。
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 失敗 |
message は term.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_key は eof を返すので、TUI ループは (eof でループ終了する作りなら) 自然に終了する。