TUI アプリの作り方
ターミナル上でインタラクティブに操作できる「TUI(Text User Interface)」アプリを NagiScript で作ります。ネイティブバイナリとして単体で動く、キーボード操作のツールを作るのがゴールです。
TUI の基本構造
TUI アプリの骨組みは「描画 → 入力待ち → 状態更新 →
再描画」のループです。NagiScript ではこのループを
while で書き、画面更新は ANSI
エスケープシーケンスの print 出力で行います。
文字列の
+
連結や数値の文字列化(str())は未実装です。ANSI
シーケンスは "\x1b[" のような文字列リテラルを
print(...) で出力し、数値は
println(n) でそのまま表示します。const
も存在しないため、定数は val を使います。
fn main() {
var frame = 0
print("\x1b[?25l") // カーソル非表示
while true {
print("\x1b[2J") // 画面クリア
print("\x1b[H") // カーソルを(1,1)へ
println(frame) // フレーム番号を描画
println("---")
println("press q to quit")
frame = frame + 1
if frame >= 120 { break }
}
print("\x1b[?25h") // カーソル表示
}
「const ESC = "\x1b[" のような定数」や「ESC + "2J"
のような文字列組み立て(+ 連結・str()
変換)は現行では未実装です。現在はリテラル全体を1つの文字列として
print /
println で出力してください。const
+ str() は将来の予定です。
画面の制御(ANSI エスケープ)
よく使う制御コードは以下のとおりです。print
でそのまま出力できます。
| コード | 効果 |
|---|---|
\x1b[2J |
画面クリア |
\x1b[H |
カーソルを左上へ移動 |
\x1b[<行>;<列>H |
指定位置へ移動 |
\x1b[<n>A/B/C/D |
カーソルを n 移動(上/下/右/左) |
\x1b[?25l / ?25h |
カーソル非表示 / 表示 |
\x1b[0m |
装飾リセット |
fn main() {
var i = 0
while i <= 100 {
print("\r") // \r で行先頭に戻る
println(i) // 進捗を数値で表示
i = i + 5
}
println("done")
}
進捗バー文字列を組み立てる
progress_bar()(下記の将来案)は文字列の
+ 連結と
str()
変換が必要なため、現行では動きません。] "
や % を含む完成形進捗バーは、連結と
str()
が実装された将来バージョンで使用できます。現在は数値を
println で出力する簡易表示にしてください。
// 将来案(未実装)
// fn progress_bar(width: i32, done: i32, total: i32) string {
// var bar = "["
// val filled = (done * width) / total
// var i = 0
// while i < width {
// if i < filled { bar = bar + "#" } else { bar = bar + "-" }
// i = i + 1
// }
// bar = bar + "] " + str((done * 100) / total) + "%"
// return bar
// }
キーボード入力を受け取る
ユーザーがキーを押した瞬間に反応させるには、端末を raw
モードにする必要があります。これは C 標準ライブラリの
termios を
extern で呼び出すと実現できます。C
との相互運用の詳細は
C言語との相互運用
を参照してください。
// termios を extern "C" で宣言(libc にリンク)
extern "C" fn tcgetattr(fd: i32, termp: *u8) -> i32
extern "C" fn tcsetattr(fd: i32, actions: i32, termp: *u8) -> i32
extern "C" fn getchar() -> i32
fn raw_mode_enter() {
var term: [64]u8 = [0; 64] // struct termios の配置領域
tcgetattr(0, &term[0] as *u8)
// ECHO ビットを落として反映(byte 位置は 実装依存)
var next = term[12] & 247 // ECHO(8) の逆ビット
term[12] = next
tcsetattr(0, 0, &term[0] as *u8)
}
fn read_key() -> i32 {
return getchar()
}
ポインタは *u8 形式で、アドレスを取るのは
&x as *u8、取り出すのは
unsafe { *ptr } です。*mut /
*const 記法は存在しません。struct termios
の正確なレイアウトは取得していません。上記は概念的な raw
モード実装であり、実際にはプラットフォームの termios
定義に合わせる必要があります。println(frame)
と同じく出力は print /
println を使います。
シンプルな対話アプリであれば、raw モードを使わず「1行の入力で Enter 確定」でも十分動きます。
1行入力で動くバージョン
行組み立てには文字列の
+ 連結が必要なため、現行では
getchar()
で押されたキーのコード値をそのまま表示するバージョンを示します。Enter(10
か 13)で終了します。
extern "C" fn getchar() -> i32
fn read_key() -> i32 {
while true {
val c = getchar()
if c >= 0 { return c }
}
}
fn main() {
println("press keys; Enter to quit")
while true {
val k = read_key()
println(k) // 押されたキーの文字コードを表示
if k == 10 or k == 13 { break } // LF / CR
}
println("bye!")
}
「プロンプトを表示して1行の文字列を組み立てる」(io.print(prompt)
→ line + str(c as u8) →
"Hello, " + name + "!")は、文字列連結と
str()
が未実装のため現行では動きません。単一引数の
print /
println とキーコード表示で代用してください。
タスクマネージャー TUI を作る
List<T> は LLVM
リンクに失敗する既知バグ、Rc は
.get() の戻り値が不確実、std:fs /
Result
型は未実装です。そのため実用的な「タスクリスト管理」アプリは固定配列
[Task; 16] +
件数カウンターで状態を持ち、保存・読み込みは行わないメモリ内版として示します。
extern "C" fn getchar() -> i32
struct Task {
title: string
done: bool
}
var tasks: [Task; 16]
var task_count: i32 = 0
fn add_task(title: string) {
if task_count < 16 {
tasks[task_count] = Task { title: title, done: false }
task_count = task_count + 1
}
}
fn toggle_task(index: i32) {
if index >= 0 and index < task_count {
tasks[index].done = not tasks[index].done
}
}
fn main() {
// 入力ループ:
// a : タスク追加(id のみ増える)
// t : 最新タスクをトグル
// q : 終了
print("\x1b[?25l") // カーソル非表示
while true {
print("\x1b[2J") // クリア
print("\x1b[H") // 左上へ
println("=== Task Manager ===")
var i = 0
while i < task_count {
val t = tasks[i]
if t.done {
println("[x]")
} else {
println("[ ]")
}
i = i + 1
}
println("---")
println("[a]dd [t]oggle [q]uit")
val key = read_key()
if key == 113 { break } // q
if key == 97 { add_task("task") } // a
if key == 116 { toggle_task(task_count - 1) } // t
}
print("\x1b[?25h") // カーソル表示
println("bye!")
println(task_count) // 件数を表示
}
fn read_key() -> i32 {
return getchar()
}
ビルドと実行
# ネイティブバイナリにコンパイル(termios は libc にリンク)
nagiscript build tasks.ngs -o task-manager
# 実行
./task-manager
単一バイナリで動くので、インストーラーやコンテナなしでそのまま配布できます。
発展させるときのヒント
-
ウィンドウ描画を抽象化 —
位置指定(
\x1b[行;列H)を使うと、複数ペインを自由に並べられます -
状態は struct でまとめる —
AppStateを1つの構造体にすると再描画・永続化が楽になります -
永久化は将来の
std:fsで — 現行ではファイル保存・読み込み(std:fs)は未実装です。保存機能はロードマップとして設計に残し、当面はメモリ内で動かします -
色を使う —
\x1b[31m(赤)等を組み合わせて選択中カーソルや完了タスクを強調できます
