A Go Library of bubbletea models leveraging the reflect package to expose structs
as forms and menus directly to CLI users, allowing them to edit fields with primitive types.
I built gostructui just as soon as I realized that I needed an easy, all-in-one method to ask for
user form input through a CLI. We shouldn't always have to ask CLI users for one thing at a time.
I personally like to expose config structs to CLI users so that they can set those values easily
through the CLI, and then I save the result.
Right now, the only user-editable fields are:
- Strings
- Integers
- Booleans
The repo contains an example of how to use the package within ./example/default/main.go. Let's walk through it!
go get github.com/bntrtm/gostructuiimport "github.com/bntrtm/gostructui/menu"In this struct, we build out a list of potential fields on a theoretical job application to illustrate the idea.
What To Know:
- The
smnametag establishes the title and formatting of the field. If the tag is not present, the menu will fall back to the default name of the struct field itself. For example, you'll see in the above demonstration that theEmailfield renders as we would expect despite the lack of thesmnametag. - The
smdestag renders an optional description when the user hovers their cursor over the field. - We'll discuss the
BlacklistedFieldbit in a minute. It will illustrate another feature!
// applicationForm holds fields typical of a job application.
type applicationForm struct {
FirstName string `smname:"First Name"`
LastName string `smname:"Last Name"`
Email string
PhoneNo int `smname:"Phone"`
Country string `smname:"Country"`
Location string `smname:"Location (City)"`
CanTravel bool `smname:"Travel" smdes:"Can you travel for work?"`
BlacklistedField string
}There are a number of custom options we could apply to our menu, such as changing the ibeam cursor rendered during string input, or what the field cursor might look like.
Here, we write a custom header to render during form interaction. Before setting any values on Because we're using custom options, we will have to initialize them before setting any of the values on them. Never forget to do this! Zero values for menu options are NOT the defaults.
customMenuOptions := menu.NewMenuOptions()
customMenuOptions.SetHeader("Apply for this job: ")Of course, you'll need a struct to expose to your CLI users! Here, we simply declare an empty one, but don't worry: if you need to provide a struct with non-zero values, you can also do that! The bubbletea model will keep those values intact, showing them to users as existing values within the field.
newApplication := applicationForm{}Provide a pointer to your struct, a list of fields used as a whitelist or blacklist, and any custom options.
Hey, there's our BlacklistedField option we set earlier! See how our
argument passed to the asBlacklist parameter is set to true? It means that any fields
with the names given within the string slice to the left will be hidden from users. You can
see it in the demo above; the field doesn't show up!
configEditMenu, err := gostructui.InitialTModelStructMenu(&newApplication, []string{"BlacklistedField"}, true, customMenuOptions)
if err != nil {
log.Fatal("Trouble generating the application.")
}The menu is a bubbletea model! That is, it implements the bubbletea package! We're now ready to run it through bubbletea and expose the menu to users to capture their input! The result is the demo you saw above.
p := tea.NewProgram(configEditMenu)
if entry, err := p.Run(); err != nil {
log.Fatal("Trouble generating the application.")
} else {
if entry.(gostructui.TModelStructMenu).QuitWithCancel {
fmt.Printf("Canceled application.\n")
os.Exit(0)
} else {
err = entry.(gostructui.TModelStructMenu).ParseStruct(&newApplication)
if err != nil {
log.Fatal("Trouble generating the application.")
}
// newApplication: "Wow, I feel like a new struct!"
}
if newApplication.FirstName == "" {
log.Fatal("ERROR: Missing First Name field!")
}
fmt.Printf("Thank you for applying, %s!\n", newApplication.FirstName)
time.Sleep(time.Second * 5)
os.Exit(0)
}You have now captured user input for one or more fields using the gostructui package!
Do what you need with these new values. In the demo, our program
prints the name of the applicant after applying.
Great! We can declare the shape of user input using a struct. But what if I'm a sucker for memory performance? If we must declare the fields of an input struct in the order by which we want them displayed to users, we may face a tradeoff in the form of a suboptimal memory layout on that struct.
See, for example, our applicationForm struct from earlier. Imagine if we
wanted to display another bool-type option, ConsentToSMS, after the PhoneNo
field, but before the Country field. Because struct fields in Go sit in
memory within a contiguous block, we end up with needless padding in two places,
generated by Go for the sake of making each field easily addressable by the CPU:
// assume we're on a 64-bit system for this example
type applicationForm struct {
// ...
PhoneNo int
ConsentToSMS bool // 1 byte
// [ PADDING ] // 7 bytes
Country string
// ...
CanTravel bool // 1 byte
// [ PADDING ] // 7 bytes
BlacklistedField string
}Go is adding 7 bytes of padding after each boolean-type struct field by design, but we know that were we to pair those boolean fields together (preferably at the end of the struct), our memory layout would be far more optimal: Go would be adding 6 bytes of padding, rather than 14! Yet, because of our selfish desire for a more sensible user form that asks for a user's consent to receive SMS text messages after inquiring about their phone number, we end up wasting a total of 8 precious bytes!
Is there anyone who could help us?
Enter the idx tag! This tag allows us to declare our struct fields in whatever
more memory-performant way we desire, while telling gostructui's bubbletea
model what order we actually want to display them in.
// applicationForm holds fields typical of a job application.
type applicationForm struct {
FirstName string `idx:"0"`
LastName string `idx:"1"`
Email string `idx:"2"`
Country string `idx:"5"`
Location string `idx:"6"`
BlacklistedField string `idx:"8"`
PhoneNo int `idx:"3"`
ConsentToSMS bool `idx:"4"`
CanTravel bool `idx:"7"`
}Sure, the order of display may become a bit less readable at a glance by us developers within the source code, but we'll have to accept some tradeoff in the end, right? It's just nice to be able to choose which tradeoff we're willing to accept.
The rules to using the idx tag are strict, but simple:
- All or Nothing: if you use it on one field, use it on all fields.
- Start at 0: the values must start at 0
- Keep sequence: the values must not break sequence.
- No values may be skipped.
- No two values may be the same.
To observe the idx tag in action, run the example at ./example/withIDX/main.go!
You'll find that it has the same behavior as seen in the default example,
despite the reordered fields under the applicationForm struct.
This is the power of the idx tag!
You can also see a demonstration of just the behavior itself by running the
subtest under idx_test.go dedicated to that using the following command in
your terminal:
go test -run TestIDXMemoryLayout -v