Fix help and mark, ready for 1.0

This commit is contained in:
rwxrob 2022-04-06 07:29:29 -04:00
parent 40b96f4174
commit e1eceeee5f
No known key found for this signature in database
GPG Key ID: 2B9111F33082AE77
7 changed files with 127 additions and 67 deletions

View File

@ -337,6 +337,11 @@ want the specific reasons.
Every Bonzai binary is its own local web server which Go's rich
standard library to draw on.
* **[]Section instead of map[string]string for Other**
This allows composition notation and ensures the author has control of
how the Other sections appear.
## Style Guidelines
* Everything through `go fmt` or equiv, no exceptions

View File

@ -17,7 +17,7 @@ type Command interface {
GetCommands() []string
GetHidden() []string
GetParams() []string
GetOther() map[string]string
GetOther() []string
GetCompleter() Completer
GetCaller() Command
}

View File

@ -4,10 +4,14 @@
package help
import (
"fmt"
"log"
"strings"
"github.com/rwxrob/bonzai/comp"
Z "github.com/rwxrob/bonzai/z"
"github.com/rwxrob/fn/filt"
"github.com/rwxrob/fn/maps"
"github.com/rwxrob/to"
)
// Cmd provides help documentation for the caller allowing the specific
@ -33,7 +37,7 @@ var Cmd = &Z.Cmd{
if len(args) == 0 {
args = append(args, "all")
}
// ForTerminal(x.Caller, args[0])
ForTerminal(x.Caller, args[0])
return nil
},
}
@ -54,7 +58,7 @@ func helpCompleter(x comp.Command, args ...string) []string {
if caller != nil {
other := caller.GetOther()
if other != nil {
list = append(list, maps.Keys(other)...)
list = append(list, other...)
}
}
@ -65,8 +69,6 @@ func helpCompleter(x comp.Command, args ...string) []string {
return filt.HasPrefix(list, args[0])
}
/*
// printIfHave takes any thing with a named field and a value, converts
// everything to string values (with to.String) and prints it with
// Print after passing it through Format. If the value is an empty
@ -76,7 +78,7 @@ func printIfHave(thing, name, value any) {
log.Printf("%v has no %v\n", to.String(thing), to.String(name))
return
}
fmt.Print(Format(to.String(value)))
Z.PrintMark(to.String(value))
}
// ForTerminal converts the collective help documentation of the given
@ -88,83 +90,110 @@ func printIfHave(thing, name, value any) {
// terminal is not interactive (see Z.Emph).
func ForTerminal(x *Z.Cmd, section string) {
switch section {
case "name":
printIfHave("command", "name", x.Name)
case "title":
printIfHave(x.Name, "title", x.Title())
case "summary":
printIfHave(x.Name, "summary", x.Summary)
case "params":
printIfHave(x.Name, "params", x.UsageParams())
case "commands":
printIfHave(x.Name, "commands", x.UsageCmdTitles())
case "description", "desc":
printIfHave(x.Name, "description", Z.Mark(x.Description))
case "examples":
log.Printf("examples are planned but not yet implemented")
case "legal":
printIfHave(x.Name, "legal", x.Legal())
case "copyright":
printIfHave(x.Name, "copyright", x.Copyright)
case "license":
printIfHave(x.Name, "license", x.License)
case "version":
printIfHave(x.Name, "version", x.Version)
case "all":
Z.Println("**NAME**")
Z.PrintlnInWrap(x.Title() + "\n")
Z.PrintEmph("**NAME**\n")
Z.PrintMark(x.Title() + "\n\n")
// always print a synopsis so we can communicate with command
// developers about invalid field combinations through ERRORs
Z.Println("**SYNOPSIS**")
Z.PrintEmph("**SYNOPSIS**\n")
switch {
case x.Usage != "":
Z.PrintMarkf("%v %v", x.Name, x.Usage)
case x.Call == nil && x.Params != nil:
// FIXME: replace with string var from lang.go
Z.PrintlnInWrap(
"{ERROR: Params without Call: "+
strings.Join(x.Params, ", ")+"}" + "\n"
Z.PrintMarkf(
"{ERROR: Params without Call: %v}\n\n",
strings.Join(x.Params, ", "),
)
case len(x.Commands) == 0 && x.Call == nil:
// FIXME: replace with string var from lang.go
Z.PrintlnInWrap("{ERROR: neither Call nor Commands defined}")+"\n"
Z.PrintMark("{ERROR: neither Call nor Commands defined}")
case len(x.Commands) > 0 && x.Call == nil:
Z.PrintMarkf("%v COMMAND", x.Name)
case len(x.Commands) > 0 && x.Call != nil && len(x.Params) > 0:
Z.PrintfInWrap("**%v** (COMMAND|%v)",x.Name,x.UsageParams())
Z.PrintMarkf("%v (COMMAND|%v)", x.Name, x.UsageParams())
case len(x.Commands) == 0 && x.Call != nil && len(x.Params) > 0:
Z.PrintfInWrap( "**%v** %v", x.Name,x.UsageParams())
Z.PrintMarkf("%v %v", x.Name, x.UsageParams())
case len(x.Commands) == 0 && x.Call != nil:
fmt.Println(to.IndentWrapped(Z.Emphasize("**"+x.Name+"**"), 7, 80))
Z.PrintMarkf(`%v`, x.Name)
default:
Z.PrintMark("{ERROR: unknown synopsis combination}")
}
fmt.Println()
if len(x.Commands) > 0 {
fmt.Println(Z.Format("**COMMANDS**"))
fmt.Println(to.IndentWrapped(x.UsageCmdTitles(), 7, 80) + "\n")
Z.PrintEmph("**COMMANDS**\n")
Z.PrintIndent(x.UsageCmdTitles())
fmt.Println()
}
if len(x.Description) > 0 {
fmt.Println(Z.Format("**DESCRIPTION**"))
body := strings.TrimSpace(to.Dedented(x.Description))
fmt.Println(to.IndentWrapped(Z.Format(body), 7, 80) + "\n")
Z.PrintEmph("**DESCRIPTION**\n")
Z.PrintMark(x.Description)
}
legal := x.Legal()
if len(legal) > 0 {
fmt.Println(Z.Format("**LEGAL**"))
fmt.Println(to.IndentWrapped(legal, 7, 80) + "\n")
Z.PrintEmph("**LEGAL**\n")
Z.PrintIndent(legal)
fmt.Println()
}
if len(x.Other) > 0 {
for section, text := range x.Other {
fmt.Println(Z.Format("**" + strings.ToUpper(section) + "**"))
fmt.Println(to.IndentWrapped(text, 7, 80) + "\n")
for _, s := range x.Other {
Z.PrintEmphf("**%v**\n", strings.ToUpper(s.Title))
Z.PrintMark(s.Body)
}
}
default:
v, has := x.Other[section]
if !has {
printIfHave(x.Name, section, "")
for _, s := range x.Other {
if s.Title == section {
Z.PrintMark(s.Body)
}
}
printIfHave(x.Name, section, Z.Format(v))
}
}
*/

View File

@ -29,7 +29,7 @@ func ExampleCmd_summary() {
func ExampleCmd_other() {
term.AttrOff()
x := &Z.Cmd{
Other: map[string]string{"foo": `some foo text`},
Other: []Z.Section{{"foo", `some foo text`}},
}
help.ForTerminal(x, "foo")
// Output:
@ -49,6 +49,7 @@ func ExampleCmd_all_Error_No_Call_No_Commands() {
//
// SYNOPSIS
// {ERROR: neither Call nor Commands defined}
}
func ExampleCmd_all_Error_Params_No_Call() {
@ -128,7 +129,7 @@ func ExampleCmd_all_Call_With_Optional_Params_and_Commands() {
//
// COMMANDS
// f|foo - foo the things
// bar - bar the things
// bar - bar the things
// nosum
}
@ -198,9 +199,9 @@ func ExampleCmd_all_Other() {
x := &Z.Cmd{
Name: `cmd`,
Call: func(_ *Z.Cmd, _ ...string) error { return nil },
Other: map[string]string{
"foo": "A whole section dedicated to foo.",
"bar": "WTF is a bar anyway",
Other: []Z.Section{
{"FOO", "A whole section dedicated to foo."},
{"bar", "WTF is a bar anyway"},
},
}
help.ForTerminal(x, "all")

View File

@ -19,22 +19,21 @@ import (
)
type Cmd struct {
Name string `json:"name,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Summary string `json:"summary,omitempty"`
Usage string `json:"usage,omitempty"`
Version string `json:"version,omitempty"`
Copyright string `json:"copyright,omitempty"`
License string `json:"license,omitempty"`
Description string `json:"description,omitempty"`
Site string `json:"site,omitempty"`
Source string `json:"source,omitempty"`
Issues string `json:"issues,omitempty"`
Commands []*Cmd `json:"commands,omitempty"`
Params []string `json:"params,omitempty"`
Hidden []string `json:"hidden,omitempty"`
Other map[string]string `json:"other,omitempty"`
Name string `json:"name,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Summary string `json:"summary,omitempty"`
Usage string `json:"usage,omitempty"`
Version string `json:"version,omitempty"`
Copyright string `json:"copyright,omitempty"`
License string `json:"license,omitempty"`
Description string `json:"description,omitempty"`
Site string `json:"site,omitempty"`
Source string `json:"source,omitempty"`
Issues string `json:"issues,omitempty"`
Commands []*Cmd `json:"commands,omitempty"`
Params []string `json:"params,omitempty"`
Hidden []string `json:"hidden,omitempty"`
Other []Section `json:"other,omitempty"`
Completer comp.Completer `json:"-"`
Conf conf.Configurer `json:"-"`
@ -47,7 +46,16 @@ type Cmd struct {
MinParm int `json:"-"` // minimum number of params required
MaxParm int `json:"-"` // maximum number of params required
_aliases map[string]*Cmd
_aliases map[string]*Cmd // see cacheAliases called from Run
_sections map[string]string // see cacheSections called from Run
}
// Section contains the Other sections of a command. Composition
// notation (without Title and Body) is not only supported but
// encouraged for clarity when reading the source for documentation.
type Section struct {
Title string
Body string
}
// Names returns the Name and any Aliases grouped such that the Name is
@ -86,8 +94,6 @@ func (x *Cmd) Title() string {
return "{ERROR: Name is empty}"
}
switch {
case len(x.Summary) > 0 && len(x.Version) > 0:
return x.Name + " (" + x.Version + ")" + " - " + x.Summary
case len(x.Summary) > 0:
return x.Name + " - " + x.Summary
default:
@ -118,6 +124,9 @@ func (x *Cmd) Legal() string {
}
}
// OtherTitles returns just the ordered titles from Other.
func (x *Cmd) OtherTitles() []string { return maps.Keys(x._sections) }
func (x *Cmd) cacheAliases() {
x._aliases = map[string]*Cmd{}
if x.Commands == nil {
@ -133,6 +142,16 @@ func (x *Cmd) cacheAliases() {
}
}
func (x *Cmd) cacheSections() {
x._sections = map[string]string{}
if len(x.Other) == 0 {
return
}
for _, s := range x.Other {
x._sections[s.Title] = s.Body
}
}
// Run is for running a command within a specific runtime (shell) and
// performs completion if completion context is detected. Otherwise, it
// executes the leaf Cmd returned from Seek calling its Method, and then
@ -146,6 +165,7 @@ func (x *Cmd) cacheAliases() {
func (x *Cmd) Run() {
x.cacheAliases()
x.cacheSections()
// resolve Z.Aliases (if completion didn't replace them)
if len(os.Args) > 1 {
@ -389,7 +409,7 @@ func (x *Cmd) GetHidden() []string { return x.Hidden }
func (x *Cmd) GetParams() []string { return x.Params }
// GetOther fulfills the comp.Command interface.
func (x *Cmd) GetOther() map[string]string { return x.Other }
func (x *Cmd) GetOther() []string { return x.OtherTitles() }
// GetCompleter fulfills the Command interface.
func (x *Cmd) GetCompleter() comp.Completer { return x.Completer }

View File

@ -17,9 +17,11 @@ import (
var IndentBy = 7
// Columns is the number of bytes (not runes) at which Wrap will wrap.
// Default is 80. Bonzai command tree creator can change this for every
// composite command imported their application in this one place.
var Columns = 80
// By default detects the terminal width (if possible) otherwise keeps
// 80 standard (see rwxrob/term.WinSize). Bonzai command tree creator
// can change this for every composite command imported their
// application in this one place.
var Columns = int(term.WinSize.Col)
// Lines returns the string converted into a slice of lines.
func Lines(in string) []string { return to.Lines(in) }
@ -145,6 +147,7 @@ MAIN:
for s.Scan() {
if s.Peek("\n\n") {
block = append(block, []byte(string(s.Rune))...)
blocks = append(blocks, &Block{Paragraph, block})
s.Scan()
s.Scan()
@ -294,11 +297,11 @@ func Mark(in string) string {
for _, block := range blocks {
switch block.T {
case Paragraph:
out += InWrap(Emph(string(block.V))) + "\n"
out += Emph(InWrap(string(block.V))) + "\n"
case Bulleted:
out += Indent(Emph(string(block.V))) + "\n"
out += Emph(Indent(string(block.V))) + "\n"
case Numbered:
out += Indent(Emph(string(block.V))) + "\n"
out += Emph(Indent(string(block.V))) + "\n"
case Verbatim:
out += to.Indented(Indent(string(block.V)), 4) + "\n"
default:

View File

@ -66,16 +66,18 @@ func ExampleBlocks_paragraph() {
in := `
Simple paragraph
here on multiple
lines
lines.
And another one here
with just a bit more.
`
fmt.Printf("%q\n", Z.Blocks(in)[0])
fmt.Printf("%q\n", Z.Blocks(in)[1])
// Output:
// "Simple paragraph here on multiple lines."
// "And another one here with just a bit more."
}