detached 9a37fb8

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 (
1414 "github.com/bnema/vev/internal/ports"
1515 portsmocks "github.com/bnema/vev/internal/ports/mocks"
1616 "github.com/bnema/vev/internal/usecase/keys"
17+ "github.com/bnema/vev/internal/usecase/ui"
1718 "github.com/bnema/vev/pkg/renderer"
1819 )
1920
@@ -54,7 +55,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {
5455
5556 d.paint(sess, ac, true)
5657 data := mustOutputData(t, sends)
57- require.NotContains(t, string(data), string(attentionGlyph))
58+ require.NotContains(t, string(data), string(ui.AttentionGlyph))
5859
5960 sess.mu.Lock()
6061 require.True(t, sess.tabs[0].attention)
@@ -63,7 +64,7 @@ func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {
6364 d.setAttentionFrame(1)
6465 d.paint(sess, ac, true)
6566 data = mustOutputData(t, sends)
66- require.Contains(t, string(data), string(attentionGlyph))
67+ require.Contains(t, string(data), string(ui.AttentionGlyph))
6768
6869 sess.mu.Lock()
6970 require.False(t, sess.tabs[0].attention)
@@ -98,7 +99,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
9899
99100 d.paint(sess, ac, true)
100101 data := mustOutputData(t, sends)
101- require.Contains(t, string(data), string(attentionGlyph))
102+ require.Contains(t, string(data), string(ui.AttentionGlyph))
102103
103104 sess.mu.Lock()
104105 require.False(t, sess.tabs[0].attention)
@@ -106,7 +107,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
106107 sess.mu.Unlock()
107108
108109 cleared := mustOutputData(t, sends)
109- require.NotContains(t, string(cleared), string(attentionGlyph))
110+ require.NotContains(t, string(cleared), string(ui.AttentionGlyph))
110111
111112 _ = pty.Close()
112113 d.sessWg.Wait()
@@ -429,13 +430,13 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) {
429430
430431 d.paint(sessB, acB, true)
431432 before := mustOutputData(t, sendsB)
432- require.Contains(t, string(before), string(attentionGlyph))
433+ require.Contains(t, string(before), string(ui.AttentionGlyph))
433434
434435 d.closeTab(sessA, ringing, true)
435436 _ = mustOutputData(t, sendsA)
436437
437438 after := mustOutputData(t, sendsB)
438- require.NotContains(t, string(after), string(attentionGlyph))
439+ require.NotContains(t, string(after), string(ui.AttentionGlyph))
439440 }
440441
441442 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) {
106106 }
107107
108108 func attentionSuffix(label string) string {
109- return label + " " + string(attentionGlyph)
109+ return label + " " + string(ui.AttentionGlyph)
110110 }
111111
112112 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 (
3838 )
3939
4040 const (
41- attentionGlyph = ''
4241 maxMRUSessions = 9
4342 pulseFrameCount = 30
4443 pulseFrameInterval = 120 * time.Millisecond
@@ -356,7 +355,7 @@ func fitHistoryNav(entries []historyNavSession, rowLen, leftUsed int, feedback s
356355 }
357356 n := 2 + statusTextWidth(name)
358357 if e.attention {
359- n += 1 + renderer.RuneWidth(attentionGlyph)
358+ n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
360359 }
361360 return n
362361 }
@@ -430,7 +429,7 @@ func fitTabLabels(tabs []statusTab, rowLen int, rightText string) []fittedTabLab
430429 full[i] = composeTabTitle(t.name, t.paneTitle)
431430 o := 2
432431 if t.attention {
433- o += 1 + renderer.RuneWidth(attentionGlyph)
432+ o += 1 + renderer.RuneWidth(ui.AttentionGlyph)
434433 }
435434 overhead[i] = o
436435 widths[i] = statusTextWidth(full[i])
@@ -496,7 +495,7 @@ func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSe
496495 }
497496 n := 2 + statusTextWidth(name)
498497 if e.attention {
499- n += 1 + renderer.RuneWidth(attentionGlyph)
498+ n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
500499 }
501500 return n
502501 }
@@ -563,7 +562,7 @@ func writeBell(row []renderer.Cell, x *int, frame int, base renderer.Style) {
563562 writeStatusText(row, x, " ", base)
564563 return
565564 }
566- writeStatusText(row, x, string(attentionGlyph), style)
565+ writeStatusText(row, x, string(ui.AttentionGlyph), style)
567566 }
568567
569568 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 = 941 maxMRUSessions = 9
43 pulseFrameCount = 3042 pulseFrameCount = 30
44 pulseFrameInterval = 120 * time.Millisecond43 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 n360 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 := 2430 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] = o434 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 n500 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 return563 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 (
1515 "github.com/bnema/vev/internal/ports"
1616 "github.com/bnema/vev/internal/usecase/layout"
1717 themeui "github.com/bnema/vev/internal/usecase/theme"
18+ "github.com/bnema/vev/internal/usecase/ui"
1819 "github.com/bnema/vev/pkg/renderer"
1920 )
2021
@@ -221,7 +222,7 @@ func TestFitTabLabels(t *testing.T) {
221222 for i, tb := range tt.tabs {
222223 overhead := 2
223224 if tb.attention {
224- overhead += 1 + renderer.RuneWidth(attentionGlyph)
225+ overhead += 1 + renderer.RuneWidth(ui.AttentionGlyph)
225226 }
226227 drawn += overhead + statusTextWidth(got[i].text)
227228 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) {
329330 // background as its tab's base style.
330331 visibleRow := make([]renderer.Cell, rowLen)
331332 drawTopBarSnapshot(visibleRow, status, 1, "", styles)
332- require.Equal(t, rune(attentionGlyph), visibleRow[tab1Bell].Rune)
333+ require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune)
333334 require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB)
334335 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)
336337 require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB)
337338 require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background")
338339 })
@@ -860,7 +861,7 @@ func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) {
860861 }
861862
862863 func TestAttentionConstants(t *testing.T) {
863- require.Equal(t, '', attentionGlyph)
864+ require.Equal(t, '', ui.AttentionGlyph)
864865 require.Equal(t, 30, pulseFrameCount)
865866 require.Equal(t, 120*time.Millisecond, pulseFrameInterval)
866867 }
@@ -924,9 +925,9 @@ func TestTopBarRendersAttentionBell(t *testing.T) {
924925 // Tab 2's label is enriched with its focused pane's title ("sh", the
925926 // default shell fallback) and truncated to fit alongside the bell. The
926927 // 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")
928929 for _, c := range frame.Row(0) {
929- if c.Rune == attentionGlyph {
930+ if c.Rune == ui.AttentionGlyph {
930931 require.True(t, c.Style.Bold)
931932 return
932933 }
@@ -1009,7 +1010,7 @@ func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) {
10091010
10101011 require.Equal(t, " cur fresh tmp*  ", rowText(row))
10111012 for _, c := range row {
1012- if c.Rune == attentionGlyph {
1013+ if c.Rune == ui.AttentionGlyph {
10131014 require.True(t, c.Style.Bold)
10141015 return
10151016 }
@@ -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 := 2223 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", the925 // 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. The926 // 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 return932 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 return1015 return
1015 }1016 }
M internal/usecase/picker/model.go +3 -8
@@ -26,7 +26,7 @@ type SessionView struct {
2626 type TabEntry struct {
2727 Name string // tab display name
2828 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
3030 }
3131
3232 // RenderStyles are the styles Render uses to draw list rows. The zero value
@@ -49,11 +49,6 @@ func defaultRenderStyles() RenderStyles {
4949 return RenderStyles{Selection: selection, SelectionName: selection, SelectionMuted: selection, Name: base, Detail: base, Base: base}
5050 }
5151
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-
5752 type Target struct {
5853 Session domain.SessionID
5954 Name string
@@ -81,7 +76,7 @@ type row struct {
8176 header bool
8277 dispName string // display name segment; bold on truecolor
8378 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
8580 session domain.SessionID
8681 // targetName is the stopped-session lookup name threaded into Target;
8782 // distinct from dispName, which is what gets drawn.
@@ -255,7 +250,7 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render
255250 }
256251
257252 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)
259254 }
260255
261256 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 name27 Name string // tab display name
28 Detail string // " (paneTitle)" or "", drawn muted28 Detail string // " (paneTitle)" or "", drawn muted
29- Attention bool // draw the attention marker after Detail29+ 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 value32 // 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.SessionID53 Session domain.SessionID
59 Name string54 Name string
@@ -81,7 +76,7 @@ type row struct {
81 header bool76 header bool
82 dispName string // display name segment; bold on truecolor77 dispName string // display name segment; bold on truecolor
83 detail string // " (paneTitle)" segment, muted; "" for headers78 detail string // " (paneTitle)" segment, muted; "" for headers
84- attention bool // draw the attention marker after detail; tab rows only79+ attention bool // draw the attention marker right after the name, before detail; tab rows only
85 session domain.SessionID80 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 (
44 "testing"
55
66 "github.com/bnema/vev/internal/domain"
7+ "github.com/bnema/vev/internal/usecase/ui"
78 "github.com/bnema/vev/pkg/renderer"
89 "github.com/stretchr/testify/require"
910 )
@@ -198,7 +199,7 @@ func TestRenderListDrawsNameAndDetailSegmentsWithDistinctStyles(t *testing.T) {
198199 require.True(t, frame.At(2, 2).Style.Equal(nameStyle), "name segment style")
199200 require.Equal(t, ' ', frame.At(6, 2).Rune, "attention marker leading space")
200201 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)
202203 require.True(t, frame.At(7, 2).Style.Equal(baseStyle), "attention marker uses the base style, not muted")
203204 require.Equal(t, ' ', frame.At(8, 2).Rune)
204205 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 = ''

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close