tui-component-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TUI Component Design Patterns

TUI组件设计模式

Best practices for building maintainable, testable TUI components using Bubbletea v2 and the Charm ecosystem, based on the hive diff viewer implementation.
基于hive差异查看器的实现,本文介绍使用Bubbletea v2和Charm生态系统构建可维护、可测试的TUI组件的最佳实践。

Component Organization

组件组织

Single Responsibility Per File

单文件单一职责

Each component should be in its own file with clear boundaries:
internal/tui/diff/
├── model.go           # Top-level compositor that orchestrates sub-components
├── diffviewer.go      # Diff content display with scrolling and selection
├── filetree.go        # File navigation tree with expand/collapse
├── lineparse.go       # Pure function utilities for parsing diff lines
├── delta.go           # External tool integration (syntax highlighting)
└── utils.go           # Shared utilities
Key principle: Each file should represent ONE component with its own Model, Update, and View methods.
每个组件应放在独立文件中,边界清晰:
internal/tui/diff/
├── model.go           # 协调子组件的顶级组合器
├── diffviewer.go      # 支持滚动和选择的差异内容展示组件
├── filetree.go        # 支持展开/折叠的文件导航树
├── lineparse.go       # 解析差异行的纯函数工具
├── delta.go           # 外部工具集成(语法高亮)
└── utils.go           # 共享工具函数
**核心原则:**每个文件对应一个组件,包含独立的Model、Update和View方法。

Component Hierarchy Pattern

组件层级模式

For complex UIs, use a compositor pattern:
go
// Top-level Model composes sub-components
type Model struct {
    fileTree      FileTreeModel      // Left panel
    diffViewer    DiffViewerModel    // Right panel
    focused       FocusedPanel       // Which component has focus
    helpDialog    *components.HelpDialog  // Modal overlay
    showHelp      bool               // Dialog visibility state
}

// Update delegates to focused component
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch m.focused {
    case FocusFileTree:
        m.fileTree, cmd = m.fileTree.Update(msg)
    case FocusDiffViewer:
        m.diffViewer, cmd = m.diffViewer.Update(msg)
    }
    return m, cmd
}
Benefits:
  • Each sub-component is independently testable
  • Clear ownership of state and behavior
  • Easy to reason about message flow
对于复杂UI,使用组合器模式:
go
// 顶级Model组合子组件
type Model struct {
    fileTree      FileTreeModel      // 左侧面板
    diffViewer    DiffViewerModel    // 右侧面板
    focused       FocusedPanel       // 当前获焦的组件
    helpDialog    *components.HelpDialog  // 模态弹窗
    showHelp      bool               // 弹窗可见状态
}

// Update方法将事件委托给获焦组件
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch m.focused {
    case FocusFileTree:
        m.fileTree, cmd = m.fileTree.Update(msg)
    case FocusDiffViewer:
        m.diffViewer, cmd = m.diffViewer.Update(msg)
    }
    return m, cmd
}
优势:
  • 每个子组件可独立测试
  • 状态和行为的归属清晰
  • 消息流转易于追踪

Component Structure

组件结构

Standard Component Template

标准组件模板

go
// 1. Model struct with all state
type ComponentModel struct {
    // Data
    items []Item

    // UI State
    selected int
    offset   int
    width    int
    height   int

    // Feature flags
    iconStyle IconStyle
    expanded  bool
}

// 2. Constructor with dependencies
func NewComponent(data []Item, cfg *config.Config) ComponentModel {
    return ComponentModel{
        items:     data,
        selected:  0,
        iconStyle: determineIconStyle(cfg),
    }
}

// 3. Update handles messages
func (m ComponentModel) Update(msg tea.Msg) (ComponentModel, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        return m.handleKeyPress(msg)
    case tea.WindowSizeMsg:
        m.width = msg.Width
        m.height = msg.Height
    }
    return m, nil
}

// 4. View renders output
func (m ComponentModel) View() string {
    return m.render()
}

// 5. Helper methods for complex logic
func (m ComponentModel) render() string {
    // Rendering logic here
}
go
// 1. 包含所有状态的Model结构体
type ComponentModel struct {
    // 数据
    items []Item

    // UI状态
    selected int
    offset   int
    width    int
    height   int

    // 功能开关
    iconStyle IconStyle
    expanded  bool
}

// 2. 带依赖的构造函数
func NewComponent(data []Item, cfg *config.Config) ComponentModel {
    return ComponentModel{
        items:     data,
        selected:  0,
        iconStyle: determineIconStyle(cfg),
    }
}

// 3. 处理消息的Update方法
func (m ComponentModel) Update(msg tea.Msg) (ComponentModel, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        return m.handleKeyPress(msg)
    case tea.WindowSizeMsg:
        m.width = msg.Width
        m.height = msg.Height
    }
    return m, nil
}

// 4. 渲染输出的View方法
func (m ComponentModel) View() string {
    return m.render()
}

// 5. 处理复杂逻辑的辅助方法
func (m ComponentModel) render() string {
    // 渲染逻辑
}

State Management

状态管理

Avoid Hidden State

避免隐藏状态

Bad:
go
// State hidden in closures or package variables
var currentSelection int

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    currentSelection++ // Modifying hidden state
}
Good:
go
// All state explicit in model
type Model struct {
    currentSelection int
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    m.currentSelection++ // Clear, traceable state change
    return m, nil
}
错误示例:
go
// 状态隐藏在闭包或包变量中
var currentSelection int

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    currentSelection++ // 修改隐藏状态
}
正确示例:
go
// 所有状态显式定义在Model中
type Model struct {
    currentSelection int
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    m.currentSelection++ // 清晰、可追踪的状态变更
    return m, nil
}

Separate UI State from Data

分离UI状态与数据

go
type DiffViewerModel struct {
    // Immutable data
    file    *gitdiff.File
    content string
    lines   []string

    // Mutable UI state
    offset         int  // Scroll position
    cursorLine     int  // Current line
    selectionMode  bool // Visual mode active
    selectionStart int  // Selection anchor
}
Benefits:
  • Easy to test rendering at different scroll positions
  • Data can be shared/cached without UI state interference
  • Clear separation of concerns
go
type DiffViewerModel struct {
    // 不可变数据
    file    *gitdiff.File
    content string
    lines   []string

    // 可变UI状态
    offset         int  // 滚动位置
    cursorLine     int  // 当前行
    selectionMode  bool // 视觉模式是否激活
    selectionStart int  // 选择锚点
}
优势:
  • 易于测试不同滚动位置下的渲染效果
  • 数据可共享/缓存,不受UI状态干扰
  • 关注点分离清晰

Async Operations and Caching

异步操作与缓存

Pattern: Command-Based Async with Caching

模式:基于命令的异步处理+缓存

For expensive operations like syntax highlighting or external tool calls:
go
type ComponentModel struct {
    cache   map[string]*CachedResult
    loading bool
}

// 1. Initiate async operation, return immediately
func (m *ComponentModel) SetData(data *Data) tea.Cmd {
    filePath := data.Path

    // Check cache first
    if cached, ok := m.cache[filePath]; ok {
        m.content = cached.content
        m.lines = cached.lines
        return nil
    }

    // Mark as loading, start async
    m.loading = true
    return func() tea.Msg {
        content, lines := generateContent(data)
        return contentGeneratedMsg{filePath, content, lines}
    }
}

// 2. Handle completion message
func (m ComponentModel) Update(msg tea.Msg) (ComponentModel, tea.Cmd) {
    switch msg := msg.(type) {
    case contentGeneratedMsg:
        // Cache result
        m.cache[msg.filePath] = &CachedResult{
            content: msg.content,
            lines:   msg.lines,
        }
        // Update display
        m.content = msg.content
        m.lines = msg.lines
        m.loading = false
    }
    return m, nil
}
Key points:
  • Never block the UI thread
  • Cache expensive computations
  • Show loading state while processing
  • Custom messages for async results
对于语法高亮或外部工具调用等耗时操作:
go
type ComponentModel struct {
    cache   map[string]*CachedResult
    loading bool
}

// 1. 触发异步操作,立即返回
func (m *ComponentModel) SetData(data *Data) tea.Cmd {
    filePath := data.Path

    // 先检查缓存
    if cached, ok := m.cache[filePath]; ok {
        m.content = cached.content
        m.lines = cached.lines
        return nil
    }

    // 标记为加载中,启动异步任务
    m.loading = true
    return func() tea.Msg {
        content, lines := generateContent(data)
        return contentGeneratedMsg{filePath, content, lines}
    }
}

// 2. 处理完成消息
func (m ComponentModel) Update(msg tea.Msg) (ComponentModel, tea.Cmd) {
    switch msg := msg.(type) {
    case contentGeneratedMsg:
        // 缓存结果
        m.cache[msg.filePath] = &CachedResult{
            content: msg.content,
            lines:   msg.lines,
        }
        // 更新展示内容
        m.content = msg.content
        m.lines = msg.lines
        m.loading = false
    }
    return m, nil
}
关键点:
  • 绝不要阻塞UI线程
  • 缓存耗时计算结果
  • 处理过程中显示加载状态
  • 为异步结果定义自定义消息

External Tool Integration

外部工具集成

For tools like
delta
(syntax highlighting):
go
// 1. Check availability once at init
func NewDiffViewer(file *gitdiff.File) DiffViewerModel {
    deltaAvailable := CheckDeltaAvailable() == nil
    return DiffViewerModel{
        deltaAvailable: deltaAvailable,
    }
}

// 2. Separate pure function for testability
func generateDiffContent(file *gitdiff.File, deltaAvailable bool) (string, []string) {
    diff := buildUnifiedDiff(file)

    if !deltaAvailable {
        return diff, strings.Split(diff, "\n")
    }

    // Apply syntax highlighting
    return applyDelta(diff)
}

// 3. Make it async with proper error handling
func (m *ComponentModel) loadContent(file *gitdiff.File) tea.Cmd {
    return func() tea.Msg {
        content, lines := generateDiffContent(file, m.deltaAvailable)
        return contentReadyMsg{content, lines}
    }
}
对于
delta
(语法高亮)这类工具:
go
// 1. 初始化时检查工具可用性
func NewDiffViewer(file *gitdiff.File) DiffViewerModel {
    deltaAvailable := CheckDeltaAvailable() == nil
    return DiffViewerModel{
        deltaAvailable: deltaAvailable,
    }
}

// 2. 分离纯函数以提升可测试性
func generateDiffContent(file *gitdiff.File, deltaAvailable bool) (string, []string) {
    diff := buildUnifiedDiff(file)

    if !deltaAvailable {
        return diff, strings.Split(diff, "\n")
    }

    // 应用语法高亮
    return applyDelta(diff)
}

// 3. 通过命令实现异步处理并完善错误处理
func (m *ComponentModel) loadContent(file *gitdiff.File) tea.Cmd {
    return func() tea.Msg {
        content, lines := generateDiffContent(file, m.deltaAvailable)
        return contentReadyMsg{content, lines}
    }
}

Visual Modes and Complex Interactions

视觉模式与复杂交互

Mode-Based Keybindings

基于模式的快捷键绑定

For vim-style interfaces with normal/visual modes:
go
type Model struct {
    mode          Mode  // Normal, Visual, Insert
    selectionMode bool  // Visual mode active
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // Handle mode transitions first
        if msg.Code == 'v' && !m.selectionMode {
            m.selectionMode = true
            m.selectionStart = m.cursorLine
            return m, nil
        }

        if msg.Code == tea.KeyEscape && m.selectionMode {
            m.selectionMode = false
            return m, nil
        }

        // Handle mode-specific behavior
        if m.selectionMode {
            return m.handleVisualMode(msg)
        }
        return m.handleNormalMode(msg)
    }
    return m, nil
}
对于支持普通/视觉模式的vim风格界面:
go
type Model struct {
    mode          Mode  // 普通、视觉、插入模式
    selectionMode bool  // 视觉模式是否激活
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // 先处理模式切换
        if msg.Code == 'v' && !m.selectionMode {
            m.selectionMode = true
            m.selectionStart = m.cursorLine
            return m, nil
        }

        if msg.Code == tea.KeyEscape && m.selectionMode {
            m.selectionMode = false
            return m, nil
        }

        // 处理模式特定行为
        if m.selectionMode {
            return m.handleVisualMode(msg)
        }
        return m.handleNormalMode(msg)
    }
    return m, nil
}

Selection State Management

选择状态管理

For visual selection (highlighting lines):
go
type Model struct {
    selectionMode  bool
    selectionStart int  // Anchor point
    cursorLine     int  // Active end
}

// Helper to get normalized selection range
func (m Model) SelectionRange() (start, end int, active bool) {
    if !m.selectionMode {
        return 0, 0, false
    }

    start = m.selectionStart
    end = m.cursorLine
    if start > end {
        start, end = end, start
    }
    return start, end, true
}

// Use in rendering
func (m Model) View() string {
    start, end, active := m.SelectionRange()

    for i, line := range m.lines {
        if active && i >= start && i <= end {
            line = highlightStyle.Render(line)
        }
        // ... render line
    }
}
对于行高亮的视觉选择功能:
go
type Model struct {
    selectionMode  bool
    selectionStart int  // 锚点位置
    cursorLine     int  // 当前激活端
}

// 获取标准化的选择范围辅助方法
func (m Model) SelectionRange() (start, end int, active bool) {
    if !m.selectionMode {
        return 0, 0, false
    }

    start = m.selectionStart
    end = m.cursorLine
    if start > end {
        start, end = end, start
    }
    return start, end, true
}

// 在渲染中使用
func (m Model) View() string {
    start, end, active := m.SelectionRange()

    for i, line := range m.lines {
        if active && i >= start && i <= end {
            line = highlightStyle.Render(line)
        }
        // ... 渲染行
    }
}

Scroll Management

滚动管理

Viewport Pattern

视口模式

For scrollable content with fixed dimensions:
go
type Model struct {
    lines   []string
    offset  int  // Top visible line
    height  int  // Viewport height
}

// Calculate visible range
func (m Model) visibleLines() []string {
    start := m.offset
    end := min(m.offset + m.contentHeight(), len(m.lines))
    return m.lines[start:end]
}

// Content height (excluding fixed UI elements)
func (m Model) contentHeight() int {
    return m.height - headerHeight - footerHeight
}

// Scroll with cursor tracking
func (m Model) scrollDown() Model {
    // Move cursor first
    if m.cursorLine < len(m.lines)-1 {
        m.cursorLine++
    }

    // Adjust viewport if cursor moved out of view
    visibleBottom := m.offset + m.contentHeight() - 1
    if m.cursorLine > visibleBottom {
        m.offset++
    }

    return m
}
Key principle: Cursor moves first, viewport follows to keep cursor visible.
针对固定尺寸的可滚动内容:
go
type Model struct {
    lines   []string
    offset  int  // 顶部可见行
    height  int  // 视口高度
}

// 计算可见范围
func (m Model) visibleLines() []string {
    start := m.offset
    end := min(m.offset + m.contentHeight(), len(m.lines))
    return m.lines[start:end]
}

// 内容高度(排除固定UI元素)
func (m Model) contentHeight() int {
    return m.height - headerHeight - footerHeight
}

// 跟随光标滚动
func (m Model) scrollDown() Model {
    // 先移动光标
    if m.cursorLine < len(m.lines)-1 {
        m.cursorLine++
    }

    // 如果光标移出可视区域,调整视口
    visibleBottom := m.offset + m.contentHeight() - 1
    if m.cursorLine > visibleBottom {
        m.offset++
    }

    return m
}
**核心原则:**光标先移动,视口跟随以保持光标可见。

Editor Integration

编辑器集成

Opening External Editors

打开外部编辑器

Pattern for jumping to specific line in editor:
go
func (m Model) openInEditor(filePath string, lineNum int) tea.Cmd {
    return func() tea.Msg {
        editor := os.Getenv("EDITOR")
        if editor == "" {
            editor = "vim"
        }

        // Format: editor +line file
        arg := fmt.Sprintf("+%d", lineNum)
        cmd := exec.Command(editor, arg, filePath)

        // Important: Connect to terminal for interactive editors
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr

        err := cmd.Run()
        return editorFinishedMsg{err: err}
    }
}
Critical: For vim/interactive editors, you must connect stdin/stdout/stderr or the editor won't work properly.
跳转到编辑器指定行的模式:
go
func (m Model) openInEditor(filePath string, lineNum int) tea.Cmd {
    return func() tea.Msg {
        editor := os.Getenv("EDITOR")
        if editor == "" {
            editor = "vim"
        }

        // 格式:editor +line file
        arg := fmt.Sprintf("+%d", lineNum)
        cmd := exec.Command(editor, arg, filePath)

        // 重要:连接终端以支持交互式编辑器
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr

        err := cmd.Run()
        return editorFinishedMsg{err: err}
    }
}
**关键注意事项:**对于vim等交互式编辑器,必须连接stdin/stdout/stderr,否则编辑器无法正常工作。

Component Communication

组件通信

Message-Based Coordination

基于消息的协调

go
// Custom messages for component coordination
type (
    fileSelectedMsg struct {
        file *gitdiff.File
    }

    diffLoadedMsg struct {
        content string
    }
)

// Parent handles coordination
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case fileSelectedMsg:
        // FileTree selected a file, tell DiffViewer
        return m, m.diffViewer.LoadFile(msg.file)
    }

    // Delegate to children
    var cmd tea.Cmd
    m.fileTree, cmd = m.fileTree.Update(msg)
    return m, cmd
}
go
// 用于组件协调的自定义消息
type (
    fileSelectedMsg struct {
        file *gitdiff.File
    }

    diffLoadedMsg struct {
        content string
    }
)

// 父组件处理协调逻辑
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case fileSelectedMsg:
        // 文件树选中了文件,通知差异查看器
        return m, m.diffViewer.LoadFile(msg.file)
    }

    // 委托消息给子组件
    var cmd tea.Cmd
    m.fileTree, cmd = m.fileTree.Update(msg)
    return m, cmd
}

Focus Management

焦点管理

go
type FocusedPanel int

const (
    FocusFileTree FocusedPanel = iota
    FocusDiffViewer
)

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == tea.KeyTab {
        // Switch focus
        m.focused = (m.focused + 1) % 2
        return m, nil
    }

    // Only focused component handles input
    switch m.focused {
    case FocusFileTree:
        m.fileTree, cmd = m.fileTree.Update(msg)
    case FocusDiffViewer:
        m.diffViewer, cmd = m.diffViewer.Update(msg)
    }
    return m, cmd
}
go
type FocusedPanel int

const (
    FocusFileTree FocusedPanel = iota
    FocusDiffViewer
)

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == tea.KeyTab {
        // 切换焦点
        m.focused = (m.focused + 1) % 2
        return m, nil
    }

    // 仅获焦组件处理输入
    switch m.focused {
    case FocusFileTree:
        m.fileTree, cmd = m.fileTree.Update(msg)
    case FocusDiffViewer:
        m.diffViewer, cmd = m.diffViewer.Update(msg)
    }
    return m, cmd
}

Helper Modal Pattern

辅助弹窗模式

For overlays like help dialogs:
go
type Model struct {
    helpDialog *components.HelpDialog
    showHelp   bool
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    // Help dialog intercepts input when visible
    if m.showHelp {
        if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == '?' {
            m.showHelp = false
            return m, nil
        }
        // Help dialog handles all input
        *m.helpDialog, cmd = m.helpDialog.Update(msg)
        return m, cmd
    }

    // Toggle help
    if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == '?' {
        m.showHelp = true
        return m, nil
    }

    // Normal input handling
    // ...
}

func (m Model) View() string {
    view := m.renderNormal()

    if m.showHelp {
        // Overlay help on top
        return m.helpDialog.View(view)
    }

    return view
}
针对帮助对话框这类浮层:
go
type Model struct {
    helpDialog *components.HelpDialog
    showHelp   bool
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    // 帮助对话框可见时拦截所有输入
    if m.showHelp {
        if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == '?' {
            m.showHelp = false
            return m, nil
        }
        // 帮助对话框处理所有输入
        *m.helpDialog, cmd = m.helpDialog.Update(msg)
        return m, cmd
    }

    // 切换帮助弹窗
    if msg, ok := msg.(tea.KeyPressMsg); ok && msg.Code == '?' {
        m.showHelp = true
        return m, nil
    }

    // 常规输入处理
    // ...
}

func (m Model) View() string {
    view := m.renderNormal()

    if m.showHelp {
        // 在顶层覆盖帮助内容
        return m.helpDialog.View(view)
    }

    return view
}

Common Pitfalls

常见陷阱

❌ Modifying State Outside Update

❌ 在Update外部修改状态

go
// BAD: State modified in View
func (m Model) View() string {
    m.offset++ // NEVER modify state in View!
    return m.render()
}
View must be pure - no side effects!
go
// 错误:在View中修改状态
func (m Model) View() string {
    m.offset++ // 绝不要在View中修改状态!
    return m.render()
}
View必须是纯函数 - 不能有任何副作用!

❌ Blocking Operations in Update

❌ 在Update中执行阻塞操作

go
// BAD: Blocking I/O in Update
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    content := os.ReadFile("large-file.txt") // BLOCKS UI!
    m.content = string(content)
    return m, nil
}
Use commands for I/O.
go
// 错误:在Update中执行阻塞I/O
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    content := os.ReadFile("large-file.txt") // 阻塞UI线程!
    m.content = string(content)
    return m, nil
}
使用命令处理I/O操作。

❌ Complex Logic in Update

❌ Update中包含复杂逻辑

go
// BAD: 200 lines of logic in Update
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // ... 200 lines of key handling ...
    }
}
Extract to helper methods:
go
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        return m.handleKeyPress(msg)
    }
}

func (m Model) handleKeyPress(msg tea.KeyPressMsg) (Model, tea.Cmd) {
    // Clear logic here
}
go
// 错误:Update中有200行逻辑
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // ... 200行按键处理逻辑 ...
    }
}
提取到辅助方法中:
go
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        return m.handleKeyPress(msg)
    }
}

func (m Model) handleKeyPress(msg tea.KeyPressMsg) (Model, tea.Cmd) {
    // 清晰的逻辑代码
}

Summary

总结

  1. One component per file with clear boundaries
  2. Compositor pattern for complex UIs (parent coordinates, children handle specifics)
  3. All state in Model - no hidden variables
  4. Commands for async - never block Update
  5. Cache expensive operations - external tools, rendering
  6. Mode-based behavior for complex interactions (vim-style)
  7. Focus management for multi-panel UIs
  8. Extract helper methods - keep Update readable
  9. Pure View - no side effects, deterministic output
  10. Message-based coordination - components communicate via messages
  1. 单文件对应一个组件,边界清晰
  2. 组合器模式构建复杂UI(父组件协调,子组件处理具体逻辑)
  3. 所有状态存于Model - 无隐藏变量
  4. 使用命令处理异步 - 绝不阻塞Update
  5. 缓存耗时操作 - 外部工具调用、渲染等
  6. 基于模式的行为实现复杂交互(vim风格)
  7. 焦点管理适配多面板UI
  8. 提取辅助方法 - 保持Update可读性
  9. 纯View函数 - 无副作用,输出确定
  10. 基于消息的协调 - 组件通过消息通信