How witr Implements Auto-Refresh and Adaptive Refresh Rates in Its TUI
witr employs a Bubble Tea-based tick driver combined with performance-aware adaptive logic to automatically refresh process and port data every 3 seconds by default, dynamically adjusting the cadence between 3 and 30 seconds based on how long each refresh operation takes.
The terminal user interface (TUI) in the pranshuparmar/witr repository continuously monitors system processes, network ports, and containers while intelligently balancing data freshness against CPU usage. This article examines how witr implements auto-refresh and adaptive refresh rates in its TUI by analyzing the timing constants, tick generation mechanism, and performance-based interval adjustment algorithms found in the source code.
Core Components of witr's Auto-Refresh System
The implementation spans three architectural layers defined across internal/tui/constants.go, internal/tui/model.go, and internal/tui/update.go. Timing constants establish baseline and boundary values, a periodic tick driver maintains the event loop heartbeat, and adaptive cadence logic measures refresh duration to adjust future intervals dynamically.
Base Refresh Interval and Timing Constants
In internal/tui/constants.go, witr defines the foundational timing parameters that govern refresh behavior. These constants establish a 3-second baseline similar to the Unix top command, while providing headroom for adaptive expansion up to 30 seconds.
// internal/tui/constants.go
const (
// Base auto-refresh cadence (mirrors `top` default)
refreshInterval = 3 * time.Second
// Adaptive-refresh parameters
maxRefreshInterval = 30 * time.Second
refreshStep = 3 * time.Second
backoffStreak = 2
slowFraction = 0.6
fastFraction = 0.3
)
The slowFraction (0.6) and fastFraction (0.3) thresholds determine when a refresh is considered expensive or cheap relative to the current interval, while backoffStreak (2) prevents rapid oscillation by requiring consecutive observations before adjustment.
The Tick Driver: Generating Periodic Updates
The waitTick function in internal/tui/update.go leverages Bubble Tea's tea.Tick primitive to generate periodic tickMsg messages. This driver maintains a constant 3-second heartbeat regardless of the current adaptive interval, delegating the actual refresh decision to separate logic.
// internal/tui/update.go
func waitTick() tea.Cmd {
return tea.Tick(refreshInterval, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
This function returns a tea.Cmd that produces a tickMsg after the base interval, ensuring the TUI remains responsive to user input while keeping the timer logic non-blocking.
Adaptive Refresh Rate Logic
When a tick arrives, the system must decide whether to execute expensive data collection operations. This decision relies on the refreshDue method and the adjustRefreshInterval function to implement a feedback loop that responds to system load.
Deciding When to Refresh: The refreshDue Method
The handleTick method in internal/tui/update.go checks refreshDue before invoking refreshProcesses() or similar data fetchers. This gatekeeper prevents overlapping refreshes and respects the current adaptive interval stored in m.refreshEvery.
// internal/tui/update.go
func (m MainModel) handleTick(msg tickMsg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if m.state == stateList && !m.quitting && ... && m.refreshDue() {
m.lastRefresh = time.Now()
m.refreshStartedAt = m.lastRefresh
cmd = m.refreshProcesses()
// refresh other tabs as needed …
}
return m, tea.Batch(cmd, waitTick())
}
The refreshDue method implements the primary gating logic by checking both in-flight status and elapsed time:
// internal/tui/update.go
func (m MainModel) refreshDue() bool {
// Skip if a previous refresh is still in-flight (but not older than max interval)
if !m.refreshStartedAt.IsZero() && time.Since(m.refreshStartedAt) < maxRefreshInterval {
return false
}
every := m.refreshEvery
if every < refreshInterval {
every = refreshInterval
}
// Trigger when the elapsed time since the last successful refresh exceeds the current cadence
return time.Since(m.lastRefresh) >= every
}
Measuring Performance and Adjusting Cadence
After a refresh completes, adjustRefreshInterval analyzes the elapsed time against the current interval. The function uses streak counters to avoid oscillation, requiring two consecutive slow or fast refreshes before modifying the interval.
// internal/tui/update.go
func adjustRefreshInterval(interval, took time.Duration, slow, fast int) (time.Duration, int, int) {
switch {
case took > time.Duration(float64(interval)*slowFraction):
// Refresh took longer than `slowFraction` → count a slow streak
slow, fast = slow+1, 0
if slow >= backoffStreak {
interval += refreshStep
if interval > maxRefreshInterval {
interval = maxRefreshInterval
}
slow = 0
}
case took < time.Duration(float64(interval)*fastFraction):
// Refresh was fast → count a fast streak
fast, slow = fast+1, 0
if fast >= backoffStreak {
interval -= refreshStep
if interval < refreshInterval {
interval = refreshInterval
}
fast = 0
}
default:
// Within the "stable" band → reset streaks
slow, fast = 0, 0
}
return interval, slow, fast
}
The adaptive algorithm follows these rules:
- Slow refreshes: If a refresh takes longer than 60% of the current interval, increment the slow streak. After two consecutive slow refreshes, increase the interval by 3 seconds (up to the 30-second maximum).
- Fast refreshes: If a refresh completes in less than 30% of the current interval, increment the fast streak. After two consecutive fast refreshes, decrease the interval by 3 seconds (down to the 3-second minimum).
- Stable zone: Refreshes between 30% and 60% of the interval reset both streaks, maintaining the current cadence.
End-to-End Refresh Flow
The complete lifecycle of a refresh cycle involves five coordinated stages:
- Initialization:
InitialModelseedsrefreshEverywith the 3-secondrefreshIntervalconstant defined ininternal/tui/model.go. - Tick Generation:
waitTickemits atickMsgevery 3 seconds via Bubble Tea's command system. - Gating:
handleTickinvokesrefreshDueto check if sufficient time has elapsed since the last refresh and no operation is currently in-flight. - Execution: Upon approval,
refreshProcesses()or equivalent functions run in the background, withrefreshStartedAttracking the start time. - Adjustment: When the background command returns, the code calculates
took := time.Since(m.refreshStartedAt)and callsadjustRefreshIntervalto potentially modifym.refreshEveryfor the next cycle.
Practical Implementation Details
Developers interacting with witr's codebase can manually trigger refreshes or inspect the current adaptive interval through the model's exposed fields.
To force an immediate refresh regardless of the adaptive timer:
case "r":
// Force a refresh of the current tab
return m, m.refreshProcesses()
To display the current refresh cadence for debugging:
case "d": // Show debug info
m.statusMsg = fmt.Sprintf("Refresh interval: %v", m.refreshEvery)
return m, nil
Summary
- witr uses Bubble Tea's
tea.Tickininternal/tui/update.goto generate a constant 3-second heartbeat that drives the UI update loop. - The adaptive refresh algorithm measures each refresh duration and adjusts the interval between 3 and 30 seconds based on performance, using 60% and 30% thresholds to detect slow or fast operations.
- Streak counters prevent rapid oscillation by requiring two consecutive slow or fast refreshes before modifying the interval.
- The
refreshDuemethod ininternal/tui/update.goacts as a gatekeeper, preventing overlapping refreshes and respecting the current adaptive cadence. - All timing parameters are centralized in
internal/tui/constants.go, making the system configurable for different performance characteristics.
Frequently Asked Questions
What is the default refresh interval in witr?
witr defaults to a 3-second refresh interval, defined by the refreshInterval constant in internal/tui/constants.go. This value mirrors the default behavior of the Unix top command and serves as both the initial value and the minimum bound for the adaptive refresh system.
How does witr prevent UI jank during expensive refreshes?
witr prevents UI jank through two mechanisms: the refreshDue method checks refreshStartedAt to ensure only one refresh runs at a time, and the adaptive algorithm increases the refresh interval (up to 30 seconds) when consecutive refreshes take longer than 60% of the current interval. This backoff strategy reduces CPU load during periods of high system activity.
Can users manually trigger a refresh in witr?
Yes, users can manually trigger refreshes by pressing r (or Ctrl+r depending on key binding configuration), which invokes m.refreshProcesses() directly. This bypasses the adaptive timer for immediate data updates while still feeding the duration into the adjustRefreshInterval function to influence future automatic refreshes.
What are the minimum and maximum refresh rates in witr?
The minimum refresh rate is 3 seconds (refreshInterval) and the maximum is 30 seconds (maxRefreshInterval), both defined in internal/tui/constants.go. The adaptive algorithm adjusts the interval in 3-second steps (refreshStep) within these bounds based on measured refresh performance.
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 →