refactor(ui): share the attention glyph constant and fix stale marker comments
Move the bell glyph out of internal/usecase/daemon/status.go and its duplicate in internal/usecase/picker/model.go into a single ui.AttentionGlyph, since both packages already import internal/usecase/ui and it's a presentation glyph, not a domain type. Also fix two doc comments in picker/model.go left over from the earlier fix that moved the picker's attention marker to draw right after the name instead of after the detail segment.
- commit
- 9a37fb8fb239f8edcb5b3897b802744e586cf3d5
- author
- brice · · 2026-07-11 13:39 UTC
- parent
- 9829e35
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +30 -28
M internal/usecase/daemon/attention_test.go +7 -6
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "github.com/bnema/vev/internal/ports" | |
| 15 | 15 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 16 | "github.com/bnema/vev/internal/usecase/keys" | |
| 17 | + | "github.com/bnema/vev/internal/usecase/ui" | |
| 17 | 18 | "github.com/bnema/vev/pkg/renderer" | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| @@ -54,7 +55,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) { | |||
| 54 | 55 | ||
| 55 | 56 | d.paint(sess, ac, true) | |
| 56 | 57 | data := mustOutputData(t, sends) | |
| 57 | - | require.NotContains(t, string(data), string(attentionGlyph)) | |
| 58 | + | require.NotContains(t, string(data), string(ui.AttentionGlyph)) | |
| 58 | 59 | ||
| 59 | 60 | sess.mu.Lock() | |
| 60 | 61 | require.True(t, sess.tabs[0].attention) | |
| @@ -63,7 +64,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) { | |||
| 63 | 64 | d.setAttentionFrame(1) | |
| 64 | 65 | d.paint(sess, ac, true) | |
| 65 | 66 | data = mustOutputData(t, sends) | |
| 66 | - | require.Contains(t, string(data), string(attentionGlyph)) | |
| 67 | + | require.Contains(t, string(data), string(ui.AttentionGlyph)) | |
| 67 | 68 | ||
| 68 | 69 | sess.mu.Lock() | |
| 69 | 70 | require.False(t, sess.tabs[0].attention) | |
| @@ -98,7 +99,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing. | |||
| 98 | 99 | ||
| 99 | 100 | d.paint(sess, ac, true) | |
| 100 | 101 | data := mustOutputData(t, sends) | |
| 101 | - | require.Contains(t, string(data), string(attentionGlyph)) | |
| 102 | + | require.Contains(t, string(data), string(ui.AttentionGlyph)) | |
| 102 | 103 | ||
| 103 | 104 | sess.mu.Lock() | |
| 104 | 105 | require.False(t, sess.tabs[0].attention) | |
| @@ -106,7 +107,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing. | |||
| 106 | 107 | sess.mu.Unlock() | |
| 107 | 108 | ||
| 108 | 109 | cleared := mustOutputData(t, sends) | |
| 109 | - | require.NotContains(t, string(cleared), string(attentionGlyph)) | |
| 110 | + | require.NotContains(t, string(cleared), string(ui.AttentionGlyph)) | |
| 110 | 111 | ||
| 111 | 112 | _ = pty.Close() | |
| 112 | 113 | d.sessWg.Wait() | |
| @@ -429,13 +430,13 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) { | |||
| 429 | 430 | ||
| 430 | 431 | d.paint(sessB, acB, true) | |
| 431 | 432 | before := mustOutputData(t, sendsB) | |
| 432 | - | require.Contains(t, string(before), string(attentionGlyph)) | |
| 433 | + | require.Contains(t, string(before), string(ui.AttentionGlyph)) | |
| 433 | 434 | ||
| 434 | 435 | d.closeTab(sessA, ringing, true) | |
| 435 | 436 | _ = mustOutputData(t, sendsA) | |
| 436 | 437 | ||
| 437 | 438 | after := mustOutputData(t, sendsB) | |
| 438 | - | require.NotContains(t, string(after), string(attentionGlyph)) | |
| 439 | + | require.NotContains(t, string(after), string(ui.AttentionGlyph)) | |
| 439 | 440 | } | |
| 440 | 441 | ||
| 441 | 442 | func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) { | |
| @@ -14,6 +14,7 @@ import ( | |||||
| 14 | "github.com/bnema/vev/internal/ports" | 14 | "github.com/bnema/vev/internal/ports" | ||
| 15 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | 15 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | ||
| 16 | "github.com/bnema/vev/internal/usecase/keys" | 16 | "github.com/bnema/vev/internal/usecase/keys" | ||
| 17 | + | "github.com/bnema/vev/internal/usecase/ui" | |||
| 17 | "github.com/bnema/vev/pkg/renderer" | 18 | "github.com/bnema/vev/pkg/renderer" | ||
| 18 | ) | 19 | ) | ||
| 19 | 20 | ||||
| @@ -54,7 +55,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) { | |||||
| 54 | 55 | ||||
| 55 | d.paint(sess, ac, true) | 56 | d.paint(sess, ac, true) | ||
| 56 | data := mustOutputData(t, sends) | 57 | data := mustOutputData(t, sends) | ||
| 57 | - | require.NotContains(t, string(data), string(attentionGlyph)) | 58 | + | require.NotContains(t, string(data), string(ui.AttentionGlyph)) |
| 58 | 59 | ||||
| 59 | sess.mu.Lock() | 60 | sess.mu.Lock() | ||
| 60 | require.True(t, sess.tabs[0].attention) | 61 | require.True(t, sess.tabs[0].attention) | ||
| @@ -63,7 +64,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) { | |||||
| 63 | d.setAttentionFrame(1) | 64 | d.setAttentionFrame(1) | ||
| 64 | d.paint(sess, ac, true) | 65 | d.paint(sess, ac, true) | ||
| 65 | data = mustOutputData(t, sends) | 66 | data = mustOutputData(t, sends) | ||
| 66 | - | require.Contains(t, string(data), string(attentionGlyph)) | 67 | + | require.Contains(t, string(data), string(ui.AttentionGlyph)) |
| 67 | 68 | ||||
| 68 | sess.mu.Lock() | 69 | sess.mu.Lock() | ||
| 69 | require.False(t, sess.tabs[0].attention) | 70 | require.False(t, sess.tabs[0].attention) | ||
| @@ -98,7 +99,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing. | |||||
| 98 | 99 | ||||
| 99 | d.paint(sess, ac, true) | 100 | d.paint(sess, ac, true) | ||
| 100 | data := mustOutputData(t, sends) | 101 | data := mustOutputData(t, sends) | ||
| 101 | - | require.Contains(t, string(data), string(attentionGlyph)) | 102 | + | require.Contains(t, string(data), string(ui.AttentionGlyph)) |
| 102 | 103 | ||||
| 103 | sess.mu.Lock() | 104 | sess.mu.Lock() | ||
| 104 | require.False(t, sess.tabs[0].attention) | 105 | require.False(t, sess.tabs[0].attention) | ||
| @@ -106,7 +107,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing. | |||||
| 106 | sess.mu.Unlock() | 107 | sess.mu.Unlock() | ||
| 107 | 108 | ||||
| 108 | cleared := mustOutputData(t, sends) | 109 | cleared := mustOutputData(t, sends) | ||
| 109 | - | require.NotContains(t, string(cleared), string(attentionGlyph)) | 110 | + | require.NotContains(t, string(cleared), string(ui.AttentionGlyph)) |
| 110 | 111 | ||||
| 111 | _ = pty.Close() | 112 | _ = pty.Close() | ||
| 112 | d.sessWg.Wait() | 113 | d.sessWg.Wait() | ||
| @@ -429,13 +430,13 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) { | |||||
| 429 | 430 | ||||
| 430 | d.paint(sessB, acB, true) | 431 | d.paint(sessB, acB, true) | ||
| 431 | before := mustOutputData(t, sendsB) | 432 | before := mustOutputData(t, sendsB) | ||
| 432 | - | require.Contains(t, string(before), string(attentionGlyph)) | 433 | + | require.Contains(t, string(before), string(ui.AttentionGlyph)) |
| 433 | 434 | ||||
| 434 | d.closeTab(sessA, ringing, true) | 435 | d.closeTab(sessA, ringing, true) | ||
| 435 | _ = mustOutputData(t, sendsA) | 436 | _ = mustOutputData(t, sendsA) | ||
| 436 | 437 | ||||
| 437 | after := mustOutputData(t, sendsB) | 438 | after := mustOutputData(t, sendsB) | ||
| 438 | - | require.NotContains(t, string(after), string(attentionGlyph)) | 439 | + | require.NotContains(t, string(after), string(ui.AttentionGlyph)) |
| 439 | } | 440 | } | ||
| 440 | 441 | ||||
| 441 | func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) { | 442 | func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) { | ||
M internal/usecase/daemon/picker.go +1 -1
| @@ -106,7 +106,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | |||
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | 108 | func attentionSuffix(label string) string { | |
| 109 | - | return label + " " + string(attentionGlyph) | |
| 109 | + | return label + " " + string(ui.AttentionGlyph) | |
| 110 | 110 | } | |
| 111 | 111 | ||
| 112 | 112 | func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |
| @@ -106,7 +106,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | |||||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | func attentionSuffix(label string) string { | 108 | func attentionSuffix(label string) string { | ||
| 109 | - | return label + " " + string(attentionGlyph) | 109 | + | return label + " " + string(ui.AttentionGlyph) |
| 110 | } | 110 | } | ||
| 111 | 111 | ||||
| 112 | func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | 112 | func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | ||
M internal/usecase/daemon/status.go +4 -5
| @@ -38,7 +38,6 @@ import ( | |||
| 38 | 38 | ) | |
| 39 | 39 | ||
| 40 | 40 | const ( | |
| 41 | - | attentionGlyph = '' | |
| 42 | 41 | maxMRUSessions = 9 | |
| 43 | 42 | pulseFrameCount = 30 | |
| 44 | 43 | pulseFrameInterval = 120 * time.Millisecond | |
| @@ -356,7 +355,7 @@ func fitHistoryNav(entries []historyNavSession, rowLen, leftUsed int, feedback s | |||
| 356 | 355 | } | |
| 357 | 356 | n := 2 + statusTextWidth(name) | |
| 358 | 357 | if e.attention { | |
| 359 | - | n += 1 + renderer.RuneWidth(attentionGlyph) | |
| 358 | + | n += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 360 | 359 | } | |
| 361 | 360 | return n | |
| 362 | 361 | } | |
| @@ -430,7 +429,7 @@ func fitTabLabels(tabs []statusTab, rowLen int, rightText string) []fittedTabLab | |||
| 430 | 429 | full[i] = composeTabTitle(t.name, t.paneTitle) | |
| 431 | 430 | o := 2 | |
| 432 | 431 | if t.attention { | |
| 433 | - | o += 1 + renderer.RuneWidth(attentionGlyph) | |
| 432 | + | o += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 434 | 433 | } | |
| 435 | 434 | overhead[i] = o | |
| 436 | 435 | widths[i] = statusTextWidth(full[i]) | |
| @@ -496,7 +495,7 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe | |||
| 496 | 495 | } | |
| 497 | 496 | n := 2 + statusTextWidth(name) | |
| 498 | 497 | if e.attention { | |
| 499 | - | n += 1 + renderer.RuneWidth(attentionGlyph) | |
| 498 | + | n += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 500 | 499 | } | |
| 501 | 500 | return n | |
| 502 | 501 | } | |
| @@ -563,7 +562,7 @@ func writeBell(row []renderer.Cell, x *int, frame int, base renderer.Style) { | |||
| 563 | 562 | writeStatusText(row, x, " ", base) | |
| 564 | 563 | return | |
| 565 | 564 | } | |
| 566 | - | writeStatusText(row, x, string(attentionGlyph), style) | |
| 565 | + | writeStatusText(row, x, string(ui.AttentionGlyph), style) | |
| 567 | 566 | } | |
| 568 | 567 | ||
| 569 | 568 | func writeStatusText(row []renderer.Cell, x *int, text string, style renderer.Style) { | |
| @@ -38,7 +38,6 @@ import ( | |||||
| 38 | ) | 38 | ) | ||
| 39 | 39 | ||||
| 40 | const ( | 40 | const ( | ||
| 41 | - | attentionGlyph = '' | |||
| 42 | maxMRUSessions = 9 | 41 | maxMRUSessions = 9 | ||
| 43 | pulseFrameCount = 30 | 42 | pulseFrameCount = 30 | ||
| 44 | pulseFrameInterval = 120 * time.Millisecond | 43 | pulseFrameInterval = 120 * time.Millisecond | ||
| @@ -356,7 +355,7 @@ func fitHistoryNav(entries []historyNavSession, rowLen, leftUsed int, feedback s | |||||
| 356 | } | 355 | } | ||
| 357 | n := 2 + statusTextWidth(name) | 356 | n := 2 + statusTextWidth(name) | ||
| 358 | if e.attention { | 357 | if e.attention { | ||
| 359 | - | n += 1 + renderer.RuneWidth(attentionGlyph) | 358 | + | n += 1 + renderer.RuneWidth(ui.AttentionGlyph) |
| 360 | } | 359 | } | ||
| 361 | return n | 360 | return n | ||
| 362 | } | 361 | } | ||
| @@ -430,7 +429,7 @@ func fitTabLabels(tabs []statusTab, rowLen int, rightText string) []fittedTabLab | |||||
| 430 | full[i] = composeTabTitle(t.name, t.paneTitle) | 429 | full[i] = composeTabTitle(t.name, t.paneTitle) | ||
| 431 | o := 2 | 430 | o := 2 | ||
| 432 | if t.attention { | 431 | if t.attention { | ||
| 433 | - | o += 1 + renderer.RuneWidth(attentionGlyph) | 432 | + | o += 1 + renderer.RuneWidth(ui.AttentionGlyph) |
| 434 | } | 433 | } | ||
| 435 | overhead[i] = o | 434 | overhead[i] = o | ||
| 436 | widths[i] = statusTextWidth(full[i]) | 435 | widths[i] = statusTextWidth(full[i]) | ||
| @@ -496,7 +495,7 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe | |||||
| 496 | } | 495 | } | ||
| 497 | n := 2 + statusTextWidth(name) | 496 | n := 2 + statusTextWidth(name) | ||
| 498 | if e.attention { | 497 | if e.attention { | ||
| 499 | - | n += 1 + renderer.RuneWidth(attentionGlyph) | 498 | + | n += 1 + renderer.RuneWidth(ui.AttentionGlyph) |
| 500 | } | 499 | } | ||
| 501 | return n | 500 | return n | ||
| 502 | } | 501 | } | ||
| @@ -563,7 +562,7 @@ func writeBell(row []renderer.Cell, x *int, frame int, base renderer.Style) { | |||||
| 563 | writeStatusText(row, x, " ", base) | 562 | writeStatusText(row, x, " ", base) | ||
| 564 | return | 563 | return | ||
| 565 | } | 564 | } | ||
| 566 | - | writeStatusText(row, x, string(attentionGlyph), style) | 565 | + | writeStatusText(row, x, string(ui.AttentionGlyph), style) |
| 567 | } | 566 | } | ||
| 568 | 567 | ||||
| 569 | func writeStatusText(row []renderer.Cell, x *int, text string, style renderer.Style) { | 568 | func writeStatusText(row []renderer.Cell, x *int, text string, style renderer.Style) { | ||
M internal/usecase/daemon/status_test.go +8 -7
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | "github.com/bnema/vev/internal/ports" | |
| 16 | 16 | "github.com/bnema/vev/internal/usecase/layout" | |
| 17 | 17 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 18 | + | "github.com/bnema/vev/internal/usecase/ui" | |
| 18 | 19 | "github.com/bnema/vev/pkg/renderer" | |
| 19 | 20 | ) | |
| 20 | 21 | ||
| @@ -221,7 +222,7 @@ func TestFitTabLabels(t *testing.T) { | |||
| 221 | 222 | for i, tb := range tt.tabs { | |
| 222 | 223 | overhead := 2 | |
| 223 | 224 | if tb.attention { | |
| 224 | - | overhead += 1 + renderer.RuneWidth(attentionGlyph) | |
| 225 | + | overhead += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 225 | 226 | } | |
| 226 | 227 | drawn += overhead + statusTextWidth(got[i].text) | |
| 227 | 228 | require.LessOrEqual(t, got[i].nameLen, len(got[i].text), "nameLen must not exceed the label's byte length") | |
| @@ -329,10 +330,10 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) { | |||
| 329 | 330 | // background as its tab's base style. | |
| 330 | 331 | visibleRow := make([]renderer.Cell, rowLen) | |
| 331 | 332 | drawTopBarSnapshot(visibleRow, status, 1, "", styles) | |
| 332 | - | require.Equal(t, rune(attentionGlyph), visibleRow[tab1Bell].Rune) | |
| 333 | + | require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune) | |
| 333 | 334 | require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB) | |
| 334 | 335 | require.Equal(t, styles.accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background") | |
| 335 | - | require.Equal(t, rune(attentionGlyph), visibleRow[tab2Bell].Rune) | |
| 336 | + | require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab2Bell].Rune) | |
| 336 | 337 | require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB) | |
| 337 | 338 | require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background") | |
| 338 | 339 | }) | |
| @@ -860,7 +861,7 @@ func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) { | |||
| 860 | 861 | } | |
| 861 | 862 | ||
| 862 | 863 | func TestAttentionConstants(t *testing.T) { | |
| 863 | - | require.Equal(t, '', attentionGlyph) | |
| 864 | + | require.Equal(t, '', ui.AttentionGlyph) | |
| 864 | 865 | require.Equal(t, 30, pulseFrameCount) | |
| 865 | 866 | require.Equal(t, 120*time.Millisecond, pulseFrameInterval) | |
| 866 | 867 | } | |
| @@ -924,9 +925,9 @@ func TestTopBarRendersAttentionBell(t *testing.T) { | |||
| 924 | 925 | // Tab 2's label is enriched with its focused pane's title ("sh", the | |
| 925 | 926 | // default shell fallback) and truncated to fit alongside the bell. The | |
| 926 | 927 | // bell sits right after the name, before the (title) segment. | |
| 927 | - | require.Contains(t, rowText(frame.Row(0)), "2 "+string(attentionGlyph)+" (s") | |
| 928 | + | require.Contains(t, rowText(frame.Row(0)), "2 "+string(ui.AttentionGlyph)+" (s") | |
| 928 | 929 | for _, c := range frame.Row(0) { | |
| 929 | - | if c.Rune == attentionGlyph { | |
| 930 | + | if c.Rune == ui.AttentionGlyph { | |
| 930 | 931 | require.True(t, c.Style.Bold) | |
| 931 | 932 | return | |
| 932 | 933 | } | |
| @@ -1009,7 +1010,7 @@ func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) { | |||
| 1009 | 1010 | ||
| 1010 | 1011 | require.Equal(t, " cur fresh tmp* ", rowText(row)) | |
| 1011 | 1012 | for _, c := range row { | |
| 1012 | - | if c.Rune == attentionGlyph { | |
| 1013 | + | if c.Rune == ui.AttentionGlyph { | |
| 1013 | 1014 | require.True(t, c.Style.Bold) | |
| 1014 | 1015 | return | |
| 1015 | 1016 | } | |
| @@ -15,6 +15,7 @@ import ( | |||||
| 15 | "github.com/bnema/vev/internal/ports" | 15 | "github.com/bnema/vev/internal/ports" | ||
| 16 | "github.com/bnema/vev/internal/usecase/layout" | 16 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 17 | themeui "github.com/bnema/vev/internal/usecase/theme" | 17 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 18 | + | "github.com/bnema/vev/internal/usecase/ui" | |||
| 18 | "github.com/bnema/vev/pkg/renderer" | 19 | "github.com/bnema/vev/pkg/renderer" | ||
| 19 | ) | 20 | ) | ||
| 20 | 21 | ||||
| @@ -221,7 +222,7 @@ func TestFitTabLabels(t *testing.T) { | |||||
| 221 | for i, tb := range tt.tabs { | 222 | for i, tb := range tt.tabs { | ||
| 222 | overhead := 2 | 223 | overhead := 2 | ||
| 223 | if tb.attention { | 224 | if tb.attention { | ||
| 224 | - | overhead += 1 + renderer.RuneWidth(attentionGlyph) | 225 | + | overhead += 1 + renderer.RuneWidth(ui.AttentionGlyph) |
| 225 | } | 226 | } | ||
| 226 | drawn += overhead + statusTextWidth(got[i].text) | 227 | drawn += overhead + statusTextWidth(got[i].text) | ||
| 227 | require.LessOrEqual(t, got[i].nameLen, len(got[i].text), "nameLen must not exceed the label's byte length") | 228 | require.LessOrEqual(t, got[i].nameLen, len(got[i].text), "nameLen must not exceed the label's byte length") | ||
| @@ -329,10 +330,10 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) { | |||||
| 329 | // background as its tab's base style. | 330 | // background as its tab's base style. | ||
| 330 | visibleRow := make([]renderer.Cell, rowLen) | 331 | visibleRow := make([]renderer.Cell, rowLen) | ||
| 331 | drawTopBarSnapshot(visibleRow, status, 1, "", styles) | 332 | drawTopBarSnapshot(visibleRow, status, 1, "", styles) | ||
| 332 | - | require.Equal(t, rune(attentionGlyph), visibleRow[tab1Bell].Rune) | 333 | + | require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune) |
| 333 | require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB) | 334 | require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB) | ||
| 334 | require.Equal(t, styles.accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background") | 335 | require.Equal(t, styles.accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background") | ||
| 335 | - | require.Equal(t, rune(attentionGlyph), visibleRow[tab2Bell].Rune) | 336 | + | require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab2Bell].Rune) |
| 336 | require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB) | 337 | require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB) | ||
| 337 | require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background") | 338 | require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background") | ||
| 338 | }) | 339 | }) | ||
| @@ -860,7 +861,7 @@ func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) { | |||||
| 860 | } | 861 | } | ||
| 861 | 862 | ||||
| 862 | func TestAttentionConstants(t *testing.T) { | 863 | func TestAttentionConstants(t *testing.T) { | ||
| 863 | - | require.Equal(t, '', attentionGlyph) | 864 | + | require.Equal(t, '', ui.AttentionGlyph) |
| 864 | require.Equal(t, 30, pulseFrameCount) | 865 | require.Equal(t, 30, pulseFrameCount) | ||
| 865 | require.Equal(t, 120*time.Millisecond, pulseFrameInterval) | 866 | require.Equal(t, 120*time.Millisecond, pulseFrameInterval) | ||
| 866 | } | 867 | } | ||
| @@ -924,9 +925,9 @@ func TestTopBarRendersAttentionBell(t *testing.T) { | |||||
| 924 | // Tab 2's label is enriched with its focused pane's title ("sh", the | 925 | // Tab 2's label is enriched with its focused pane's title ("sh", the | ||
| 925 | // default shell fallback) and truncated to fit alongside the bell. The | 926 | // default shell fallback) and truncated to fit alongside the bell. The | ||
| 926 | // bell sits right after the name, before the (title) segment. | 927 | // bell sits right after the name, before the (title) segment. | ||
| 927 | - | require.Contains(t, rowText(frame.Row(0)), "2 "+string(attentionGlyph)+" (s") | 928 | + | require.Contains(t, rowText(frame.Row(0)), "2 "+string(ui.AttentionGlyph)+" (s") |
| 928 | for _, c := range frame.Row(0) { | 929 | for _, c := range frame.Row(0) { | ||
| 929 | - | if c.Rune == attentionGlyph { | 930 | + | if c.Rune == ui.AttentionGlyph { |
| 930 | require.True(t, c.Style.Bold) | 931 | require.True(t, c.Style.Bold) | ||
| 931 | return | 932 | return | ||
| 932 | } | 933 | } | ||
| @@ -1009,7 +1010,7 @@ func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) { | |||||
| 1009 | 1010 | ||||
| 1010 | require.Equal(t, " cur fresh tmp* ", rowText(row)) | 1011 | require.Equal(t, " cur fresh tmp* ", rowText(row)) | ||
| 1011 | for _, c := range row { | 1012 | for _, c := range row { | ||
| 1012 | - | if c.Rune == attentionGlyph { | 1013 | + | if c.Rune == ui.AttentionGlyph { |
| 1013 | require.True(t, c.Style.Bold) | 1014 | require.True(t, c.Style.Bold) | ||
| 1014 | return | 1015 | return | ||
| 1015 | } | 1016 | } | ||
M internal/usecase/picker/model.go +3 -8
| @@ -26,7 +26,7 @@ type SessionView struct { | |||
| 26 | 26 | type TabEntry struct { | |
| 27 | 27 | Name string // tab display name | |
| 28 | 28 | Detail string // " (paneTitle)" or "", drawn muted | |
| 29 | - | Attention bool // draw the attention marker after Detail | |
| 29 | + | Attention bool // draw the attention marker right after Name, before Detail | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | // RenderStyles are the styles Render uses to draw list rows. The zero value | |
| @@ -49,11 +49,6 @@ func defaultRenderStyles() RenderStyles { | |||
| 49 | 49 | return RenderStyles{Selection: selection, SelectionName: selection, SelectionMuted: selection, Name: base, Detail: base, Base: base} | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | - | // attentionGlyph mirrors the bell icon internal/usecase/daemon draws beside | |
| 53 | - | // tab/session labels with a pending attention event. Duplicated here since | |
| 54 | - | // picker cannot import daemon. | |
| 55 | - | const attentionGlyph = '' | |
| 56 | - | ||
| 57 | 52 | type Target struct { | |
| 58 | 53 | Session domain.SessionID | |
| 59 | 54 | Name string | |
| @@ -81,7 +76,7 @@ type row struct { | |||
| 81 | 76 | header bool | |
| 82 | 77 | dispName string // display name segment; bold on truecolor | |
| 83 | 78 | detail string // " (paneTitle)" segment, muted; "" for headers | |
| 84 | - | attention bool // draw the attention marker after detail; tab rows only | |
| 79 | + | attention bool // draw the attention marker right after the name, before detail; tab rows only | |
| 85 | 80 | session domain.SessionID | |
| 86 | 81 | // targetName is the stopped-session lookup name threaded into Target; | |
| 87 | 82 | // distinct from dispName, which is what gets drawn. | |
| @@ -255,7 +250,7 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render | |||
| 255 | 250 | } | |
| 256 | 251 | ||
| 257 | 252 | if r.attention { | |
| 258 | - | x = ui.DrawText(frame, x, rect.Y+y, clipX, " "+string(attentionGlyph), base) | |
| 253 | + | x = ui.DrawText(frame, x, rect.Y+y, clipX, " "+string(ui.AttentionGlyph), base) | |
| 259 | 254 | } | |
| 260 | 255 | ||
| 261 | 256 | detail := ui.TruncateText(r.detail, clipX-x) | |
| @@ -26,7 +26,7 @@ type SessionView struct { | |||||
| 26 | type TabEntry struct { | 26 | type TabEntry struct { | ||
| 27 | Name string // tab display name | 27 | Name string // tab display name | ||
| 28 | Detail string // " (paneTitle)" or "", drawn muted | 28 | Detail string // " (paneTitle)" or "", drawn muted | ||
| 29 | - | Attention bool // draw the attention marker after Detail | 29 | + | Attention bool // draw the attention marker right after Name, before Detail |
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | // RenderStyles are the styles Render uses to draw list rows. The zero value | 32 | // RenderStyles are the styles Render uses to draw list rows. The zero value | ||
| @@ -49,11 +49,6 @@ func defaultRenderStyles() RenderStyles { | |||||
| 49 | return RenderStyles{Selection: selection, SelectionName: selection, SelectionMuted: selection, Name: base, Detail: base, Base: base} | 49 | return RenderStyles{Selection: selection, SelectionName: selection, SelectionMuted: selection, Name: base, Detail: base, Base: base} | ||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | - | // attentionGlyph mirrors the bell icon internal/usecase/daemon draws beside | |||
| 53 | - | // tab/session labels with a pending attention event. Duplicated here since | |||
| 54 | - | // picker cannot import daemon. | |||
| 55 | - | const attentionGlyph = '' | |||
| 56 | - | ||||
| 57 | type Target struct { | 52 | type Target struct { | ||
| 58 | Session domain.SessionID | 53 | Session domain.SessionID | ||
| 59 | Name string | 54 | Name string | ||
| @@ -81,7 +76,7 @@ type row struct { | |||||
| 81 | header bool | 76 | header bool | ||
| 82 | dispName string // display name segment; bold on truecolor | 77 | dispName string // display name segment; bold on truecolor | ||
| 83 | detail string // " (paneTitle)" segment, muted; "" for headers | 78 | detail string // " (paneTitle)" segment, muted; "" for headers | ||
| 84 | - | attention bool // draw the attention marker after detail; tab rows only | 79 | + | attention bool // draw the attention marker right after the name, before detail; tab rows only |
| 85 | session domain.SessionID | 80 | session domain.SessionID | ||
| 86 | // targetName is the stopped-session lookup name threaded into Target; | 81 | // targetName is the stopped-session lookup name threaded into Target; | ||
| 87 | // distinct from dispName, which is what gets drawn. | 82 | // distinct from dispName, which is what gets drawn. | ||
| @@ -255,7 +250,7 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render | |||||
| 255 | } | 250 | } | ||
| 256 | 251 | ||||
| 257 | if r.attention { | 252 | if r.attention { | ||
| 258 | - | x = ui.DrawText(frame, x, rect.Y+y, clipX, " "+string(attentionGlyph), base) | 253 | + | x = ui.DrawText(frame, x, rect.Y+y, clipX, " "+string(ui.AttentionGlyph), base) |
| 259 | } | 254 | } | ||
| 260 | 255 | ||||
| 261 | detail := ui.TruncateText(r.detail, clipX-x) | 256 | detail := ui.TruncateText(r.detail, clipX-x) | ||
M internal/usecase/picker/model_test.go +2 -1
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/vev/internal/domain" | |
| 7 | + | "github.com/bnema/vev/internal/usecase/ui" | |
| 7 | 8 | "github.com/bnema/vev/pkg/renderer" | |
| 8 | 9 | "github.com/stretchr/testify/require" | |
| 9 | 10 | ) | |
| @@ -198,7 +199,7 @@ func TestRenderListDrawsNameAndDetailSegmentsWithDistinctStyles(t *testing.T) { | |||
| 198 | 199 | require.True(t, frame.At(2, 2).Style.Equal(nameStyle), "name segment style") | |
| 199 | 200 | require.Equal(t, ' ', frame.At(6, 2).Rune, "attention marker leading space") | |
| 200 | 201 | require.True(t, frame.At(6, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | |
| 201 | - | require.Equal(t, rune(attentionGlyph), frame.At(7, 2).Rune) | |
| 202 | + | require.Equal(t, rune(ui.AttentionGlyph), frame.At(7, 2).Rune) | |
| 202 | 203 | require.True(t, frame.At(7, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | |
| 203 | 204 | require.Equal(t, ' ', frame.At(8, 2).Rune) | |
| 204 | 205 | require.True(t, frame.At(8, 2).Style.Equal(detailStyle), "detail segment style") | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/vev/internal/domain" | 6 | "github.com/bnema/vev/internal/domain" | ||
| 7 | + | "github.com/bnema/vev/internal/usecase/ui" | |||
| 7 | "github.com/bnema/vev/pkg/renderer" | 8 | "github.com/bnema/vev/pkg/renderer" | ||
| 8 | "github.com/stretchr/testify/require" | 9 | "github.com/stretchr/testify/require" | ||
| 9 | ) | 10 | ) | ||
| @@ -198,7 +199,7 @@ func TestRenderListDrawsNameAndDetailSegmentsWithDistinctStyles(t *testing.T) { | |||||
| 198 | require.True(t, frame.At(2, 2).Style.Equal(nameStyle), "name segment style") | 199 | require.True(t, frame.At(2, 2).Style.Equal(nameStyle), "name segment style") | ||
| 199 | require.Equal(t, ' ', frame.At(6, 2).Rune, "attention marker leading space") | 200 | require.Equal(t, ' ', frame.At(6, 2).Rune, "attention marker leading space") | ||
| 200 | require.True(t, frame.At(6, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | 201 | require.True(t, frame.At(6, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | ||
| 201 | - | require.Equal(t, rune(attentionGlyph), frame.At(7, 2).Rune) | 202 | + | require.Equal(t, rune(ui.AttentionGlyph), frame.At(7, 2).Rune) |
| 202 | require.True(t, frame.At(7, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | 203 | require.True(t, frame.At(7, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted") | ||
| 203 | require.Equal(t, ' ', frame.At(8, 2).Rune) | 204 | require.Equal(t, ' ', frame.At(8, 2).Rune) | ||
| 204 | require.True(t, frame.At(8, 2).Style.Equal(detailStyle), "detail segment style") | 205 | require.True(t, frame.At(8, 2).Style.Equal(detailStyle), "detail segment style") | ||
A internal/usecase/ui/glyphs.go +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | // AttentionGlyph is the bell icon drawn beside tab/session labels with a | |
| 4 | + | // pending attention event. | |
| 5 | + | const AttentionGlyph = '' | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | // AttentionGlyph is the bell icon drawn beside tab/session labels with a | |||
| 4 | + | // pending attention event. | |||
| 5 | + | const AttentionGlyph = '' | |||