Skip to content

Commit 4268db5

Browse files
committed
Fix preview ui to use box instead break line for separators
1 parent 9653603 commit 4268db5

1 file changed

Lines changed: 135 additions & 25 deletions

File tree

cmd/preview.go

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"fmt"
55
"os"
66
"os/signal"
7+
"regexp"
78
"strings"
89
"syscall"
910
"time"
11+
"unicode/utf8"
1012

1113
"github.com/fatih/color"
1214
"github.com/spf13/cobra"
@@ -506,8 +508,8 @@ func watchAndMerge(previewWorktreePath, baseBranch string, branchEnabled map[str
506508
// Calculate header height based on branches
507509
getHeaderHeight := func() int {
508510
branches := getEnabledBranches()
509-
// Title (1) + preview status (1) + separator (1) + "Source branches:" (1) + branches + separator (1) + help (1)
510-
return 6 + len(branches)
511+
// Title (1) + preview box (3: top + content + bottom) + source box (2 + branches: top + branches + bottom) + help (1)
512+
return 1 + 3 + 2 + len(branches) + 1
511513
}
512514

513515
// Get preview branch status
@@ -549,46 +551,77 @@ func watchAndMerge(previewWorktreePath, baseBranch string, branchEnabled map[str
549551
clearScreen()
550552
hideCursor()
551553

554+
// Row tracker
555+
row := 1
556+
552557
// Draw header - title bar
553-
moveCursor(1, 1)
558+
moveCursor(row, 1)
554559
title := fmt.Sprintf(" PREVIEW: %s ", previewBranchName)
555560
fmt.Print(color.New(color.BgBlue, color.FgWhite, color.Bold).Sprint(title))
556561
padding := termWidth - len(title)
557562
if padding > 0 {
558563
fmt.Print(color.New(color.BgBlue).Sprint(strings.Repeat(" ", padding)))
559564
}
565+
row++
566+
567+
// Preview box - top border with title
568+
moveCursor(row, 1)
569+
previewBoxTitle := "─ Preview "
570+
previewBoxTitleLen := utf8.RuneCountInString(previewBoxTitle) // visual length
571+
remainingWidth := termWidth - previewBoxTitleLen - 2 // -2 for corners
572+
if remainingWidth < 0 {
573+
remainingWidth = 0
574+
}
575+
fmt.Print("┌" + previewBoxTitle + strings.Repeat("─", remainingWidth) + "┐")
576+
row++
560577

561-
// Draw preview branch status line
562-
moveCursor(2, 1)
578+
// Preview box - content
579+
moveCursor(row, 1)
563580
previewStatus := getPreviewStatus()
564-
previewLine := fmt.Sprintf(" %s → %s", color.New(color.Bold).Sprint(previewBranchName), previewStatus)
565-
fmt.Print(previewLine)
566-
567-
// Draw separator
568-
moveCursor(3, 1)
569-
fmt.Print(strings.Repeat("─", termWidth))
570-
571-
// Section header for source branches
572-
moveCursor(4, 1)
573-
fmt.Print(color.New(color.Faint).Sprint(" Source branches:"))
581+
previewContent := fmt.Sprintf(" %s → %s", color.New(color.Bold).Sprint(previewBranchName), previewStatus)
582+
// Calculate visible length (rune count without ANSI codes) for padding
583+
contentPadding := termWidth - visualLen(previewContent) - 2 // -2 for box sides
584+
if contentPadding < 0 {
585+
contentPadding = 0
586+
}
587+
fmt.Print("│" + previewContent + strings.Repeat(" ", contentPadding) + "│")
588+
row++
589+
590+
// Preview box - bottom border
591+
moveCursor(row, 1)
592+
fmt.Print("└" + strings.Repeat("─", termWidth-2) + "┘")
593+
row++
594+
595+
// Source branches box - top border with title
596+
moveCursor(row, 1)
597+
sourceBoxTitle := "─ Source Branches "
598+
sourceBoxTitleLen := utf8.RuneCountInString(sourceBoxTitle) // visual length
599+
remainingWidth = termWidth - sourceBoxTitleLen - 2
600+
if remainingWidth < 0 {
601+
remainingWidth = 0
602+
}
603+
fmt.Print("┌" + sourceBoxTitle + strings.Repeat("─", remainingWidth) + "┐")
604+
row++
574605

575-
// Draw branch status
576-
for i, branch := range branches {
577-
moveCursor(5+i, 1)
606+
// Source branches box - content (branch lines)
607+
for _, branch := range branches {
608+
moveCursor(row, 1)
578609
info := branchInfo[branch]
579-
line := formatBranchLine(branch, info, stagedEnabled, unstagedEnabled, termWidth)
610+
line := formatBranchLineBoxed(branch, info, stagedEnabled, unstagedEnabled, termWidth)
580611
fmt.Print(line)
612+
row++
581613
}
582614

583-
// Separator after branches
584-
separatorRow := 5 + len(branches)
585-
moveCursor(separatorRow, 1)
586-
fmt.Print(strings.Repeat("─", termWidth))
615+
// Source branches box - bottom border
616+
moveCursor(row, 1)
617+
fmt.Print("└" + strings.Repeat("─", termWidth-2) + "┘")
618+
row++
587619

588620
// Help line
589-
moveCursor(separatorRow+1, 1)
590-
helpText := " [b]ranches [s]taged [u]nstaged [q]uit "
621+
moveCursor(row, 1)
622+
helpText := " [b]ranches [s]taged [u]nstaged [q]uit"
591623
fmt.Print(color.New(color.Faint).Sprint(helpText))
624+
row++
592625

593626
// Draw log area
594627
logStartRow := headerHeight + 1
@@ -1171,3 +1204,80 @@ func formatBranchLine(branch string, info *BranchInfo, stagedEnabled, unstagedEn
11711204

11721205
return fmt.Sprintf(" %s %s: %s", indicatorStr, branch, strings.Join(changeParts, " + "))
11731206
}
1207+
1208+
// stripAnsi removes ANSI escape codes from a string to get visible length
1209+
func stripAnsi(s string) string {
1210+
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
1211+
return ansiRegex.ReplaceAllString(s, "")
1212+
}
1213+
1214+
// visualLen returns the visual width of a string (rune count after stripping ANSI)
1215+
func visualLen(s string) int {
1216+
return utf8.RuneCountInString(stripAnsi(s))
1217+
}
1218+
1219+
// formatBranchLineBoxed formats a branch line with box borders for the header display
1220+
func formatBranchLineBoxed(branch string, info *BranchInfo, stagedEnabled, unstagedEnabled map[string]bool, termWidth int) string {
1221+
if info == nil {
1222+
content := fmt.Sprintf(" %s", branch)
1223+
padding := termWidth - visualLen(content) - 2 // -2 for box sides
1224+
if padding < 0 {
1225+
padding = 0
1226+
}
1227+
return "│" + content + strings.Repeat(" ", padding) + "│"
1228+
}
1229+
1230+
// Build change counts
1231+
var changeParts []string
1232+
changeParts = append(changeParts, fmt.Sprintf("%d commits", info.CommitsAhead))
1233+
1234+
if info.WorktreePath != "" {
1235+
changes, err := getWorktreeChangeCounts(info.WorktreePath)
1236+
if err == nil {
1237+
if changes.Staged > 0 {
1238+
if stagedEnabled[branch] {
1239+
changeParts = append(changeParts, color.GreenString("%d staged", changes.Staged))
1240+
} else {
1241+
changeParts = append(changeParts, color.New(color.Faint).Sprintf("%d staged", changes.Staged))
1242+
}
1243+
}
1244+
if changes.Unstaged > 0 {
1245+
if unstagedEnabled[branch] {
1246+
changeParts = append(changeParts, color.YellowString("%d unstaged", changes.Unstaged))
1247+
} else {
1248+
changeParts = append(changeParts, color.New(color.Faint).Sprintf("%d unstaged", changes.Unstaged))
1249+
}
1250+
}
1251+
if changes.Untracked > 0 {
1252+
if unstagedEnabled[branch] {
1253+
changeParts = append(changeParts, color.CyanString("%d untracked", changes.Untracked))
1254+
} else {
1255+
changeParts = append(changeParts, color.New(color.Faint).Sprintf("%d untracked", changes.Untracked))
1256+
}
1257+
}
1258+
}
1259+
}
1260+
1261+
// Build indicators
1262+
var indicators []string
1263+
if stagedEnabled[branch] {
1264+
indicators = append(indicators, color.GreenString("S"))
1265+
}
1266+
if unstagedEnabled[branch] {
1267+
indicators = append(indicators, color.YellowString("U"))
1268+
}
1269+
indicatorStr := ""
1270+
if len(indicators) > 0 {
1271+
indicatorStr = "[" + strings.Join(indicators, "") + "]"
1272+
}
1273+
1274+
content := fmt.Sprintf(" %s %s: %s", indicatorStr, branch, strings.Join(changeParts, " + "))
1275+
1276+
// Calculate visible length (rune count without ANSI codes) for padding
1277+
padding := termWidth - visualLen(content) - 2 // -2 for box sides
1278+
if padding < 0 {
1279+
padding = 0
1280+
}
1281+
1282+
return "│" + content + strings.Repeat(" ", padding) + "│"
1283+
}

0 commit comments

Comments
 (0)