commit 3d6f3ef46761df8d2cade4dba2a74ded8fc75301
parent e5f78550b431ffd6e06b6e8fdb2d4996aacd231d
Author: Damian Myrda <damian@prime8.dev>
Date: Thu, 22 Oct 2026 01:34:11 +0000
tui: blink re-renders the prompt; cursor solid after a tab switch
The viewport keeps a rendered copy of the page, so the blink tick never
reached the screen and a switch could freeze the cursor in its off phase.
Diffstat:
2 files changed, 23 insertions(+), 0 deletions(-)
diff --git a/internal/tui/shell_test.go b/internal/tui/shell_test.go
@@ -308,3 +308,18 @@ func TestChromeUsesTerminalPalette(t *testing.T) {
}
}
}
+
+func TestCursorShownAfterTabSwitch(t *testing.T) {
+ m := testModel(t, 120, 40)
+ m.shell.lastKey = time.Now().Add(-cursorBlink * 3) // mid "off" phase
+ m.activateTab(tabProjects)
+ m.activateTab(tabAbout)
+ if out := plainView(m); !strings.Contains(out, "█") {
+ t.Fatalf("cursor missing after switching back to about")
+ }
+ m.shell.lastKey = time.Now().Add(-cursorBlink * 3)
+ next, _ := m.Update(cursorBlinkMsg{})
+ if out := plainView(next.(portfolioModel)); strings.Contains(out, "█") {
+ t.Fatalf("blink tick did not re-render the prompt (cursor stuck on)")
+ }
+}
diff --git a/internal/tui/update.go b/internal/tui/update.go
@@ -51,6 +51,12 @@ func (m portfolioModel) update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.syncViewport()
return m, m.refreshDataCmd()
case cursorBlinkMsg:
+ // The viewport holds a rendered copy of the page, so the prompt line
+ // has to be re-rendered for the blink to show. SetContent keeps the
+ // scroll offset.
+ if m.activeTab == tabAbout && !m.bootActive {
+ m.viewport.SetContent(m.detailContent())
+ }
return m, cursorBlinkCmd()
case tickMsg:
m.now = time.Time(msg)
@@ -207,6 +213,8 @@ func (m *portfolioModel) activateTab(tab int) tea.Cmd {
m.activeTab = tab
m.projectReadmeScroll = false
+ // coming back to a tab shows the cursor solid, like a fresh keystroke
+ m.shell.lastKey = time.Now()
m.resetViewport()
return m.refreshDataCmd()