gocui

TUITerminalMinimalistViewsGo

GitHub概要

jroimartin/gocui

Minimalist Go package aimed at creating Console User Interfaces.

スター10,308
ウォッチ129
フォーク625
作成日:2014年1月4日
言語:Go
ライセンス:BSD 3-Clause "New" or "Revised" License

トピックス

cuigogocuigui

スター履歴

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

gocui

gocuiは、Go言語向けのミニマリストなTUIライブラリです。ビューベースのアーキテクチャを採用し、ウィンドウ管理とキーバインディングに焦点を当てています。シンプルで軽量な設計により、素早くTUIアプリケーションを構築できます。

特徴

ビューベースアーキテクチャ

  • ビュー管理: オーバーラップ可能なウィンドウ
  • フォーカス管理: アクティブビューの切り替え
  • レイアウト: カスタムレイアウトマネージャー
  • 座標系: 絶対座標と相対座標のサポート

シンプルなAPI

  • ミニマリスト設計: 必要最小限の機能
  • イベントハンドリング: キーバインディングシステム
  • エディタモード: 組み込みテキスト編集機能
  • カーソル管理: ビュー内でのカーソル制御

パフォーマンス

  • 軽量: 最小限の依存関係
  • 効率的: 高速なレンダリング
  • 低メモリ: 小さなメモリフットプリント
  • スレッドセーフ: 並行処理対応

拡張性

  • カスタムハンドラー: イベントハンドラーの追加
  • レイアウトカスタマイズ: 独自レイアウトの実装
  • ビューカスタマイズ: ビューの外観変更
  • 組み込みエディタ: テキスト編集機能の拡張

基本的な使用方法

インストール

go get github.com/jroimartin/gocui

Hello World

package main

import (
    "fmt"
    "log"
    
    "github.com/jroimartin/gocui"
)

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()
    
    g.SetManagerFunc(layout)
    
    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }
    
    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

func layout(g *gocui.Gui) error {
    maxX, maxY := g.Size()
    if v, err := g.SetView("hello", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        fmt.Fprintln(v, "Hello World!")
    }
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

複数ビューの管理

package main

import (
    "fmt"
    "log"
    
    "github.com/jroimartin/gocui"
)

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()
    
    g.Highlight = true
    g.Cursor = true
    g.SelFgColor = gocui.ColorGreen
    
    g.SetManagerFunc(layout)
    
    // キーバインディング
    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }
    if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
        log.Panicln(err)
    }
    
    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

func layout(g *gocui.Gui) error {
    maxX, maxY := g.Size()
    
    // サイドバー
    if v, err := g.SetView("side", -1, -1, 30, maxY); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "サイドバー"
        v.Highlight = true
        v.SelBgColor = gocui.ColorGreen
        v.SelFgColor = gocui.ColorBlack
        
        fmt.Fprintln(v, "アイテム1")
        fmt.Fprintln(v, "アイテム2")
        fmt.Fprintln(v, "アイテム3")
        
        if _, err := g.SetCurrentView("side"); err != nil {
            return err
        }
    }
    
    // メインビュー
    if v, err := g.SetView("main", 30, -1, maxX, maxY); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "メイン"
        v.Wrap = true
        v.Autoscroll = true
        
        fmt.Fprintln(v, "メインコンテンツエリア")
        fmt.Fprintln(v, "ここに詳細情報が表示されます")
    }
    
    return nil
}

func nextView(g *gocui.Gui, v *gocui.View) error {
    if v == nil || v.Name() == "side" {
        _, err := g.SetCurrentView("main")
        return err
    }
    _, err := g.SetCurrentView("side")
    return err
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

エディタ機能

package main

import (
    "fmt"
    "log"
    "strings"
    
    "github.com/jroimartin/gocui"
)

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()
    
    g.SetManagerFunc(layout)
    
    // エディタのキーバインディング
    if err := g.SetKeybinding("editor", gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
        log.Panicln(err)
    }
    if err := g.SetKeybinding("editor", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
        log.Panicln(err)
    }
    if err := g.SetKeybinding("editor", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
        log.Panicln(err)
    }
    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }
    
    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

func layout(g *gocui.Gui) error {
    maxX, maxY := g.Size()
    
    // エディタビュー
    if v, err := g.SetView("editor", 0, 0, maxX-1, maxY-5); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "エディタ"
        v.Editable = true
        v.Wrap = true
        
        fmt.Fprintln(v, "テキストを入力してください...")
        fmt.Fprintln(v, "Enterキーで現在の行を下に表示します")
        
        if _, err := g.SetCurrentView("editor"); err != nil {
            return err
        }
    }
    
    // 出力ビュー
    if v, err := g.SetView("output", 0, maxY-5, maxX-1, maxY-1); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "出力"
        v.Wrap = true
        v.Autoscroll = true
    }
    
    return nil
}

func getLine(g *gocui.Gui, v *gocui.View) error {
    var l string
    var err error
    
    _, cy := v.Cursor()
    if l, err = v.Line(cy); err != nil {
        l = ""
    }
    
    // 出力ビューに表示
    outputView, err := g.View("output")
    if err != nil {
        return err
    }
    
    fmt.Fprintln(outputView, strings.TrimSpace(l))
    
    return nil
}

func cursorDown(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        cx, cy := v.Cursor()
        if err := v.SetCursor(cx, cy+1); err != nil {
            ox, oy := v.Origin()
            if err := v.SetOrigin(ox, oy+1); err != nil {
                return err
            }
        }
    }
    return nil
}

func cursorUp(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        ox, oy := v.Origin()
        cx, cy := v.Cursor()
        if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
            if err := v.SetOrigin(ox, oy-1); err != nil {
                return err
            }
        }
    }
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

カスタムレイアウト

package main

import (
    "fmt"
    "log"
    
    "github.com/jroimartin/gocui"
)

type ViewConfig struct {
    name   string
    x0, y0 float32
    x1, y1 float32
    title  string
}

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()
    
    // ビュー設定
    views := []ViewConfig{
        {"header", 0, 0, 1, 0.1, "ヘッダー"},
        {"sidebar", 0, 0.1, 0.3, 0.9, "サイドバー"},
        {"main", 0.3, 0.1, 1, 0.7, "メイン"},
        {"bottom", 0.3, 0.7, 1, 0.9, "ボトム"},
        {"footer", 0, 0.9, 1, 1, "フッター"},
    }
    
    g.SetManagerFunc(func(g *gocui.Gui) error {
        return dynamicLayout(g, views)
    })
    
    // キーバインディング
    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }
    if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
        log.Panicln(err)
    }
    
    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

func dynamicLayout(g *gocui.Gui, views []ViewConfig) error {
    maxX, maxY := g.Size()
    
    for _, vc := range views {
        x0 := int(float32(maxX) * vc.x0)
        y0 := int(float32(maxY) * vc.y0)
        x1 := int(float32(maxX)*vc.x1) - 1
        y1 := int(float32(maxY)*vc.y1) - 1
        
        if v, err := g.SetView(vc.name, x0, y0, x1, y1); err != nil {
            if err != gocui.ErrUnknownView {
                return err
            }
            v.Title = vc.title
            v.Wrap = true
            
            // コンテンツ追加
            switch vc.name {
            case "header":
                fmt.Fprintln(v, "アプリケーション v1.0")
            case "sidebar":
                fmt.Fprintln(v, "メニュー1")
                fmt.Fprintln(v, "メニュー2")
                fmt.Fprintln(v, "メニュー3")
            case "main":
                fmt.Fprintln(v, "メインコンテンツエリア")
            case "footer":
                fmt.Fprintln(v, "Ctrl+C: 終了 | Tab: 切替")
            }
        }
    }
    
    return nil
}

func nextView(g *gocui.Gui, v *gocui.View) error {
    views := []string{"header", "sidebar", "main", "bottom", "footer"}
    
    currentView := g.CurrentView()
    if currentView == nil {
        return g.SetCurrentView(views[0])
    }
    
    currentName := currentView.Name()
    for i, name := range views {
        if name == currentName {
            nextIndex := (i + 1) % len(views)
            return g.SetCurrentView(views[nextIndex])
        }
    }
    
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

高度な機能

マウスサポート

g.Mouse = true

// マウスクリックハンドラー
if err := g.SetKeybinding("viewname", gocui.MouseLeft, gocui.ModNone, 
    func(g *gocui.Gui, v *gocui.View) error {
        _, err := g.SetCurrentView(v.Name())
        return err
    }); err != nil {
    return err
}

オーバーレイビュー

func showModal(g *gocui.Gui, message string) error {
    maxX, maxY := g.Size()
    
    if v, err := g.SetView("modal", maxX/2-20, maxY/2-2, maxX/2+20, maxY/2+2); err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "モーダル"
        fmt.Fprintln(v, message)
        
        // 最前面に表示
        if _, err := g.SetViewOnTop("modal"); err != nil {
            return err
        }
        if _, err := g.SetCurrentView("modal"); err != nil {
            return err
        }
    }
    
    return nil
}

非同期更新

func updateAsync(g *gocui.Gui) {
    go func() {
        for {
            time.Sleep(1 * time.Second)
            
            g.Update(func(g *gocui.Gui) error {
                v, err := g.View("status")
                if err != nil {
                    return err
                }
                
                v.Clear()
                fmt.Fprintf(v, "時刻: %s", time.Now().Format("15:04:05"))
                return nil
            })
        }
    }()
}

エコシステム

派生プロジェクト

  • awesome-gocui: リソースと例のコレクション
  • gocui-component: 再利用可能なコンポーネント

採用例

  • lazygit: Git TUIクライアント
  • lazydocker: Docker管理ツール
  • docui: Docker TUI
  • sshportal: SSHゲートウェイ

利点

  • シンプル: ミニマリストな設計
  • 軽量: 小さな依存関係
  • 柔軟: カスタムレイアウトが容易
  • 安定: 成熟したコードベース
  • 人気: 多くのプロジェクトで採用

制約事項

  • 機能限定: 基本的な機能のみ
  • ウィジェット不足: 組み込みウィジェットが少ない
  • ドキュメント: 限定的なドキュメント
  • メンテナンス: 更新頻度が低い

他のライブラリとの比較

項目gocuiBubble Teatview
設計思想ミニマリストモダンフル機能
ウィジェット基本的コンポーネント豊富
学習コスト
カスタマイズ性非常に高
活発さ非常に高

まとめ

gocuiは、Go言語でシンプルなTUIアプリケーションを構築するための優れたライブラリです。ミニマリストな設計により、学習が容易で、カスタムレイアウトの実装も簡単です。高度なウィジェットは提供されていませんが、その分柔軟性が高く、lazygitやlazydockerなど多くの人気ツールで採用されています。シンプルで軽量なTUIライブラリを求める開発者に最適な選択肢です。