Problem
When a widget refreshes (either on a timer or via the r keyboard shortcut), the UI can become unresponsive while waiting for API calls to complete. This is because Refresh() runs synchronously — the widget's scheduler goroutine (or the tview event loop, in the case of keyboard-triggered refresh) is blocked on network I/O until all HTTP calls finish.
This is noticeable on widgets that make multiple or slow API calls (e.g. GitHub, NewRelic, GitLab, Jira).
Proposed approach
1. Run keyboard-triggered refresh in a background goroutine
Currently, pressing r calls widget.Refresh() directly on the tview input handler goroutine. Wrapping this in a goroutine would keep the UI responsive:
widget.InitializeRefreshKeyboardControl(func() {
go widget.Refresh()
})
This could be done once in KeyboardWidget.InitializeRefreshKeyboardControl() so all widgets benefit.
2. Add a context.WithTimeout at the scheduler level
Individual modules shouldn't need to manage their own timeouts. app.Schedule() could wrap each Refresh() call with a deadline:
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
defer cancel()
widget.Refresh(ctx)
This would require updating the Wtfable interface to accept a context, which is a larger change but would protect against any single module hanging the scheduler.
3. Ensure content() functions are always CPU-only
Establish as a project convention that the function passed to Redraw() should never do network I/O. All data fetching should happen in Refresh(), with results cached for content() to read. This was addressed for the GitHub widget in #2117 but other modules (e.g. NewRelic) have the same pattern.
Modules affected
A quick scan shows these modules make HTTP calls without timeouts or have network calls in their display path:
- NewRelic —
content() makes HTTP calls directly
- DigitalOcean — keyboard handlers call API methods synchronously
- Transmission — keyboard handlers call API methods synchronously
- PagerDuty, AzureDevOps, DevTo, Gerrit — use
context.Background() without timeouts
Impact
This is a quality-of-life improvement. The app remains functional but feels sluggish when widgets are refreshing, especially with slow or rate-limited APIs.
Problem
When a widget refreshes (either on a timer or via the
rkeyboard shortcut), the UI can become unresponsive while waiting for API calls to complete. This is becauseRefresh()runs synchronously — the widget's scheduler goroutine (or the tview event loop, in the case of keyboard-triggered refresh) is blocked on network I/O until all HTTP calls finish.This is noticeable on widgets that make multiple or slow API calls (e.g. GitHub, NewRelic, GitLab, Jira).
Proposed approach
1. Run keyboard-triggered refresh in a background goroutine
Currently, pressing
rcallswidget.Refresh()directly on the tview input handler goroutine. Wrapping this in a goroutine would keep the UI responsive:This could be done once in
KeyboardWidget.InitializeRefreshKeyboardControl()so all widgets benefit.2. Add a
context.WithTimeoutat the scheduler levelIndividual modules shouldn't need to manage their own timeouts.
app.Schedule()could wrap eachRefresh()call with a deadline:This would require updating the
Wtfableinterface to accept a context, which is a larger change but would protect against any single module hanging the scheduler.3. Ensure
content()functions are always CPU-onlyEstablish as a project convention that the function passed to
Redraw()should never do network I/O. All data fetching should happen inRefresh(), with results cached forcontent()to read. This was addressed for the GitHub widget in #2117 but other modules (e.g. NewRelic) have the same pattern.Modules affected
A quick scan shows these modules make HTTP calls without timeouts or have network calls in their display path:
content()makes HTTP calls directlycontext.Background()without timeoutsImpact
This is a quality-of-life improvement. The app remains functional but feels sluggish when widgets are refreshing, especially with slow or rate-limited APIs.