mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 13:20:40 +02:00
427 lines
11 KiB
Markdown
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.*
|