Skip to content

Commit 8ba1478

Browse files
committed
Clamp side pane titles to pane width
1 parent 1b53cff commit 8ba1478

File tree

2 files changed

+156
-6
lines changed

2 files changed

+156
-6
lines changed

internal/tui/tui.go

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -710,15 +710,15 @@ func (m model) View() string {
710710
}
711711
}
712712
oursPane := oursStyle.Render(
713-
titleStyle.Render(oursTitle) + "\n" +
713+
renderPaneTitle(oursTitle, m.viewportOurs.Width, titleStyle) + "\n" +
714714
m.viewportOurs.View(),
715715
)
716716

717717
resultStyle := resultUnresolvedPaneStyle
718718
if allResolved(m.doc, m.manualResolved) {
719719
resultStyle = resultResolvedPaneStyle
720720
}
721-
resultTitle := resultTitleStyle.Render("RESULT " + statusStyle.Render("("+statusText+")"))
721+
resultTitle := renderResultPaneTitle(statusText, m.viewportResult.Width, resultTitleStyle, statusStyle)
722722
resultPane := resultStyle.Render(
723723
resultTitle + "\n" +
724724
m.viewportResult.View(),
@@ -735,7 +735,7 @@ func (m model) View() string {
735735
}
736736
}
737737
theirsPane := theirsStyle.Render(
738-
titleStyle.Render(theirsTitle) + "\n" +
738+
renderPaneTitle(theirsTitle, m.viewportTheirs.Width, titleStyle) + "\n" +
739739
m.viewportTheirs.View(),
740740
)
741741

@@ -1233,6 +1233,83 @@ func formatLabel(label string) string {
12331233
return label
12341234
}
12351235

1236+
func renderPaneTitle(title string, paneWidth int, style lipgloss.Style) string {
1237+
if paneWidth <= 0 {
1238+
return ""
1239+
}
1240+
1241+
frameWidth := style.GetHorizontalFrameSize()
1242+
if paneWidth <= frameWidth {
1243+
return truncateDisplayWidth(title, paneWidth)
1244+
}
1245+
1246+
trimmed := truncateDisplayWidth(title, paneWidth-frameWidth)
1247+
return style.Render(trimmed)
1248+
}
1249+
1250+
func renderResultPaneTitle(statusText string, paneWidth int, titleStyle lipgloss.Style, statusStyle lipgloss.Style) string {
1251+
const prefix = "RESULT "
1252+
statusSegment := "(" + statusText + ")"
1253+
rawTitle := prefix + statusSegment
1254+
1255+
if paneWidth <= 0 {
1256+
return ""
1257+
}
1258+
1259+
frameWidth := titleStyle.GetHorizontalFrameSize()
1260+
if paneWidth <= frameWidth {
1261+
return truncateDisplayWidth(rawTitle, paneWidth)
1262+
}
1263+
1264+
trimmed := truncateDisplayWidth(rawTitle, paneWidth-frameWidth)
1265+
if !strings.HasPrefix(trimmed, prefix) {
1266+
return titleStyle.Render(trimmed)
1267+
}
1268+
1269+
trimmedStatus := strings.TrimPrefix(trimmed, prefix)
1270+
if trimmedStatus == "" {
1271+
return titleStyle.Render(prefix)
1272+
}
1273+
1274+
return titleStyle.Render(prefix + statusStyle.Render(trimmedStatus))
1275+
}
1276+
1277+
func truncateDisplayWidth(value string, maxWidth int) string {
1278+
if maxWidth <= 0 {
1279+
return ""
1280+
}
1281+
if lipgloss.Width(value) <= maxWidth {
1282+
return value
1283+
}
1284+
1285+
const ellipsis = "..."
1286+
ellipsisWidth := lipgloss.Width(ellipsis)
1287+
if maxWidth <= ellipsisWidth {
1288+
return trimDisplayWidth(value, maxWidth)
1289+
}
1290+
1291+
return trimDisplayWidth(value, maxWidth-ellipsisWidth) + ellipsis
1292+
}
1293+
1294+
func trimDisplayWidth(value string, maxWidth int) string {
1295+
if maxWidth <= 0 {
1296+
return ""
1297+
}
1298+
1299+
var b strings.Builder
1300+
currentWidth := 0
1301+
for _, r := range value {
1302+
runeWidth := lipgloss.Width(string(r))
1303+
if currentWidth+runeWidth > maxWidth {
1304+
break
1305+
}
1306+
b.WriteRune(r)
1307+
currentWidth += runeWidth
1308+
}
1309+
1310+
return b.String()
1311+
}
1312+
12361313
func firstHexRun(label string) (int, int) {
12371314
start := -1
12381315
for i, r := range label {

internal/tui/tui_test.go

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/charmbracelet/bubbles/viewport"
1515
tea "github.com/charmbracelet/bubbletea"
16+
"github.com/charmbracelet/lipgloss"
1617
"github.com/chojs23/ec/internal/cli"
1718
"github.com/chojs23/ec/internal/engine"
1819
"github.com/chojs23/ec/internal/gitmerge"
@@ -412,6 +413,43 @@ func TestFormatLabel(t *testing.T) {
412413
}
413414
}
414415

416+
func TestRenderPaneTitleFitsPaneWidth(t *testing.T) {
417+
title := "OURS (/var/folders/n5/10r8gvt52mq58dpz62c7_jt00000gn/T/ec-local-766054358)"
418+
paneWidth := 34
419+
420+
got := renderPaneTitle(title, paneWidth, titleStyle)
421+
if lipgloss.Width(got) > paneWidth {
422+
t.Fatalf("renderPaneTitle width = %d, want <= %d", lipgloss.Width(got), paneWidth)
423+
}
424+
if !strings.Contains(got, "...") {
425+
t.Fatalf("expected truncated title with ellipsis, got %q", got)
426+
}
427+
}
428+
429+
func TestRenderPaneTitleHandlesVeryNarrowPane(t *testing.T) {
430+
got := renderPaneTitle("OURS (HEAD)", 1, titleStyle)
431+
if lipgloss.Width(got) > 1 {
432+
t.Fatalf("renderPaneTitle width = %d, want <= 1", lipgloss.Width(got))
433+
}
434+
}
435+
436+
func TestRenderResultPaneTitleFitsPaneWidth(t *testing.T) {
437+
got := renderResultPaneTitle("Resolved (manual)", 18, resultTitleStyle, statusResolvedStyle)
438+
if lipgloss.Width(got) > 18 {
439+
t.Fatalf("renderResultPaneTitle width = %d, want <= 18", lipgloss.Width(got))
440+
}
441+
if !strings.Contains(got, "...") {
442+
t.Fatalf("expected truncated title with ellipsis, got %q", got)
443+
}
444+
}
445+
446+
func TestRenderResultPaneTitleKeepsStatusWhenWide(t *testing.T) {
447+
got := renderResultPaneTitle("Unresolved", 50, resultTitleStyle, statusUnresolvedStyle)
448+
if !strings.Contains(got, "RESULT (Unresolved)") {
449+
t.Fatalf("expected full result status title, got %q", got)
450+
}
451+
}
452+
415453
func TestFirstHexRun(t *testing.T) {
416454
start, end := firstHexRun("x1234567y")
417455
if start != 1 || end != 8 {
@@ -530,9 +568,9 @@ func TestModelViewShowsBranchLabels(t *testing.T) {
530568
{OursLabel: "HEAD", TheirsLabel: "feature/add-auth"},
531569
},
532570
manualResolved: map[int][]byte{},
533-
viewportOurs: viewport.New(10, 5),
534-
viewportResult: viewport.New(10, 5),
535-
viewportTheirs: viewport.New(10, 5),
571+
viewportOurs: viewport.New(40, 5),
572+
viewportResult: viewport.New(40, 5),
573+
viewportTheirs: viewport.New(40, 5),
536574
width: 120,
537575
height: 20,
538576
}
@@ -547,6 +585,41 @@ func TestModelViewShowsBranchLabels(t *testing.T) {
547585
}
548586
}
549587

588+
func TestModelViewTruncatesLongBranchLabels(t *testing.T) {
589+
doc := parseSingleConflictDoc(t)
590+
state, err := engine.NewState(doc)
591+
if err != nil {
592+
t.Fatalf("NewState error = %v", err)
593+
}
594+
longLabel := "/var/folders/n5/10r8gvt52mq58dpz62c7_jt00000gn/T/ec-local-766054358"
595+
m := model{
596+
ready: true,
597+
opts: cliOptionsWithMergedPath("merged.txt"),
598+
state: state,
599+
doc: doc,
600+
currentConflict: 0,
601+
selectedSide: selectedOurs,
602+
mergedLabels: []conflictLabels{
603+
{OursLabel: longLabel, TheirsLabel: longLabel},
604+
},
605+
manualResolved: map[int][]byte{},
606+
viewportOurs: viewport.New(10, 5),
607+
viewportResult: viewport.New(10, 5),
608+
viewportTheirs: viewport.New(10, 5),
609+
width: 90,
610+
height: 20,
611+
}
612+
m.updateViewports()
613+
614+
view := m.View()
615+
if strings.Contains(view, longLabel) {
616+
t.Fatalf("expected long labels to be truncated, got:\n%s", view)
617+
}
618+
if !strings.Contains(view, "...") {
619+
t.Fatalf("expected truncated labels with ellipsis, got:\n%s", view)
620+
}
621+
}
622+
550623
func TestModelViewNoLabelsWithoutMergedLabels(t *testing.T) {
551624
doc := parseSingleConflictDoc(t)
552625
state, err := engine.NewState(doc)

0 commit comments

Comments
 (0)