66 "io"
77 "os"
88 "path/filepath"
9+ "strconv"
910 "strings"
1011
1112 "github.com/urfave/cli/v3"
@@ -25,6 +26,14 @@ const (
2526 detachedKeyword = "detached"
2627)
2728
29+ const (
30+ defaultMaxPathWidth = 56
31+ superWideThreshold = 160
32+ pathPadding = 2
33+ minPathWidth = 20
34+ columnSpacing = 3
35+ )
36+
2837// GitRepository interface for mocking
2938type GitRepository interface {
3039 GetWorktrees () ([]git.Worktree , error )
@@ -55,6 +64,16 @@ func NewListCommand() *cli.Command {
5564 Description : "Shows all worktrees with their paths, branches, and HEAD commits." ,
5665 ShellComplete : completeList ,
5766 Flags : []cli.Flag {
67+ & cli.BoolFlag {
68+ Name : "compact" ,
69+ Aliases : []string {"c" },
70+ Usage : "Minimize column widths for narrow or redirected output" ,
71+ },
72+ & cli.IntFlag {
73+ Name : "max-path-width" ,
74+ Usage : fmt .Sprintf ("Maximum width for PATH column (default %d)" , defaultMaxPathWidth ),
75+ Value : defaultMaxPathWidth ,
76+ },
5877 & cli.BoolFlag {
5978 Name : "quiet" ,
6079 Aliases : []string {"q" },
@@ -93,16 +112,20 @@ func listCommand(_ context.Context, cmd *cli.Command) error {
93112 // Load config to get base_dir
94113 cfg , _ := config .LoadConfig (mainRepoPath )
95114
115+ // Resolve display options
116+ opts := resolveListDisplayOptions (cmd , w )
117+
96118 // Get quiet flag
97119 quiet := cmd .Bool ("quiet" )
98120
99121 // Use CommandExecutor-based implementation
100122 executor := listNewExecutor ()
101- return listCommandWithCommandExecutor (cmd , w , executor , cfg , mainRepoPath , quiet )
123+ return listCommandWithCommandExecutor (cmd , w , executor , cfg , mainRepoPath , quiet , opts )
102124}
103125
104126func listCommandWithCommandExecutor (
105127 _ * cli.Command , w io.Writer , executor command.Executor , cfg * config.Config , mainRepoPath string , quiet bool ,
128+ opts listDisplayOptions ,
106129) error {
107130 // Get current working directory
108131 cwd , err := listGetwd ()
@@ -131,7 +154,18 @@ func listCommandWithCommandExecutor(
131154 if quiet {
132155 displayWorktreesQuiet (w , worktrees , cfg , mainRepoPath )
133156 } else {
134- displayWorktreesRelative (w , worktrees , cwd , cfg , mainRepoPath )
157+ termWidth := getTerminalWidth ()
158+ if opts .MaxPathWidth <= 0 {
159+ opts .MaxPathWidth = defaultMaxPathWidth
160+ }
161+ if ! opts .Compact {
162+ if ! opts .OutputIsTTY {
163+ opts .Compact = true
164+ } else if termWidth >= superWideThreshold {
165+ opts .Compact = true
166+ }
167+ }
168+ displayWorktreesRelative (w , worktrees , cwd , cfg , mainRepoPath , termWidth , opts )
135169 }
136170 return nil
137171}
@@ -243,113 +277,207 @@ func displayWorktreesQuiet(w io.Writer, worktrees []git.Worktree, cfg *config.Co
243277// displayWorktreesRelative formats and displays worktree information with relative paths
244278func displayWorktreesRelative (
245279 w io.Writer , worktrees []git.Worktree , currentPath string , cfg * config.Config , mainRepoPath string ,
280+ termWidth int , opts listDisplayOptions ,
246281) {
247- termWidth := getTerminalWidth ()
282+ if termWidth <= 0 {
283+ termWidth = 80
284+ }
285+
286+ items , metrics := collectListDisplayData (worktrees , currentPath , cfg , mainRepoPath )
287+ if len (items ) == 0 {
288+ return
289+ }
290+
291+ pathWidth , branchWidth , statusWidth := computeListColumnWidths (metrics , termWidth , opts )
292+
293+ fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " , pathWidth , "PATH" , branchWidth , "BRANCH" , statusWidth , "STATUS" , "HEAD" )
294+ fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
295+ pathWidth , strings .Repeat ("-" , pathHeaderDashes ),
296+ branchWidth , strings .Repeat ("-" , branchHeaderDashes ),
297+ statusWidth , strings .Repeat ("-" , len ("STATUS" )),
298+ "----" )
248299
249- // Minimum widths for columns
250- const minPathWidth = 20
251- const headWidth = headDisplayLength
252- const spacing = 3 // Spaces between columns
300+ for _ , item := range items {
301+ headShort := item .head
302+ if len (headShort ) > headDisplayLength {
303+ headShort = headShort [:headDisplayLength ]
304+ }
253305
254- // Calculate initial column widths
255- maxBranchLen := 6 // "BRANCH"
256- maxPathLen := 4 // "PATH"
257- maxStatusLen := 6 // "STATUS"
306+ fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
307+ pathWidth , truncatePath (item .path , pathWidth ),
308+ branchWidth , truncatePath (item .branch , branchWidth ),
309+ statusWidth , truncatePath (item .status , statusWidth ),
310+ headShort )
311+ }
312+ }
258313
259- // Find main worktree path is no longer needed since we pass it from the caller
314+ type listDisplayData struct {
315+ path string
316+ branch string
317+ head string
318+ status string
319+ }
260320
261- // First pass: calculate max widths and prepare display data
262- type displayData struct {
263- path string
264- branch string
265- head string
266- status string
267- isCurrent bool
321+ type listColumnMetrics struct {
322+ maxPathLen int
323+ maxBranchLen int
324+ maxStatusLen int
325+ }
326+
327+ func collectListDisplayData (
328+ worktrees []git.Worktree , currentPath string , cfg * config.Config , mainRepoPath string ,
329+ ) ([]listDisplayData , listColumnMetrics ) {
330+ metrics := listColumnMetrics {
331+ maxPathLen : len ("PATH" ),
332+ maxBranchLen : len ("BRANCH" ),
333+ maxStatusLen : len ("STATUS" ),
268334 }
269335
270- var displayItems [] displayData
336+ items := make ([] listDisplayData , 0 , len ( worktrees ))
271337
272338 for _ , wt := range worktrees {
273- var isCurrent bool
274-
275- // Get worktree display name
276339 pathDisplay := getWorktreeDisplayName (wt , cfg , mainRepoPath )
277-
278- // Check if this is the current worktree
279340 if wt .Path == currentPath {
280- isCurrent = true
281341 pathDisplay += "*"
282342 }
283343
284344 branchDisplay := formatBranchDisplay (wt .Branch )
285345
286- // Determine management status
287- var statusDisplay string
346+ statusDisplay := "unmanaged"
288347 if isWorktreeManagedList (wt .Path , cfg , mainRepoPath , wt .IsMain ) {
289348 statusDisplay = "managed"
290- } else {
291- statusDisplay = "unmanaged"
292349 }
293350
294- if len (pathDisplay ) > maxPathLen {
295- maxPathLen = len (pathDisplay )
351+ if len (pathDisplay ) > metrics . maxPathLen {
352+ metrics . maxPathLen = len (pathDisplay )
296353 }
297- if len (branchDisplay ) > maxBranchLen {
298- maxBranchLen = len (branchDisplay )
354+ if len (branchDisplay ) > metrics . maxBranchLen {
355+ metrics . maxBranchLen = len (branchDisplay )
299356 }
300- if len (statusDisplay ) > maxStatusLen {
301- maxStatusLen = len (statusDisplay )
357+ if len (statusDisplay ) > metrics . maxStatusLen {
358+ metrics . maxStatusLen = len (statusDisplay )
302359 }
303360
304- displayItems = append (displayItems , displayData {
305- path : pathDisplay ,
306- branch : branchDisplay ,
307- head : wt .HEAD ,
308- status : statusDisplay ,
309- isCurrent : isCurrent ,
361+ items = append (items , listDisplayData {
362+ path : pathDisplay ,
363+ branch : branchDisplay ,
364+ head : wt .HEAD ,
365+ status : statusDisplay ,
310366 })
311367 }
312368
313- // Calculate available width for path column
314- // Total = path + spacing + branch + spacing + status + spacing + head
315- availableForPath := termWidth - spacing - maxBranchLen - spacing - maxStatusLen - spacing - headWidth
369+ return items , metrics
370+ }
371+
372+ func computeListColumnWidths (
373+ metrics listColumnMetrics ,
374+ termWidth int ,
375+ opts listDisplayOptions ,
376+ ) (pathWidth , branchWidth , statusWidth int ) {
377+ branchWidth , statusWidth = clampBranchAndStatusWidths (metrics .maxBranchLen , metrics .maxStatusLen , termWidth )
378+ pathWidth = derivePathWidth (metrics .maxPathLen , branchWidth , statusWidth , termWidth , opts )
379+ return pathWidth , branchWidth , statusWidth
380+ }
381+
382+ func clampBranchAndStatusWidths (
383+ maxBranchLen , maxStatusLen , termWidth int ,
384+ ) (branchWidth , statusWidth int ) {
385+ const (
386+ minPathWidth = 20
387+ headWidth = headDisplayLength
388+ spacing = 3
389+ )
390+
391+ branchWidth = maxBranchLen
392+ statusWidth = maxStatusLen
393+
394+ branchHeaderWidth := len ("BRANCH" )
395+ statusHeaderWidth := len ("STATUS" )
396+
397+ if branchWidth < branchHeaderWidth {
398+ branchWidth = branchHeaderWidth
399+ }
400+ if statusWidth < statusHeaderWidth {
401+ statusWidth = statusHeaderWidth
402+ }
403+
404+ maxAvailableForBranch := termWidth - minPathWidth - spacing - statusWidth - spacing - spacing - headWidth
405+ if branchWidth > maxAvailableForBranch {
406+ branchWidth = maxAvailableForBranch
407+ if branchWidth < branchHeaderWidth {
408+ branchWidth = branchHeaderWidth
409+ }
410+ }
316411
317- // If branch column is too wide, limit it as well
318- maxAvailableForBranch := termWidth - minPathWidth - spacing - maxStatusLen - spacing - spacing - headWidth
319- if maxBranchLen > maxAvailableForBranch {
320- maxBranchLen = maxAvailableForBranch
321- // Recalculate path width with truncated branch width
322- availableForPath = termWidth - spacing - maxBranchLen - spacing - maxStatusLen - spacing - headWidth
412+ return branchWidth , statusWidth
413+ }
414+
415+ func derivePathWidth (maxPathLen , branchWidth , statusWidth , termWidth int , opts listDisplayOptions ) int {
416+ pathHeaderWidth := len ("PATH" )
417+ availableForPath := termWidth - columnSpacing - branchWidth - columnSpacing - statusWidth -
418+ columnSpacing - headDisplayLength
419+ availableForPath = max (availableForPath , pathHeaderWidth )
420+
421+ pathWidth := availableForPath
422+
423+ if opts .Compact {
424+ pathWidth = clampCompactPathWidth (pathWidth , maxPathLen )
425+ } else {
426+ pathWidth = clampExpandedPathWidth (pathWidth , maxPathLen )
323427 }
324428
325- // Ensure minimum path width
326- if availableForPath < minPathWidth {
327- availableForPath = minPathWidth
429+ if opts .MaxPathWidth > 0 {
430+ pathWidth = min (pathWidth , opts .MaxPathWidth )
328431 }
329432
330- // Print header
331- fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " , availableForPath , "PATH" , maxBranchLen , "BRANCH" , maxStatusLen , "STATUS" , "HEAD" )
332- fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
333- availableForPath , strings .Repeat ("-" , pathHeaderDashes ),
334- maxBranchLen , strings .Repeat ("-" , branchHeaderDashes ),
335- maxStatusLen , strings .Repeat ("-" , len ("STATUS" )),
336- "----" )
433+ pathWidth = max (min (pathWidth , availableForPath ), pathHeaderWidth )
434+ pathWidth = max (pathWidth , 1 )
337435
338- // Print worktrees
339- for _ , item := range displayItems {
340- headShort := item .head
341- if len (headShort ) > headDisplayLength {
342- headShort = headShort [:headDisplayLength ]
436+ return pathWidth
437+ }
438+
439+ func clampCompactPathWidth (currentWidth , maxPathLen int ) int {
440+ pathHeaderWidth := len ("PATH" )
441+ minCompactWidth := max (maxPathLen , pathHeaderWidth )
442+ return max (min (currentWidth , minCompactWidth ), pathHeaderWidth )
443+ }
444+
445+ func clampExpandedPathWidth (currentWidth , maxPathLen int ) int {
446+ pathHeaderWidth := len ("PATH" )
447+
448+ desiredWidth := max (maxPathLen + pathPadding , minPathWidth , pathHeaderWidth )
449+ return max (min (currentWidth , desiredWidth ), minPathWidth )
450+ }
451+
452+ type listDisplayOptions struct {
453+ Compact bool
454+ MaxPathWidth int
455+ OutputIsTTY bool
456+ }
457+
458+ func resolveListDisplayOptions (cmd * cli.Command , w io.Writer ) listDisplayOptions {
459+ maxPathWidth := cmd .Int ("max-path-width" )
460+ if maxPathWidth == defaultMaxPathWidth && ! cmd .IsSet ("max-path-width" ) {
461+ if envValue := os .Getenv ("WTP_LIST_MAX_PATH" ); envValue != "" {
462+ if parsed , err := strconv .Atoi (envValue ); err == nil && parsed > 0 {
463+ maxPathWidth = parsed
464+ }
343465 }
466+ }
467+ if maxPathWidth <= 0 {
468+ maxPathWidth = defaultMaxPathWidth
469+ }
344470
345- pathDisplay := truncatePath (item .path , availableForPath )
346- branchDisplayTrunc := truncatePath (item .branch , maxBranchLen )
347- statusDisplayTrunc := truncatePath (item .status , maxStatusLen )
471+ compact := cmd .Bool ("compact" )
348472
349- fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
350- availableForPath , pathDisplay ,
351- maxBranchLen , branchDisplayTrunc ,
352- maxStatusLen , statusDisplayTrunc ,
353- headShort )
473+ outputIsTTY := false
474+ if file , ok := w .(* os.File ); ok {
475+ outputIsTTY = term .IsTerminal (int (file .Fd ()))
476+ }
477+
478+ return listDisplayOptions {
479+ Compact : compact ,
480+ MaxPathWidth : maxPathWidth ,
481+ OutputIsTTY : outputIsTTY ,
354482 }
355483}
0 commit comments