Go言語のターミナルアプリでテーブルを画面に合わせて表示する方法

# はじめに

Go言語でターミナルアプリを作る時に便利なパッケージBubble Tea

https://github.com/charmbracelet/bubbletea/tree/main


を使ってターミナルに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/222d9329-22f9-176c-b977-208fae6c0647.png)


のようなテーブルを表示する方法の紹介です。


# Bubble Teaのサンプル

テーブル表示のサンプルは、


https://github.com/charmbracelet/bubbletea/blob/main/examples/table/main.go

にあります。

ソースコードの要点を抜粋すると

```go

package main

import (
	"fmt"
	"os"

	"github.com/charmbracelet/bubbles/table"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// テーブルのスタイル
var baseStyle = lipgloss.NewStyle().
	BorderStyle(lipgloss.NormalBorder()).
	BorderForeground(lipgloss.Color("240"))

type model struct {
	table table.Model
}

func (m model) Init() tea.Cmd { return nil }

// 更新メッセージの処理
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			if m.table.Focused() {
				m.table.Blur()
			} else {
				m.table.Focus()
			}
		case "q", "ctrl+c":
			return m, tea.Quit
		case "enter":
			return m, tea.Batch(
				tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]),
			)
		}
	}
	m.table, cmd = m.table.Update(msg)
	return m, cmd
}

// 表示する
func (m model) View() string {
	return baseStyle.Render(m.table.View()) + "\n"
}

func main() {
//カラム
	columns := []table.Column{
		{Title: "Rank", Width: 4},
		{Title: "City", Width: 10},
		{Title: "Country", Width: 10},
		{Title: "Population", Width: 10},
	}
// データ（省略しています）
	rows := []table.Row{
		{"1", "Tokyo", "Japan", "37,274,000"},
		{"2", "Delhi", "India", "32,065,760"},
	}
// テーブルの作成
	t := table.New(
		table.WithColumns(columns),
		table.WithRows(rows),
		table.WithFocused(true),
		table.WithHeight(7),
	)
// テーブルのスタイル設定（ヘッダー、選択時の表示など）
	s := table.DefaultStyles()
	s.Header = s.Header.
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("240")).
		BorderBottom(true).
		Bold(false)
	s.Selected = s.Selected.
		Foreground(lipgloss.Color("229")).
		Background(lipgloss.Color("57")).
		Bold(false)
	t.SetStyles(s)

// プログラムを起動
	m := model{t}
	if _, err := tea.NewProgram(m).Run(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

```

です。このまま動作させると


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/62e3e737-5d75-6eda-846c-ad6d17d49d65.png)

のように画面の一部にしか表示されません。ターミナルをリサイズしても大きさは変わりません。


# 画面の大きさに合わせてリサイズできるようにする

少し変更するだけで、画面に合わせてリサイズできるようになります。

```diff_go

package main

import (
	"fmt"
	"os"

	"github.com/charmbracelet/bubbles/table"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

var baseStyle = lipgloss.NewStyle().
	BorderStyle(lipgloss.NormalBorder()).
	BorderForeground(lipgloss.Color("240"))

+ var columns = []table.Column{
+	{Title: "Rank", Width: 4},
+	{Title: "City", Width: 10},
+	{Title: "Country", Width: 10},
+	{Title: "Population", Width: 10},
+}

type model struct {
	table table.Model
}

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.table.SetWidth(msg.Width - 4)
+		m.table.SetHeight(msg.Height - 4)
+		w := m.table.Width() - 6
+		columns[0].Width = w * 4 / 34
+		columns[1].Width = w * 10 / 34
+		columns[2].Width = w * 10 / 34
+		columns[3].Width = w * 10 / 34
+		m.table.SetColumns(columns)
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			if m.table.Focused() {
				m.table.Blur()
			} else {
				m.table.Focus()
			}
		case "q", "ctrl+c":
			return m, tea.Quit
		case "enter":
			return m, tea.Batch(
				tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]),
			)
		}
	}
	m.table, cmd = m.table.Update(msg)
	return m, cmd
}

func (m model) View() string {
	return baseStyle.Render(m.table.View()) + "\n"
}

func main() {
-	columns := []table.Column{
-		{Title: "Rank", Width: 4},
-		{Title: "City", Width: 10},
-		{Title: "Country", Width: 10},
-		{Title: "Population", Width: 10},
-	}

	rows := []table.Row{
		{"1", "Tokyo", "Japan", "37,274,000"},
		{"2", "Delhi", "India", "32,065,760"},
	}

	t := table.New(
		table.WithColumns(columns),
		table.WithRows(rows),
		table.WithFocused(true),
		table.WithHeight(7),
	)

	s := table.DefaultStyles()
	s.Header = s.Header.
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("240")).
		BorderBottom(true).
		Bold(false)
	s.Selected = s.Selected.
		Foreground(lipgloss.Color("229")).
		Background(lipgloss.Color("57")).
		Bold(false)
	t.SetStyles(s)

	m := model{t}
	if _, err := tea.NewProgram(m).Run(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

```

これを動作させると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/20c7dfab-e66d-00bf-5e0b-490395044a62.png)

画面いっぱいに表示されます。

Bubble Teaのターミナルアプリでは、画面のサイズがわかると
tea.WindowSizeMsg
というメッセージが送られてきます。このメッセージに反応してテーブルのサイズとカラムのサイズを調整しています。
新しいテーブルの幅に合わせて、元のカラム幅の割合で計算しています。

ターミナルのWindowをリサイズすると

![2024-10-18_06-51-20 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/8d7f04cd-2d94-8ffd-0d63-2b8d33d31af0.gif)

のようにテーブルもリサイズできます。


# 余談

この他ターミナルアプリのテーブル表示は

https://note.com/twsnmp/n/nd90978e728c0

の開発で使っています。