termbox-go

TUITerminalLow-levelLightweightGo

GitHub概要

nsf/termbox-go

Pure Go termbox implementation

スター4,734
ウォッチ92
フォーク375
作成日:2012年1月12日
言語:Go
ライセンス:MIT License

トピックス

なし

スター履歴

nsf/termbox-go Star History
データ取得日時: 2025/7/25 11:09

termbox-go

termbox-goは、シンプルで軽量なターミナルUIライブラリです。C言語のtermboxライブラリのPure Go実装で、ミニマリストな設計と最小限の依存関係により、素早くターミナルアプリケーションを開発できます。

特徴

シンプルなAPI

  • 最小限の機能: 必要不可欠な機能のみ提供
  • 簡潔な設計: 理解しやすいAPI
  • 低レベル操作: セル単位の直接制御
  • イベント駆動: キーボードとマウスイベント

パフォーマンス

  • 軽量: 最小限のメモリ使用
  • 高速: 効率的なレンダリング
  • Pure Go: CGO不要
  • ゼロ依存: 外部ライブラリ不要

互換性

  • クロスプラットフォーム: Windows、macOS、Linux
  • 256色サポート: モダンターミナル対応
  • UTF-8サポート: Unicode文字の表示
  • マウスサポート: クリックイベント対応

バッファリング

  • ダブルバッファ: ちらつき防止
  • セルバッファ: 効率的な画面更新
  • 差分更新: 必要な部分のみ再描画
  • スマートレンダリング: 最適化された描画

基本的な使用方法

インストール

go get github.com/nsf/termbox-go

Hello World

package main

import (
    "github.com/nsf/termbox-go"
)

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()
    
    // 画面クリア
    termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
    
    // "Hello, World!"を表示
    text := "Hello, World!"
    for i, ch := range text {
        termbox.SetCell(i+5, 5, ch, termbox.ColorYellow, termbox.ColorBlue)
    }
    
    // 描画
    termbox.Flush()
    
    // キー待ち
    termbox.PollEvent()
}

イベントハンドリング

package main

import (
    "fmt"
    tb "github.com/nsf/termbox-go"
)

func main() {
    err := tb.Init()
    if err != nil {
        panic(err)
    }
    defer tb.Close()
    
    // マウスサポートを有効化
    tb.SetInputMode(tb.InputEsc | tb.InputMouse)
    
    drawAll()
    
    // イベントループ
    for {
        switch ev := tb.PollEvent(); ev.Type {
        case tb.EventKey:
            if ev.Key == tb.KeyEsc || ev.Key == tb.KeyCtrlC {
                return
            }
            handleKey(ev)
        case tb.EventMouse:
            handleMouse(ev)
        case tb.EventResize:
            drawAll()
        case tb.EventError:
            panic(ev.Err)
        }
        tb.Flush()
    }
}

func drawAll() {
    w, h := tb.Size()
    tb.Clear(tb.ColorDefault, tb.ColorDefault)
    
    // ヘッダー
    drawBox(0, 0, w, 3, "Termbox-go Demo")
    
    // ステータスバー
    status := fmt.Sprintf(" Size: %dx%d | Press ESC to quit ", w, h)
    drawText(0, h-1, w, status, tb.ColorWhite, tb.ColorBlue)
    
    // 中央メッセージ
    msg := "Press any key or click the mouse"
    x := (w - len(msg)) / 2
    y := h / 2
    drawText(x, y, len(msg), msg, tb.ColorYellow, tb.ColorDefault)
}

func drawBox(x, y, w, h int, title string) {
    // 上線
    tb.SetCell(x, y, '┌', tb.ColorWhite, tb.ColorDefault)
    for i := 1; i < w-1; i++ {
        tb.SetCell(x+i, y, '─', tb.ColorWhite, tb.ColorDefault)
    }
    tb.SetCell(x+w-1, y, '┐', tb.ColorWhite, tb.ColorDefault)
    
    // 側線
    for i := 1; i < h-1; i++ {
        tb.SetCell(x, y+i, '│', tb.ColorWhite, tb.ColorDefault)
        tb.SetCell(x+w-1, y+i, '│', tb.ColorWhite, tb.ColorDefault)
    }
    
    // 下線
    tb.SetCell(x, y+h-1, '└', tb.ColorWhite, tb.ColorDefault)
    for i := 1; i < w-1; i++ {
        tb.SetCell(x+i, y+h-1, '─', tb.ColorWhite, tb.ColorDefault)
    }
    tb.SetCell(x+w-1, y+h-1, '┘', tb.ColorWhite, tb.ColorDefault)
    
    // タイトル
    if title != "" {
        titleX := x + (w-len(title))/2
        drawText(titleX, y, len(title), title, tb.ColorCyan, tb.ColorDefault)
    }
}

func drawText(x, y, w int, text string, fg, bg tb.Attribute) {
    for i, ch := range text {
        if i < w {
            tb.SetCell(x+i, y, ch, fg, bg)
        }
    }
}

func handleKey(ev tb.Event) {
    w, h := tb.Size()
    info := fmt.Sprintf("Key: %v, Char: %c", ev.Key, ev.Ch)
    x := (w - len(info)) / 2
    y := h/2 + 2
    
    // クリア
    for i := 0; i < w; i++ {
        tb.SetCell(i, y, ' ', tb.ColorDefault, tb.ColorDefault)
    }
    
    drawText(x, y, len(info), info, tb.ColorGreen, tb.ColorDefault)
}

func handleMouse(ev tb.Event) {
    w, h := tb.Size()
    info := fmt.Sprintf("Mouse: X=%d, Y=%d, Button=%v", ev.MouseX, ev.MouseY, ev.Key)
    x := (w - len(info)) / 2
    y := h/2 + 2
    
    // クリア
    for i := 0; i < w; i++ {
        tb.SetCell(i, y, ' ', tb.ColorDefault, tb.ColorDefault)
    }
    
    drawText(x, y, len(info), info, tb.ColorMagenta, tb.ColorDefault)
    
    // マウス位置にマーカー
    tb.SetCell(ev.MouseX, ev.MouseY, 'X', tb.ColorRed, tb.ColorDefault)
}

ゲームの作成

package main

import (
    "math/rand"
    "time"
    tb "github.com/nsf/termbox-go"
)

type Game struct {
    px, py  int // プレイヤー位置
    ex, ey  int // 敵位置
    score   int
    gameOver bool
}

func NewGame() *Game {
    w, h := tb.Size()
    return &Game{
        px: w / 2,
        py: h / 2,
        ex: rand.Intn(w),
        ey: rand.Intn(h),
    }
}

func (g *Game) Update(ev tb.Event) {
    if g.gameOver {
        return
    }
    
    w, h := tb.Size()
    
    // プレイヤー移動
    switch ev.Ch {
    case 'w', 'W':
        if g.py > 0 {
            g.py--
        }
    case 's', 'S':
        if g.py < h-1 {
            g.py++
        }
    case 'a', 'A':
        if g.px > 0 {
            g.px--
        }
    case 'd', 'D':
        if g.px < w-1 {
            g.px++
        }
    }
    
    // 敵を捕まえたかチェック
    if g.px == g.ex && g.py == g.ey {
        g.score++
        g.ex = rand.Intn(w)
        g.ey = rand.Intn(h)
    }
    
    // 敵のランダム移動
    if rand.Float32() < 0.1 {
        g.ex += rand.Intn(3) - 1
        g.ey += rand.Intn(3) - 1
        
        if g.ex < 0 {
            g.ex = 0
        }
        if g.ex >= w {
            g.ex = w - 1
        }
        if g.ey < 0 {
            g.ey = 0
        }
        if g.ey >= h {
            g.ey = h - 1
        }
    }
}

func (g *Game) Draw() {
    tb.Clear(tb.ColorDefault, tb.ColorDefault)
    
    // プレイヤー
    tb.SetCell(g.px, g.py, '@', tb.ColorYellow, tb.ColorDefault)
    
    // 敵
    tb.SetCell(g.ex, g.ey, '*', tb.ColorRed, tb.ColorDefault)
    
    // スコア
    scoreText := fmt.Sprintf("Score: %d | WASD to move | ESC to quit", g.score)
    for i, ch := range scoreText {
        tb.SetCell(i, 0, ch, tb.ColorWhite, tb.ColorDefault)
    }
    
    tb.Flush()
}

func main() {
    err := tb.Init()
    if err != nil {
        panic(err)
    }
    defer tb.Close()
    
    rand.Seed(time.Now().UnixNano())
    game := NewGame()
    
    // タイマーイベント用チャネル
    eventQueue := make(chan tb.Event)
    go func() {
        for {
            eventQueue <- tb.PollEvent()
        }
    }()
    
    // ゲームループ
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()
    
    game.Draw()
    
    for {
        select {
        case ev := <-eventQueue:
            if ev.Type == tb.EventKey && (ev.Key == tb.KeyEsc || ev.Key == tb.KeyCtrlC) {
                return
            }
            game.Update(ev)
            game.Draw()
            
        case <-ticker.C:
            game.Update(tb.Event{})
            game.Draw()
        }
    }
}

カスタムウィジェット

package main

import tb "github.com/nsf/termbox-go"

// ボタンウィジェット
type Button struct {
    x, y   int
    width  int
    text   string
    active bool
}

func NewButton(x, y int, text string) *Button {
    return &Button{
        x:     x,
        y:     y,
        width: len(text) + 4,
        text:  text,
    }
}

func (b *Button) Draw() {
    fg := tb.ColorWhite
    bg := tb.ColorBlue
    
    if b.active {
        bg = tb.ColorRed
    }
    
    // ボタン背景
    for i := 0; i < b.width; i++ {
        tb.SetCell(b.x+i, b.y, ' ', fg, bg)
    }
    
    // テキスト
    textX := b.x + (b.width-len(b.text))/2
    for i, ch := range b.text {
        tb.SetCell(textX+i, b.y, ch, fg, bg)
    }
}

func (b *Button) HandleClick(x, y int) bool {
    if y == b.y && x >= b.x && x < b.x+b.width {
        b.active = !b.active
        return true
    }
    return false
}

// プログレスバーウィジェット
type ProgressBar struct {
    x, y     int
    width    int
    progress float64
}

func NewProgressBar(x, y, width int) *ProgressBar {
    return &ProgressBar{
        x:     x,
        y:     y,
        width: width,
    }
}

func (p *ProgressBar) SetProgress(progress float64) {
    if progress < 0 {
        progress = 0
    } else if progress > 1 {
        progress = 1
    }
    p.progress = progress
}

func (p *ProgressBar) Draw() {
    // 枠
    tb.SetCell(p.x, p.y, '[', tb.ColorWhite, tb.ColorDefault)
    tb.SetCell(p.x+p.width-1, p.y, ']', tb.ColorWhite, tb.ColorDefault)
    
    // バー
    filled := int(float64(p.width-2) * p.progress)
    for i := 1; i < p.width-1; i++ {
        if i <= filled {
            tb.SetCell(p.x+i, p.y, '█', tb.ColorGreen, tb.ColorDefault)
        } else {
            tb.SetCell(p.x+i, p.y, '░', tb.ColorGray, tb.ColorDefault)
        }
    }
    
    // パーセンテージ
    percent := fmt.Sprintf(" %d%% ", int(p.progress*100))
    percentX := p.x + (p.width-len(percent))/2
    for i, ch := range percent {
        fg := tb.ColorWhite
        if percentX+i-p.x-1 <= filled {
            fg = tb.ColorBlack
        }
        tb.SetCell(percentX+i, p.y, ch, fg, tb.ColorDefault)
    }
}

高度な機能

256色モード

// 256色モードを有効化
err := tb.SetOutputMode(tb.Output256)
if err != nil {
    // フォールバック
    tb.SetOutputMode(tb.OutputNormal)
}

// 256色を使用
tb.SetCell(x, y, '█', tb.Attribute(196), tb.ColorDefault) // 赤

バッファ操作

// バックバッファを直接操作
cells := tb.CellBuffer()
w, h := tb.Size()

// 高速な全画面塗りつぶし
for i := 0; i < w*h; i++ {
    cells[i] = tb.Cell{
        Ch: ' ',
        Fg: tb.ColorDefault,
        Bg: tb.ColorBlue,
    }
}

カスタム入力モード

// Altキーを有効化
tb.SetInputMode(tb.InputEsc | tb.InputAlt)

// イベント処理
if ev.Type == tb.EventKey && ev.Mod == tb.ModAlt {
    switch ev.Ch {
    case 'f':
        // Alt+F
    }
}

エコシステム

termbox-goを使用したプロジェクト

  • micro: モダンなテキストエディタ
  • lf: ファイルマネージャー
  • hecate: Hexエディタ
  • goyo: ディストラクションフリーライティング

代替ライブラリ

  • tcell: より機能豊富な代替
  • termui: ダッシュボード向け
  • gocui: ビューベースの代替

利点

  • シンプル: 学習が容易
  • 軽量: 最小限のリソース使用
  • 高速: 効率的なレンダリング
  • 独立: 外部依存なし
  • 安定: 成熟したコードベース

制約事項

  • 機能限定: 基本機能のみ
  • メンテナンス: 開発が停滞
  • ウィジェットなし: すべて自作が必要
  • モダン機能: True Colorなど未サポート

他のライブラリとの比較

項目termbox-gotcellgocui
レベル最低
機能基本的豊富中程度
パフォーマンス優秀優秀良好
依存関係なし
メンテナンス停滞活発

まとめ

termbox-goは、シンプルで軽量なターミナルUIライブラリを求める開発者に最適な選択肢です。最小限の機能とゼロ依存により、学習が容易で、パフォーマンスも優れています。現在はメンテナンスが停滞しているため、新規プロジェクトではtcellなどの代替を検討することをお勧めしますが、既存のプロジェクトやシンプルなツールには依然として有用なライブラリです。