@@ -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