@@ -6,8 +6,11 @@ import (
66 "github.com/charmbracelet/lipgloss"
77)
88
9- // HeaderHeight is the total number of lines the header occupies.
10- const HeaderHeight = 12
9+ // HeaderHeight is the maximum number of lines the header occupies (a top border,
10+ // the content rows, and a bottom border). The actual height for a given config
11+ // is HeaderHeightFor, which sizes the box to its content with no trailing empty
12+ // rows.
13+ const HeaderHeight = 9
1114
1215// MinHeightForHeader is the minimum terminal height to show the header.
1316const MinHeightForHeader = 25
@@ -18,9 +21,16 @@ const MinWidthForShortcuts = 65
1821// MinWidthForHeader is the minimum width to show the header at all.
1922const MinWidthForHeader = 53
2023
21- // MinWidthForArt is the minimum width to show ASCII art in the header.
24+ // MinWidthForArt is the minimum width to show the logo in the header.
2225const MinWidthForArt = 96
2326
27+ // MinHeightForArt is the minimum terminal height to show the logo. It is a bit
28+ // higher than MinHeightForHeader: at very short heights a vertical resize can
29+ // leave a transient ghost of the inline image (kitty graphics live in a layer
30+ // the text renderer can't repaint cleanly mid-resize), so the logo is dropped a
31+ // little before the rest of the header to avoid the artifact.
32+ const MinHeightForArt = 30
33+
2434// ShortcutEntry represents a keyboard shortcut for the header.
2535type ShortcutEntry struct {
2636 Key string
@@ -35,22 +45,31 @@ type HeaderInfoLine struct {
3545 IconStyle * lipgloss.Style // optional override; nil uses default HeaderInfoStyle (cyan)
3646}
3747
38- // ArtLines is the braille ASCII art for the View header.
39- var ArtLines = [10 ]string {
40- "⠀⠀⠀⠀⠀⠀⣀⣤⣤⣤⣤⣤⣤⣀⠀⠀⠀⠀⠀⠀" ,
41- "⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀⠀⠀" ,
42- "⠀⢀⣼⣿⣿⠛⠛⠿⠿⠿⠿⠿⠿⠛⠛⣿⣿⣷⡀⠀" ,
43- "⠀⣾⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣷⡀" ,
44- "⢸⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇" ,
45- "⢸⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇" ,
46- "⠘⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⢀⣤⣿⣿⣿⣿⠇" ,
47- "⠀⠹⣿⣦⡈⠻⢿⠟⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⠏⠀" ,
48- "⠀⠀⠈⠻⣷⣤⣀⡀⠀⠀⠀⠀⢸⣿⣿⣿⡿⠃⠀⠀" ,
49- "⠀⠀⠀⠀⠈⠙⠻⠇⠀⠀⠀⠀⠸⠟⠛⠁⠀⠀⠀⠀" ,
50- }
48+ // headerLeftMargin is the left padding, in columns, before the logo and the
49+ // info lines (which share this left edge). It is kept small so it visually
50+ // matches the header's top and bottom padding.
51+ const headerLeftMargin = 1
52+
53+ // The logo image sits in the top-left corner spanning the title and subtitle
54+ // rows. logoImageCols is its width in cells, which drives the size: the mark is
55+ // square and a terminal cell is about twice as tall as it is wide, so the logo
56+ // renders about logoImageCols/2 cells tall. Width is the controlled dimension
57+ // (kitty scales the square mark to logoImageCols cells wide; iTerm2 fits it
58+ // within logoImageCols x logoImageRows), so the slot width is exact. 4 cols
59+ // gives a ~2-cell-tall logo. logoImageRows bounds the height (and the layout
60+ // slot's rows).
61+ const (
62+ logoImageCols = 4
63+ logoImageRows = 2
64+ )
5165
52- // ArtDisplayWidth is the visual column width of each art line.
53- const ArtDisplayWidth = 20
66+ // logoTextGap is the number of blank columns between the logo and the title /
67+ // subtitle text, so the heading has a little room to breathe.
68+ const logoTextGap = 2
69+
70+ // logoSlotWidth is the width reserved on the logo rows: the logo image plus the
71+ // gap before the title and subtitle text.
72+ const logoSlotWidth = logoImageCols + logoTextGap
5473
5574// HeaderConfig controls what the header displays.
5675type HeaderConfig struct {
@@ -72,6 +91,48 @@ func ShouldShowShortcuts(width int) bool {
7291 return width >= MinWidthForShortcuts
7392}
7493
94+ // artFitsViewport reports whether the viewport is wide and tall enough to show
95+ // the logo. The height bound (MinHeightForArt) is a little above
96+ // MinHeightForHeader so the logo is dropped before the header itself at short
97+ // heights, where a vertical resize can otherwise leave a transient ghost of the
98+ // inline image.
99+ func artFitsViewport (width , height int ) bool {
100+ return width >= MinWidthForArt && height >= MinHeightForArt
101+ }
102+
103+ // shortcutRowCount returns how many rows the shortcut block occupies for the
104+ // config's column count.
105+ func shortcutRowCount (cfg HeaderConfig ) int {
106+ n := len (cfg .Shortcuts )
107+ if n == 0 {
108+ return 0
109+ }
110+ cols := cfg .ShortcutColumns
111+ if cols < 1 {
112+ cols = 1
113+ }
114+ return (n + cols - 1 ) / cols
115+ }
116+
117+ // headerContentRows returns how many content rows the header needs: enough for
118+ // the title/subtitle/info block or the shortcut block, whichever is taller. This
119+ // keeps the box exactly as tall as its content, with no trailing empty row.
120+ func headerContentRows (cfg HeaderConfig ) int {
121+ // title (row 0), subtitle (row 1), a gap (row 2), then the info lines.
122+ info := 3 + len (cfg .InfoLines )
123+ sc := shortcutRowCount (cfg )
124+ if sc > info {
125+ return sc
126+ }
127+ return info
128+ }
129+
130+ // HeaderHeightFor returns the number of screen lines the header occupies for the
131+ // given config (its content rows plus the top and bottom borders).
132+ func HeaderHeightFor (cfg HeaderConfig ) int {
133+ return headerContentRows (cfg ) + 2
134+ }
135+
75136// RenderHeader renders the full-width header box.
76137// Progressive disclosure as width narrows: first hides the art, then the
77138// info text, keeping keyboard shortcuts always visible.
@@ -170,16 +231,32 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
170231 rightColWidth = maxShortcutWidth + 2
171232 }
172233
173- // Determine what fits: shortcuts always shown, art and info are progressive.
174- // Hide art first (below 88 cols), then info text, as width narrows.
175- showArt := cfg .ShowArt
234+ // Determine what fits: shortcuts always shown, the logo and info are
235+ // progressive. The logo is image-or-nothing: it shows only when an
236+ // inline-image protocol is available and the viewport is wide enough.
237+ showArt := cfg .ShowArt && LogoAvailable ()
176238 showInfo := true
177239
178- // Hide art when viewport is too narrow for art + info + shortcuts
179- if showArt && width < MinWidthForArt {
240+ // Hide the logo when the viewport is too narrow or too short. The height
241+ // guard drops the logo a little before the rest of the header because a
242+ // vertical resize at very short heights can otherwise leave a transient
243+ // ghost of the inline image. The ClearLogo below removes any drawn logo.
244+ if showArt && ! artFitsViewport (width , height ) {
180245 showArt = false
181246 }
182247
248+ // The logo image escape, emitted once on the first content row; it spans
249+ // logoImageRows rows and logoImageCols columns in the top-left corner.
250+ logoEsc := ""
251+ if showArt {
252+ logoEsc = renderHeaderLogo (logoImageCols , logoImageRows )
253+ if logoEsc == "" {
254+ showArt = false
255+ }
256+ }
257+
258+ cr := headerContentRows (cfg )
259+
183260 // If info + shortcuts don't fit, hide info
184261 infoMinWidth := 20 // rough minimum for title/info text
185262 if innerWidth < rightColWidth + infoMinWidth + 4 {
@@ -189,13 +266,13 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
189266 // Map info lines to row indices
190267 infoByRow := make (map [int ]string )
191268 if showInfo {
192- infoByRow [2 ] = HeaderTitleStyle .Render (cfg .Title )
269+ infoByRow [0 ] = HeaderTitleStyle .Render (cfg .Title )
193270 if cfg .Subtitle != "" {
194- infoByRow [3 ] = HeaderInfoLabelStyle .Render (cfg .Subtitle )
271+ infoByRow [1 ] = HeaderInfoLabelStyle .Render (cfg .Subtitle )
195272 }
196273 for i , info := range cfg .InfoLines {
197- row := 5 + i
198- if row > 9 {
274+ row := 3 + i
275+ if row > cr - 1 {
199276 break
200277 }
201278 iconStyle := HeaderInfoStyle
@@ -206,44 +283,53 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
206283 }
207284 }
208285
209- // Left content base width
210- leftContentBase := 1 // margin
211- if showArt {
212- leftContentBase += ArtDisplayWidth
213- }
214-
215286 // Vertically center shortcuts
216287 scStartRow := 0
217288 if len (shortcuts ) > 0 {
218- scStartRow = (10 - len (shortcuts )) / 2
289+ scStartRow = (cr - len (shortcuts )) / 2
290+ if scStartRow < 0 {
291+ scStartRow = 0
292+ }
219293 }
220294
221- gap := " "
295+ // When the logo is hidden but the terminal could show one (e.g. resized too
296+ // narrow), remove any previously-drawn logo so it does not linger.
297+ if ! showArt {
298+ b .WriteString (ClearLogo ())
299+ }
222300
223301 // Top border
224302 b .WriteString (HeaderBorderStyle .Render ("┌" + strings .Repeat ("─" , innerWidth ) + "┐" ))
225303 b .WriteString ("\n " )
226304
227- // Content rows
228- for i := 0 ; i < 10 ; i ++ {
229- // Left column: art (optional) + info
230- artText := ""
231- if showArt {
232- artText = ArtLines [i ]
305+ // Content rows. The logo occupies the top-left corner across the title and
306+ // subtitle rows, which indent their text past the logo. Every other row (the
307+ // blank spacer and the info lines) starts at the shared left margin, so the
308+ // logo and the info icons line up on the same left edge.
309+ for i := 0 ; i < cr ; i ++ {
310+ var left strings.Builder
311+ left .WriteString (strings .Repeat (" " , headerLeftMargin ))
312+ leftWidth := headerLeftMargin
313+
314+ if showArt && i < logoImageRows {
315+ if i == 0 {
316+ left .WriteString (logoEsc )
317+ }
318+ left .WriteString (strings .Repeat (" " , logoSlotWidth ))
319+ leftWidth += logoSlotWidth
233320 }
234321
235- infoText := ""
236- infoVisualLen := 0
237322 if info , ok := infoByRow [i ]; ok {
238- infoText = gap + info
239- infoVisualLen = 2 + lipgloss .Width (info )
323+ left . WriteString ( info )
324+ leftWidth += lipgloss .Width (info )
240325 }
241326
242- leftUsed := leftContentBase + infoVisualLen
327+ b .WriteString (HeaderBorderStyle .Render ("│" ))
328+ b .WriteString (left .String ())
243329
244330 if len (shortcuts ) > 0 {
245331 shortcutCol := innerWidth - rightColWidth
246- midPad := shortcutCol - leftUsed
332+ midPad := shortcutCol - leftWidth
247333 if midPad < 0 {
248334 midPad = 0
249335 }
@@ -260,31 +346,18 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
260346 scTrailingPad = 0
261347 }
262348
263- b .WriteString (HeaderBorderStyle .Render ("│" ))
264- b .WriteString (" " )
265- if showArt {
266- b .WriteString (artText )
267- }
268- b .WriteString (infoText )
269349 b .WriteString (strings .Repeat (" " , midPad ))
270350 b .WriteString (shortcutRendered )
271351 b .WriteString (strings .Repeat (" " , scTrailingPad ))
272- b .WriteString (HeaderBorderStyle .Render ("│" ))
273352 } else {
274- trailingPad := innerWidth - leftUsed
353+ trailingPad := innerWidth - leftWidth
275354 if trailingPad < 0 {
276355 trailingPad = 0
277356 }
278-
279- b .WriteString (HeaderBorderStyle .Render ("│" ))
280- b .WriteString (" " )
281- if showArt {
282- b .WriteString (artText )
283- }
284- b .WriteString (infoText )
285357 b .WriteString (strings .Repeat (" " , trailingPad ))
286- b .WriteString (HeaderBorderStyle .Render ("│" ))
287358 }
359+
360+ b .WriteString (HeaderBorderStyle .Render ("│" ))
288361 b .WriteString ("\n " )
289362 }
290363
0 commit comments