Reusable message presentation system for Go development tools. DevTUI is a pure display layer built on bubbletea that formats and organizes messages from your business logic handlers.
Decoupled Design: DevTUI follows consumer-driven interface design - your application defines UI interfaces, DevTUI implements them. This enables zero coupling, easy testing, and pluggable UI implementations.
What DevTUI does: Takes messages from your handlers via a progress channel and displays them in a clean, organized terminal interface with tabs, navigation, and automatic formatting.
What DevTUI doesn't do: Validate data, handle errors, or manage business logic. Your handlers are responsible for their own state and decisions - DevTUI just shows whatever they tell it to show.
β Why DevTUI? - Complete purpose and functionality description
DevTUI uses specialized handler interfaces that require minimal implementation. Here is a complete example using the new universal registration API:
If your handler implements both the HandlerEdit interface and a Content() method, DevTUI will only refresh the display when the value changes, and will not create a new timestamped message. This is ideal for cases like language selectors, dashboards, or any field where the content is always shown in a custom format.
Example:
type LanguageHandler struct {
lang string
}
func (h *LanguageHandler) Name() string { return "Language" }
func (h *LanguageHandler) Label() string { return "Language" }
func (h *LanguageHandler) Value() string { return h.lang }
func (h *LanguageHandler) Change(newValue string, progress chan<- string) {
h.lang = newValue
// Send a single human-readable status message. The caller owns the
// channel lifecycle (do NOT close it from here).
progress <- "Language changed to " + newValue // Will only refresh, not create a message
}
func (h *LanguageHandler) Content() string {
return "Current language: " + h.lang
}When the user changes the value, the UI will update the content, but no new message will appear in the message area. This behavior is now fully tested and guaranteed by the DevTUI test suite.
DevTUI uses specialized handler interfaces that require minimal implementation. Here is a complete example using the new universal registration API:
// HandlerExecution with MessageTracker - Action buttons with progress tracking
type BackupHandler struct {
lastOpID string
}
func (h *BackupHandler) Name() string { return "SystemBackup" }
func (h *BackupHandler) Label() string { return "Create System Backup" }
func (h *BackupHandler) Execute(progress chan<- string) {
progress <- "Preparing backup..."
time.Sleep(200 * time.Millisecond)
progress <- "Backing up database..."
time.Sleep(500 * time.Millisecond)
progress <- "Backup completed successfully"
}
// MessageTracker implementation for operation tracking
func (h *BackupHandler) GetLastOperationID() string { return h.lastOpID }
func (h *BackupHandler) SetLastOperationID(id string) { h.lastOpID = id }
func main() {
tui := devtui.NewTUI(&devtui.TuiConfig{
AppName: "Demo",
ExitChan: make(chan bool),
Color: &devtui.ColorPalette{
Foreground: "#F4F4F4",
Background: "#000000",
Primary: "#FF6600",
Secondary: "#666666",
},
Logger: func(messages ...any) {
// Replace with actual logging implementation
},
})
// Operations tab with universal AddHandler registration
ops := tui.NewTabSection("Operations", "System Operations")
tui.AddHandler(&BackupHandler{}, 5*time.Second, "#10b981", ops) // Universal registration with MessageTracker detection
var wg sync.WaitGroup
wg.Add(1)
go tui.Start(&wg)
wg.Wait()
}π See complete example with all handler types
DevTUI provides 6 specialized handler types, each requiring minimal implementation:
type HandlerDisplay interface {
Name() string // Full text to display in footer
Content() string // Content shown immediately
}β See complete implementation example
type HandlerEdit interface {
Name() string // Unique identifier for logging
Label() string // Field label
Value() string // Current/initial value
// Change receives a progress channel for status updates. Implementations
// should send human-readable messages to the channel (e.g. "validating...",
// "saving...", "done"). The caller manages the channel lifecycle and will
// close it; implementations MUST NOT close the channel. Avoid blocking
// indefinitely when sending (the channel may be unbuffered).
Change(newValue string, progress chan<- string)
}Optional Shortcut Support: Add Shortcuts() []map[string]string method to enable global keyboard shortcuts while preserving the registration order. Each element in the returned slice should be a single-entry map where the key is the shortcut and the value is a human-readable description. Example:
// Shortcuts work from any tab and automatically navigate to this field
func (h *DatabaseHandler) Shortcuts() []map[string]string {
return []map[string]string{
{"t": "test connection"}, // Pressing 't' calls Change("t", progress)
{"b": "backup database"}, // Pressing 'b' calls Change("b", progress)
}
}β See complete implementation example
type HandlerExecution interface {
Name() string // Unique identifier for logging
Label() string // Button label
// Execute runs the action and can send progress updates via the provided
// channel. Do not close the channel from the implementation; the caller
// owns the lifecycle. Avoid blocking sends on the channel to prevent UI
// deadlocks.
Execute(progress chan<- string)
}β See complete implementation example
type HandlerInteractive interface {
Name() string // Identifier for logging
Label() string // Field label (updates dynamically)
Value() string // Current input value
// Change receives a progress channel for streaming content and status
// updates. Implementations should not close the channel and should avoid
// blocking sends.
Change(newValue string, progress chan<- string) // Handle user input + content display
WaitingForUser() bool // Should edit mode be auto-activated?
}Key Features:
- Dynamic Content: All content updates through
progress()for consistency - Auto Edit Mode:
WaitingForUser()controls when edit mode is activated - Content Display: Use empty
newValue+WaitingForUser() == falseto trigger content display - Perfect for: Chat interfaces, configuration wizards, interactive help systems
β See complete implementation example
type HandlerLogger interface {
Name() string // Writer identifier
}β See complete HandlerLogger implementation example
β See complete HandlerLoggerTracker implementation example
DevTUI uses a universal registration method with automatic type detection and MessageTracker detection. Simply pass any handler and DevTUI automatically detects the interface type and capabilities:
// Universal AddHandler method - works with ALL handler types
tab := tui.NewTabSection("MyTab", "My Description")
tui.AddHandler(handler, timeout, color, tab)
// Supported handler interfaces (detected automatically):
// - HandlerDisplay: Static/dynamic content display (timeout ignored)
// - HandlerEdit: Interactive text input fields
// - HandlerExecution: Action buttons
// - HandlerInteractive: Combined display + interaction
// - HandlerLogger: Basic line-by-line logging (via MessageTracker detection)
//
// Optional interfaces (detected automatically):
// - MessageTracker: Enables message update tracking
// - ShortcutProvider: Registers global keyboard shortcuts
// Examples:
tui.AddHandler(myDisplayHandler, 0, "", tab) // Display handler (timeout ignored)
tui.AddHandler(myEditHandler, 2*time.Second, "#3b82f6", tab) // Edit handler with timeout
tui.AddHandler(myExecutionHandler, 5*time.Second, "#10b981", tab) // Execution handler with timeout
tui.AddHandler(myInteractiveHandler, 3*time.Second, "#f59e0b", tab) // Interactive handler
// Logger creation (returns func(message ...any))
logger := tui.AddLogger("LogWriter", false, "#6b7280", tab) // Basic logger (new lines only)
loggerWithTracker := tui.AddLogger("TrackedWriter", true, "#3b82f6", tab) // Advanced logger (message tracking)
logger("Log message 1")
logger("Another log entry")To enable operation tracking (updating existing messages instead of creating new ones), simply implement the MessageTracker interface:
type MessageTracker interface {
GetLastOperationID() string
SetLastOperationID(id string)
}DevTUI automatically detects when a handler implements MessageTracker and enables operation tracking without any additional registration steps.
- Minimal Implementation: 1-5 methods per handler
- Universal Registration: Single
AddHandler()method for all handler types with automatic type detection - Specialized Interfaces: Clear separation by purpose (Display, Edit, Execution, Interactive, Logger)
- Progress Callbacks: Real-time feedback for long-running operations
- Automatic MessageTracker Detection: Optionally implement
MessageTrackerinterface for operation tracking - Decoupled Architecture: Consumers define their own interfaces - DevTUI implements them
- Thread-Safe: Concurrent handler registration and execution
Progress callbacks (channel contract)
DevTUI provides a progress channel to handlers for streaming human-readable
status messages. A small contract to keep in mind:
- The
progressparameter has typechan<- stringon handler methods. - The caller (DevTUI) owns the channel lifecycle and will close it when the operation is finished. Handler implementations MUST NOT close the channel.
- Handlers may send zero or more messages. Messages should be plain strings intended for display to users (for example: "validating...", "step 2 done", or "error: ").
- To avoid deadlocks, avoid blocking sends on the channel. If a send may block (for example when the channel is unbuffered and the UI may be busy), send from a goroutine or use a non-blocking select with a default case.
- MessageTracker implementations can be used alongside progress messages to enable updating existing messages instead of appending new ones.
DevTUI follows a consumer-driven interface design where consuming applications define their own UI interfaces, and DevTUI implements them. This enables:
- Zero coupling: Consumer packages never import DevTUI
- Testability: Easy mocking for unit tests without UI dependencies
- Pluggability: UI implementations can be swapped without changing business logic
- Clean separation: Business logic knows nothing about the UI layer
Example consumer interface definition:
// Consumer defines its own interface (NO DevTUI import)
type TuiInterface interface {
NewTabSection(title, description string) any
AddHandler(handler any, timeout time.Duration, color string, tabSection any)
AddLogger(name string, enableTracking bool, color string, tabSection any) func(message ...any)
Start(wg *sync.WaitGroup)
}Usage in main.go (ONLY place that imports DevTUI):
// main.go - ONLY file that knows about DevTUI
ui := devtui.NewTUI(config)
consumer.Start(ui) // Pass UI as interface- Tab/Shift+Tab: Switch between tabs
- Left/Right: Navigate fields within tab
- Up/Down: Scroll viewport line by line
- Page Up/Page Down: Scroll viewport page by page
- Mouse Wheel: Scroll viewport (when available)
- Enter: Edit/Execute
- Esc: Cancel edit
- Ctrl+C: Exit
- Global Shortcuts: Single key shortcuts (e.g., "t", "b") work from any tab when defined in handlers
Shortcut System: Handlers implementing Shortcuts() []map[string]string automatically register global keyboard shortcuts in the order returned by the slice. When pressed, shortcuts navigate to the handler's tab/field and execute the Change() method with the shortcut key as the newValue parameter.
Example: If shortcuts return []map[string]string{{"t":"test connection"}}, pressing 't' calls Change("t", progress).
Note: DevTUI automatically loads a built-in ShortcutsHandler at position 0 in the first tab, which displays detailed keyboard navigation commands. This handler demonstrates the HandlerEdit interface and provides interactive help within the application.
Text Selection: Terminal text selection is enabled for copying error messages and logs. Mouse scroll functionality may vary depending on bubbletea version and terminal capabilities.
DevTUI is built on top of the excellent libraries from github.com/charmbracelet: bubbletea, bubbles and lipgloss, which provide the solid foundation for creating terminal interfaces in Go.
