Skip to content

Commit 7b6e7e4

Browse files
committed
tui styling updates
1 parent b685096 commit 7b6e7e4

3 files changed

Lines changed: 81 additions & 65 deletions

File tree

internal/tui/modifyview/model.go

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ type Model struct {
113113
actionStack []StagedAction
114114

115115
// Rename mode
116-
renameMode bool
117-
renameInput textinput.Model
116+
renameMode bool
117+
renameInput textinput.Model
118+
renameOriginal string // original branch name shown as label
118119

119120
// Help overlay
120121
showHelp bool
@@ -701,7 +702,9 @@ func (m *Model) startRename() {
701702
}
702703

703704
m.renameMode = true
705+
m.renameOriginal = node.Ref.Branch
704706
m.renameInput.SetValue(node.Ref.Branch)
707+
m.renameInput.Prompt = ""
705708
m.renameInput.Focus()
706709
m.renameInput.CursorEnd()
707710
}
@@ -830,7 +833,7 @@ func (m Model) nodeLineCount(idx int) int {
830833
}
831834

832835
func (m Model) contentViewHeight() int {
833-
reserved := 1 // status line
836+
reserved := 3 // post-scroll newline + context line + status bar
834837
if shared.ShouldShowHeader(m.width, m.height) {
835838
reserved += shared.HeaderHeight
836839
}
@@ -983,55 +986,51 @@ func (m Model) View() string {
983986
shared.RenderHeader(&out, m.buildHeaderConfig(), m.width, m.height)
984987
}
985988

989+
// Build the scrollable branch list content
986990
var b strings.Builder
987-
988-
// Branch list — all nodes rendered with shared renderer, annotations for pending actions
989991
for i := 0; i < len(m.nodes); i++ {
990992
nodeData := toNodeData(m.nodes[i], i)
991993
isFocused := i == m.cursor
992994
annotation := nodeAnnotation(m.nodes[i], i)
993995
shared.RenderNode(&b, nodeData, isFocused, m.width, annotation)
994996
}
995-
996-
// Trunk
997997
shared.RenderTrunk(&b, m.trunk.Branch)
998998

999-
// Rename input line (if in rename mode)
1000-
if m.renameMode {
1001-
b.WriteString("\n")
1002-
b.WriteString(renameBadge.Render("Rename: "))
1003-
b.WriteString(m.renameInput.View())
1004-
b.WriteString("\n")
1005-
}
1006-
1007-
// Transient status message
1008-
if m.statusMessage != "" {
1009-
b.WriteString("\n")
1010-
if m.statusIsError {
1011-
b.WriteString(transientErrorStyle.Render("✗ " + m.statusMessage))
1012-
} else {
1013-
b.WriteString(transientInfoStyle.Render(m.statusMessage))
1014-
}
1015-
b.WriteString("\n")
1016-
}
1017-
1018-
content := b.String()
999+
// Count fixed bottom lines (always visible, not scrollable).
1000+
// The bottom section always has 2 lines: one for contextual info
1001+
// (rename prompt or error, blank when neither) and one for the status bar.
1002+
bottomLines := 2 // post-scroll newline + context line + status bar
10191003

1020-
// Scrolling
1021-
reservedLines := 0
1004+
// Scrolling — reserve space for header and fixed bottom
1005+
reservedLines := bottomLines
10221006
if showHeader {
1023-
reservedLines = shared.HeaderHeight
1007+
reservedLines += shared.HeaderHeight
10241008
}
1025-
viewHeight := m.height - reservedLines - 1 // -1 for status line
1009+
viewHeight := m.height - reservedLines
10261010
if viewHeight < 1 {
10271011
viewHeight = 1
10281012
}
10291013

1030-
out.WriteString(shared.ApplyScrollToContent(content, m.scrollOffset, viewHeight))
1014+
out.WriteString(shared.ApplyScrollToContent(b.String(), m.scrollOffset, viewHeight))
10311015
out.WriteString("\n")
10321016

1033-
// Status line at the bottom
1034-
out.WriteString(renderStatusLine(m.nodes, m.width))
1017+
// Second-to-bottom: error/status message line (always present, blank when empty)
1018+
if m.statusMessage != "" {
1019+
if m.statusIsError {
1020+
out.WriteString(transientErrorStyle.Render("✗ " + m.statusMessage))
1021+
} else {
1022+
out.WriteString(transientInfoStyle.Render(m.statusMessage))
1023+
}
1024+
}
1025+
1026+
// Bottom line: rename prompt (when active) or status bar
1027+
out.WriteString("\n")
1028+
if m.renameMode {
1029+
out.WriteString(renameBadge.Render(fmt.Sprintf("Rename: %s → ", m.renameOriginal)))
1030+
out.WriteString(m.renameInput.View())
1031+
} else {
1032+
out.WriteString(renderStatusLine(m.nodes, m.width))
1033+
}
10351034

10361035
return out.String()
10371036
}
@@ -1079,12 +1078,12 @@ func (m Model) buildHeaderConfig() shared.HeaderConfig {
10791078
ShortcutColumns: 2,
10801079
Shortcuts: []shared.ShortcutEntry{
10811080
// Left column // Right column
1082-
{Key: "↑↓", Desc: "select branch"}, {Key: "x", Desc: "drop", Disabled: structureDisabled},
1083-
{Key: "f", Desc: "view files"}, {Key: "r", Desc: "rename", Disabled: structureDisabled},
1084-
{Key: "c", Desc: "view commits"}, {Key: "u", Desc: "fold up", Disabled: structureDisabled},
1085-
{Key: "?", Desc: "help"}, {Key: "d", Desc: "fold down", Disabled: structureDisabled},
1086-
{Key: "q/esc", Desc: "quit"}, {Key: "shift+↑↓", Desc: "reorder", Disabled: reorderDisabled},
1087-
{Key: "^S", Desc: "apply changes"}, {Key: "z", Desc: "undo"},
1081+
{Key: "↑↓", Desc: "select branch"}, {Key: "x", Desc: "drop", Disabled: structureDisabled},
1082+
{Key: "f", Desc: "view files"}, {Key: "r", Desc: "rename", Disabled: structureDisabled},
1083+
{Key: "c", Desc: "view commits"}, {Key: "u", Desc: "fold up", Disabled: structureDisabled},
1084+
{Key: "?", Desc: "help"}, {Key: "d", Desc: "fold down", Disabled: structureDisabled},
1085+
{Key: "q/esc", Desc: "quit"}, {Key: "shift+↑↓", Desc: "reorder", Disabled: reorderDisabled},
1086+
{Key: "^S", Desc: "apply changes"}, {Key: "z", Desc: "undo"},
10881087
},
10891088
}
10901089
}

internal/tui/shared/header.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ func ShouldShowShortcuts(width int) bool {
7070
}
7171

7272
// RenderHeader renders the full-width header box.
73+
// Progressive disclosure as width narrows: first hides the art, then the
74+
// info text, keeping keyboard shortcuts always visible.
7375
func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
7476
if width < 2 {
7577
return
7678
}
7779
innerWidth := width - 2
7880

79-
showShortcuts := ShouldShowShortcuts(width)
80-
81-
// Build shortcut lines (possibly multi-column)
81+
// Always build shortcut lines
8282
type shortcutLine struct {
8383
text string
8484
visWidth int
@@ -92,7 +92,7 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
9292
cols = 1
9393
}
9494

95-
if showShortcuts && len(cfg.Shortcuts) > 0 {
95+
if len(cfg.Shortcuts) > 0 {
9696
if cols >= 2 {
9797
// Two-column layout with aligned keys and descriptions.
9898
// First pass: compute max visual key width per column.
@@ -167,27 +167,45 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
167167
rightColWidth = maxShortcutWidth + 2
168168
}
169169

170+
// Determine what fits: shortcuts always shown, art and info are progressive.
171+
// Hide art first (below 88 cols), then info text, as width narrows.
172+
showArt := cfg.ShowArt
173+
showInfo := true
174+
175+
// Hide art when viewport is too narrow for art + info + shortcuts
176+
if showArt && width < 88 {
177+
showArt = false
178+
}
179+
180+
// If info + shortcuts don't fit, hide info
181+
infoMinWidth := 20 // rough minimum for title/info text
182+
if innerWidth < rightColWidth+infoMinWidth+4 {
183+
showInfo = false
184+
}
185+
170186
// Map info lines to row indices
171187
infoByRow := make(map[int]string)
172-
infoByRow[2] = HeaderTitleStyle.Render(cfg.Title)
173-
if cfg.Subtitle != "" {
174-
infoByRow[3] = HeaderInfoLabelStyle.Render(cfg.Subtitle)
175-
}
176-
for i, info := range cfg.InfoLines {
177-
row := 5 + i
178-
if row > 9 {
179-
break
188+
if showInfo {
189+
infoByRow[2] = HeaderTitleStyle.Render(cfg.Title)
190+
if cfg.Subtitle != "" {
191+
infoByRow[3] = HeaderInfoLabelStyle.Render(cfg.Subtitle)
180192
}
181-
iconStyle := HeaderInfoStyle
182-
if info.IconStyle != nil {
183-
iconStyle = *info.IconStyle
193+
for i, info := range cfg.InfoLines {
194+
row := 5 + i
195+
if row > 9 {
196+
break
197+
}
198+
iconStyle := HeaderInfoStyle
199+
if info.IconStyle != nil {
200+
iconStyle = *info.IconStyle
201+
}
202+
infoByRow[row] = iconStyle.Render(info.Icon) + HeaderInfoLabelStyle.Render(" "+info.Label)
184203
}
185-
infoByRow[row] = iconStyle.Render(info.Icon) + HeaderInfoLabelStyle.Render(" "+info.Label)
186204
}
187205

188206
// Left content base width
189207
leftContentBase := 1 // margin
190-
if cfg.ShowArt {
208+
if showArt {
191209
leftContentBase += ArtDisplayWidth
192210
}
193211

@@ -207,7 +225,7 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
207225
for i := 0; i < 10; i++ {
208226
// Left column: art (optional) + info
209227
artText := ""
210-
if cfg.ShowArt {
228+
if showArt {
211229
artText = ArtLines[i]
212230
}
213231

@@ -220,7 +238,7 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
220238

221239
leftUsed := leftContentBase + infoVisualLen
222240

223-
if showShortcuts && len(shortcuts) > 0 {
241+
if len(shortcuts) > 0 {
224242
shortcutCol := innerWidth - rightColWidth
225243
midPad := shortcutCol - leftUsed
226244
if midPad < 0 {
@@ -241,7 +259,7 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
241259

242260
b.WriteString(HeaderBorderStyle.Render("│"))
243261
b.WriteString(" ")
244-
if cfg.ShowArt {
262+
if showArt {
245263
b.WriteString(artText)
246264
}
247265
b.WriteString(infoText)
@@ -257,7 +275,7 @@ func RenderHeader(b *strings.Builder, cfg HeaderConfig, width, height int) {
257275

258276
b.WriteString(HeaderBorderStyle.Render("│"))
259277
b.WriteString(" ")
260-
if cfg.ShowArt {
278+
if showArt {
261279
b.WriteString(artText)
262280
}
263281
b.WriteString(infoText)

internal/tui/stackview/model_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,17 @@ func TestView_HeaderHiddenWhenNarrow(t *testing.T) {
221221
assert.NotContains(t, view, "View Stack")
222222
}
223223

224-
func TestView_HeaderWithoutShortcutsWhenMediumWidth(t *testing.T) {
224+
func TestView_HeaderShortcutsAlwaysVisible(t *testing.T) {
225225
nodes := makeNodes("b1", "b2")
226226
m := New(nodes, testTrunk, "0.0.1")
227227

228-
// Wide enough for header but not for shortcuts (between minWidthForHeader and minWidthForShortcuts)
228+
// Even at medium width, shortcuts should still be visible
229229
updated, _ := m.Update(tea.WindowSizeMsg{Width: 60, Height: 40})
230230
m = updated.(Model)
231231

232232
view := m.View()
233233
assert.Contains(t, view, "┌", "header should be shown")
234-
assert.Contains(t, view, "View Stack", "info should be shown")
235-
assert.NotContains(t, view, "checkout", "shortcuts should be hidden at this width")
234+
assert.Contains(t, view, "checkout", "shortcuts should always be visible")
236235
}
237236

238237
func TestView_HeaderShowsMergedCount(t *testing.T) {

0 commit comments

Comments
 (0)