1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Bubble Teaを使った選択肢と入力フォームの切り替え処理

Last updated at Posted at 2024-09-30

目次

  1. はじめに
  2. 環境
  3. Bubble Tea フレームワークの紹介
  4. プロジェクトの構成とセットアップ
  5. 作成手順
  6. 全体の流れ
  7. おわりに
  8. 参考

1. はじめに

今回は、Bubble Teaを使用して、ユーザーが選択した選択肢に応じて入力フォームを動的に切り替えるコードを紹介します。

このプロジェクトでは、以下の機能を実装します。

  1. ユーザーが選択肢からニックネーム、ニックネームとメールアドレス、ニックネームとメールアドレスとパスワードのいずれかを選択できるインターフェースを提供します。
  2. 選択肢に応じて、必要な入力フィールドを動的に表示し、ユーザー情報を収集します。
  3. 収集した情報は後の処理に利用できるように格納します。

なお、実行だけであれば、セットアップを行い、完成したコードで実行するだけで(ほぼ)動作すると思いますが、理解のためにコードの解説を行います。


2. 環境

筆者の実行環境は以下のとおりです。

環境 バージョン
OS macOS 14.5
シェル zsh 5.9.3 (arm-apple-darwin22.1.0)
Go 1.23.1
bubbletea 1.1.1
bubbles 0.20.0

3. Bubble Teaフレームワークの紹介

Bubble Teaは、Go 言語で CLI アプリケーションを構築するためのフレームワークです。主な特徴は以下の通りです。

  • 状態管理: ユーザーとの対話を可能にし、アプリケーションの状態を効率的に管理します。
  • カスタマイズ性: スタイルや UI を自由に変更でき、独自のデザインが可能です。
  • テストの容易さ: シンプルな構造により、テストを容易に実施できます。

具体的な情報は、GitHub の公式リポジトリドキュメントを参照してください。

[補足] CLI とは、Command Line Interface の略で、コマンドラインを通じてユーザーと対話するインターフェースのことです。


4. プロジェクトの構成とセットアップ

プロジェクトのディレクトリ構成とセットアップ手順を説明します。

4.1 プロジェクトのディレクトリ構成

以下に、今回のプロジェクトのディレクトリ構成を示します。

.
├── go.mod
├── go.sum
├── main.go
└── pkg
    └── config
        └── terminal
             └── terminal.go

terminal.goBubble Tea のコードを記述し、main.go で呼び出すことで実行します。

この構成にはプロジェクトが大きくなった際にコードの管理がしやすくなるというメリットがあります。

4.2 プロジェクトのセットアップ

以下の手順でプロジェクトをセットアップします。

4.2.1. 依存パッケージのインストール

必要な依存パッケージをインストールします。以下のコマンドを実行してください。

go get github.com/charmbracelet/bubbletea
go get github.com/charmbracelet/bubbles/textinput
go get github.com/charmbracelet/lipgloss
go get github.com/charmbracelet/bubbles/cursor

4.2.2. main.goの作成

main.goに以下のコードを記述します。

package main

import (
    "xxx/pkg/config/terminal" // xxxはプロジェクト名(適宜修正してください)
)

// アプリケーションのエントリーポイント
func main() {
    terminal.StartTerminal()
}

4.2.3. terminal.goの作成

terminal.goに以下のコードを記述します。

package terminal

// Bubble Teaの関連パッケージをインポートします。
import (
	"fmt"
	"os"
	"strings"

	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

これでプロジェクトのセットアップが完了です。次は、Bubble Tea を使った動的な入力フォームの実装に進みます。

5. 作成手順

Bubble Teaは状態を保持するためのmodel構造体と、ユーザーの入力を受け取るためのUpdate関数、表示を設定するためのView関数、初期化関数Initの実装を必要します。

そのため、以下とそれに関係するコードを作成します。

  1. model構造体の作成
  2. Init関数の作成
  3. Update関数の作成
  4. View関数の作成
  5. StartTerminal関数の作成(アプリケーションの実行)

5.1 構造体の作成(model, State, UserInfo...)

まず、model構造体とそれに関連する構造体を作成します。

各構造体の役割は以下の通りです。

構造体 役割
model アプリケーションの状態を管理します。
State アプリケーションの状態を定義します。
UserInfo ユーザー情報を格納する構造体です。

また、選択肢やスタイルなどのパッケージ変数も定義します。

// パッケージ変数の定義(公式ドキュメントのコードをそのまま使用)
var (
	focusedStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
	blurredStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
	cursorStyle         = focusedStyle
	noStyle             = lipgloss.NewStyle()
	helpStyle           = blurredStyle
	cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))

	focusedButton = focusedStyle.Render("[ 送信 ]")
	blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("送信"))
)

// 選択肢を定義(定数)
const (
	ChoiceNickName      = "ニックネーム"
	ChoiceNickNameEmail = "ニックネーム + メールアドレス"
	ChoiceAll           = "全て"
)

// 選択肢をスライスに格納
var choices = []string{
	ChoiceNickName,
	ChoiceNickNameEmail,
	ChoiceAll,
}

// 状態を定義
type State string

// 状態を定数で定義
const (
	StateSelect        State = "未選択"
	StateNickName      State = "ニックネーム"
	StateNickNameEmail State = "ニックネーム + メールアドレス"
	StateAll           State = "全て"
)

// ユーザー情報を格納する構造体
type UserInfo struct {
	Choice   string // 選択された選択肢を格納
	Nickname string // ニックネームを格納
	Email    string // メールアドレスを格納
	Password string // パスワードを格納
}

var userInfo UserInfo // ユーザー情報を格納する構造体

// モデルを定義
type model struct {
	focusIndex int               //フォーカスされている入力フィールドのインデックスを保持します。
	inputs     []textinput.Model //テキスト入力フィールドのモデルを格納します。
	cursorMode cursor.Mode       //カーソルのモードを保持します。
	state      State             //状態を保持します。(列挙型)
	cursor     int               //カーソルの位置を保持します。
}

5.2 初期化関数の作成(Init, InitialModel)

次にアプリケーションの初期化に必要な関数を作成します。

関数名 説明
initialModel() モデルの初期化を行う関数。stateを「未選択」に設定し、cursorfocusIndexをそれぞれ0に初期化する。
Init() Teaアプリケーションの初期化時に実行される関数。tea.Cmdとして、カーソルの点滅を開始する`
// モデルの初期化
func initialModel() model {
	m := model{
		state:      "未選択", // 初期状態は未選択
		cursor:     0,     // カーソルの初期位置は0
		focusIndex: 0,     // フォーカスされている入力フィールドのインデックスは0
	}
	return m
}

// 初期化関数(必須):初期化時に実行するコマンドを返す
func (m model) Init() tea.Cmd {
	// カーソルを点滅させる関数を返す
	return textinput.Blink
}

5.3 メッセージを受け取る関数の作成(Update)

そしてユーザーの入力に応じてアプリケーションの状態を更新する関数を作成します。

関数名 説明
Update(msg tea.Msg) メインのメッセージ受け取り関数。状態 (state) に応じて、選択フォーム (updateSelectState) か入力フォーム (updateInputState) の更新関数に処理を分岐。
updateSelectState(msg tea.Msg) 選択フォームの状態を更新する関数。主にキー入力 (KeyMsg) に応じた処理を行う。
handleSelectKeyMsg(msg tea.Msg) 選択フォームのキー入力を処理する関数。特定のキー(Ctrl+C、Esc、Enter、上下キー)に応じてカーソル移動や終了処理を行う。
handleSelection() ユーザーが選択肢を決定した際の処理。選択肢に応じて状態を設定し、入力フォームの準備を行う。
createTextInput(placeholder string, charLimit int, isPassword bool) テキスト入力フォームを作成する関数。プレースホルダ、文字数制限、パスワードモードの指定が可能。
updateCursor(key string) 選択肢のカーソル位置を更新する関数。上下キーやタブキーの入力に応じてカーソルを移動する。
updateInputState(msg tea.Msg) 入力フォームの状態を更新する関数。ユーザーの入力に応じてテキストフィールドのフォーカスや終了処理を行う。
updateInputs(msg tea.Msg) 入力フォーム内の各テキスト入力フィールドを更新する関数。複数の入力欄の状態を同時に更新する。

// メッセージを受け取る関数(必須)
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	// 状態に応じて処理を振り分ける
	switch m.state {
	case "未選択":
		// 未選択状態の場合は選択肢を表示
		return m.updateSelectState(msg)
	default:
		// それ以外は入力フォームを表示
		return m.updateInputState(msg)
	}
}

//
// 選択フォームの状態を更新する関数
//

// 選択フォームの状態を更新する関数
func (m model) updateSelectState(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		return m.handleSelectKeyMsg(msg)
	}
	// メッセージがキー入力でない場合はそのまま返す
	return m, nil
}

func (m *model) handleSelectKeyMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			// Ctrl+C または esc が押された場合は終了
			return m, tea.Quit
		case "enter":
			// Enter が押された場合は決定
			m.handleSelection()
		case "up", "down":
			// 上下キーが押された場合はカーソルを移動
			m.updateCursor(msg.String())
		}
	}
	return m, nil
}

// 選択肢に基づいて状態を設定
func (m *model) handleSelection() (tea.Model, tea.Cmd) {
	if m.cursor < 0 || m.cursor >= len(choices) {
		// カーソルが選択肢の範囲外の場合は何もしない
		return m, nil
	}

	// 選択された選択肢に基づいて状態を設定
	m.state = State(choices[m.cursor])
	userInfo.Choice = choices[m.cursor]
	m.focusIndex = 0

	// 入力フォームを作成する関数に値を渡す
	switch m.state {
	case StateNickName:
		m.inputs = []textinput.Model{createTextInput("ニックネーム", 32, false)}
	case StateNickNameEmail:
		m.inputs = []textinput.Model{
			createTextInput("ニックネーム", 32, false),
			createTextInput("メールアドレス", 64, false),
		}
	case StateAll:
		m.inputs = []textinput.Model{
			createTextInput("ニックネーム", 32, false),
			createTextInput("メールアドレス", 64, false),
			createTextInput("パスワード", 64, true),
		}
	}

	// フォーカスを最初の入力に設定
	if len(m.inputs) > 0 {
		m.inputs[0].Focus()
	}
	return m, nil
}

// 入力フォームを作成する関数
func createTextInput(placeholder string, charLimit int, isPassword bool) textinput.Model {
	t := textinput.New()
	t.Cursor.Style = cursorStyle
	t.CharLimit = charLimit
	t.Placeholder = placeholder
	t.PromptStyle = focusedStyle
	t.TextStyle = focusedStyle

	// パスワードの場合はエコーモードを設定(入力値を 「•」に置き換える)
	if isPassword {
		t.EchoMode = textinput.EchoPassword
		t.EchoCharacter = '•'
	}

	return t
}

// カーソルの位置を更新する関数
func (m *model) updateCursor(key string) {
	switch key {
	case "up", "shift+tab":
		// 上キーまたは shift+tab が押された場合はカーソルを上に移動
		// カーソルが先頭にある場合は末尾に移動
		m.cursor = (m.cursor + len(choices) - 1) % len(choices)
	case "down", "tab":
		// 下キーまたは tab が押された場合はカーソルを下に移動
		// カーソルが末尾にある場合は先頭に移動
		m.cursor = (m.cursor + 1) % len(choices)
	}
}

//
// 入力フォームの状態を更新する関数
//

func (m model) updateInputState(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			// Ctrl+C または esc が押された場合は終了
			return m, tea.Quit
		case "tab", "shift+tab", "enter", "up", "down":
			// タブ、Enter、上下キーが押された場合は処理を振り分ける
			s := msg.String()
			// enterが押された場合はフォーカスが最後の入力にある場合のみ処理を行う
			if s == "enter" && m.focusIndex == len(m.inputs) {
				// ユーザー情報を構造体に格納
				for v, k := range m.inputs {
					switch v {
					case 0:
						userInfo.Nickname = k.Value()
					case 1:
						userInfo.Email = k.Value()
					case 2:
						userInfo.Password = k.Value()
					}
				}
				// 終了
				return m, tea.Quit
			}

			// タブ、上下キーが押された場合はフォーカスを移動
			if s == "up" || s == "shift+tab" {
				m.focusIndex--
			} else {
				// それ以外の入力はフォーカスを次の入力に移動
				m.focusIndex++
			}

			// フォーカスが最後の入力を超えた場合は最初の入力に戻す
			if m.focusIndex > len(m.inputs) {
				m.focusIndex = 0
			} else if m.focusIndex < 0 {
				m.focusIndex = len(m.inputs) - 1
			}

			// フォーカスを設定
			cmds := make([]tea.Cmd, len(m.inputs))
			for i := 0; i <= len(m.inputs)-1; i++ {
				if i == m.focusIndex {
					// 現在の位置にフォーカスを設定
					cmds[i] = m.inputs[i].Focus()
					m.inputs[i].PromptStyle = focusedStyle
					m.inputs[i].TextStyle = focusedStyle
					continue
				}
				// フォーカスがない場合はフォーカスを解除
				m.inputs[i].Blur()
				m.inputs[i].PromptStyle = noStyle
				m.inputs[i].TextStyle = noStyle
			}
			return m, tea.Batch(cmds...)
		}
	}

	// enterやtabなどのキー入力以外の場合はフォーム入力として処理
	cmd := m.updateInputs(msg)
	return m, cmd
}

// 入力フォームの状態を更新する関数
func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
	cmds := make([]tea.Cmd, len(m.inputs))
	for i := range m.inputs {
		m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
	}
	return tea.Batch(cmds...)
}

このコードのマーメイド図を以下に示します。(ChatGPTで生成)


5.4 UIの表示を設定する関数(View)

またアプリケーションの表示を設定する View() 関数を作成します。

関数名 説明
View() TeaアプリケーションでUIの表示を行う関数。状態によって選択肢の表示か、入力フォームの表示を切り替える。
viewSelectState() 選択肢が未選択の状態で、選択肢をリスト形式で表示する関数。カーソル位置に応じて選択肢の表示を変える。
viewInputState() 選択肢が選択された後の入力フォームを表示する関数。フォームと送信ボタンを含むUIを生成する。
// UIの表示を行う関数(必須)
func (m model) View() string {
	if m.state == "未選択" {
		// 未選択状態の場合は選択肢を表示
		return m.viewSelectState()
	}
	return m.viewInputState()
}

// 選択肢を表示する関数
func (m model) viewSelectState() string {
	var b strings.Builder
	b.WriteString("選択肢から選んでください\n\n")
	// 選択肢の数を表示
	for i, choice := range choices {
		if m.cursor == i {
			// カーソルが選択肢にある場合は選択肢の前に「(•)」を表示
			b.WriteString("(•) ")
		} else {
			// それ以外の場合は「( )」を表示
			b.WriteString("( ) ")
		}
		// 選択肢を表示
		b.WriteString(choice)
		b.WriteString("\n")
	}
	return b.String()
}

func (m *model) viewInputState() string {
	var b strings.Builder
	b.WriteString("情報を入力してください\n\n")
	b.WriteString("タブキーで入力を切り替え、Enterキーで送信します。\n\n")
	for i := range m.inputs {
		b.WriteString(m.inputs[i].View())
		if i < len(m.inputs)-1 {
			b.WriteRune('\n')
		}
	}

	button := &blurredButton
	if m.focusIndex == len(m.inputs) {
		button = &focusedButton
	}
	fmt.Fprintf(&b, "\n\n%s\n\n", *button)
	return b.String()
}


5.5 アプリケーションを実行する関数(StartTerminal)

最後にアプリケーションを実行するための StartTerminal() 関数を作成します。

func StartTerminal() {
	_, err := tea.NewProgram(initialModel()).Run()
	if err != nil {
		fmt.Printf("エラーが発生しました: %v\n", err)
		os.Exit(1)
	}

	// 構造体に保存されたデータを表示
	fmt.Printf("ユーザー情報: %+v\n", userInfo)
}

これで、アプリケーションを実行するための関数を作成しました。

ここまでのコードを全て繋げたものを以下に示します。

5.6 完成したコード

package terminal

// パッケージのインポート
import (
	"fmt"
	"os"
	"strings"

	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// パッケージ変数の定義(公式ドキュメントのコードをそのまま使用)
var (
	focusedStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
	blurredStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
	cursorStyle         = focusedStyle
	noStyle             = lipgloss.NewStyle()
	helpStyle           = blurredStyle
	cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))

	focusedButton = focusedStyle.Render("[ 送信 ]")
	blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("送信"))
)

// 選択肢を定義(定数)
const (
	ChoiceNickName      = "ニックネーム"
	ChoiceNickNameEmail = "ニックネーム + メールアドレス"
	ChoiceAll           = "全て"
)

// 選択肢をスライスに格納
var choices = []string{
	ChoiceNickName,
	ChoiceNickNameEmail,
	ChoiceAll,
}

// 状態を定義
type State string

// 状態を定数で定義
const (
	StateSelect        State = "未選択"
	StateNickName      State = "ニックネーム"
	StateNickNameEmail State = "ニックネーム + メールアドレス"
	StateAll           State = "全て"
)

// ユーザー情報を格納する構造体
type UserInfo struct {
	Choice   string // 選択された選択肢を格納
	Nickname string // ニックネームを格納
	Email    string // メールアドレスを格納
	Password string // パスワードを格納
}

var userInfo UserInfo // ユーザー情報を格納する構造体

// モデルを定義
type model struct {
	focusIndex int               //フォーカスされている入力フィールドのインデックスを保持します。
	inputs     []textinput.Model //テキスト入力フィールドのモデルを格納します。
	cursorMode cursor.Mode       //カーソルのモードを保持します。
	state      State             //状態を保持します。(列挙型)
	cursor     int               //カーソルの位置を保持します。
}

// モデルの初期化
func initialModel() model {
	m := model{
		state:      "未選択", // 初期状態は未選択
		cursor:     0,     // カーソルの初期位置は0
		focusIndex: 0,     // フォーカスされている入力フィールドのインデックスは0
	}
	return m
}

// 初期化関数(必須):初期化時に実行するコマンドを返す
func (m model) Init() tea.Cmd {
	// カーソルを点滅させる関数を返す
	return textinput.Blink
}

// メッセージを受け取る関数(必須)
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	// 状態に応じて処理を振り分ける
	switch m.state {
	case "未選択":
		// 未選択状態の場合は選択肢を表示
		return m.updateSelectState(msg)
	default:
		// それ以外は入力フォームを表示
		return m.updateInputState(msg)
	}
}

//
// 選択フォームの状態を更新する関数
//

// 選択フォームの状態を更新する関数
func (m model) updateSelectState(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		return m.handleSelectKeyMsg(msg)
	}
	// メッセージがキー入力でない場合はそのまま返す
	return m, nil
}

func (m *model) handleSelectKeyMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			// Ctrl+C または esc が押された場合は終了
			return m, tea.Quit
		case "enter":
			// Enter が押された場合は決定
			m.handleSelection()
		case "up", "down":
			// 上下キーが押された場合はカーソルを移動
			m.updateCursor(msg.String())
		}
	}
	return m, nil
}

// 選択肢に基づいて状態を設定
func (m *model) handleSelection() (tea.Model, tea.Cmd) {
	if m.cursor < 0 || m.cursor >= len(choices) {
		// カーソルが選択肢の範囲外の場合は何もしない
		return m, nil
	}

	// 選択された選択肢に基づいて状態を設定
	m.state = State(choices[m.cursor])
	userInfo.Choice = choices[m.cursor]
	m.focusIndex = 0

	// 入力フォームを作成する関数に値を渡す
	switch m.state {
	case StateNickName:
		m.inputs = []textinput.Model{createTextInput("ニックネーム", 32, false)}
	case StateNickNameEmail:
		m.inputs = []textinput.Model{
			createTextInput("ニックネーム", 32, false),
			createTextInput("メールアドレス", 64, false),
		}
	case StateAll:
		m.inputs = []textinput.Model{
			createTextInput("ニックネーム", 32, false),
			createTextInput("メールアドレス", 64, false),
			createTextInput("パスワード", 64, true),
		}
	}

	// フォーカスを最初の入力に設定
	if len(m.inputs) > 0 {
		m.inputs[0].Focus()
	}
	return m, nil
}

// 入力フォームを作成する関数
func createTextInput(placeholder string, charLimit int, isPassword bool) textinput.Model {
	t := textinput.New()
	t.Cursor.Style = cursorStyle
	t.CharLimit = charLimit
	t.Placeholder = placeholder
	t.PromptStyle = focusedStyle
	t.TextStyle = focusedStyle

	// パスワードの場合はエコーモードを設定(入力値を 「•」に置き換える)
	if isPassword {
		t.EchoMode = textinput.EchoPassword
		t.EchoCharacter = '•'
	}

	return t
}

// カーソルの位置を更新する関数
func (m *model) updateCursor(key string) {
	switch key {
	case "up", "shift+tab":
		// 上キーまたは shift+tab が押された場合はカーソルを上に移動
		// カーソルが先頭にある場合は末尾に移動
		m.cursor = (m.cursor + len(choices) - 1) % len(choices)
	case "down", "tab":
		// 下キーまたは tab が押された場合はカーソルを下に移動
		// カーソルが末尾にある場合は先頭に移動
		m.cursor = (m.cursor + 1) % len(choices)
	}
}

//
// 入力フォームの状態を更新する関数
//

func (m model) updateInputState(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			// Ctrl+C または esc が押された場合は終了
			return m, tea.Quit
		case "tab", "shift+tab", "enter", "up", "down":
			// タブ、Enter、上下キーが押された場合は処理を振り分ける
			s := msg.String()
			// enterが押された場合はフォーカスが最後の入力にある場合のみ処理を行う
			if s == "enter" && m.focusIndex == len(m.inputs) {
				// ユーザー情報を構造体に格納
				for v, k := range m.inputs {
					switch v {
					case 0:
						userInfo.Nickname = k.Value()
					case 1:
						userInfo.Email = k.Value()
					case 2:
						userInfo.Password = k.Value()
					}
				}
				// 終了
				return m, tea.Quit
			}

			// タブ、上下キーが押された場合はフォーカスを移動
			if s == "up" || s == "shift+tab" {
				m.focusIndex--
			} else {
				// それ以外の入力はフォーカスを次の入力に移動
				m.focusIndex++
			}

			// フォーカスが最後の入力を超えた場合は最初の入力に戻す
			if m.focusIndex > len(m.inputs) {
				m.focusIndex = 0
			} else if m.focusIndex < 0 {
				m.focusIndex = len(m.inputs) - 1
			}

			// フォーカスを設定
			cmds := make([]tea.Cmd, len(m.inputs))
			for i := 0; i <= len(m.inputs)-1; i++ {
				if i == m.focusIndex {
					// 現在の位置にフォーカスを設定
					cmds[i] = m.inputs[i].Focus()
					m.inputs[i].PromptStyle = focusedStyle
					m.inputs[i].TextStyle = focusedStyle
					continue
				}
				// フォーカスがない場合はフォーカスを解除
				m.inputs[i].Blur()
				m.inputs[i].PromptStyle = noStyle
				m.inputs[i].TextStyle = noStyle
			}
			return m, tea.Batch(cmds...)
		}
	}

	// enterやtabなどのキー入力以外の場合はフォーム入力として処理
	cmd := m.updateInputs(msg)
	return m, cmd
}

// 入力フォームの状態を更新する関数
func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
	cmds := make([]tea.Cmd, len(m.inputs))
	for i := range m.inputs {
		m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
	}
	return tea.Batch(cmds...)
}

//
// UIの表示を行う関数
//

// UIの表示を行う関数(必須)
func (m model) View() string {
	if m.state == "未選択" {
		// 未選択状態の場合は選択肢を表示
		return m.viewSelectState()
	}
	return m.viewInputState()
}

// 選択肢を表示する関数
func (m model) viewSelectState() string {
	var b strings.Builder
	b.WriteString("選択肢から選んでください\n\n")
	// 選択肢の数を表示
	for i, choice := range choices {
		if m.cursor == i {
			// カーソルが選択肢にある場合は選択肢の前に「(•)」を表示
			b.WriteString("(•) ")
		} else {
			// それ以外の場合は「( )」を表示
			b.WriteString("( ) ")
		}
		// 選択肢を表示
		b.WriteString(choice)
		b.WriteString("\n")
	}
	return b.String()
}

func (m *model) viewInputState() string {
	var b strings.Builder
	b.WriteString("情報を入力してください\n\n")
	b.WriteString("タブキーで入力を切り替え、Enterキーで送信します。\n\n")
	for i := range m.inputs {
		b.WriteString(m.inputs[i].View())
		if i < len(m.inputs)-1 {
			b.WriteRune('\n')
		}
	}

	button := &blurredButton
	if m.focusIndex == len(m.inputs) {
		button = &focusedButton
	}
	fmt.Fprintf(&b, "\n\n%s\n\n", *button)
	return b.String()
}

func StartTerminal() {
	_, err := tea.NewProgram(initialModel()).Run()
	if err != nil {
		fmt.Printf("エラーが発生しました: %v\n", err)
		os.Exit(1)
	}

	// 構造体に保存されたデータを表示
	fmt.Printf("ユーザー情報: %+v\n", userInfo)
}


6. 全体の流れ

作成したコード全体の流れを解説します。

  1. main.goStartTerminal() 関数を呼び出します。
  2. StartTerminal() 関数で、tea.NewProgram() 関数を使用してアプリケーションを開始します。
  3. initialModel() 関数で、アプリケーションの初期状態を設定します。
  4. Init() 関数で、カーソルを点滅させるためのテキスト入力モデルのコマンドを返します。
  5. Update() 関数で、メッセージを受け取り、状態を更新します。
    • [未選択]状態では、カーソルの移動や選択肢の選択を処理します。
    • それ以外の状態では、入力フォームの表示や入力値の更新を処理します。
  6. View() 関数で、アプリケーションの表示を設定します。
    • [未選択]状態では、選択肢を表示します。
    • それ以外の状態では、入力フォームを表示します。
  7. StartTerminal() 関数に戻り、ユーザー情報を表示します。

7. おわりに

ここまで長々と説明しましたが、「ユーザーの選択を保持するための状態を定義し、その状態に応じて表示内容を切り替える」処理を実装すれば、選択式の入力フォームを作成することができます。

かなり力技で実装したので、もっと良い方法があれば教えていただけると幸いです。

8. 参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?