66 "io"
77 "os"
88 "path/filepath"
9+ "strconv"
910 "strings"
1011
1112 "github.com/urfave/cli/v3"
@@ -25,6 +26,11 @@ const (
2526 detachedKeyword = "detached"
2627)
2728
29+ const (
30+ defaultMaxPathWidth = 56
31+ superWideThreshold = 160
32+ )
33+
2834// GitRepository interface for mocking
2935type GitRepository interface {
3036 GetWorktrees () ([]git.Worktree , error )
@@ -55,6 +61,16 @@ func NewListCommand() *cli.Command {
5561 Description : "Shows all worktrees with their paths, branches, and HEAD commits." ,
5662 ShellComplete : completeList ,
5763 Flags : []cli.Flag {
64+ & cli.BoolFlag {
65+ Name : "compact" ,
66+ Aliases : []string {"c" },
67+ Usage : "Minimize column widths for narrow or redirected output" ,
68+ },
69+ & cli.IntFlag {
70+ Name : "max-path-width" ,
71+ Usage : fmt .Sprintf ("Maximum width for PATH column (default %d)" , defaultMaxPathWidth ),
72+ Value : defaultMaxPathWidth ,
73+ },
5874 & cli.BoolFlag {
5975 Name : "quiet" ,
6076 Aliases : []string {"q" },
@@ -93,16 +109,20 @@ func listCommand(_ context.Context, cmd *cli.Command) error {
93109 // Load config to get base_dir
94110 cfg , _ := config .LoadConfig (mainRepoPath )
95111
112+ // Resolve display options
113+ opts := resolveListDisplayOptions (cmd , w )
114+
96115 // Get quiet flag
97116 quiet := cmd .Bool ("quiet" )
98117
99118 // Use CommandExecutor-based implementation
100119 executor := listNewExecutor ()
101- return listCommandWithCommandExecutor (cmd , w , executor , cfg , mainRepoPath , quiet )
120+ return listCommandWithCommandExecutor (cmd , w , executor , cfg , mainRepoPath , quiet , opts )
102121}
103122
104123func listCommandWithCommandExecutor (
105124 _ * cli.Command , w io.Writer , executor command.Executor , cfg * config.Config , mainRepoPath string , quiet bool ,
125+ opts listDisplayOptions ,
106126) error {
107127 // Get current working directory
108128 cwd , err := listGetwd ()
@@ -131,7 +151,18 @@ func listCommandWithCommandExecutor(
131151 if quiet {
132152 displayWorktreesQuiet (w , worktrees , cfg , mainRepoPath )
133153 } else {
134- displayWorktreesRelative (w , worktrees , cwd , cfg , mainRepoPath )
154+ termWidth := getTerminalWidth ()
155+ if opts .MaxPathWidth <= 0 {
156+ opts .MaxPathWidth = defaultMaxPathWidth
157+ }
158+ if ! opts .Compact {
159+ if ! opts .OutputIsTTY {
160+ opts .Compact = true
161+ } else if termWidth >= superWideThreshold {
162+ opts .Compact = true
163+ }
164+ }
165+ displayWorktreesRelative (w , worktrees , cwd , cfg , mainRepoPath , termWidth , opts )
135166 }
136167 return nil
137168}
@@ -243,9 +274,8 @@ func displayWorktreesQuiet(w io.Writer, worktrees []git.Worktree, cfg *config.Co
243274// displayWorktreesRelative formats and displays worktree information with relative paths
244275func displayWorktreesRelative (
245276 w io.Writer , worktrees []git.Worktree , currentPath string , cfg * config.Config , mainRepoPath string ,
277+ termWidth int , opts listDisplayOptions ,
246278) {
247- termWidth := getTerminalWidth ()
248-
249279 // Minimum widths for columns
250280 const minPathWidth = 20
251281 const headWidth = headDisplayLength
@@ -312,27 +342,81 @@ func displayWorktreesRelative(
312342
313343 // Calculate available width for path column
314344 // Total = path + spacing + branch + spacing + status + spacing + head
315- availableForPath := termWidth - spacing - maxBranchLen - spacing - maxStatusLen - spacing - headWidth
316-
345+ if termWidth <= 0 {
346+ termWidth = 80
347+ }
317348 // If branch column is too wide, limit it as well
318349 maxAvailableForBranch := termWidth - minPathWidth - spacing - maxStatusLen - spacing - spacing - headWidth
319350 if maxBranchLen > maxAvailableForBranch {
320351 maxBranchLen = maxAvailableForBranch
321- // Recalculate path width with truncated branch width
322- availableForPath = termWidth - spacing - maxBranchLen - spacing - maxStatusLen - spacing - headWidth
323352 }
324353
325- // Ensure minimum path width
326- if availableForPath < minPathWidth {
327- availableForPath = minPathWidth
354+ pathHeaderWidth := len ("PATH" )
355+ branchHeaderWidth := len ("BRANCH" )
356+ statusHeaderWidth := len ("STATUS" )
357+
358+ if maxBranchLen < branchHeaderWidth {
359+ maxBranchLen = branchHeaderWidth
360+ }
361+ if maxStatusLen < statusHeaderWidth {
362+ maxStatusLen = statusHeaderWidth
363+ }
364+
365+ availableForPath := termWidth - spacing - maxBranchLen - spacing - maxStatusLen - spacing - headWidth
366+
367+ if availableForPath < pathHeaderWidth {
368+ availableForPath = pathHeaderWidth
369+ }
370+
371+ pathWidth := availableForPath
372+
373+ if opts .MaxPathWidth > 0 && pathWidth > opts .MaxPathWidth {
374+ pathWidth = opts .MaxPathWidth
375+ }
376+
377+ if opts .Compact {
378+ minCompactWidth := pathHeaderWidth
379+ if maxPathLen > minCompactWidth {
380+ minCompactWidth = maxPathLen
381+ }
382+ if pathWidth > minCompactWidth {
383+ pathWidth = minCompactWidth
384+ }
385+ if pathWidth < minCompactWidth {
386+ pathWidth = minCompactWidth
387+ }
388+ } else {
389+ desiredWidth := maxPathLen + 2
390+ if desiredWidth < minPathWidth {
391+ desiredWidth = minPathWidth
392+ }
393+ if desiredWidth < pathHeaderWidth {
394+ desiredWidth = pathHeaderWidth
395+ }
396+ if pathWidth > desiredWidth {
397+ pathWidth = desiredWidth
398+ }
399+ if pathWidth < minPathWidth {
400+ pathWidth = minPathWidth
401+ }
402+ }
403+
404+ if pathWidth > availableForPath {
405+ pathWidth = availableForPath
406+ }
407+ if pathWidth < pathHeaderWidth {
408+ pathWidth = pathHeaderWidth
409+ }
410+ if pathWidth < 1 {
411+ pathWidth = 1
328412 }
329413
330414 // Print header
331- fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " , availableForPath , "PATH" , maxBranchLen , "BRANCH" , maxStatusLen , "STATUS" , "HEAD" )
415+ fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " , pathWidth , "PATH" , maxBranchLen , "BRANCH" , maxStatusLen , "STATUS" , "HEAD" )
332416 fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
333- availableForPath , strings .Repeat ("-" , pathHeaderDashes ),
417+ pathWidth , strings .Repeat ("-" , pathHeaderDashes ),
334418 maxBranchLen , strings .Repeat ("-" , branchHeaderDashes ),
335- maxStatusLen , strings .Repeat ("-" , len ( "STATUS" ) ),
419+ maxStatusLen , strings .Repeat ("-" , statusHeaderWidth ),
336420 "----" )
337421
338422 // Print worktrees
@@ -342,14 +426,47 @@ func displayWorktreesRelative(
342426 headShort = headShort [:headDisplayLength ]
343427 }
344428
345- pathDisplay := truncatePath (item .path , availableForPath )
429+ pathDisplay := truncatePath (item .path , pathWidth )
346430 branchDisplayTrunc := truncatePath (item .branch , maxBranchLen )
347431 statusDisplayTrunc := truncatePath (item .status , maxStatusLen )
348432
349433 fmt .Fprintf (w , "%-*s %-*s %-*s %s\n " ,
350- availableForPath , pathDisplay ,
434+ pathWidth , pathDisplay ,
351435 maxBranchLen , branchDisplayTrunc ,
352436 maxStatusLen , statusDisplayTrunc ,
353437 headShort )
354438 }
355439}
440+
441+ type listDisplayOptions struct {
442+ Compact bool
443+ MaxPathWidth int
444+ OutputIsTTY bool
445+ }
446+
447+ func resolveListDisplayOptions (cmd * cli.Command , w io.Writer ) listDisplayOptions {
448+ maxPathWidth := cmd .Int ("max-path-width" )
449+ if maxPathWidth == defaultMaxPathWidth && ! cmd .IsSet ("max-path-width" ) {
450+ if envValue := os .Getenv ("WTP_LIST_MAX_PATH" ); envValue != "" {
451+ if parsed , err := strconv .Atoi (envValue ); err == nil && parsed > 0 {
452+ maxPathWidth = parsed
453+ }
454+ }
455+ }
456+ if maxPathWidth <= 0 {
457+ maxPathWidth = defaultMaxPathWidth
458+ }
459+
460+ compact := cmd .Bool ("compact" )
461+
462+ outputIsTTY := false
463+ if file , ok := w .(* os.File ); ok {
464+ outputIsTTY = term .IsTerminal (int (file .Fd ()))
465+ }
466+
467+ return listDisplayOptions {
468+ Compact : compact ,
469+ MaxPathWidth : maxPathWidth ,
470+ OutputIsTTY : outputIsTTY ,
471+ }
472+ }
0 commit comments