Skip to content

Commit f6c51a0

Browse files
committed
feat: add width controls for list command output
- Add --compact flag to minimize column widths - Add --max-path-width flag to cap PATH column width (default: 56) - Support WTP_LIST_MAX_PATH environment variable - Auto-enable compact mode on non-TTY output (pipes, redirects) - Auto-enable compact mode on super-wide terminals (≥160 columns) - Improve readability on very wide terminals by preventing excessive padding Closes #29
1 parent 6a8895d commit f6c51a0

File tree

2 files changed

+334
-31
lines changed

2 files changed

+334
-31
lines changed

cmd/wtp/list.go

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
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
2935
type 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

104123
func 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
244275
func 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

Comments
 (0)