-
-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wrapper for long lines in help (#770)
* main terminal wrapper for long lines * preserving the original length writer * set widths to be dynamic * chore: improve error handling * improvements * clean up flags * general fixes and styles for terminal width feature * align columns * dont make columns equal width * clean code and column fixes help printer
- Loading branch information
1 parent
3342e8e
commit ffb64eb
Showing
6 changed files
with
288 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package templates | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/mitchellh/go-wordwrap" | ||
"github.com/spf13/pflag" | ||
) | ||
|
||
const ( | ||
defaultOffset = 10 | ||
minWidth = 80 | ||
flagIndent = " " | ||
nameIndentWidth = 4 | ||
minDescWidth = 20 | ||
) | ||
|
||
type HelpFlagPrinter struct { | ||
wrapLimit uint | ||
out io.Writer | ||
maxFlagLen int | ||
} | ||
|
||
func NewHelpFlagPrinter(out io.Writer, wrapLimit uint, flags *pflag.FlagSet) *HelpFlagPrinter { | ||
if out == nil { | ||
panic("output writer cannot be nil") | ||
} | ||
if flags == nil { | ||
panic("flag set cannot be nil") | ||
} | ||
if wrapLimit < minWidth { | ||
wrapLimit = minWidth | ||
} | ||
|
||
return &HelpFlagPrinter{ | ||
wrapLimit: wrapLimit, | ||
out: out, | ||
maxFlagLen: calculateMaxFlagLength(flags), | ||
} | ||
} | ||
|
||
func calculateMaxFlagLength(flags *pflag.FlagSet) int { | ||
maxLen := 0 | ||
flags.VisitAll(func(flag *pflag.Flag) { | ||
length := len(flagIndent) | ||
|
||
if len(flag.Shorthand) > 0 { | ||
if flag.Value.Type() != "bool" { | ||
length += len(fmt.Sprintf("-%s, --%s %s", flag.Shorthand, flag.Name, flag.Value.Type())) | ||
} else { | ||
length += len(fmt.Sprintf("-%s, --%s", flag.Shorthand, flag.Name)) | ||
} | ||
} else { | ||
if flag.Value.Type() != "bool" { | ||
length += len(fmt.Sprintf(" --%s %s", flag.Name, flag.Value.Type())) | ||
} else { | ||
length += len(fmt.Sprintf(" --%s", flag.Name)) | ||
} | ||
} | ||
|
||
if length > maxLen { | ||
maxLen = length | ||
} | ||
}) | ||
return maxLen | ||
} | ||
|
||
func (p *HelpFlagPrinter) PrintHelpFlag(flag *pflag.Flag) { | ||
nameIndent := nameIndentWidth | ||
|
||
flagName := "" | ||
if flag.Shorthand != "" { | ||
if flag.Value.Type() != "bool" { | ||
flagName = fmt.Sprintf("%s-%s, --%s %s", strings.Repeat(" ", nameIndent), | ||
flag.Shorthand, flag.Name, flag.Value.Type()) | ||
} else { | ||
flagName = fmt.Sprintf("%s-%s, --%s", strings.Repeat(" ", nameIndent), | ||
flag.Shorthand, flag.Name) | ||
} | ||
} else { | ||
if flag.Value.Type() != "bool" { | ||
flagName = fmt.Sprintf("%s --%s %s", strings.Repeat(" ", nameIndent), | ||
flag.Name, flag.Value.Type()) | ||
} else { | ||
flagName = fmt.Sprintf("%s --%s", strings.Repeat(" ", nameIndent), | ||
flag.Name) | ||
} | ||
} | ||
|
||
flagSection := fmt.Sprintf("%-*s", p.maxFlagLen, flagName) | ||
descIndent := p.maxFlagLen + 4 | ||
|
||
description := flag.Usage | ||
if flag.DefValue != "" { | ||
description = fmt.Sprintf("%s (default %q)", description, flag.DefValue) | ||
} | ||
|
||
descWidth := int(p.wrapLimit) - descIndent | ||
if descWidth < minDescWidth { | ||
descWidth = minDescWidth | ||
} | ||
|
||
wrapped := wordwrap.WrapString(description, uint(descWidth)) | ||
lines := strings.Split(wrapped, "\n") | ||
|
||
if _, err := fmt.Fprintf(p.out, "%-*s%s\n", descIndent, flagSection, lines[0]); err != nil { | ||
return | ||
} | ||
|
||
// Print remaining lines with proper indentation | ||
for _, line := range lines[1:] { | ||
if _, err := fmt.Fprintf(p.out, "%s%s\n", strings.Repeat(" ", descIndent), line); err != nil { | ||
return | ||
} | ||
} | ||
|
||
if _, err := fmt.Fprintln(p.out); err != nil { | ||
return | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package templates | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/spf13/pflag" | ||
) | ||
|
||
// Templater handles the generation and management of command usage templates. | ||
type Templater struct { | ||
UsageTemplate string | ||
} | ||
|
||
// SetCustomUsageFunc configures a custom usage template for the provided cobra command. | ||
// It returns an error if the command is nil. | ||
func SetCustomUsageFunc(cmd *cobra.Command) error { | ||
if cmd == nil { | ||
return fmt.Errorf("command cannot be nil") | ||
} | ||
t := &Templater{ | ||
UsageTemplate: MainUsageTemplate(), | ||
} | ||
|
||
cmd.SetUsageTemplate(t.UsageTemplate) | ||
return nil | ||
} | ||
|
||
// MainUsageTemplate returns the usage template for the root command and wrap cobra flag usages to the terminal width | ||
func MainUsageTemplate() string { | ||
return `Usage:{{if .Runnable}} | ||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} | ||
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} | ||
Aliases: | ||
{{.NameAndAliases}}{{end}}{{if .HasExample}} | ||
Examples: | ||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}} | ||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} | ||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} | ||
Flags: | ||
{{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} | ||
Global Flags: | ||
{{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} | ||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} | ||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} | ||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} | ||
` | ||
} | ||
|
||
// Default terminal width if actual width cannot be determined | ||
const maxWidth = 80 | ||
|
||
// WrappedFlagUsages formats the flag usage string to fit within the terminal width | ||
func WrappedFlagUsages(f *pflag.FlagSet) string { | ||
var builder strings.Builder | ||
printer := NewHelpFlagPrinter(&builder, maxWidth, f) | ||
|
||
printer.maxFlagLen = calculateMaxFlagLength(f) | ||
|
||
f.VisitAll(func(flag *pflag.Flag) { | ||
printer.PrintHelpFlag(flag) | ||
}) | ||
|
||
return builder.String() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package term | ||
|
||
import ( | ||
"io" | ||
"os" | ||
|
||
"github.com/mitchellh/go-wordwrap" | ||
"golang.org/x/term" | ||
) | ||
|
||
// TerminalWriter wraps an io.Writer and provides automatic line wrapping based on terminal width | ||
// It ensures that output text is formatted to fit within the terminal's dimensions. | ||
type TerminalWriter struct { | ||
width uint | ||
writer io.Writer | ||
} | ||
|
||
const ( | ||
maxWidth = 120 | ||
mediumWidth = 100 | ||
minWidth = 80 | ||
) | ||
|
||
// NewResponsiveWriter creates a terminal-aware writer that automatically wraps text | ||
// based on the terminal width. If the provided writer is not a terminal or if width | ||
// detection fails, it will return the original writer unchanged. | ||
func NewResponsiveWriter(w io.Writer) io.Writer { | ||
file, ok := w.(*os.File) | ||
if !ok { | ||
return w | ||
} | ||
|
||
if !term.IsTerminal(int(file.Fd())) { | ||
return w | ||
} | ||
|
||
width, _, err := term.GetSize(int(file.Fd())) | ||
if err != nil { | ||
return w | ||
} | ||
|
||
// Use optimal width based on terminal size | ||
var limit uint | ||
switch { | ||
case width >= maxWidth: | ||
limit = maxWidth | ||
case width >= mediumWidth: | ||
limit = mediumWidth | ||
case width >= minWidth: | ||
limit = minWidth | ||
default: | ||
limit = uint(width) | ||
} | ||
|
||
return &TerminalWriter{ | ||
width: limit, | ||
writer: w, | ||
} | ||
} | ||
|
||
func (w *TerminalWriter) Write(p []byte) (int, error) { | ||
if w.width == 0 { | ||
return w.writer.Write(p) | ||
} | ||
|
||
// Preserving the original length for correct return value | ||
originalLen := len(p) | ||
wrapped := wordwrap.WrapString(string(p), w.width) | ||
n, err := w.writer.Write([]byte(wrapped)) | ||
if err != nil { | ||
return n, err | ||
} | ||
// return the original length as per io.Writer contract | ||
return originalLen, nil | ||
} | ||
|
||
func (w *TerminalWriter) GetWidth() uint { | ||
return w.width | ||
} |