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 andadjustRefreshIntervaltuning to self-throttle under load - The system measures refresh duration against
slowFractionandfastFractionthresholds, adjusting the interval byrefreshStepafterbackoffStreakconsecutive measurements - Mouse navigation routes
tea.MouseMsgthroughhandleMouseto area-specific handlers, supporting clicks, double-clicks, and wheel scrolling - Wheel events convert to
KeyUp/KeyDownmessages for consistent table behavior, while header clicks map X-coordinates to sortable columns viagetColumnAtX - All implementations reside in
internal/tui/update.goandinternal/tui/mouse.goper 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →