tcell

TUITerminalLow-levelCross-platformGo

GitHub概要

gdamore/tcell

Tcell is an alternate terminal package, similar in some ways to termbox, but better in others.

スター4,879
ウォッチ72
フォーク329
作成日:2015年9月27日
言語:Go
ライセンス:Apache License 2.0

トピックス

なし

スター履歴

gdamore/tcell Star History
データ取得日時: 2025/7/25 11:09

tcell

tcellは、Go言語向けの低レベルターミナル操作ライブラリです。クロスプラットフォームで動作し、モダンなターミナル機能をサポートします。tviewやcviewなど、多くの高レベルTUIライブラリの基盤として使用されています。

特徴

クロスプラットフォーム

  • OSサポート: Windows、macOS、Linux、多くのUnix系システム
  • ターミナル互換: xterm、VT100、Windows Consoleなど
  • SSHサポート: SSHセッション経由での動作
  • 組み込みテーマ: 256色およびTrue Colorサポート

モダンな機能

  • Unicodeサポート: 完全なUnicode対応(絵文字、結合文字含む)
  • マウスサポート: クリック、ドラッグ、ホイールイベント
  • リサイズイベント: ターミナルサイズ変更の検出
  • パフォーマンス: 効率的なセルバッファリング

低レベルAPI

  • 直接セル操作: 座標指定での文字描画
  • 属性制御: 色、太字、下線、点滅など
  • イベントシステム: キーボード、マウス、リサイズイベント
  • バッファリング: ダブルバッファリングによるちらつき防止

拡張性

  • カスタムターミナル: 新しいターミナルタイプの追加
  • シミュレーション: テスト用の仮想スクリーン
  • テーマシステム: カスタムカラースキーム
  • 国際化: ロケール対応

基本的な使用方法

インストール

go get github.com/gdamore/tcell/v2

Hello World

package main

import (
    "fmt"
    "os"
    
    "github.com/gdamore/tcell/v2"
)

func main() {
    // スクリーンの初期化
    s, err := tcell.NewScreen()
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    if err := s.Init(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer s.Fini()
    
    // デフォルトスタイル
    defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
    s.SetStyle(defStyle)
    
    // 画面クリア
    s.Clear()
    
    // "Hello, World!"を表示
    text := "Hello, World!"
    x, y := 10, 5
    style := tcell.StyleDefault.Foreground(tcell.ColorCyan).Background(tcell.ColorBlue)
    
    for i, r := range text {
        s.SetContent(x+i, y, r, nil, style)
    }
    
    // 描画
    s.Show()
    
    // イベントループ
    for {
        ev := s.PollEvent()
        switch ev := ev.(type) {
        case *tcell.EventKey:
            if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
                return
            }
        case *tcell.EventResize:
            s.Sync()
        }
    }
}

インタラクティブなアプリケーション

package main

import (
    "fmt"
    "os"
    "time"
    
    "github.com/gdamore/tcell/v2"
)

type App struct {
    screen tcell.Screen
    px, py int
    mx, my int
    style  tcell.Style
}

func NewApp() (*App, error) {
    s, err := tcell.NewScreen()
    if err != nil {
        return nil, err
    }
    if err := s.Init(); err != nil {
        return nil, err
    }
    
    s.EnableMouse()
    s.EnablePaste()
    s.Clear()
    
    return &App{
        screen: s,
        px:     10,
        py:     10,
        style:  tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorWhite),
    }, nil
}

func (a *App) Run() {
    defer a.screen.Fini()
    
    // ステータスバーを描画
    go a.drawStatus()
    
    // メインループ
    for {
        a.draw()
        a.screen.Show()
        
        ev := a.screen.PollEvent()
        if !a.handleEvent(ev) {
            return
        }
    }
}

func (a *App) draw() {
    w, h := a.screen.Size()
    
    // 枠を描画
    for x := 0; x < w; x++ {
        a.screen.SetContent(x, 0, '─', nil, a.style)
        a.screen.SetContent(x, h-2, '─', nil, a.style)
    }
    for y := 1; y < h-2; y++ {
        a.screen.SetContent(0, y, '│', nil, a.style)
        a.screen.SetContent(w-1, y, '│', nil, a.style)
    }
    
    // コーナー
    a.screen.SetContent(0, 0, '┌', nil, a.style)
    a.screen.SetContent(w-1, 0, '┐', nil, a.style)
    a.screen.SetContent(0, h-2, '└', nil, a.style)
    a.screen.SetContent(w-1, h-2, '┘', nil, a.style)
    
    // プレイヤーを描画
    playerStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow)
    a.screen.SetContent(a.px, a.py, '@', nil, playerStyle)
    
    // マウス位置を描画
    if a.mx > 0 && a.my > 0 {
        mouseStyle := tcell.StyleDefault.Foreground(tcell.ColorRed)
        a.screen.SetContent(a.mx, a.my, 'X', nil, mouseStyle)
    }
}

func (a *App) drawStatus() {
    for {
        w, h := a.screen.Size()
        statusStyle := tcell.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite)
        
        // ステータスバーをクリア
        for x := 0; x < w; x++ {
            a.screen.SetContent(x, h-1, ' ', nil, statusStyle)
        }
        
        // 情報を表示
        status := fmt.Sprintf(" Pos: (%d,%d) | Size: %dx%d | Time: %s | ESC: Exit ", 
            a.px, a.py, w, h, time.Now().Format("15:04:05"))
        
        for i, r := range status {
            if i < w {
                a.screen.SetContent(i, h-1, r, nil, statusStyle)
            }
        }
        
        a.screen.Show()
        time.Sleep(1 * time.Second)
    }
}

func (a *App) handleEvent(ev tcell.Event) bool {
    switch ev := ev.(type) {
    case *tcell.EventKey:
        return a.handleKey(ev)
    case *tcell.EventMouse:
        return a.handleMouse(ev)
    case *tcell.EventResize:
        a.screen.Sync()
        return true
    }
    return true
}

func (a *App) handleKey(ev *tcell.EventKey) bool {
    w, h := a.screen.Size()
    
    switch ev.Key() {
    case tcell.KeyEscape, tcell.KeyCtrlC:
        return false
    case tcell.KeyUp:
        if a.py > 1 {
            a.py--
        }
    case tcell.KeyDown:
        if a.py < h-3 {
            a.py++
        }
    case tcell.KeyLeft:
        if a.px > 1 {
            a.px--
        }
    case tcell.KeyRight:
        if a.px < w-2 {
            a.px++
        }
    }
    
    switch ev.Rune() {
    case 'h':
        if a.px > 1 {
            a.px--
        }
    case 'j':
        if a.py < h-3 {
            a.py++
        }
    case 'k':
        if a.py > 1 {
            a.py--
        }
    case 'l':
        if a.px < w-2 {
            a.px++
        }
    }
    
    return true
}

func (a *App) handleMouse(ev *tcell.EventMouse) bool {
    x, y := ev.Position()
    a.mx, a.my = x, y
    
    switch ev.Buttons() {
    case tcell.Button1:
        // 左クリックで移動
        w, h := a.screen.Size()
        if x > 0 && x < w-1 && y > 0 && y < h-2 {
            a.px, a.py = x, y
        }
    }
    
    return true
}

func main() {
    app, err := NewApp()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
    
    app.Run()
}

Unicodeと絵文字のサポート

package main

import (
    "github.com/gdamore/tcell/v2"
    "github.com/mattn/go-runewidth"
)

func drawText(s tcell.Screen, x, y int, style tcell.Style, text string) {
    for _, r := range text {
        s.SetContent(x, y, r, nil, style)
        x += runewidth.RuneWidth(r)
    }
}

func main() {
    s, _ := tcell.NewScreen()
    s.Init()
    defer s.Fini()
    
    s.Clear()
    
    // 絵文字と日本語を表示
    style := tcell.StyleDefault
    drawText(s, 5, 2, style, "🌟 Star ⭐")
    drawText(s, 5, 3, style, "🚀 Rocket")
    drawText(s, 5, 4, style, "🌈 Rainbow")
    drawText(s, 5, 6, style, "こんにちは世界🌏")
    drawText(s, 5, 7, style, "テキストベースUI💻")
    
    s.Show()
    
    // キー待ち
    for {
        ev := s.PollEvent()
        if ev, ok := ev.(*tcell.EventKey); ok {
            if ev.Key() == tcell.KeyEscape {
                return
            }
        }
    }
}

カスタムウィジェットの作成

package main

import (
    "github.com/gdamore/tcell/v2"
)

// ボタンウィジェット
type Button struct {
    x, y   int
    width  int
    text   string
    style  tcell.Style
    hover  bool
    click  func()
}

func NewButton(x, y int, text string) *Button {
    return &Button{
        x:     x,
        y:     y,
        width: len(text) + 4,
        text:  text,
        style: tcell.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite),
    }
}

func (b *Button) Draw(s tcell.Screen) {
    style := b.style
    if b.hover {
        style = style.Background(tcell.ColorLightBlue)
    }
    
    // ボタンの背景
    for i := 0; i < b.width; i++ {
        s.SetContent(b.x+i, b.y, ' ', nil, style)
    }
    
    // テキスト
    textX := b.x + (b.width-len(b.text))/2
    for i, r := range b.text {
        s.SetContent(textX+i, b.y, r, nil, style)
    }
}

func (b *Button) HandleMouse(x, y int, buttons tcell.ButtonMask) {
    if x >= b.x && x < b.x+b.width && y == b.y {
        b.hover = true
        if buttons&tcell.Button1 != 0 && b.click != nil {
            b.click()
        }
    } else {
        b.hover = false
    }
}

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

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

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

func (p *ProgressBar) Draw(s tcell.Screen) {
    // 枠
    s.SetContent(p.x, p.y, '[', nil, p.style)
    s.SetContent(p.x+p.width-1, p.y, ']', nil, p.style)
    
    // バー
    filled := int(float64(p.width-2) * p.progress)
    for i := 1; i < p.width-1; i++ {
        if i <= filled {
            s.SetContent(p.x+i, p.y, '█', nil, p.style.Foreground(tcell.ColorGreen))
        } else {
            s.SetContent(p.x+i, p.y, '░', nil, p.style.Foreground(tcell.ColorDarkGray))
        }
    }
}

高度な機能

True Colorサポート

// RGB色の使用
style := tcell.StyleDefault.
    Background(tcell.NewRGBColor(32, 32, 32)).
    Foreground(tcell.NewRGBColor(255, 128, 0))

// HSL色の使用
hslColor := tcell.NewHSLColor(120, 100, 50) // 緑色

シミュレーションスクリーン

// テスト用の仮想スクリーン
simScreen := tcell.NewSimulationScreen("UTF-8")
simScreen.Init()
simScreen.SetSize(80, 24)

// イベントのインジェクション
simScreen.InjectKey(tcell.KeyEnter, ' ', tcell.ModNone)
simScreen.InjectMouse(10, 5, tcell.Button1, tcell.ModNone)

カスタムターミナルの定義

import "github.com/gdamore/tcell/v2/terminfo"

// カスタムターミナル情報の作成
ti := &terminfo.Terminfo{
    Name:      "myterm",
    Columns:   80,
    Lines:     24,
    Colors:    256,
    // その他の設定...
}

// 登録
terminfo.AddTerminfo(ti)

エコシステム

tcellを基盤とするライブラリ

  • tview: リッチなウィジェットライブラリ
  • cview: tviewのフォーク(並行性重視)
  • tcell-term: ターミナルエミュレータ
  • gowid: ウィジェットライブラリ

採用例

  • micro: モダンなテキストエディタ
  • aerc: メールクライアント
  • fzf: ファジーファインダー(オプション)

利点

  • クロスプラットフォーム: 幅広いOSとターミナルサポート
  • モダン: Unicode、True Color、マウスサポート
  • パフォーマンス: 効率的なレンダリング
  • 安定性: 成熟したコードベース
  • 柔軟性: 低レベルAPIで完全な制御

制約事項

  • 低レベル: 高レベルウィジェットは自作が必要
  • 学習曲線: 直接セル操作の理解が必要
  • ボイラープレート: 基本的な機能も自分で実装
  • ドキュメント: APIドキュメント中心

他のライブラリとの比較

項目tcelltermbox-goncurses
レベル
クロスプラットフォーム優秀良好Unix系のみ
モダン機能完全基本的部分的
パフォーマンス優秀優秀良好
Goネイティブ×

まとめ

tcellは、Go言語でクロスプラットフォームなターミナルアプリケーションを構築するための強力な低レベルライブラリです。モダンなターミナル機能を完全にサポートし、多くの人気TUIライブラリの基盤となっています。高レベルウィジェットが必要な場合はtviewなどを使用することをお勧めしますが、完全な制御が必要な場合やカスタムTUIフレームワークを構築する場合には、tcellが最適な選択肢です。