How witr Implements Adaptive Refresh Rates and Mouse Navigation in Its Bubble Tea TUI

The witr TUI uses a self-tuning tick-based loop that adjusts refresh intervals based on measured completion times, combined with a mouse dispatcher that converts clicks, wheels, and double-clicks into semantic UI actions.

The witr tool is a terminal-based system monitor built on Charm's Bubble Tea framework. This article examines how the project balances responsive updates against system load through adaptive refresh rates, and how it implements mouse navigation across multiple data views without losing keyboard accessibility.


Adaptive Refresh Rate Mechanism

The adaptive refresh system lives in internal/tui/update.go and operates through three coordinated components: a base ticker, a refresh guard, and a self-tuning interval adjuster.

Base Ticker and Refresh Guard

The waitTick() function creates a 3-second heartbeat using tea.Tick:

func waitTick() tea.Cmd {
    return tea.Tick(refreshInterval, func(t time.Time) tea.Msg {
        return tickMsg(t)               // fires every 3 s
    })
}

Each tick reaches refreshDue() (lines 80-104 in update.go) before triggering any data fetch. This guard checks refreshStartedAt against maxRefreshInterval to suppress overlapping refreshes when a previous background operation hasn't completed.

Self-Tuning Interval Adjustment

After every refresh, adjustRefreshInterval recalibrates the target cadence:

Condition Action Threshold
Slow refresh Increase interval Duration > slowFraction * currentInterval for backoffStreak consecutive runs
Fast refresh Decrease interval Duration < fastFraction * currentInterval for backoffStreak consecutive runs

The interval changes by refreshStep (defined in internal/tui/constants.go) and clamps between base and maxRefreshInterval values. This creates load-proportional refresh behavior: heavy system activity automatically throttles updates to prevent UI lag, while idle periods receive more frequent data.

func (m MainModel) handleTick(msg tickMsg) (tea.Model, tea.Cmd) {
    if m.state == stateList && m.refreshDue() {
        m.lastRefresh = time.Now()
        cmd = m.refreshProcesses()
        // refresh other tabs as needed …
    }
    return m, tea.Batch(cmd, waitTick())
}

Mouse Navigation Architecture

witr's mouse support spans four distinct content areas: processes, ports, containers, and locks. The implementation separates event routing from area-specific handling to maintain clean separation of concerns.

Event Dispatch Flow

The Update method in update.go (lines 30-71) routes tea.MouseMsg to handleMouse, which classifies clicks into three categories:

  • Title bar clicks — return to process list view
  • Detail view clicks — delegate to handleDetailMouse
  • Content area clicks — route to view-specific handlers: handleProcessAreaMouse, handlePortAreaMouse, handleContainerAreaMouse, handleLockAreaMouse

Wheel Scrolling Without Cursor Dependency

Rather than mapping wheel events to absolute Y positions, witr converts them into directional key messages. This preserves Bubble Tea's table selection model while adding scroll convenience:

if isWheel {
    var keyMsg tea.KeyMsg
    if msg.Button == tea.MouseButtonWheelUp {
        keyMsg = tea.KeyMsg{Type: tea.KeyUp}
    } else {
        keyMsg = tea.KeyMsg{Type: tea.KeyDown}
    }
    m.table, cmd = m.table.Update(keyMsg)     // scroll one row
}

Double-Click Detection

Double-clicks trigger detail expansion or panel focus. The detection uses timestamp (lastClickTime) and distance thresholds to distinguish intentional doubles from accidental repeats:

if isDoubleClick {
    m.state = stateDetail
    m.viewport.GotoTop()
    m.envViewport.GotoTop()
    return m, m.fetchProcessDetail(pid)        // open the selected PID
}

Column-Based Sorting from Header Clicks

Table headers accept clicks for sort operation. The getColumnAtX helper in internal/tui/mouse.go (lines 5-16) translates X-pixel coordinates to column indices:

func (m *MainModel) handleProcessHeaderClick(x int) {
    cols := m.table.Columns()
    colIdx := m.getColumnAtX(x, cols)          // map X → column
    // map column index → sort key, toggle direction, re-sort
    // …
}

Key Implementation Files

File Responsibility
internal/tui/update.go Core Update loop, tick handling, adaptive refresh logic, mouse event dispatch
internal/tui/mouse.go Coordinate-to-column mapping (getColumnAtX) and header click helpers
internal/tui/constants.go Timing parameters: refreshInterval, maxRefreshInterval, refreshStep, slowFraction, fastFraction, backoffStreak
internal/tui/view.go Layout definitions for tables, viewports, and interactive regions

Summary

  • Adaptive refresh rates in witr use a 3-second base ticker with refreshDue() guards and adjustRefreshInterval tuning to self-throttle under load
  • The system measures refresh duration against slowFraction and fastFraction thresholds, adjusting the interval by refreshStep after backoffStreak consecutive measurements
  • Mouse navigation routes tea.MouseMsg through handleMouse to area-specific handlers, supporting clicks, double-clicks, and wheel scrolling
  • Wheel events convert to KeyUp/KeyDown messages for consistent table behavior, while header clicks map X-coordinates to sortable columns via getColumnAtX
  • All implementations reside in internal/tui/update.go and internal/tui/mouse.go per the pranshuparmar/witr source code

Frequently Asked Questions

How does witr prevent refresh storms when the system is overloaded?

The refreshDue() guard in internal/tui/update.go checks refreshStartedAt against maxRefreshInterval before allowing a new refresh. Additionally, the adjustRefreshInterval function increases the refresh interval by refreshStep after backoffStreak consecutive slow refreshes, automatically throttling the update rate when measured durations exceed slowFraction * currentInterval.

Why does witr convert mouse wheel events to keyboard messages instead of using absolute positioning?

Converting tea.MouseButtonWheelUp and tea.MouseButtonWheelDown to tea.KeyMsg{Type: tea.KeyUp} and tea.KeyMsg{Type: tea.KeyDown} preserves Bubble Tea's built-in table selection semantics. This approach scrolls by logical rows without requiring coordinate-to-row mapping or cursor tracking, maintaining compatibility with the existing keyboard-driven interface.

How does witr determine which table column was clicked for sorting?

The getColumnAtX function in internal/tui/mouse.go translates the X-coordinate of a mouse click into a column index by accumulating column widths until the coordinate falls within a column's bounds. The header click handler then maps this index to the corresponding sort key, toggles sort direction, and re-sorts the table data.

What constants control the adaptive refresh behavior in witr?

The timing parameters are defined in internal/tui/constants.go: refreshInterval (base 3s tick), maxRefreshInterval (upper bound), refreshStep (adjustment increment), slowFraction and fastFraction (performance thresholds), and backoffStreak (consecutive measurements required before adjustment). These values tune how aggressively the UI responds to system load changes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →