mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-06 21:00:45 +02:00
11 KiB
11 KiB
GitHub Copilot Instructions for Go + Bubble Tea (TUI)
These project-level instructions tell Copilot how to generate idiomatic, production-quality Go code using the Bubble Tea ecosystem. Follow and prefer these rules over generic patterns.
1) Project Scope & Goals
- Build terminal UIs with Bubble Tea and Bubbles components.
- Use Lip Gloss for styling and Huh/Bubbles forms for prompts where useful.
- Favor small, composable models and message-driven state.
- Prioritize maintainability, testability, and clear separation of update vs. view.
2) Go Conventions to Prefer
- Go version: 1.22+.
- Module:
go.mod
with minimal, pinned dependencies; usego get -u
only deliberately. - Code style:
gofmt
,go vet
,staticcheck
(when available),golangci-lint
. - Names: short, meaningful; exported symbols require GoDoc comments.
- Errors: return wrapped errors with
%w
anderrors.Is/As
. No panics for flow control. - Concurrency: use
context.Context
anderrgroup
where applicable. Avoid goroutine leaks; cancel contexts inQuit
/Stop
. - Testing:
*_test.go
, table-driven tests, golden tests forView()
when helpful. - Logging: prefer structured logs (e.g.,
slog
) and keep logs separate from UI rendering.
3) Bubble Tea Architecture Rules
3.1 Model layout
// Model holds all state needed to render and update.
type Model struct {
width, height int
ready bool
// Domain state
items []Item
cursor int
err error
// Child components
list list.Model
spinner spinner.Model
// Styles
styles Styles
}
Guidelines
- Keep domain state (data) separate from UI components (Bubbles models) and styles.
- Add a
Styles
struct to centralize Lip Gloss styles; initialize once. - Track terminal size (
width
,height
); re-calc layout ontea.WindowSizeMsg
.
3.2 Init
- Return batch of startup commands for IO (e.g., loading data) and component inits.
- Never block in
Init
; do IO intea.Cmd
s.
func (m Model) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick, loadItemsCmd())
}
3.3 Update
- Pure function style: transform
Model
+Msg
→(Model, Cmd)
. - Always handle
tea.WindowSizeMsg
to setm.width
/m.height
and recompute layout. - Use type-switched message handling; push side effects into
tea.Cmd
s. - Bubble components: call
Update(msg)
on children and return their Cmd.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.styles = NewStyles(m.width) // recompute if responsive
return m, nil
case errMsg:
m.err = msg
return m, nil
case itemsLoaded:
m.items = msg
return m, nil
}
// delegate to children last
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
3.4 View
- Never mutate state in
View()
. - Compose layout with Lip Gloss; gracefully handle small terminals.
- Put errors and help at the bottom.
func (m Model) View() string {
if !m.ready {
return m.styles.Loading.Render(m.spinner.View() + " Loading…")
}
main := lipgloss.JoinVertical(lipgloss.Left,
m.styles.Title.Render("My App"),
m.list.View(),
)
if m.err != nil {
main += "\n" + m.styles.Error.Render(m.err.Error())
}
return m.styles.App.Render(main)
}
3.5 Messages & Commands
- Define typed messages for domain events, not raw strings.
- Each async operation returns a message type; handle errors with a dedicated
errMsg
.
type itemsLoaded []Item
type errMsg error
func loadItemsCmd() tea.Cmd {
return func() tea.Msg {
items, err := fetchItems()
if err != nil { return errMsg(err) }
return itemsLoaded(items)
}
}
3.6 Keys & Help
- Centralize keybindings and help text. Prefer
bubbles/key
+bubbles/help
.
type keyMap struct {
Up, Down, Select, Quit key.Binding
}
var keys = keyMap{
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
}
Handle keys in Update
using key.Matches(msg, keys.X)
and show a help.Model
in the footer.
3.7 Submodels (Component Composition)
- For complex screens, create submodels with their own
(Model, Init, Update, View)
and wire them into a parent. - Exchange messages via custom Msg types and/or parent state.
- Keep submodels pure; IO still goes through parent-level
tea.Cmd
s or via submodel commands returned to parent.
3.8 Program Options
- Start programs with
tea.NewProgram(m, tea.WithOutput(os.Stdout), tea.WithAltScreen())
when full-screen; avoid AltScreen for simple tools. - Always handle TTY absence (e.g., piping); fall back to non-interactive.
4) Styling with Lip Gloss
- Maintain a single
Styles
struct with named styles. - Compute widths once per resize; avoid per-cell Lip Gloss allocations in tight loops.
- Use
lipgloss.JoinHorizontal/Vertical
for layout; avoid manual spacing where possible.
type Styles struct {
App, Title, Error, Loading lipgloss.Style
}
func NewStyles(width int) Styles {
return Styles{
App: lipgloss.NewStyle().Padding(1),
Title: lipgloss.NewStyle().Bold(true),
Error: lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
Loading: lipgloss.NewStyle().Faint(true),
}
}
5) IO, Concurrency & Performance
- Never perform blocking IO in
Update
directly; always return atea.Cmd
that does the work. - Use
context.Context
inside commands; respect cancellation when program exits. - Be careful with goroutine leaks: ensure commands stop when model quits.
- Batch commands with
tea.Batch
to keep updates snappy. - For large lists, prefer
bubbles/list
with virtualization; avoid generating huge strings per frame. - Debounce high-frequency events (typing) with timer-based commands.
6) Error Handling & UX
- Represent recoverable errors in the UI; do not exit on first error.
- Use
errMsg
for async failures; show a concise, styled error line. - For fatal initialization errors, return
tea.Quit
with an explanatory message printed once.
7) Keys, Shortcuts, and Accessibility
- Provide discoverable shortcuts via a footer help view.
- Offer Emacs-style alternatives where it makes sense (e.g.,
ctrl+n/p
). - Use consistent navigation patterns across screens.
8) Testing Strategy
- Unit test message handling with deterministic messages.
- Snapshot/golden-test
View()
output for known terminal sizes. - Fuzz-test parsers/formatters used by the UI.
func TestUpdate_Select(t *testing.T) {
m := newTestModel()
_, _ = m.Update(tea.KeyMsg{Type: tea.KeyEnter})
if got, want := m.cursor, 1; got != want { t.Fatalf("cursor=%d want %d", got, want) }
}
9) Project Structure Template
cmd/
app/
main.go
internal/
tui/
model.go // root model, styles
update.go // Update + messages
view.go // View
keys.go // keymap/help
components/ // submodels
domain/ // business logic, pure Go
io/ // adapters (API, FS, net)
Makefile // lint, test, run targets
10) Scaffolding Snippets (Ask Copilot to use these)
10.1 Root main.go
package main
import (
"context"
"log"
"os"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
if !isTTY() { // optional: detect piping
log.Println("Non-interactive mode not implemented.")
return
}
p := tea.NewProgram(NewModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatalf("error: %v", err)
}
}
10.2 NewModel()
func NewModel() Model {
s := NewStyles(0)
return Model{
list: newList(),
spinner: spinner.New(),
styles: s,
}
}
10.3 Custom messages
type (
errMsg error
itemsLoaded []Item
)
10.4 Command helper
func do(cmd func(context.Context) (tea.Msg, error)) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
msg, err := cmd(ctx)
if err != nil { return errMsg(err) }
return msg
}
}
11) Dependencies to Prefer
github.com/charmbracelet/bubbletea
github.com/charmbracelet/bubbles
github.com/charmbracelet/lipgloss
golang.org/x/sync/errgroup
(for non-UI workloads)log/slog
(Go 1.21+) for logging
Pin versions in go.mod
. Avoid extra UI deps unless justified.
12) Copilot Prompting Rules (Important)
-
When the user writes a new TUI screen, scaffold
(Model, Init, Update, View)
with:- Window size handling
- Keymap/help wiring
- Styles struct and
NewStyles(width)
- Commands for all IO
-
Prefer typed messages and return
tea.Cmd
; do not perform blocking work inUpdate
. -
Always update child bubble components via
child.Update(msg)
and collect cmds withtea.Batch
. -
Generate tests for key message paths.
-
Include help footer with keybindings.
-
Keep
View()
pure and free of side effects.
Bad
- Doing HTTP/FS work directly in
Update
. - Printing to stdout from
Update
/View
. - Storing
context.Context
in the model. - Creating goroutines that outlive the program.
Good
- Commands that return typed messages.
- Centralized keymap + help.
- Single source of truth for styles.
- Small submodels and composition.
13) Security & Reliability
- Validate all external inputs; sanitize strings rendered into the terminal.
- Respect user locale and UTF-8; avoid slicing strings by bytes for widths (use
lipgloss.Width
). - Handle small terminal sizes (min-width fallbacks).
- Ensure graceful shutdown; propagate quit via
tea.Quit
and cancel pending work.
14) Makefile Targets (suggested)
.PHONY: run test lint fmt tidy
run:; go run ./cmd/app
fmt:; go fmt ./...
lint:; golangci-lint run
test:; go test ./...
tidy:; go mod tidy
15) Example Key Handling Pattern
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
case key.Matches(msg, keys.Up):
if m.cursor > 0 { m.cursor-- }
case key.Matches(msg, keys.Down):
if m.cursor < len(m.items)-1 { m.cursor++ }
}
16) Documentation & Comments
- Exported types/functions must have a sentence GoDoc.
- At the top of each file, include a short comment describing its responsibility.
- For non-obvious state transitions, include a brief state diagram in comments.
17) Acceptance Criteria for Generated Code
- Builds with
go build ./...
- Passes
go vet
andgolangci-lint
(if configured) - Has at least one table-driven test per major update path
- Handles window resize and quit
- No side effects in
View()
- Commands wrap errors and return
errMsg
End of instructions.