TTY

ターミナルの入力イベント、スタイル付き出力、プレゼンテーションサーフェス、ローカルな仮想ビューポート。

すべての関数は、呼び出し元のプロセスフレームにアタッチされたターミナルポートを解決します。ターミナルホスト上のプロセスは物理ターミナルを所有します。通常の process.host 上の process.lua は、ビューポートグラント付きでスポーンされた場合に仮想ターミナルを所有します。どちらのアタッチもない場合、このモジュールは "no terminal context" を返します。

ロード

local tty = require("tty")

モデル

サーフェスは、1つのプロセスがそのターミナルポートに対して持つ排他的なプレゼンテーションリースです。完全な行スナップショットを公開し、差分計算とターミナルの復旧はバックエンドが担います。1つのポートで同時に開けるサーフェスは1つだけです。

キャンバスは、プロセス内のスタイル付きセルの合成バッファです。セル境界でクリップし、独自のターミナル制御コマンドを送出することはありません。

ビューポートは、バイトストリームを共有せずに1つのプロセスが別のプロセスのサーフェスをホストできるようにする、ローカルで構造化されたターミナル境界です。シェルはビューポートの内容をどこに表示するかを決め、入力を子プロセスの座標系へ変換します。子プロセスは通常のターミナルポートを見ており、自身が全画面なのか、タイル表示なのか、タブ内なのか、非表示なのかを知りません。

ビューポートは1つのランタイムノード内にローカルです。グラントとハンドルは不透明なローカルケーパビリティであり、シリアライズ可能なネットワーク参照ではありません。

入力ループ

入力配信を開始し、イベントを購読し、ループで処理します:

local tty = require("tty")
local io = require("io")

local function handler()
    local events = tty.events()
    tty.start()

    while true do
        local ev, open = events:receive()
        if not open then break end

        if ev.type == "key" then
            if ev.key == "q" or (ev.ctrl and ev.key == "c") then
                break
            end
            local _, print_err = io.print("Key: " .. ev.key)
            if print_err then loop_err = print_err; break end

        elseif ev.type == "resize" then
            local _, print_err = io.print("Size: " .. ev.width .. "x" .. ev.height)
            if print_err then loop_err = print_err; break end
        end
    end

    local _, stop_err = tty.stop()
    if loop_err then return nil, loop_err end
    if stop_err then return nil, stop_err end
    return started
end

最初のイベントが届いたときに受信側が準備できているよう、events() は start() より前に呼び出してください。仮想ポートでは start() がビューアからプロデューサーへのイベント配信を開き、stop() がそれを閉じます。この区間の外での Viewport:send() は、入力を黙って破棄するのではなく失敗します。リサイズの配信は入力状態とは独立しています。

入力制御

tty.start()

現在のポートの入力配信を開始します。物理ターミナルは生モードへ切り替わります。

local ok, err = tty.start()

戻り値: boolean, error

tty.stop()

入力配信を停止し、ターミナルを通常モードへ戻します。

local ok, err = tty.stop()

戻り値: boolean, error

tty.events()

ポートのターミナルイベントを購読し、チャネルを返します。イベントは type フィールドを持つテーブルとして配信されます。購読は一度だけ行い、チャネルを再利用してください。

local events, err = tty.events()

戻り値: EventChannel, error

EventChannel は receive() と case_receive() を持つため、channel.select と組み合わせられます。

tty.screen_size()

現在のターミナルの大きさを問い合わせます。

local width, height, err = tty.screen_size()

戻り値: number, number, error

tty.mouse(enable)

マウスイベントトラッキングを有効化または無効化します。

local ok, err = tty.mouse(true)
パラメータ 型 説明
enable boolean 有効化する場合は true、無効化する場合は false

戻り値: boolean, error

サーフェス

サーフェスはポートのプレゼンテーションリースです。取得し、完全なフレームを公開し、終わったら閉じます。

tty.surface(options?)

local surface, err = tty.surface({
    alternate_screen = true,
    hide_cursor = true,
    synchronized_output = true,
})
オプション 型 デフォルト 説明
alternate_screen boolean false ターミナルの代替スクリーンバッファ上に表示する
hide_cursor boolean false サーフェスが開いている間、ターミナルカーソルを隠す
synchronized_output boolean false 各フレームを同期出力マーカーで囲む

戻り値: Surface, error

すでにサーフェスがあるポートで2つ目のサーフェスを開くと失敗します。仮想ポートはこれらのオプションをサーフェスのメタデータとして保持し、物理ポートはターミナルモードへ変換してクローズ時に復元します。

surface:present(rows, options?)

行文字列の完全な配列を公開します。行 1 が最上行です。

local stats, err = surface:present(rows, {
    cursor = {x = 12, y = 3, visible = true},
    images = {
        {placement_id = "logo", image = logo, x = 2, y = 2, cols = 20, rows = 8, alt = "Logo"},
    },
})
パラメータ 型 説明
rows string[] 完全なフレーム。最大 16384 行
options.cursor table 1始まりのサーフェス座標での {x, y, visible}
options.images table[] フレームで保持する画像配置の完全なセット

cursor を省略すると、最後に明示されたカーソル状態が維持されます。cursor を指定する場合、3つのフィールドすべてが必要です。

戻り値: stats, error — rows、changed_rows、bytes_written を持つイミュータブルなレコード。直前と同一の物理フレームは何も書き込みません。

surface:invalidate()

論理フレームを消すことなく、バックエンドのプレゼンテーション状態を忘れます。次の present は行が変化していなくてもコミットされます。外側のターミナルがリサイズされた後や、別の所有者が物理状態を乱した可能性がある場合に使用します。

戻り値: boolean

surface:close()

リースを解放します。冪等で、以降の呼び出しは最初のクローズ結果を返します。物理バックエンドはターミナルモードを復元します。

戻り値: boolean, error

surface:capabilities()

{images = "native" | "kitty" | "pending" | "none"} を返します。検査の前に ターミナル入力を開始してください。物理バックエンドは問い合わせ中に一時的に pending を返すことがあり、仮想サーフェスは検査なしで画像を保持します。

戻り値: table, error

surface:clipboard(text)

物理サーフェスに OSC 52 クリップボード要求を書き込みます。テキストは有効な UTF-8 で最大 65,536 バイトです。成功は出力が要求を受け付けたことを示すだけで、 ターミナルのポリシーにより無視されることがあります。仮想サーフェスは未サポート エラーを返し、読み取りや確認 API はありません。

戻り値: boolean, error

保持された画像

PNG を上限付きランタイムストレージへ取り込み、ハンドルを完全なフレームに配置します。

local image = assert(tty.image(png_bytes))
local info = image:info() -- id, format, width, height, bytes
assert(surface:present(rows, {images = {{
    placement_id = "preview", image = image,
    x = 1, y = 1, cols = 40, rows = 12,
    src = {x = 0, y = 0, width = info.width, height = info.height},
    z = 1, alt = "Preview",
}}}))

tty.image() は PNG バイトを非同期に検証します。image:read() はエンコード済み バイトを明示的にエクスポートし、image:close() は参照を解放します。元画像の ピクセル座標は 0 始まり、宛先セル座標は 1 始まりです。後続の present で images を省略すると以前の配置が消去されます。未対応の物理ターミナルでは alt テキストを表示し、仮想サーフェスではビューア向けに画像を保持します。

キャンバス

キャンバスは、フレームを表示前に合成するための、境界を持つスタイル付きセルバッファです。

tty.canvas(width, height)

local canvas = tty.canvas(width, height)

幅は 16384 カラム、高さは 16384 行、面積は 262,144 セルが上限です。範囲外の引数は引数エラーを発生させます。

戻り値: Canvas

描画はターミナルコマンドではなく、スタイル付きテキストを受け付けます。SGR カラーと OSC 8 リンクは保持されます。消去やカーソル移動などの制御専用の出力は送出されません。各配置はグラフィムの幅を考慮してセル境界で個別にクリップされるため、クリップされたエスケープシーケンスが隣接するコンテンツへ漏れることはありません。

canvas:clear(fill?)

すべてのセルをクリアします。オプションのスタイル付き fill 文字列は各行にわたって繰り返されます。

canvas:clear()
canvas:clear(tty.style():background("#1a1a1a"):render(" "))

戻り値: boolean

canvas:put(x, y, text, width?)

1始まりの x、y にスタイル付きの1行を配置し、width セル(デフォルトはキャンバス幅)にクリップします。座標は負の値や端を越えた値でもよく、その場合は拒否されるのではなくクリップされます。改行はその行を終端するため、複数行のコンテンツには put_rows を使用してください。

canvas:put(3, 1, tty.style():bold():render("Title"), 40)

戻り値: boolean

canvas:put_rows(x, y, rows, width?)

x、y を起点に、スタイル付きの行の配列を1行ずつ下方向に配置します。描画開始前にすべての要素が検証されます。

canvas:put_rows(2, 2, child_rows, inner_width)

戻り値: boolean

canvas:rows()

surface:present に渡せる完全な行配列をレンダリングします。

戻り値: string[]

ビューポート

ビューポートは仮想ターミナルポートです。作成したプロセスが最初のビューアであり、そのグラントで受け入れられたプロセスがプロデューサーです。

tty.viewport(options?)

local view, err = tty.viewport({width = 80, height = 24})
オプション 型 デフォルト 説明
width number 80 カラム数(1〜65535)
height number 24 行数(1〜65535)
page table なし 不透明な #RRGGBB の前景色・背景色のデフォルト

面積は 262,144 セルが上限です。

戻り値: Viewport, error

tty.attach(handle)

既存のビューポートにローカルなビューアを追加します。ハンドルが与えるのは閲覧権のみで、プレゼンテーションの所有権ではありません。また、他のノードでは無効です。

local view, err = tty.attach(handle)

戻り値: Viewport, error

viewport:grant()

ワンショットのプロデューサーケーパビリティを返します。terminal スポーンオプションとして渡します:

local grant = assert(view:grant())
local child = assert(process.with_options({terminal = grant})
    :spawn_monitored("app:child", "app:workers"))

アドミッションはグラントをトランザクショナルに消費します。起動が拒否された場合は未解決のグラントが復元され、ポートを解決したプロセスはグラントを恒久的に消費します。ターミナルのアタッチをサポートしないホストは、オプションを破棄するのではなくスポーンを拒否します。プロセスを参照してください。

戻り値: string, error

viewport:handle()

tty.attach 用のローカルビューアハンドルを返します。

戻り値: string

viewport:snapshot(after_revision?)

現在の寸法、行、カーソル、リビジョンを読み取ります。after_revision を指定すると、リビジョンが変化していない場合は nil を返します。

local frame = view:snapshot(revision)
if frame then
    revision = frame.revision
    canvas:put_rows(2, 2, frame.rows, inner_width)
end

戻り値: snapshot または nil

フィールド 型 説明
revision number このフレームの単調増加リビジョン
width number ビューポートのカラム数
height number ビューポートの行数
rows string[] プロデューサーが最後に公開した行
cursor table 1始まり座標での {x, y, visible}。プロデューサーが明示的なカーソル状態を公開するまでは存在しない
images table[] 保持された画像配置のメタデータ
layers table[] 順序付けられたプレゼンテーションレイヤー
images_omitted boolean 画像リソースは存在するが、通常のスナップショットでは保持されていない

ページはターミナルのデフォルトセルと省略行を明示的な色へ解決します。 viewport:set_page(page) で変更でき、nil を渡すとプロデューサーの元の行を 復元します。ページ変更は再描画なしにリビジョンを進めます。

viewport:updates()

まとめられたリビジョンのウォーターマークのチャネルを返します。receive() はリビジョン番号を返し、case_receive() は channel.select と組み合わせられます。

local updates = assert(view:updates())

更新は境界のあるヒントであり、イベントログではありません。遅いビューアは最新のウォーターマークのみを受け取り、状態を得るには snapshot() を呼び出す必要があります。表示とリサイズが遅いビューアによってブロックされることはありません。

戻り値: ViewportUpdateChannel, error

viewport:send(event)

検証済みのイベントレコードをプロデューサーへ転送します。プロデューサーが tty.start() を呼び出している必要があります。そうでない場合、この呼び出しはイベントを破棄するのではなく失敗します。

assert(view:send(event))
assert(view:send({type = "close"}))

戻り値: boolean, error

viewport:resize(width, height)

ビューポートのジオメトリを更新します。サイズが変化すると、ビューアは新しいリビジョンを受け取り、プロデューサーは resize イベントを受け取ります。

戻り値: boolean, error

viewport:close()

このビューアのみをデタッチします。最後のビューアを閉じても稼働中のプロデューサーは終了せず、プロデューサーのポートを閉じてもビューアが残っている限り状態は破棄されません。

戻り値: boolean, error

viewport:mount(recipient_pid, rights)

ローカルまたはリモートのビューア向けに、プロセスにバインドされた参照を発行します。 権限は独立しており、デフォルトは false です。

local observation = assert(view:mount(agent_pid, {observe = true}))
local control = assert(view:mount(agent_pid, {input = true, resize = true}))

-- 正確な受信プロセスで、このノードまたは認証済み mesh peer 上:
local observer = assert(tty.attach(observation))
local controller = assert(tty.attach(control))

マウントは宛先の完全な PID にバインドされ、一度だけ引き換えられます。マウントされた ビューアはプロデューサーグラントや追加のマウントを作成できません。発行済み参照は viewport:revoke(reference) で取り消せます。所有者のビューポートを閉じるか所有者 プロセスが終了すると、そのマウントも取り消されます。リモートマウントは更新可能な リースを使い、再接続時には新しいマウントが必要です。端末入力を再生することはできません。

viewport:capture()

ビューポートのリビジョンと保持された画像リソースをアトミックに固定します。

local capture = assert(view:capture())
local snapshot = capture:snapshot()
local image = assert(capture:image(snapshot.images[1].image_id))
assert(capture:close())

通常の snapshot() は画像バイトを保持しません。Capture は閉じるまで保持し、そこから 取得した画像ハンドルは独立して所有されます。

戻り値: Capture, error

イベント種別

イベントは type フィールドを持つテーブルで、それによってどの他のフィールドが存在するかが決まります。座標は1始まりです。同じレコードを viewport:send() が受け付けます。

キーイベント

{
    type = "key",
    key = "a",           -- printable character or key name
    key_type = "runes",  -- "runes" for printable, or special key name
    action = "press",    -- "press" or "release"
    alt = false,
    ctrl = false,
    shift = false
}

マウスイベント

tty.mouse(true) が必要です。

{
    type = "mouse",
    action = "press",    -- "press", "release", "motion", "wheel"
    button = "left",     -- button name
    x = 10,
    y = 5,
    alt = false,
    ctrl = false,
    shift = false
}

リサイズイベント

{type = "resize", width = 120, height = 40}

スタートイベント

tty.start() 後に初期サイズとともに 1 度だけ送出されます。

{type = "start", width = 120, height = 40}

フォーカスイベント

キーボードの所有権を報告します。

{type = "focus", focused = true}

可視性イベント

再描画に意味があるかどうかを報告します。アプリケーションのライフサイクルやバックグラウンド計算を規定するものではありません。

{type = "visibility", visible = true}

ペーストイベント

{type = "paste", text = "pasted content"}

クローズイベント

プロデューサーにシャットダウンを要求します。シェルは viewport:send を通じてこれを送り、子プロセスのグレースフルな終了を要求します。

{type = "close"}

キーバインディング

キーイベントに照合する再利用可能なキーバインディングを作成します:

local quit = tty.bind({
    keys = {"q", "ctrl+c"},
    help = {key = "q/ctrl+c", desc = "quit"}
})

-- In event loop
if quit:matches(ev) then
    break
end

tty.bind(config)

フィールド 型 説明
keys string[] 必須。一致させるキーパターン(例:"a"、"ctrl+c"、"enter")
help table 任意。ヘルプテキスト用の {key = "...", desc = "..."}

戻り値: KeyBinding

型スキーマでは keys が必須です。実行時に keys を省略するか空のテーブルを指定すると、どの入力にも一致しないバインディングが作成されます。

KeyBinding メソッド

メソッド 戻り値 説明
matches(event) boolean キーイベントがこのバインディングに一致するかをテスト
set_enabled(bool) self バインディングを有効化または無効化
is_enabled() boolean バインディングが有効かをチェック
help() table {key, desc} のヘルプ情報を返す

スタイル

lipgloss ベースのスタイリングを使用してスタイル付きテキスト出力を作成します。すべてのスタイルメソッドは新しいスタイルを返します(不変)。

local tty = require("tty")
local io = require("io")

local title = tty.style()
    :bold()
    :foreground("#FF0000")
    :padding(0, 1)

local box = tty.style()
    :border(tty.borders.ROUNDED)
    :border_foreground("#00FF00")
    :width(40)
    :padding(1, 2)

local _, print_err = io.print(box:render(title:render("Hello"), "World"))
if print_err then return nil, print_err end

tty.style()

新しい空のスタイルを作成します。

戻り値: Style

Style メソッド

すべてのメソッドは新しい Style を返し、チェーン可能です。

テキスト装飾

メソッド パラメータ 説明
foreground(color) string テキストカラー(hex "#FF0000"、ANSI "9"、または名前)
background(color) string 背景色
bold(enable?) boolean 太字テキスト(デフォルト: true)
italic(enable?) boolean イタリック体テキスト
underline(enable?) boolean 下線付きテキスト
strikethrough(enable?) boolean 取り消し線付きテキスト
faint(enable?) boolean 薄いテキスト
blink(enable?) boolean 点滅テキスト
reverse(enable?) boolean 前景/背景の入れ替え

レイアウト

メソッド パラメータ 説明
width(n) number 固定幅
height(n) number 固定高さ
max_width(n) number 最大幅
max_height(n) number 最大高さ
padding(...) numbers パディング(CSS スタイル:top、right、bottom、left)
margin(...) numbers マージン(CSS スタイル)
align(pos) number 水平方向の配置
align_vertical(pos) number 垂直方向の配置
inline(enable?) boolean インラインレンダリングモード

ボーダー

メソッド パラメータ 説明
border(name, ...) string, booleans ボーダースタイル、辺ごとの任意トグル
border_foreground(...) strings ボーダーカラー
border_background(...) strings ボーダー背景色

その他

メソッド 説明
render(...) このスタイルを適用して文字列をレンダリング
copy() このスタイルのコピーを作成

ボーダー定数

tty.borders.NORMAL
tty.borders.ROUNDED
tty.borders.THICK
tty.borders.DOUBLE
tty.borders.HIDDEN

アライメント定数

tty.align.LEFT    -- 0
tty.align.CENTER  -- 0.5
tty.align.RIGHT   -- 1

テキストユーティリティ

スタイル付きテキスト用のレイアウトと計測関数。tty.text の下で利用可能です。

計測

local w = tty.text.width("hello")         -- printable width (ANSI-aware)
local h = tty.text.height("a\nb\nc")      -- line count
local w, h = tty.text.size("hello\nworld") -- both

クリッピング

-- 印刷可能幅で切り詰める。末尾文字列は任意
local head = tty.text.truncate(line, 40)
local head = tty.text.truncate(line, 40, "…")

-- 印刷可能セル範囲 [left, right) を取り出す
local middle = tty.text.cut(line, 10, 30)

どちらも ANSI の状態とグラフィム境界を保持するため、スタイル付きテキストをエスケープシーケンスを壊さずにクリップしたり継ぎ合わせたりできます。truncate は幅が 0 以下の場合に空文字列を返します。cut は right が left より大きくない場合に空文字列を返します。

結合

-- Join side by side, aligned at top
local row = tty.text.join_horizontal(tty.text.position.TOP, left, right)

-- Stack vertically, centered
local col = tty.text.join_vertical(tty.text.position.CENTER, top, bottom)

最大寸法

local w = tty.text.max_width({"short", "a longer string"})   -- widest
local h = tty.text.max_height({"one\ntwo", "single"})         -- tallest

配置

指定された寸法のボックス内に文字列を配置します:

-- Center in a 80x24 box
local out = tty.text.place(80, 24, tty.text.position.CENTER, tty.text.position.CENTER, content)

-- Horizontal only
local out = tty.text.place_horizontal(80, tty.text.position.RIGHT, content)

-- Vertical only
local out = tty.text.place_vertical(24, tty.text.position.BOTTOM, content)

ポジション定数

tty.text.position.TOP      -- 0
tty.text.position.LEFT     -- 0
tty.text.position.CENTER   -- 0.5
tty.text.position.BOTTOM   -- 1
tty.text.position.RIGHT    -- 1

権限

physical terminal は process frame から与えられます。process.with_options({terminal = grant}) で producer を接続するには、spawn 側に process.context が必要です。委譲された viewport は owner viewport handle に対して tty.mount、tty.observe、tty.input、tty.resize も検査します。

関連項目