Please purchase the course to watch this video.

Full Course
Creating interactive command-line forms in Go significantly enhances user experience, especially for tasks like adding resources in a CMS. While traditional CLI commands with flags are efficient, integrating libraries such as huh
from charmbracelet empowers developers to build rich, interactive terminal UIs—including text inputs, select fields, multi-selects, and confirmation prompts—similar to web forms but within the terminal. huh
's fluent API allows for flexible form composition and dynamic features, such as prepopulated fields and option bindings, making data entry intuitive and adaptable. Implementing this approach provides a powerful alternative to static CLI workflows, offering users a choice between quick flag-driven commands and guided, form-based input, thus broadening the accessibility and usability of CLI applications.
No links available for this lesson.
If you've been keeping up with your homework, then you should have the ability to create a new resource of whatever resource your CMS is managing using CLI commands with flags. For example, in my case, if I go ahead and run my CLI CMS, I can call the posts create
or post add
command, either one works, passing in the title as follows. Let's go ahead and pass in "my new blog post".
Now when I go ahead and execute this command, you can see it opens up NeoVim, my text editor, with my title already populated and I can go ahead and actually start adding in this blog post. So let's say "this is my first" or "this is my next blog post about something interesting". Then when I go ahead and save this and execute this, it creates my new blog post and inserts it into the database for me.
The Need for Interactive Forms
Whilst this is my preferred approach when it comes to creating CLI applications in order to collect data, in some cases you might want to add a more interactive user experience, such as what we saw when it came to TUI applications in the last lesson.
To achieve this, we can make use of another library by the charm bracelet team called huh, which allows you to build simple, powerful, interactive forms and prompts for the terminal. This is similar to say building out forms for a web UI. However, in this case, we can actually build them out in our terminal and we can have many different fields that you would typically see, including:
- Text inputs
- Text field
- Select field, so we can select between a number of different options
- Multiple selects
- Confirmation box as well
So let's take a look at how the huh application works by creating a simple form to represent what I would be doing if I had an interactive mode inside of my CMS system. I'll set some homework to set up an interactive mode in your own after this lesson, but for the moment let's take a look at how huh works.
Installing huh
To do so, we first need to go ahead and add it into our code. And there is no install command on huh, so I'm just going to go ahead and copy this and we can do the go get command, passing it in as follows:
go get github.com/charmbracelet/huh
This will add all of the dependencies, including, as you can see, bubble tea, because it makes use of bubble tea under the hood.
Creating a Simple Text Input
Okay, with huh added to our project, we can now go about using it. The official or the correct approach is to generate a new form with a bunch of different form groups inside. However, before we do that, let's go ahead and take a more simple approach and just set up a simple text input.
To do so, we can go ahead and use the huh package and we can do a NewInput
or call the NewInput
function, which creates a new input field. The input field is a field that allows the user to enter text. We can also collect different forms of text, such as passwords, standard text, etc, etc. For the moment, let's just go ahead and use it.
The way that huh components work is that they use a fluent API, meaning that rather than passing in parameters, let's say, you instead access them through methods on the actual property, which will return the instance of the form itself so that you can keep composing method calls until you then run the actual component itself. If it's inside of a form, you just then run the form.
For the moment, let's go ahead and just run this input as follows, which you can see returns an error:
field := huh.NewInput()
err := field.Run()
if err != nil {
log.Fatal(err)
}
So let's go ahead and capture this error. Actually, we're going to go ahead and capture it just as a field and let's not do the single inline. Then we could just do log.Fatal
line error. Nice and simple.
Now, if I go ahead and run this, oh, I need to go ahead and change this to be a main package. Okay. Now, if I go ahead and run this, you can see we get a simple form field that prompts up, which we can go ahead and input. So let's go ahead and do "hello world". And once we press enter, the code just exits. So far, pretty simple.
Capturing Input Values
But how do we actually capture the input that we pass into this form. Well, in order to do so, we need to use the Value
method, which sets the value of the input field to the string that's provided, which not only sets the value of the input field, but as you can tell, it's a pointer to a string. So it will update the value that we pass in as well.
So let's go ahead and define a new field called title. Let's go ahead and say, or titleValue
. Let's call it. And we could just do var
string as follows:
var titleValue string
field := huh.NewInput().Value(&titleValue)
err := field.Run()
if err != nil {
log.Fatal(err)
}
fmt.Println("Title:", titleValue)
Now, if we go ahead and pass this in as a pointer and we can do a fmt.Println
and we'll do title and just go ahead and print out the title value. Now, if I go ahead and run this, you can see this time, let's go ahead and do "my new blog post". And you can see I've captured the title in "my new blog post" value, and it's been printed out as well.
However, you can also go ahead and actually set this value initially. So we could go ahead and do titleValue = "my new blog post"
. And if I go ahead and run this, you can see "my new blog post" is now already populated in the title field, which I can go ahead and change if I want to. "I want something else" and it should print it out as well. So far, pretty simple.
Adding Form Properties
As you can see, we're able to do quite a few things when it comes to our input field. And similar to say the web, we also have a number of other properties we can use with it. So for example, we could go ahead and set a character limit. We can set a description. So let's go ahead and say "the title of the blog post". And we can also set a placeholder as well, I believe:
field := huh.NewInput().
Value(&titleValue).
Title("Post Title").
Description("The title of the blog post").
Placeholder("Enter a title")
So yep, we can do Placeholder
. You can also do PlaceholderFunc
, which is means if you want to have a dynamic placeholder. So you can add bindings to any other properties or fields. And if those bindings change, then the placeholder will change as well. It's a very powerful feature when it comes to huh.
Let's go ahead and just set this to be a placeholder for the moment. So we're just going to go ahead and say "enter a title". And now let's go ahead and run this and see what happens. I think actually we can set a title as well. Yeah, we can also set a title. So we can go ahead and say "post title". And we need to add a dot afterwards as well.
Okay, now if I go ahead and run this, we can see we get a little bit more information. So we have a "post title", title, "the title of the blog post". And we also have a nice placeholder that we can enter as well. So let's go ahead and say "learning forms with huh". There we go. That would be the title of the blog post. Very cool.
As you can see, huh is very powerful. Whilst you can create very simple input fields, it also allows you to extend and add various different properties as well, including, of course, password. So you can go ahead and actually hide the user input in this case, which is very useful for password fields, as we saw in the last lesson. And it will be obscured. As you can see, "the user aborted". That was because I pressed Ctrl and C.
Creating Forms with Multiple Fields
Okay, so in addition to just specifying individual properties, we can also specify multiple properties as well. Let's go ahead and get rid of the run field. We can go ahead and define this as titleInput
for the moment. And let's go ahead and get rid of this.
Now let's go ahead and actually create a form which will have a group of components. To do so, we can create a new form using the huh.NewForm
property. I need to get rid of this. huh.NewForm
. And you'll see this takes a variadic array of groups.
A group is basically a logical grouping of different form elements. You can think of it as like a page on a form so that every group will represent a single page in your data collection. We're going to go ahead and just set one group for the moment, but you may want to organize into multiple groups when it comes to your own implementations:
titleInput := huh.NewInput().
Value(&titleValue).
Title("Post Title").
Description("The title of the blog post").
Placeholder("Enter a title")
form := huh.NewForm(
huh.NewGroup(titleInput),
)
err := form.Run()
if err != nil {
log.Fatal(err)
}
So let's go ahead and set the title input as follows. And we can then just go ahead and actually run this form. And we'll do the if error check, if error. Oh yeah, let's go ahead and actually capture the form here. Then we can do form.Run
. You also see that we have a RunWithContext
function as well, which is useful if you have a context that you want to pass in. For this example, I don't. So I'm going to go ahead and just use the Run
method.
Let's go ahead and capture the error and do a simple if error check. We'll just do the log.Fatal
line. I need to do a comma here as well. Okay, now if we go ahead and run this, it should work the exact same way that it did before, which it does. "Hello". Oh, I still have the, okay, that did, I spelled hello wrong. Let's go ahead and get rid of the password. But as you can see, it's working the same as it did before.
Adding a Select Field
However, now we can start to add in other components to this. Another component that I really like is the select field, which allows you to select between a number of different options. So let's go ahead and actually define a new select. And we could either do it inline here. So we could do huh.NewSelect
, which in some cases is actually preferable. I'm going to go ahead and define it above just to make it a little easier for us to see:
var author string
authorsInput := huh.NewSelect[string]().
Value(&author).
Options(
huh.NewOption("Dreams of Code", "dreams-of-code"),
huh.NewOption("Dreams of Autonomy", "dreams-of-autonomy"),
huh.NewOption("Dreams of Motion", "dreams-of-motion"),
)
So selectInput
. And we could actually go ahead and do category, I think. And we'll do huh.NewSelect
. You could do like authors or something. Authors is actually a good one. Let's go ahead and do authors. So we can select between a number of different authors.
Here, this is a generic type. So we need to pass in the type that we want to select from. So this could be an integer, it could be a struct, it could be any type you want. In this case, I'm going to go ahead and just set a string. But let's say you did have an author struct type, you could make use of this with the actual select itself, which is very, very powerful.
Okay, so we have NewSelect
, and we want to go ahead and set the value. Let's go ahead and define the value even. So the value in this case is going to be our author. And we could just do var author
and we do string. Go ahead and put this up here. Then we can pass this in as a pointer. So &author
.
And then we need to specify the options. Again, here you can see we have Options
and OptionsFunc
. Both of these are actually methods. The OptionsFunc
, again, allows us to have bindings. So if we want to dynamically generate the options based on what a previous or a prior field is set to, then you can make use of the OptionsFunc
. It's one of the more powerful features when it comes to huh.
I actually make use of this a lot. So in my lesson uploads, I use the huh form in order to do this. Course is the first selection, and then I generate the list of modules as options based on what the course you selected is, which allows me to have multiple course uploads.
In any case, let's just take a look at the options here. Okay, so we need to pass in huh.NewOptions
. So let's go ahead and generate some new options. We can go ahead and do, we could either do NewOptions
returns new options from a list of values, or we can just do key value here. Let's go ahead and do key value. We can do huh.NewOption
. We can do "dreams of code" as one of the authors, and the value here will be "dreams of code". Let's say you could also say it was like an ID value. So if you had a UUID, I don't know how to do a UUID off the top of my head, but let's say you had a UUID as a value, but the key would be "dreams of code". It would display this key, and then the value itself will be "dreams of code", let's say.
We could also go ahead and do another option. So huh.NewOption
. Let's say "dreams of autonomy". That is a second channel that I own. And let's go ahead and do another NewOption
of "dreams of motion", because if I ever do a graphics motion video, of course, we'll do it on there.
Okay, so far so good. We now have the authors select being defined. Now we can go ahead and actually pass it in to our new group:
form := huh.NewForm(
huh.NewGroup(titleInput, authorsInput),
)
So let's see what this looks like. If we go ahead and run it, we can see we have a title. So "learning the Go standard library", and we can select between the different authors that we want. We can also go ahead and actually search for them, although I don't think this is actually working. It's not very fuzzy. That's one thing I think is a bit of a shame, but you can also select all of these options. It's a bit hard to see because they all start with the same name. But you can see now I have "dreams of code" and "dreams of autonomy" and "dreams of motion".
So you can also search through these and you can escape to get rid of that search. But then if you go ahead and select one, we're not actually printing anything out. Let's go ahead and actually make sure we do print this out. So we can do title, title, author, and we'll do the author value as well:
fmt.Println("Title:", titleValue)
fmt.Println("Author:", author)
Okay, now I want to go ahead and run this. You can see I can enter in a title. So "hello world", and we can go ahead and do this on "dreams of motion". As you can see, I now have the property of "hello world" and the author of "dreams of motion".
Available Field Types
As you can see, huh is really powerful when it comes to being able to create simple form inputs. And there are a number of different fields you can actually select from:
- Input, which we've seen already, which is a single line text field
- Text, which is a multi-line text input, although I prefer to use a text editor, but it's really powerful
- Select, which is being able to select an option from a list, and then a multi-select, which means you can select multiple options from a list, very similar to what we've seen before
- And then there's the confirmation, which will allow you to determine whether or not something should be confirmed
Adding a Confirmation Field
Let's take a look at this before I then go ahead and set some homework to add this into your own application. So to do so, let's go ahead and actually just copy this huh.NewConfirm
as follows. And we can go ahead and just define a new confirm or confirmation. We'll do confirmField
actually. Let's go ahead and do that:
var confirm bool
confirmField := huh.NewConfirm().
Title("Create blog post?").
Value(&confirm)
Is huh.NewConfirm
. "Are you sure". Yes or no. We can do it, say like "create blog post". And you could do yes, no. And then we'll go ahead and actually define a confirmation field. So var confirm
. I'll set this be a boolean.
Okay, so we have our new confirm defined. Let's go ahead and actually do confirmField
. I always like to do this kind of separate. So after or inside of a new group, and then you can kind of print out the information to the user. So let's go ahead and actually do that:
err = confirmField.Run()
if err != nil {
log.Fatal(err)
}
if !confirm {
fmt.Println("Post not being created. Goodbye.")
return
}
fmt.Println("Post was created!")
So we can just go ahead and do confirmField
. Let's see. And we'll do run if error. We'll do a simple error check as always. And this is important to do the error check when it comes to the confirmation field, because if a user presses control on C, you want to make sure that you don't proceed. That's something I've definitely learned when it comes to using huh in my own tooling.
Okay, so now that we've got the confirm field run, we can go ahead and do if !confirm
. And we'll just go ahead and do a fmt.Println
"post not being created. Goodbye." And otherwise, we'll just go ahead and do "post was created". Yeah.
Okay. Now if we go ahead and run this, we can see let's go ahead and enter in the title of our post. I'm going to do "my test post". And we'll just set this to be "dreams of code". As you can see now, we're printing out title, "my test post" and the author "dreams of code". Do we want to create it. Let's go. No, you can see "post is not created". I have a bug here. We need to return early. Let's try that again. And we'll do "my second test post". Well, it's always good to do a test one, right. "Second test post". Let's do "dreams of motion" this time. Create it. No "post not created. Goodbye."
As you can see, being able to use huh allows you to specify very interactive forms when it comes to data collection. And whilst it's not my own personal favorite type of interface when it comes to collecting data on the CLI, it's still really useful to be able to provide that option when you release your form or when you release your CLI to other users.
Homework Assignment
Therefore, as homework, I'm going to set a task so that you can add the huh package into your current CMS application. But you should toggle it using, say, the --interactive
flag. So you'll want to add kind of a global flag into your application in order to be able to specify whether or not interactive mode is enabled.
So in my case, it would be, say, like:
cli-cms posts create --interactive
And then this would use the huh form experience rather than using the CLI experience. Additionally, if I go ahead and pass and say the title, so:
cli-cms posts create --interactive --title "my new post"
Then this should already be populated when I open up the CMS form.
As always, you should be able to see an example of this code in my own project and you can take inspiration from it. But again, I want you to think about how you can implement the huh form when it comes to your own individual project. But this is how I would do it.
So upon executing this, I would create basically this form would pop up, but the title post would already have "my new post" in it. I don't have author in mine because I am the author. But yeah, something similar to this is how I would do it when it comes to your own code.
Once that's done, you're ready to move on to the next lesson, where we're going to take a look at another package provided by Charm Bracelet, this one called Skate, which acts as a more key value store, allowing you to have a really simple storage-based solution which can also sync to the cloud.
No homework tasks for this lesson.