devtui

package module
v0.2.26 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 31, 2025 License: MIT Imports: 14 Imported by: 0

README

DevTUI

Project Badges

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 or an internal log() function and displays them in a clean, organized terminal interface with tabs, navigation, and automatic formatting.

Unified Architecture: DevTUI uses a single entry point (AddHandler) for all types of content. Handlers can optionally implement the Loggable interface to receive a logger from DevTUI, enabling automatic message tracking and a clean "last log only" display.

devtui

→ Why DevTUI? - Complete purpose and functionality description

Quick Start

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:

// Loggable Handler - Automatic logging with Name() tracking
type BackupHandler struct {
    log func(message ...any)
}

func (h *BackupHandler) Name() string                     { return "SystemBackup" }
func (h *BackupHandler) Label() string                    { return "Create System Backup" }
func (h *BackupHandler) SetLog(f func(message ...any))    { h.log = f }
func (h *BackupHandler) Execute(progress chan<- string) {
    h.log("Preparing backup...")
    time.Sleep(200 * time.Millisecond)
    h.log("Backing up database...")
    time.Sleep(500 * time.Millisecond)
    h.log("Backup completed successfully")
}

func main() {
    tui := devtui.NewTUI(&devtui.TuiConfig{
        AppName:  "Demo",
        ExitChan: make(chan bool),
        // ... colors ...
    })

    // Operations tab with universal AddHandler registration
    ops := tui.NewTabSection("Operations", "System Operations")
    
    // DevTUI detects BackupHandler implements Loggable and calls SetLog()
    tui.AddHandler(&BackupHandler{}, 5*time.Second, "#10b981", ops)

    var wg sync.WaitGroup
    wg.Add(1)
    go tui.Start(&wg)
    wg.Wait()
}

👉 See complete example with all handler types

Handler Interfaces

DevTUI provides 6 specialized handler types, each requiring minimal implementation:

1. HandlerDisplay - Read-only Information (2 methods)
type HandlerDisplay interface {
    Name() string    // Full text to display in footer
    Content() string // Content shown immediately
}

→ See complete implementation example

2. HandlerEdit - Interactive Input Fields (4 methods)
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

3. HandlerExecution - Action Buttons (3 methods)
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

4. HandlerInteractive - Interactive Content Management (5 methods)
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() == false to trigger content display
  • Perfect for: Chat interfaces, configuration wizards, interactive help systems

→ See complete implementation example

5. Loggable Interface - Automatic Logging (2 methods)
// Loggable handlers receive a logger from DevTUI. All messages sent 
// to this logger are automatically tracked by Name().
type Loggable interface {
    Name() string
    SetLog(logger func(message ...any))
}

Key Advantage: When a handler uses its internal logger, DevTUI ensures the terminal view remains clean by showing only the most recent message for that specific handler (ordered newest first). Full history is preserved internally and remains accessible via MCP tools.

→ See complete Loggable implementation example

Registration Methods

// 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
//   - Loggable: Automatic logging with Name() tracking
//
// Optional interfaces (detected automatically):
//   - ShortcutProvider: Registers global keyboard shortcuts

// Examples:
tui.AddHandler(myDisplayHandler, 0, "", tab)                    // Display handler
tui.AddHandler(myEditHandler, 2*time.Second, "#3b82f6", tab)    // Edit handler 
tui.AddHandler(myLoggableHandler, 0, "#6b7280", tab)            // Loggable handler
Automatic Tracking and Clean View

DevTUI implements a clean terminal policy. By default:

  1. Every message sent via SetLog() is matched to the handler's Name().
  2. The terminal displays only the last log entry for each registered handler.
  3. This prevents log saturation and keeps the interface focused on the current state.
  4. MCP tools can still request the full history for debugging purposes.

Key Features

  • Minimal Implementation: 1-5 methods per handler
  • Universal Registration: Single AddHandler() method for all handler types
  • Loggable Interface: Automatic logging with SetLog and name-based tracking
  • Clean Terminal Display: Only the most recent log per handler is shown by default
  • Progress Callbacks: Real-time feedback for long-running operations
  • 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 progress parameter has type chan<- string on 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 functionality is now built-in for all Loggable handlers.

Decoupled Architecture

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)
    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

Navigation

  • 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.

Acknowledgments

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.

Contributing

Documentation

Index

Constants

View Source
const (
	LogOpen  = "[..." // Start or update same line with auto-animation
	LogClose = "...]" // Update same line and stop auto-animation
)
View Source
const HandlerNameWidth = 8
View Source
const (
	MCPToolName = "app_get_logs"
)

Variables

This section is empty.

Functions

func NewDisplayHandler

func NewDisplayHandler(h HandlerDisplay, color string) *anyHandler

func NewEditHandler

func NewEditHandler(h HandlerEdit, timeout time.Duration, color string) *anyHandler

func NewExecutionHandler

func NewExecutionHandler(h HandlerExecution, timeout time.Duration, color string) *anyHandler

func NewInteractiveHandler

func NewInteractiveHandler(h HandlerInteractive, timeout time.Duration, color string) *anyHandler

Types

type ColorPalette

type ColorPalette struct {
	// Base (2 colores)
	Foreground string // #F4F4F4
	Background string // #000000

	// Accent (2 colores)
	Primary   string // #FF6600 (tu actual Primary)
	Secondary string // #666666 (tu actual Secondary)

	// Semantic (4 colores)
	Success string // #00FF00
	Warning string // #FFFF00
	Error   string // #FF0000
	Info    string // #00FFFF

	// UI (2-4 colores adicionales)
	Border   string // #444444
	Muted    string // #999999
	Selected string // Derivado de Primary
	Hover    string // Derivado de Primary
}

func DefaultPalette

func DefaultPalette() *ColorPalette

type DevTUI

type DevTUI struct {
	*TuiConfig

	TabSections []*tabSection // represent sections in the tui
	// contains filtered or unexported fields
}

DevTUI mantiene el estado de la aplicación

func NewTUI

func NewTUI(c *TuiConfig) *DevTUI

NewTUI creates a new DevTUI instance and initializes it.

Usage Example:

config := &TuiConfig{
    AppName: "MyApp",
    ExitChan: make(chan bool),
    Color: nil, // or your *ColorPalette
    Logger: func(err any) { os.Stdout.WriteString(fmt.Fmt("%v\n", err)) },
}
tui := NewTUI(config)

func (*DevTUI) AddHandler

func (t *DevTUI) AddHandler(handler any, timeout time.Duration, color string, tabSection any)

AddHandler is the ONLY method to register handlers of any type. It accepts any handler interface and internally detects the type. Does NOT return anything - enforces complete decoupling.

Supported handler interfaces (from interfaces.go):

  • HandlerDisplay: Static/dynamic content display
  • 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

Parameters:

  • handler: ANY handler implementing one of the supported interfaces
  • timeout: Operation timeout (used for Edit/Execution/Interactive handlers, ignored for Display)
  • color: Hex color for handler messages (e.g., "#1e40af", empty string for default)
  • tabSection: The tab section returned by NewTabSection (as any for decoupling)

Example:

tab := tui.NewTabSection("BUILD", "Compiler")
tui.AddHandler(myEditHandler, 2*time.Second, "#3b82f6", tab)
tui.AddHandler(myDisplayHandler, 0, "", tab)

func (*DevTUI) ContentView

func (h *DevTUI) ContentView() string

ContentView renderiza los mensajes para una sección de contenido

func (*DevTUI) ContentViewPlain added in v0.2.24

func (h *DevTUI) ContentViewPlain(tabIndex int) string

ContentViewPlain renders messages for a content section without ANSI codes (for MCP)

func (*DevTUI) GetMCPToolsMetadata added in v0.2.24

func (d *DevTUI) GetMCPToolsMetadata() []MCPToolMetadata

GetMCPToolsMetadata returns MCP tools provided by DevTUI. This method is called via reflection by mcpserve to discover tools.

func (*DevTUI) GetTabSections added in v0.2.25

func (t *DevTUI) GetTabSections() []any

GetTabSections returns all tab sections as a slice of any for interface compatibility

func (*DevTUI) Init

func (h *DevTUI) Init() tea.Cmd

Init initializes the terminal UI application.

func (*DevTUI) Name added in v0.2.26

func (d *DevTUI) Name() string

Name implements Loggable interface for MCP integration

func (*DevTUI) NewTabSection

func (t *DevTUI) NewTabSection(title, description string) any

NewTabSection creates a new tab section and returns it as any for interface decoupling. The returned value must be passed to the AddHandler method.

Example:

tab := tui.NewTabSection("BUILD", "Compiler Section")
tui.AddHandler(myHandler, 2*time.Second, "#3b82f6", tab)

func (*DevTUI) RefreshUI

func (h *DevTUI) RefreshUI()

RefreshUI updates the TUI display for the currently active tab. This method is designed to be called from external tools/handlers to notify devtui that the UI needs to be refreshed without creating coupling.

Thread-safe and can be called from any goroutine. Only updates the view if the TUI is actively running.

Usage from external tools:

tui.RefreshUI() // Triggers a UI refresh for the active tab

func (*DevTUI) ReturnFocus

func (t *DevTUI) ReturnFocus() error

func (*DevTUI) SetLog added in v0.2.26

func (d *DevTUI) SetLog(log func(message ...any))

SetLog implements Loggable interface for MCP integration This allows mcpserve to inject a capturing logger

func (*DevTUI) SetTestMode

func (h *DevTUI) SetTestMode(enabled bool)

SetTestMode enables or disables test mode for synchronous behavior in tests. This should only be used in test files to make tests deterministic.

func (*DevTUI) Start

func (h *DevTUI) Start(args ...any)

Start initializes and runs the terminal UI application.

It accepts optional variadic arguments of any type. If a *sync.WaitGroup is provided among these arguments, Start will call its Done() method before returning.

The method runs the UI using the internal tea engine, and handles any errors that may occur during execution. If an error occurs, it will be displayed on the console and the application will wait for user input before exiting.

Parameters:

  • args ...any: Optional arguments. Can include a *sync.WaitGroup for synchronization.

func (*DevTUI) Update

func (h *DevTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update maneja las actualizaciones del estado

func (*DevTUI) View

func (h *DevTUI) View() string

type HandlerDisplay

type HandlerDisplay interface {
	Name() string    // Full text to display in footer (handler responsible for content) eg. "System Status Information Display"
	Content() string // Display content (e.g., "help\n1-..\n2-...", "executing deploy wait...")
}

HandlerDisplay defines the interface for read-only information display handlers. These handlers show static or dynamic content without user interaction.

type HandlerEdit

type HandlerEdit interface {
	Name() string           // Identifier for logging: "ServerPort", "DatabaseURL"
	Label() string          // Field label (e.g., "Server Port", "Host Configuration")
	Value() string          // Current/initial value (e.g., "8080", "localhost")
	Change(newValue string) // Handle user input + content display via log
}

HandlerEdit defines the interface for interactive fields that accept user input. These handlers allow users to modify values through text input.

type HandlerExecution

type HandlerExecution interface {
	Name() string  // Identifier for logging: "DeployProd", "BuildProject"
	Label() string // Button label (e.g., "Deploy to Production", "Build Project")
	Execute()      // Execute action + content display via log
}

HandlerExecution defines the interface for action buttons that execute operations. These handlers trigger business logic when activated by the user.

type HandlerInteractive

type HandlerInteractive interface {
	Name() string           // Identifier for logging: "ChatBot", "ConfigWizard"
	Label() string          // Field label (updates dynamically)
	Value() string          // Current input value
	Change(newValue string) // Handle user input + content display via log
	WaitingForUser() bool   // Should edit mode be auto-activated?
}

HandlerInteractive defines the interface for interactive content handlers. These handlers combine content display with user interaction capabilities. All content display is handled through progress() for consistency.

type Loggable added in v0.2.25

type Loggable interface {
	Name() string
	SetLog(logger func(message ...any))
}

Loggable defines optional logging capability for handlers. Handlers implementing this receive a logger function from DevTUI when registered via AddHandler.

The log function provided by DevTUI: - Is never nil (safe to call immediately) - Automatically tracks messages by handler Name() - Stores full history internally - Displays only most recent log in terminal (clean view)

Example implementation:

type WasmClient struct {
    log func(message ...any)
}

func NewWasmClient() *WasmClient {
    return &WasmClient{
        log: func(message ...any) {}, // no-op until SetLog called
    }
}

func (w *WasmClient) Name() string { return "WASM" }

func (w *WasmClient) SetLog(logger func(message ...any)) {
    w.log = logger
}

func (w *WasmClient) Compile() {
    w.log("Compiling...")
}

type MCPParameterMetadata added in v0.2.24

type MCPParameterMetadata struct {
	Name        string
	Description string
	Required    bool
	Type        string // "string", "number", "boolean"
	EnumValues  []string
	Default     any
}

MCPParameterMetadata describes a tool parameter. Fields must match mcpserve.ParameterMetadata for reflection compatibility.

type MCPToolMetadata added in v0.2.24

type MCPToolMetadata struct {
	Name        string
	Description string
	Parameters  []MCPParameterMetadata
	Execute     ToolExecutor // Changed from 2-param to 1-param signature to match client pattern
}

MCPToolMetadata provides MCP tool configuration metadata. Fields must match mcpserve.ToolMetadata for reflection compatibility. DevTUI does NOT import mcpserve to maintain decoupling.

type ShortcutEntry

type ShortcutEntry struct {
	Key         string // The shortcut key (e.g., "c", "d", "p")
	Description string // Human-readable description (e.g., "coding mode", "debug mode")
	TabIndex    int    // Index of the tab containing the handler
	FieldIndex  int    // Index of the field within the tab
	HandlerName string // Handler name for identification
	Value       string // Value to pass to Change()
}

ShortcutEntry represents a registered shortcut

type ShortcutProvider

type ShortcutProvider interface {
	Shortcuts() []map[string]string // Returns ordered list of single-entry maps with shortcut->description, preserving registration order
}

ShortcutProvider defines the optional interface for handlers that provide global shortcuts. HandlerEdit implementations can implement this interface to enable global shortcut keys.

type ShortcutRegistry

type ShortcutRegistry struct {
	// contains filtered or unexported fields
}

ShortcutRegistry manages global shortcut keys

func (*ShortcutRegistry) Get

func (sr *ShortcutRegistry) Get(key string) (*ShortcutEntry, bool)

func (*ShortcutRegistry) GetAll

func (sr *ShortcutRegistry) GetAll() map[string]*ShortcutEntry

GetAll returns all registered shortcuts for UI display

func (*ShortcutRegistry) List

func (sr *ShortcutRegistry) List() []string

func (*ShortcutRegistry) Register

func (sr *ShortcutRegistry) Register(key string, entry *ShortcutEntry)

func (*ShortcutRegistry) Unregister

func (sr *ShortcutRegistry) Unregister(key string)

type StreamingLoggable added in v0.2.25

type StreamingLoggable interface {
	Loggable
	AlwaysShowAllLogs() bool // Return true to show all messages
}

StreamingLoggable enables handlers to display ALL log messages instead of the default "last message only" behavior.

type ToolExecutor added in v0.2.26

type ToolExecutor func(args map[string]any)

ToolExecutor defines how a tool should be executed

type TuiConfig

type TuiConfig struct {
	AppName  string    // app name eg: "MyApp"
	ExitChan chan bool //  global chan to close app eg: make(chan bool)
	/*// *ColorPalette style for the TUI
	  // if nil it will use default style:
	type ColorPalette struct {
	 Foreground string // eg: #F4F4F4
	 Background string // eg: #000000
	 Primary  string // eg: #FF6600
	 Secondary   string // eg: #666666
	}*/
	Color *ColorPalette

	Logger func(messages ...any) // function to write log error
}

Directories

Path Synopsis
demo command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL