sshm/.github/copilot-instructions.md

427 lines
11 KiB
Markdown

# 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](https://github.com/charmbracelet/bubbletea)** 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; use `go 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` and `errors.Is/As`. No panics for flow control.
* Concurrency: use `context.Context` and `errgroup` where applicable. Avoid goroutine leaks; cancel contexts in `Quit`/`Stop`.
* Testing: `*_test.go`, table-driven tests, golden tests for `View()` 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
```go
// 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 on `tea.WindowSizeMsg`.
### 3.2 Init
* Return **batch** of startup commands for IO (e.g., loading data) and component inits.
* Never block in `Init`; do IO in `tea.Cmd`s.
```go
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 set `m.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**.
```go
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.
```go
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`.
```go
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`.
```go
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.
```go
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 a `tea.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.
```go
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
```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()
```go
func NewModel() Model {
s := NewStyles(0)
return Model{
list: newList(),
spinner: spinner.New(),
styles: s,
}
}
```
### 10.3 Custom messages
```go
type (
errMsg error
itemsLoaded []Item
)
```
### 10.4 Command helper
```go
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 in `Update`.
* Always update child bubble components via `child.Update(msg)` and collect cmds with `tea.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
```go
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` and `golangci-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.*