Skip to content

Commit 1995f5c

Browse files
authored
Merge pull request #140 from ipfs/feat/wrap-text
improve help text on narrow terminals
2 parents fa07c33 + 6a9e61f commit 1995f5c

File tree

4 files changed

+133
-69
lines changed

4 files changed

+133
-69
lines changed

cli/helptext.go

+120-67
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"os"
78
"sort"
89
"strings"
910
"text/template"
1011

11-
"github.com/ipfs/go-ipfs-cmds"
12+
cmds "github.com/ipfs/go-ipfs-cmds"
13+
"golang.org/x/crypto/ssh/terminal"
1214
)
1315

1416
const (
15-
requiredArg = "<%v>"
16-
optionalArg = "[<%v>]"
17-
variadicArg = "%v..."
18-
shortFlag = "-%v"
19-
longFlag = "--%v"
20-
optionType = "(%v)"
17+
defaultTerminalWidth = 80
18+
requiredArg = "<%v>"
19+
optionalArg = "[<%v>]"
20+
variadicArg = "%v..."
21+
shortFlag = "-%v"
22+
longFlag = "--%v"
23+
optionType = "(%v)"
2124

2225
whitespace = "\r\n\t "
2326

@@ -28,7 +31,6 @@ type helpFields struct {
2831
Indent string
2932
Usage string
3033
Path string
31-
ArgUsage string
3234
Tagline string
3335
Arguments string
3436
Options string
@@ -48,7 +50,7 @@ type helpFields struct {
4850
// `
4951
func (f *helpFields) TrimNewlines() {
5052
f.Path = strings.Trim(f.Path, "\n")
51-
f.ArgUsage = strings.Trim(f.ArgUsage, "\n")
53+
f.Usage = strings.Trim(f.Usage, "\n")
5254
f.Tagline = strings.Trim(f.Tagline, "\n")
5355
f.Arguments = strings.Trim(f.Arguments, "\n")
5456
f.Options = strings.Trim(f.Options, "\n")
@@ -66,17 +68,16 @@ func (f *helpFields) IndentAll() {
6668
return indentString(s, indentStr)
6769
}
6870

71+
f.Usage = indent(f.Usage)
6972
f.Arguments = indent(f.Arguments)
7073
f.Options = indent(f.Options)
7174
f.Synopsis = indent(f.Synopsis)
7275
f.Subcommands = indent(f.Subcommands)
7376
f.Description = indent(f.Description)
7477
}
7578

76-
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
77-
7879
const longHelpFormat = `USAGE
79-
{{.Indent}}{{template "usage" .}}
80+
{{.Usage}}
8081
8182
{{if .Synopsis}}SYNOPSIS
8283
{{.Synopsis}}
@@ -96,11 +97,12 @@ const longHelpFormat = `USAGE
9697
{{end}}{{if .Subcommands}}SUBCOMMANDS
9798
{{.Subcommands}}
9899
99-
{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
100+
{{.Indent}}For more information about each command, use:
101+
{{.Indent}}'{{.Path}} <subcmd> --help'
100102
{{end}}
101103
`
102104
const shortHelpFormat = `USAGE
103-
{{.Indent}}{{template "usage" .}}
105+
{{.Usage}}
104106
{{if .Synopsis}}
105107
{{.Synopsis}}
106108
{{end}}{{if .Description}}
@@ -109,18 +111,30 @@ const shortHelpFormat = `USAGE
109111
SUBCOMMANDS
110112
{{.Subcommands}}
111113
{{end}}{{if .MoreHelp}}
112-
Use '{{.Path}} --help' for more information about this command.
114+
{{.Indent}}For more information about each command, use:
115+
{{.Indent}}'{{.Path}} <subcmd> --help'
113116
{{end}}
114117
`
115118

116-
var usageTemplate *template.Template
117119
var longHelpTemplate *template.Template
118120
var shortHelpTemplate *template.Template
119121

122+
func getTerminalWidth(out io.Writer) int {
123+
file, ok := out.(*os.File)
124+
if ok {
125+
if terminal.IsTerminal(int(file.Fd())) {
126+
width, _, err := terminal.GetSize(int(file.Fd()))
127+
if err == nil {
128+
return width
129+
}
130+
}
131+
}
132+
return defaultTerminalWidth
133+
}
134+
120135
func init() {
121-
usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
122-
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
123-
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
136+
longHelpTemplate = template.Must(template.New("longHelp").Parse(longHelpFormat))
137+
shortHelpTemplate = template.Must(template.New("shortHelp").Parse(shortHelpFormat))
124138
}
125139

126140
var ErrNoHelpRequested = errors.New("no help requested")
@@ -154,7 +168,6 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
154168
fields := helpFields{
155169
Indent: indentStr,
156170
Path: pathStr,
157-
ArgUsage: usageText(cmd),
158171
Tagline: cmd.Helptext.Tagline,
159172
Arguments: cmd.Helptext.Arguments,
160173
Options: cmd.Helptext.Options,
@@ -165,22 +178,29 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
165178
MoreHelp: (cmd != root),
166179
}
167180

181+
width := getTerminalWidth(out) - len(indentStr)
182+
168183
if len(cmd.Helptext.LongDescription) > 0 {
169184
fields.Description = cmd.Helptext.LongDescription
170185
}
171186

172187
// autogen fields that are empty
188+
if len(cmd.Helptext.Usage) > 0 {
189+
fields.Usage = cmd.Helptext.Usage
190+
} else {
191+
fields.Usage = commandUsageText(width, cmd, rootName, path)
192+
}
173193
if len(fields.Arguments) == 0 {
174-
fields.Arguments = strings.Join(argumentText(cmd), "\n")
194+
fields.Arguments = strings.Join(argumentText(width, cmd), "\n")
175195
}
176196
if len(fields.Options) == 0 {
177-
fields.Options = strings.Join(optionText(cmd), "\n")
197+
fields.Options = strings.Join(optionText(width, cmd), "\n")
178198
}
179199
if len(fields.Subcommands) == 0 {
180-
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
200+
fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
181201
}
182202
if len(fields.Synopsis) == 0 {
183-
fields.Synopsis = generateSynopsis(cmd, pathStr)
203+
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
184204
}
185205

186206
// trim the extra newlines (see TrimNewlines doc)
@@ -212,21 +232,26 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
212232
fields := helpFields{
213233
Indent: indentStr,
214234
Path: pathStr,
215-
ArgUsage: usageText(cmd),
216235
Tagline: cmd.Helptext.Tagline,
217236
Synopsis: cmd.Helptext.Synopsis,
218237
Description: cmd.Helptext.ShortDescription,
219238
Subcommands: cmd.Helptext.Subcommands,
220-
Usage: cmd.Helptext.Usage,
221239
MoreHelp: (cmd != root),
222240
}
223241

242+
width := getTerminalWidth(out) - len(indentStr)
243+
224244
// autogen fields that are empty
245+
if len(cmd.Helptext.Usage) > 0 {
246+
fields.Usage = cmd.Helptext.Usage
247+
} else {
248+
fields.Usage = commandUsageText(width, cmd, rootName, path)
249+
}
225250
if len(fields.Subcommands) == 0 {
226-
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
251+
fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
227252
}
228253
if len(fields.Synopsis) == 0 {
229-
fields.Synopsis = generateSynopsis(cmd, pathStr)
254+
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
230255
}
231256

232257
// trim the extra newlines (see TrimNewlines doc)
@@ -238,8 +263,17 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
238263
return shortHelpTemplate.Execute(out, fields)
239264
}
240265

241-
func generateSynopsis(cmd *cmds.Command, path string) string {
266+
func generateSynopsis(width int, cmd *cmds.Command, path string) string {
242267
res := path
268+
currentLineLength := len(res)
269+
appendText := func(text string) {
270+
if currentLineLength+len(text)+1 > width {
271+
res += "\n" + strings.Repeat(" ", len(path))
272+
currentLineLength = len(path)
273+
}
274+
currentLineLength += len(text) + 1
275+
res += " " + text
276+
}
243277
for _, opt := range cmd.Options {
244278
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()]
245279
if !ok {
@@ -267,10 +301,10 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
267301
}
268302
}
269303
}
270-
res = fmt.Sprintf("%s [%s]", res, sopt)
304+
appendText("[" + sopt + "]")
271305
}
272306
if len(cmd.Arguments) > 0 {
273-
res = fmt.Sprintf("%s [--]", res)
307+
appendText("[--]")
274308
}
275309
for _, arg := range cmd.Arguments {
276310
sarg := fmt.Sprintf("<%s>", arg.Name)
@@ -281,25 +315,52 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
281315
if !arg.Required {
282316
sarg = fmt.Sprintf("[%s]", sarg)
283317
}
284-
res = fmt.Sprintf("%s %s", res, sarg)
318+
appendText(sarg)
285319
}
286320
return strings.Trim(res, " ")
287321
}
288322

289-
func argumentText(cmd *cmds.Command) []string {
323+
func argumentText(width int, cmd *cmds.Command) []string {
290324
lines := make([]string, len(cmd.Arguments))
291325

292326
for i, arg := range cmd.Arguments {
293327
lines[i] = argUsageText(arg)
294328
}
295329
lines = align(lines)
296330
for i, arg := range cmd.Arguments {
297-
lines[i] += " - " + arg.Description
331+
lines[i] += " - "
332+
lines[i] = appendWrapped(lines[i], arg.Description, width)
298333
}
299334

300335
return lines
301336
}
302337

338+
func appendWrapped(prefix, text string, width int) string {
339+
offset := len(prefix)
340+
bWidth := width - offset
341+
342+
text = strings.Trim(text, whitespace)
343+
// Minimum help-text width is 30 characters.
344+
if bWidth < 30 {
345+
prefix += text
346+
return prefix
347+
}
348+
349+
for len(text) > bWidth {
350+
idx := strings.LastIndexAny(text[:bWidth], whitespace)
351+
if idx < 0 {
352+
idx = strings.IndexAny(text, whitespace)
353+
}
354+
if idx < 0 {
355+
break
356+
}
357+
prefix += text[:idx] + "\n" + strings.Repeat(" ", offset)
358+
text = strings.TrimLeft(text[idx:], whitespace)
359+
}
360+
prefix += text
361+
return prefix
362+
}
363+
303364
func optionFlag(flag string) string {
304365
if len(flag) == 1 {
305366
return fmt.Sprintf(shortFlag, flag)
@@ -308,7 +369,7 @@ func optionFlag(flag string) string {
308369
}
309370
}
310371

311-
func optionText(cmd ...*cmds.Command) []string {
372+
func optionText(width int, cmd ...*cmds.Command) []string {
312373
// get a slice of the options we want to list out
313374
options := make([]cmds.Option, 0)
314375
for _, c := range cmd {
@@ -317,53 +378,33 @@ func optionText(cmd ...*cmds.Command) []string {
317378
}
318379
}
319380

320-
// add option names to output (with each name aligned)
321-
lines := make([]string, 0)
322-
j := 0
323-
for {
324-
done := true
325-
i := 0
326-
for _, opt := range options {
327-
if len(lines) < i+1 {
328-
lines = append(lines, "")
329-
}
330-
331-
names := sortByLength(opt.Names())
332-
if len(names) >= j+1 {
333-
lines[i] += optionFlag(names[j])
334-
}
335-
if len(names) > j+1 {
336-
lines[i] += ", "
337-
done = false
338-
}
339-
340-
i++
341-
}
342-
343-
if done {
344-
break
381+
// add option names to output
382+
lines := make([]string, len(options))
383+
for i, opt := range options {
384+
flags := sortByLength(opt.Names())
385+
for j, f := range flags {
386+
flags[j] = optionFlag(f)
345387
}
346-
347-
lines = align(lines)
348-
j++
388+
lines[i] = strings.Join(flags, ", ")
349389
}
350390
lines = align(lines)
351391

352392
// add option types to output
353393
for i, opt := range options {
354-
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
394+
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
355395
}
356396
lines = align(lines)
357397

358398
// add option descriptions to output
359399
for i, opt := range options {
360-
lines[i] += " - " + opt.Description()
400+
lines[i] += " - "
401+
lines[i] = appendWrapped(lines[i], opt.Description(), width)
361402
}
362403

363404
return lines
364405
}
365406

366-
func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
407+
func subcommandText(width int, cmd *cmds.Command, rootName string, path []string) []string {
367408
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
368409
if len(path) > 0 {
369410
prefix += " "
@@ -392,12 +433,24 @@ func subcommandText(cmd *cmds.Command, rootName string, path []string) []string
392433

393434
lines = align(lines)
394435
for i, sub := range subcmds {
395-
lines[i] += " - " + sub.Helptext.Tagline
436+
lines[i] += " - "
437+
lines[i] = appendWrapped(lines[i], sub.Helptext.Tagline, width)
396438
}
397439

398440
return lines
399441
}
400442

443+
func commandUsageText(width int, cmd *cmds.Command, rootName string, path []string) string {
444+
text := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
445+
argUsage := usageText(cmd)
446+
if len(argUsage) > 0 {
447+
text += " " + argUsage
448+
}
449+
text += " - "
450+
text = appendWrapped(text, cmd.Helptext.Tagline, width)
451+
return text
452+
}
453+
401454
func usageText(cmd *cmds.Command) string {
402455
s := ""
403456
for i, arg := range cmd.Arguments {

cli/helptext_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"strings"
55
"testing"
66

7-
"github.com/ipfs/go-ipfs-cmds"
7+
cmds "github.com/ipfs/go-ipfs-cmds"
88
)
99

1010
func TestSynopsisGenerator(t *testing.T) {
@@ -22,7 +22,8 @@ func TestSynopsisGenerator(t *testing.T) {
2222
},
2323
},
2424
}
25-
syn := generateSynopsis(command, "cmd")
25+
terminalWidth := 100
26+
syn := generateSynopsis(terminalWidth, command, "cmd")
2627
t.Logf("Synopsis is: %s", syn)
2728
if !strings.HasPrefix(syn, "cmd ") {
2829
t.Fatal("Synopsis should start with command name")

0 commit comments

Comments
 (0)