memory
ALWAYS ACTIVE — Persistent memory protocol. You MUST save decisions, conventions, bugs, and discoveries to engram proactively. Do NOT wait for the user to ask.
Bubbletea TUI patterns for Gentleman.Dots installer. Trigger: When editing Go files in installer/internal/tui/, working on TUI screens, or adding new UI features.
$ npx -y skills add Gentleman-Programming/engram --skill gentleman-bubbletea --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/gentleman-bubbleteaContext preview
The summary Claude sees to decide when to auto-load this skill.
Bubbletea TUI patterns for Gentleman.Dots installer. Trigger: When editing Go files in installer/internal/tui/, working on TUI screens, or adding new UI features.
name: gentleman-bubbletea description: > Bubbletea TUI patterns for Gentleman.Dots installer. Trigger: When editing Go files in installer/internal/tui/, working on TUI screens, or adding new UI features. license: Apache-2.0 metadata: author: gentleman-programming version: "1.0"
Use this skill when:
---
All screens MUST be defined as `Screen` constants in `model.go`:
type Screen int
const (
ScreenWelcome Screen = iota
ScreenMainMenu
ScreenOSSelect
// ... new screens go here
ScreenNewFeature // Add new screen
ScreenNewFeatureCat // Add category screen if needed
)The `Model` struct in `model.go` holds ALL application state:
type Model struct {
Screen Screen
PrevScreen Screen // For back navigation
Width int
Height int
Cursor int
// Add new state here
NewFeatureData []SomeType
NewFeatureScroll int
}All input handling goes through `Update()` with a type switch:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKeyPress(msg)
case tea.WindowSizeMsg:
m.Width = msg.Width
m.Height = msg.Height
return m, nil
case customMsg:
// Handle custom messages
return m, nil
}
return m, nil
}Separate handler per screen, always return `(tea.Model, tea.Cmd)`:
func (m Model) handleNewFeatureKeys(key string) (tea.Model, tea.Cmd) {
options := m.GetCurrentOptions()
switch key {
case "up", "k":
if m.Cursor > 0 {
m.Cursor--
// Skip separator
if strings.HasPrefix(options[m.Cursor], "───") && m.Cursor > 0 {
m.Cursor--
}
}
case "down", "j":
if m.Cursor < len(options)-1 {
m.Cursor++
if strings.HasPrefix(options[m.Cursor], "───") && m.Cursor < len(options)-1 {
m.Cursor++
}
}
case "enter", " ":
// Handle selection
return m.handleNewFeatureSelection()
case "esc":
m.Screen = m.PrevScreen
m.Cursor = 0
}
return m, nil
}---
Adding a new screen?
├── Define Screen constant in model.go
├── Add state fields to Model struct
├── Add handler in handleKeyPress switch
├── Create handle{Screen}Keys function in update.go
├── Add view case in view.go
└── Add title in GetScreenTitle()
Adding navigation to existing screen?
├── Use m.PrevScreen for back navigation
├── Reset m.Cursor = 0 on screen change
└── Save scroll position if scrollable
Adding scrollable content?
├── Add {Screen}Scroll int to Model
├── Calculate visibleItems from m.Height
├── Handle up/down for scroll position
└── Reset scroll on screen exit---
// In handleKeyPress switch statement:
case ScreenNewFeature:
return m.handleNewFeatureKeys(key)
case ScreenNewFeatureCat:
return m.handleNewFeatureCatKeys(key)func (m Model) GetCurrentOptions() []string {
switch m.Screen {
case ScreenNewFeature:
categories := make([]string, len(m.NewFeatureData)+2)
for i, item := range m.NewFeatureData {
categories[i] = item.Name
}
categories[len(m.NewFeatureData)] = "─────────────"
categories[len(m.NewFeatureData)+1] = "← Back"
return categories
// ...
}
}func (m Model) handleNewFeatureCatKeys(key string) (tea.Model, tea.Cmd) {
data := m.NewFeatureData[m.SelectedNewFeature]
visibleItems := m.Height - 9
if visibleItems < 5 {
visibleItems = 5
}
maxScroll := len(data.Items) - visibleItems
if maxScroll < 0 {
maxScroll = 0
}
switch key {
case "up", "k":
if m.NewFeatureScroll > 0 {
m.NewFeatureScroll--
}
case "down", "j":
if m.NewFeatureScroll < maxScroll {
m.NewFeatureScroll++
}
case "esc", "q", "enter", " ":
m.Screen = ScreenNewFeature
m.NewFeatureScroll = 0
}
return m, nil
}// Define message type
type newFeatureLoadedMsg struct {
data []SomeType
err error
}
// Send message from command
func loadNewFeatureCmd() tea.Cmd {
return func() tea.Msg {
data, err := loadData()
return newFeatureLoadedMsg{data: data, err: err}
}
}
// Handle in Update
case newFeatureLoadedMsg:
if msg.err != nil {
m.ErrorMsg = msg.err.Error()
return m, nil
}
m.NewFeatureData = msg.data
return m, nil---
cd installer && go build ./cmd/gentleman-installer # Build installer cd installer && go test ./internal/tui/... # Run TUI tests cd installer && go test -run TestNewFeature # Run specific test
---
Persistent memory system for AI coding agents. Agent-agnostic Go binary with SQLite + FTS5, MCP server, HTTP API, CLI, and TUI.
Repo: Gentleman-Programming/engram
ALWAYS ACTIVE — Persistent memory protocol. You MUST save decisions, conventions, bugs, and discoveries to engram proactively. Do NOT wait for the user to ask.
Architecture guardrails for Engram across local store, cloud sync, dashboard, and plugins. Trigger: Any change that affects system boundaries, ownership, state…
Backlog triage protocol for Engram: audit open issues and PRs, classify each item, infer maintainer ideology from comments, and produce an actionable…
PR creation workflow for Engram following the issue-first enforcement system. Trigger: When creating a pull request, opening a PR, or preparing changes for…
Product and business-rule guardrails for Engram. Trigger: Any change that affects sync behavior, project controls, permissions, or memory semantics.
Commit and branch naming standards for Engram contributors, enforced by GitHub rulesets. Trigger: Any commit creation, review, or branch cleanup.