Component Architecture
December 27, 2025 · View on GitHub
This document describes the architecture and design patterns of the FontGet components library.
Component Hierarchy
Base Components (bubbletea primitives)
↓
Simple Components (Button, Checkbox, Switch, TextInput)
↓
Composite Components (ButtonGroup, CheckboxList)
↓
Form Components (UnifiedFormModel, FormModel, FormNavigation)
↓
Command Models (backup, sources_manage, etc.)
Component Categories
1. Input Components (User Input)
These components handle direct user input:
-
TextInput: Use
textinput.Modeldirectly fromgithub.com/charmbracelet/bubbles/textinput- No wrapper component needed
- Apply styles directly:
input.TextStyle = ui.FormInput - Handle background styling at render time if needed
-
CheckboxList: List of checkboxes with navigation
HasFocus,SetFocus(),HandleKey(),Render()- Well-designed, keep as-is
-
Switch: Toggle switch component
HasFocus,SetFocus(),HandleKey(),Render()- Standardized interface
2. Action Components (User Actions)
These components handle user actions:
-
Button / ButtonGroup: Button navigation and selection
HasFocus,SetFocus(),HandleKey(),Render()- Well-designed, keep as-is
-
ConfirmModel: Confirmation dialog
- Uses ButtonGroup internally
- Full
tea.Modelimplementation
3. Form Components (Composite)
These components combine multiple input/action components:
-
UnifiedFormModel: Comprehensive form component
- Supports mixed component types (text inputs, checkboxes, buttons)
- Unified navigation and validation
- Use for new forms
-
FormModel: Simple text-only form
- Deprecated in favor of UnifiedFormModel
- Keep for backward compatibility
-
FormNavigation: Navigation helper for list + buttons
- Handles Tab navigation between list and buttons
- Can be enhanced to work with UnifiedFormModel
4. Display Components (Information)
These components display information:
- CardModel: Card display component
- PreviewModel: Preview display component
- ProgressBarModel: Progress indicator
- Well-designed, keep as-is
5. Layout Components (Structure)
These components provide layout structure:
- OverlayModel: Overlay/modal layout
- BlankBackgroundModel: Blank background for modals
Standard Component Interface
All interactive components should implement:
type Component interface {
HasFocus bool
SetFocus(bool)
HandleKey(string) (handled bool, ...)
Render() string
}
Focus Management
HasFocus: Boolean indicating if component currently has focusSetFocus(bool): Set focus state- Components should handle focus in
HandleKey()when appropriate keys are pressed
Key Handling
HandleKey(string): Process keyboard input- Returns whether the key was handled
- May return additional data (e.g., button actions)
Rendering
Render(): Return string representation of component- Should respect
HasFocusstate - Use UI styles from
internal/uipackage
Design Principles
1. Composition over Inheritance
Prefer composition of simple components over complex inheritance hierarchies.
2. Single Responsibility
Each component should have a single, well-defined purpose.
3. Consistent Interfaces
All similar components should follow the same interface patterns.
4. Direct Use of Primitives
Use bubbletea primitives directly (e.g., textinput.Model) rather than wrapping unnecessarily.
5. Focus Management
Centralize focus management using integer-based indices rather than string-based states.
Navigation Patterns
Tab Navigation
Use modulo arithmetic for wrapping navigation:
// Forward
focusedIdx = (focusedIdx + 1) % len(components)
// Backward
focusedIdx = (focusedIdx - 1 + len(components)) % len(components)
Focus Updates
Centralize focus updates in a single method:
func (m *Model) updateFocus() {
// Blur all components
for i := range m.components {
m.blurComponent(i)
}
// Focus current component
m.focusComponent(m.focusedIdx)
}
Best Practices
- Use raw
textinput.Model: Don't wrap unnecessarily - Integer-based focus: Use
focusedComponent intinstead ofFocusState string - Centralized focus management: Single
updateFocus()method - Simple navigation: 2-line modulo arithmetic for Tab navigation
- Consistent styling: Use
internal/uistyles - Type-safe: Prefer enums/constants over strings for types
Migration Guide
From TextInput Wrapper to Raw textinput.Model
Before:
pathInput := components.NewTextInput(components.TextInputOptions{
Placeholder: defaultPath,
FixedWidth: 60,
WithBackground: true,
})
After:
pathInput := textinput.New()
pathInput.Placeholder = defaultPath
pathInput.Width = 60
pathInput.TextStyle = ui.FormInput
pathInput.PlaceholderStyle = ui.FormPlaceholder
// Handle background at render time if needed
From String-Based Focus to Integer-Based
Before:
FocusState string // "path", "checkboxes", "buttons"
if m.FocusState == "path" { ... }
After:
focusedComponent int // 0=path, 1=checkboxes, 2=buttons
if m.focusedComponent == 0 { ... }
From Manual Navigation to Modulo Arithmetic
Before:
if key == "tab" {
switch m.FocusState {
case "path":
m.FocusState = "checkboxes"
case "checkboxes":
m.FocusState = "buttons"
case "buttons":
m.FocusState = "path"
}
}
After:
if key == "tab" {
m.focusedComponent = (m.focusedComponent + 1) % 3
m.updateFocus()
}
Component Lifecycle
- Initialization: Create component with
New*()function - Focus: Set initial focus with
SetFocus(true)orFocus()for text inputs - Update: Handle messages in
Update()method - Render: Display component in
View()method - Cleanup: Blur components when done
Testing
All components should have comprehensive tests covering:
- Rendering with different states
- Key handling
- Focus management
- Edge cases (empty lists, out of bounds, etc.)
See existing test files for examples:
button_test.gocheckbox_test.goswitch_test.gounified_form_test.go