This document provides guidance for AI assistants (Claude, GPT, etc.) working on this codebase. It documents key patterns, common pitfalls, and project-specific conventions.
lazypg is a terminal UI client for PostgreSQL built with:
- Bubble Tea - TUI framework
- Lip Gloss - Terminal styling
- pgx - PostgreSQL driver
lazypg/
├── cmd/lazypg/ # Main entry point
├── internal/
│ ├── app/ # Main application logic
│ ├── ui/components/ # Reusable UI components
│ ├── ui/theme/ # Color themes
│ ├── db/ # Database operations
│ ├── models/ # Data structures
│ └── ... # Other internal packages
├── config/ # Default configuration
└── docs/ # Documentation
When using lipgloss borders with Width() or MaxWidth(), borders and padding render outside the content area. This causes overflow if not handled correctly.
Always use GetHorizontalFrameSize() to calculate available content width:
// Define style FIRST
containerStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2)
// Calculate content width
contentWidth := terminalWidth - containerStyle.GetHorizontalFrameSize()
// Render with calculated width
content := renderContent(contentWidth)
return containerStyle.Render(content)// DON'T DO THIS - width doesn't account for frame
containerStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2).
Width(80) // Will overflow by 6 chars!Pass content width down through component hierarchy:
// Parent calculates and passes width
contentWidth := maxWidth - parentStyle.GetHorizontalFrameSize()
childContent := child.Render(contentWidth)
// Child subtracts its own frame
childContentWidth := contentWidth - childStyle.GetHorizontalFrameSize()Create styles once and reuse them. Don't create new lipgloss.Style objects in render loops:
// GOOD: Cache styles in struct
type MyComponent struct {
cachedStyles *myStyles
}
func (c *MyComponent) initStyles() {
c.cachedStyles = &myStyles{
header: lipgloss.NewStyle().Bold(true),
// ...
}
}
// BAD: Creating styles on every render
func (c *MyComponent) View() string {
style := lipgloss.NewStyle().Bold(true) // Don't do this
}Use bubblezone for mouse click detection:
// Mark clickable zones
zone.Mark("button-id", buttonContent)
// Check clicks in Update
if zone.Get("button-id").InBounds(mouseMsg) {
// Handle click
}Initialize bubblezone in tests:
func init() {
zone.NewGlobal()
}- Log errors but don't crash on non-critical failures
- Show user-friendly messages in UI overlays
- Use
log.Printf("Warning: ...")for non-fatal errors
- Initialize
zone.NewGlobal()in test files that callView()methods - Test UI at 80-char terminal width
- Run
go test ./...before committing
- Use conventional commit format:
feat:,fix:,perf:, etc. - Keep commits focused and atomic
- Create file in
internal/ui/components/ - Implement
tea.Modelinterface (Init, Update, View) - Add style caching with
initStyles()method - Add zone marks for mouse support if needed
- Add tests with
zone.NewGlobal()in init
- Add command type in
internal/commands/commands.go - Register in command registry
- Handle message in
internal/app/app.goUpdate method