Skip to content

Commit

Permalink
feat: migrate to huh for query engine (#129)
Browse files Browse the repository at this point in the history
* migrate to huh engine
* add new support for looped text input
* additional group of input features
  • Loading branch information
hay-kot committed May 13, 2024
1 parent cd5caae commit 38c4eed
Show file tree
Hide file tree
Showing 20 changed files with 1,237 additions and 211 deletions.
1 change: 1 addition & 0 deletions .examples/cli/scaffold.yaml
Expand Up @@ -19,6 +19,7 @@ questions:
prompt:
multi: true
message: "Colors of the project"
default: ["red", "green"]
options:
- "red"
- "green"
Expand Down
76 changes: 76 additions & 0 deletions .examples/prompts/scaffold.yaml
@@ -0,0 +1,76 @@
messages:
pre: |
# Example Prompts
This is an example of all the prompts available in the CLI.
post: |
# All Set
You're Done!
- Input: {{ .Scaffold.input }} (type={{ typeOf .Scaffold.input }})
- Looped: {{ .Scaffold.looped | join ", " }} (type={{ typeOf .Scaffold.looped }})
- Multiline: {{ .Scaffold.multiline }} (type={{ typeOf .Scaffold.multiline }})
- Single Select: {{ .Scaffold.singleselect }} (type={{ typeOf .Scaffold.singleselect }})
- Multi Select: {{ .Scaffold.multiselect | join ", " }} (type={{ typeOf .Scaffold.multiselect }})
- Confirm: {{ .Scaffold.confirm }} (type={{ typeOf .Scaffold.confirm }})
questions:
- name: "input"
group: inputs
prompt:
message: "Basic Input"
description: "This is a basic text input"
required: true
- name: "looped"
group: "inputs"
prompt:
message: "Looped Input"
description: "This is a looped input, submit empty value to stop looping"
loop: true
required: true
- name: "multiline"
group: "inputs"
prompt:
message: "Multiline Input"
multi: true
required: true
- name: "hidden"
group: "hidden"
when: '{{ eq .input "hidden" }}'
prompt:
message: "Hidden Input"
description: "This input group is shown conditionally"
- name: "singleselect"
group: selects
prompt:
message: "Single Select"
description: "This is a single select prompt"
default: ["Option 2"]
options:
- "Option 1"
- "Option 2"
- "Option 3"
- "Option 4"
required: true
- name: "multiselect"
group: selects
prompt:
message: "Multi Select"
description: "This is a multi select prompt"
multi: true
default: ["Option 4"]
options:
- "Option 1"
- "Option 2"
- "Option 3"
- "Option 4"
required: true
- name: "confirm"
group: confirms
prompt:
message: "Confirm"
description: "This is a confirm prompt"
confirm: true
required: true
Empty file.
39 changes: 17 additions & 22 deletions app/commands/prompts.go
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)

Expand All @@ -14,31 +14,26 @@ var (
)

func httpAuthPrompt() (username string, password string, err error) {
qs := []*survey.Question{
{
Name: "username",
Prompt: &survey.Input{Message: "Username:"},
Validate: survey.Required,
},
{
Name: "password",
Prompt: &survey.Password{
Message: "Password/Access Token:",
},
},
}

answers := struct {
Username string
Password string
}{}
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Username").
Description("Enter your username").
Value(&username),
huh.NewInput().
Title("Password").
Description("Enter your password (or token)").
Value(&password).
Password(true),
),
)

err = survey.Ask(qs, &answers)
err = form.Run()
if err != nil {
return "", "", fmt.Errorf("failed to parse http auth input: %w", err)
return "", "", err
}

return answers.Username, answers.Password, nil
return username, password, nil
}

func didYouMeanPrompt(given, suggestion string) bool {
Expand Down
87 changes: 87 additions & 0 deletions app/scaffold/askable.go
@@ -0,0 +1,87 @@
package scaffold

import (
"strings"

"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/hay-kot/scaffold/app/core/engine"
)

type Askable struct {
Name string
Key string
Hook func(vars engine.Vars) error
Field huh.Field
}

func NewAskable(name string, key string, field huh.Field, fn func(vars engine.Vars) error) *Askable {
return &Askable{
Name: name,
Key: key,
Field: field,
Hook: fn,
}
}

var (
bold = lipgloss.NewStyle().Bold(true).Render
light = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF83D5")).Render
base = lipgloss.NewStyle().Foreground(lipgloss.Color("#6C74FD")).Render
)

func (a *Askable) String() string {
bldr := strings.Builder{}

bldr.WriteString(" ")
bldr.WriteString(light("?"))
bldr.WriteString(" ")
bldr.WriteString(bold(a.Name))
bldr.WriteString(" ")

val := a.Field.GetValue()

switch v := val.(type) {
case string:
if v == "" {
return ""
}

if strings.Contains(v, "\n") {
bldr.WriteString(base("|"))

for _, line := range strings.Split(v, "\n") {
bldr.WriteString("\n")
bldr.WriteString(" ")
bldr.WriteString(base(line))
}

break
}

bldr.WriteString(base(v))
case []string:
if len(v) == 0 {
return ""
}

for _, v := range v {
bldr.WriteString("\n")
bldr.WriteString(" - ")
bldr.WriteString(base(v))
}
case bool:
if v {
bldr.WriteString(base("true"))
} else {
bldr.WriteString(base("false"))
}

default:
bldr.WriteString("unknown type, please report this issue to the scaffold maintainer.")
}

bldr.WriteString("\n")

return bldr.String()
}
9 changes: 0 additions & 9 deletions app/scaffold/merge.go
Expand Up @@ -11,12 +11,3 @@ func MergeMaps[T any](maps ...map[string]T) map[string]T {
}
return out
}

// CastMap casts a map[string]T to a man[string]any map.
func CastMap[T any](m map[string]T) map[string]any {
out := map[string]any{}
for k, v := range m {
out[k] = v
}
return out
}
109 changes: 77 additions & 32 deletions app/scaffold/project.go
Expand Up @@ -7,8 +7,7 @@ import (
"maps"
"strconv"

"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/core"
"github.com/charmbracelet/huh"
"github.com/hay-kot/scaffold/app/core/engine"
"github.com/hay-kot/scaffold/app/core/rwfs"
"github.com/rs/zerolog"
Expand Down Expand Up @@ -103,13 +102,15 @@ func (p *Project) AskQuestions(def map[string]any, e *engine.Engine) (map[string

switch ok {
case false:
msg := "Project name"
msg := "Project Name"
decs := "The name your project will be generated with"

pre := []Question{
{
Name: "Project",
Prompt: AnyPrompt{
Message: &msg,
Message: &msg,
Desciption: &decs,
},
Required: true,
},
Expand All @@ -128,49 +129,87 @@ func (p *Project) AskQuestions(def map[string]any, e *engine.Engine) (map[string

vars := maps.Clone(def)

for _, q := range p.Conf.Questions {
if q.When != "" {
result, err := e.TmplString(q.When, vars)
qgroups := QuestionGroupBy(p.Conf.Questions)
askables := []*Askable{}
patchvars := func() error {
for _, askable := range askables {
err := askable.Hook(vars)
if err != nil {
return nil, err
}

resultBool, _ := strconv.ParseBool(result)
if !resultBool {
continue
return err
}
}

question := q.ToSurveyQuestion(vars)
return nil
}

var form *huh.Form
formgroups := []*huh.Group{}

for _, qgroup := range qgroups {
fields := []huh.Field{}

for _, q := range qgroup {
question := q.ToAskable(vars[q.Name])
fields = append(fields, question.Field)
askables = append(askables, question)
}

err := survey.Ask([]*survey.Question{question}, &vars)
if err != nil {
return nil, err
group := huh.NewGroup(fields...)

firstq := qgroup[0]
if firstq.When != "" {
group.WithHideFunc(func() bool {
if form == nil {
return false
}

// extract existing properties
_ = patchvars()

first := qgroup[0]

// we check the first question in the group to see if it has a when
// and if so, we evaluate it and skip the group if it's false
if first.When != "" {
result, err := e.TmplString(first.When, vars)
if err != nil {
return true
}

resultBool, _ := strconv.ParseBool(result)
if !resultBool {
return true
}
}
return false
})
}

formgroups = append(formgroups, group)
}

form = huh.NewForm(formgroups...)

err := form.Run()
if err != nil {
return nil, err
}

// Ensure properts are set on vars
err = patchvars()
if err != nil {
return nil, err
}

// Grab the project name from the vars/answers to ensure that
// it's set.
if projectMode {
p.Name = vars["Project"].(string)
} else {
p.Name = "templates"
vars["Project"] = "templates"
}

// Unwrap core.OptionAnswer types into their values
for k, v := range vars {
switch vt := v.(type) {
case core.OptionAnswer:
vars[k] = vt.Value
case []core.OptionAnswer:
values := make([]string, len(vt))
for i, v := range vt {
values[i] = v.Value
}

vars[k] = values
}
}

if log.Logger.GetLevel() == zerolog.DebugLevel {
for k, v := range vars {
log.Debug().
Expand All @@ -181,5 +220,11 @@ func (p *Project) AskQuestions(def map[string]any, e *engine.Engine) (map[string
}
}

for _, askable := range askables {
fmt.Print(askable.String())
}

fmt.Print("\n")

return vars, nil
}

0 comments on commit 38c4eed

Please sign in to comment.