Skip to content

Commit

Permalink
Add word-wrap support, with wrap length provided by the user
Browse files Browse the repository at this point in the history
We could try to automatically detect the terminal width and wrap at that
point, but this would increase the binary footprint for all users even
if not using this feature.

Instead, we can allow users to specify their preferred line length limit
(if any), and those who want to bear the cost of checking the terminal
size can do so if they wish. This also makes the feature more testable.

Original patch by Sascha Grunert <sgrunert@suse.com>
  • Loading branch information
mostynb committed May 9, 2020
1 parent 6102689 commit 1486cc2
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 39 deletions.
2 changes: 2 additions & 0 deletions app.go
Expand Up @@ -74,6 +74,8 @@ type App struct {
Copyright string
// Writer writer to write output to
Writer io.Writer
// If greater than zero, wrap help text lines at this length (experimental).
WrapAt int
// ErrWriter writes error output
ErrWriter io.Writer
// ExitErrHandler processes any error encountered while running an App before
Expand Down
2 changes: 1 addition & 1 deletion app_test.go
Expand Up @@ -1197,7 +1197,7 @@ func TestAppHelpPrinter(t *testing.T) {
}()

var wasCalled = false
HelpPrinter = func(w io.Writer, template string, data interface{}) {
HelpPrinter = func(w io.Writer, template string, data interface{}, wrapAt int) {
wasCalled = true
}

Expand Down
4 changes: 3 additions & 1 deletion go.mod
Expand Up @@ -4,6 +4,8 @@ go 1.11

require (
github.com/BurntSushi/toml v0.3.1
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d
gopkg.in/yaml.v2 v2.2.2
github.com/sergi/go-diff v1.1.0 // indirect
gopkg.in/yaml.v2 v2.2.4
)
14 changes: 14 additions & 0 deletions go.sum
@@ -1,14 +1,28 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
83 changes: 73 additions & 10 deletions help.go
Expand Up @@ -42,10 +42,10 @@ var helpSubcommand = &Command{
}

// Prints help for the App or Command
type helpPrinter func(w io.Writer, templ string, data interface{})
type helpPrinter func(w io.Writer, templ string, data interface{}, wrapAt int)

// Prints help for the App or Command with custom template function.
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}, wrapAt int)

// HelpPrinter is a function that writes the help output. If not set explicitly,
// this calls HelpPrinterCustom using only the default template functions.
Expand Down Expand Up @@ -78,7 +78,7 @@ func ShowAppHelp(c *Context) error {
}

if c.App.ExtraInfo == nil {
HelpPrinter(c.App.Writer, template, c.App)
HelpPrinter(c.App.Writer, template, c.App, c.App.WrapAt)
return nil
}

Expand All @@ -87,7 +87,7 @@ func ShowAppHelp(c *Context) error {
"ExtraInfo": c.App.ExtraInfo,
}
}
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData())
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData(), c.App.WrapAt)

return nil
}
Expand Down Expand Up @@ -189,7 +189,7 @@ func ShowCommandHelpAndExit(c *Context, command string, code int) {
func ShowCommandHelp(ctx *Context, command string) error {
// show the subcommand help for a command with subcommands
if command == "" {
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App, ctx.App.WrapAt)
return nil
}

Expand All @@ -200,7 +200,7 @@ func ShowCommandHelp(ctx *Context, command string) error {
templ = CommandHelpTemplate
}

HelpPrinter(ctx.App.Writer, templ, c)
HelpPrinter(ctx.App.Writer, templ, c, ctx.App.WrapAt)

return nil
}
Expand Down Expand Up @@ -261,9 +261,22 @@ func ShowCommandCompletions(ctx *Context, command string) {
//
// The customFuncs map will be combined with a default template.FuncMap to
// allow using arbitrary functions in template rendering.
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}, wrapAt int) {

wrapFunc := func(input string, offset int) string {
return input
}

if wrapAt > 0 {
wrapFunc = func(input string, offset int) string {
return wrap(input, offset, wrapAt)
}
}

funcMap := template.FuncMap{
"join": strings.Join,
"join": strings.Join,
"wrap": wrapFunc,
"offset": offset,
}
for key, value := range customFuncs {
funcMap[key] = value
Expand All @@ -284,8 +297,8 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs
_ = w.Flush()
}

func printHelp(out io.Writer, templ string, data interface{}) {
HelpPrinterCustom(out, templ, data, nil)
func printHelp(out io.Writer, templ string, data interface{}, wrapAt int) {
HelpPrinterCustom(out, templ, data, nil, wrapAt)
}

func checkVersion(c *Context) bool {
Expand Down Expand Up @@ -366,3 +379,53 @@ func checkCommandCompletions(c *Context, name string) bool {
ShowCommandCompletions(c, name)
return true
}

func wrap(input string, offset int, wrapAt int) string {
var sb strings.Builder

lines := strings.Split(input, "\n")

for i, line := range lines {
if i != 0 {
sb.WriteString(strings.Repeat(" ", offset))
}

sb.WriteString(wrapLine(line, offset, wrapAt))

if i != len(lines)-1 {
sb.WriteString("\n")
}
}

return sb.String()
}

func wrapLine(input string, offset int, wrapAt int) string {
if wrapAt <= offset || len(input) <= wrapAt-offset {
return input
}

lineWidth := wrapAt - offset
words := strings.Fields(strings.TrimSpace(input))
if len(words) == 0 {
return input
}

wrapped := words[0]
spaceLeft := lineWidth - len(wrapped)
for _, word := range words[1:] {
if len(word)+1 > spaceLeft {
wrapped += "\n" + strings.Repeat(" ", offset) + word
spaceLeft = lineWidth - len(word)
} else {
wrapped += " " + word
spaceLeft -= 1 + len(word)
}
}

return wrapped
}

func offset(input string, fixed int) int {
return len(input) + fixed
}

0 comments on commit 1486cc2

Please sign in to comment.