Aujourd’hui je vous propose de découvrir comment créer une interface en ligne de commande (TUI) avec le framework go Bubble Tea. L’architecture n’est pas forcément évidente à appréhender au premier abord, donc on va prendre un exemple simple pour bien comprendre son fonctionnement : une todo-list dans le terminal.
Fonctionnement de base
Pour découvrir le framework commençons par créer un petit compteur
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
fmt.Println("Erreur:", err)
os.Exit(1)
}
}
Bubble Tea fonctionne autour d’un modèle qui représente l’état de notre application. Ce modèle doit implémenter trois méthodes :
Init
, qui sera appelé à l'initialisation du programme.Update
, qui recevra un message et devra modifier le modèle en fonction.
View
., qui permet de générer le rendu final
type model struct {
count int
}
func NewModel() model {
return model{count: 0}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
return m, tea.Quit
case tea.KeyEnter:
m.count++
}
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("Count: %d\n", m.count)
}
Comprendre les commandes et messages
Lorsque l'on souhaite effectuer une action il faudra utiliser une commande, qui est une fonction qui doit renvoyer un message.
type Cmd func() Msg
type Msg interface{}
Par exemple, si on souhaite incrémenter le compteur toutes les secondes, on peut créer un message et utiliser la commande Init
pour lancer ce message. Dans la méthode Update
on pourra détecter cet évènement et le relancer.
type tickMsg struct{}
func tickCmd() tea.Cmd {
return tea.Tick(time.Second, func(time.Time) tea.Msg {
return tickMsg{}
})
}
func (m model) Init() tea.Cmd {
return tickCmd()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tickMsg:
m.count++
return m, tickCmd()
}
return m, nil
}
Dans bubbletea les commandes sont exécuté au sein d'une goroutine et ne bloque pas l'interface pendant leur traitement.
Les "bubbles"
Maintenant qu’on a vu le principe de base, on peut mettre en place notre todo-list mais on a besoin d'un champs pour pouvoir entrer une tâche. Pour cela, bubbletea offre une bibliothèques de composant, nommée bubbles.
import "github.com/charmbracelet/bubbles/textinput"
import "github.com/charmbracelet/lipgloss"
type model struct {
input textinput.Model
}
func NewModel() model {
ti := textinput.New()
ti.Placeholder = "Nouvelle tâche à faire"
ti.Focus()
return model{input: ti}
}
func (m model) View() string {
return m.input.View()
}
Dans la méthode Update
il faudra transférer les message vers le champs afin qu'il réagisse à ce que fait l'utilisateur.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Si on n'a pas déjà capturé le message
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return m, cmd
}
Les "bubbles" fonctionnent comme notre modèle principal avec les 3 méthodes Init
, Update
et View
. Il est aussi possible de créer nos propres composants en créant des sous-modèles.
Il est aussi possible de personnaliser l'apparence de notre champs avec la librairie lipgloss.
var (
inputStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#FFFFFF"))
)
func (m model) View() string {
return inputStyle.
Width(30).
Render(m.input.View())
}