6.7 KiB
draft | title | aliases | series | date | author | cover | keywords | summary | showFullContent | tags | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
false | Putting Lipgloss on a Snake: Prettier Help Output for Cobra |
|
2023-05-08 | Nick Dumas | Using lipgloss to abstract away the specifics of nice terminal output. | false |
|
What am I Doing?
Cobra is a library for building command line applications in Go. It's pretty fully featured with sub-commands, automatic completion generators for zsh and bash, and more.
I'm building a helper application to work with my blog publishing pipeline and to keep things modular I'm trying to break it down into smaller utilities.
Why does it work?
Cobra is a well established library and feature-rich. It's got a great helper utility (cobra-cli
) to set up a skeleton you can use to jump in.
One thing that Cobra automatically handles in particular is help output. Veering away from the tradition of docopt
, you build a series of objects, set callbacks and configuration values, and Cobra works backwards from that to generate a nice help output listing flags and all the usual stuff.
Cobra lets you override a few members on the Command
structs to further customize the outputs: Short
and Long
.
Readable Line Length
What tripped me up as I prototyped the first few commands was the fact that Cobra will simply print whatever strings to provide to Short
and Long
verbatim.
I don't appreciate this because most of my displays are pretty wide. Paragraphs get smeared across the entire screen and become really challenging to read.
What are the options?
The naïve solution is to just manually terminate my lines at 80 or 120 or some other count. This is "simple" and portable, but extremely tedious.
The other option, as always, is "delegate". I know that there's a few different toolkits out there for terminal interaction, but this time I knew what I wanted to use.
Charming serpents...
charm is a constellation of golang libraries for building terminal applications. My target was lipgloss which handles the bulk of aligning, positioning, and styling terminal output for the rest of Charm's suite.
lipgloss is nice to work with, it has a pretty simple API that seems to mostly stay out of your way. Cobra, on the other hand, is an extremely opinionated library with lots of (mostly optional) wiring.
The lipgloss documentation has plenty of good examples so for brevity I'm going to jump right into where things got interesting.
is easier than taming them
The parts of Cobra we care about here are related to the help and usage outputs. These are handled publicly by Command.Set{Help,Usage}Func
, and privately by a handful of unexported functions that take *cobra.Command
and shove that into a template.
Setting our helpers
Telling Cobra to use our custom Usage and Help functions is pretty straightforward:
func init() {
rootCmd.SetUsageFunc(demo.CharmUsage)
rootCmd.SetHelpFunc(demo.CharmHelp)
}
One helpful feature of Cobra is that child commands inherit properties from their parent, including Usage and Help funcs. This means you only have to set this up once on your root command, and your entire application will be able to leverage this.
Help and Usage
Below, we have the definitions for each function. As you can see, I've managed to cleverly abstract away the hard work and real knowledge by yet another layer; each simply calls out to tmpl()
and pretty()
, both of which will be explained further.
Because tmpl()
is unexported, I had to dig into the Cobra source and copy it out, but that's coming up. For now, it's enough to say that it takes a writer, a template string, and a cobra.Command
and executes the template.
The only particularly clever part of this code is leveraging UsageTemplate()
and HelpTemplate()
. My original implementation copied those templates verbatim as well. If all you need to do is wrap the standard output, you can get the built-in template this way.
package demo
import (
"bytes"
"fmt"
"io"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
func CharmUsage(c *cobra.Command) error {
var b bytes.Buffer
err := tmpl(&b, c.UsageTemplate(), c)
if err != nil {
c.PrintErrln(err)
return err
}
pretty(c.ErrOrStderr(), b.String())
return nil
}
func CharmHelp(c *cobra.Command, a []string) {
var b bytes.Buffer
// The help should be sent to stdout
// See https://github.com/spf13/cobra/issues/1002
err := tmpl(&b, c.HelpTemplate(), c)
if err != nil {
c.PrintErrln(err)
}
pretty(c.ErrOrStderr(), b.String())
}
pretty()
Below we'll find the implementation of pretty()
, a very straightforward function. Take a string and write out the Render()
'd version.
var BaseStyle = lipgloss.NewStyle().Bold(true).BorderStyle(lipgloss.RoundedBorder()).Width(60).PaddingLeft(1).PaddingRight(1).PaddingBottom(2)
func pretty(w io.Writer, s string) {
fmt.Fprintf(w, "%s\n", BaseStyle.Render(s))
}
tmpl() and friends
cobra implements a set of template helper functions, and tmpl(w io.Writer, text string, data interface{}) error
which simply executes a template against a writer.
package demo
import (
"fmt"
"io"
"reflect"
"strconv"
"strings"
"text/template"
"unicode"
)
var templateFuncs = template.FuncMap{
"trim": strings.TrimSpace,
"trimRightSpace": trimRightSpace,
"trimTrailingWhitespaces": trimRightSpace,
"appendIfNotPresent": appendIfNotPresent,
"rpad": rpad,
"gt": Gt,
"eq": Eq,
}
// i'm eliding the bodies of these functions for brevity
func Gt(a interface{}, b interface{}) bool {}
func Eq(a interface{}, b interface{}) bool {}
func trimRightSpace(s string) string {}
func appendIfNotPresent(s, stringToAppend string) string {}
func rpad(s string, padding int) string {}
// tmpl executes the given template text on data, writing the result to w.
func tmpl(w io.Writer, text string, data interface{}) error {
t := template.New("top")
t.Funcs(templateFuncs)
template.Must(t.Parse(text))
return t.Execute(w, data)
}
How does it look?
Not bad:
You can find the full code here.
Success Story???
I'm not sure if this is even particularly useful yet. There's edge cases where adding a border causes things to break, and probably more. I'm pretty satisfied with learning more about how cobra is wired up.