-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add auto create step in UI #10
Changes from 15 commits
dcbe0d3
537a008
7d4263d
334cc09
0c6b041
441e985
334f054
e5ec1af
463c67e
eeaf8ba
a3240dc
a1ad6c6
d4e2dd7
f1bdbca
ae35578
222c726
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package setup | ||
|
||
// A simple example that shows how to retrieve a value from a Bubble Tea | ||
// program after the Bubble Tea has exited. | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/charmbracelet/bubbles/key" | ||
"github.com/charmbracelet/bubbles/list" | ||
tea "github.com/charmbracelet/bubbletea" | ||
"github.com/charmbracelet/lipgloss" | ||
) | ||
|
||
var ( | ||
choiceStyle = lipgloss.NewStyle().PaddingLeft(4) | ||
selectedChoiceItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) | ||
|
||
_ list.Item = choice{} | ||
) | ||
|
||
type choice struct { | ||
Key string `json:"key"` | ||
Name string `json:"name"` | ||
} | ||
|
||
func (p choice) FilterValue() string { return "" } | ||
|
||
type autoCreateModel struct { | ||
choice string | ||
err error | ||
list list.Model | ||
} | ||
|
||
func NewAutoCreate() autoCreateModel { | ||
choices := []choice{ | ||
{ | ||
Key: "yes", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably only need the name here and the check later on could be for "Yes" instead of "yes". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Works for me |
||
Name: "Yes", | ||
}, | ||
{ | ||
Key: "no", | ||
Name: "No", | ||
}, | ||
} | ||
l := list.New(choicesToItems(choices), autoCreateDelegate{}, 85, 14) | ||
l.Title = "Do you want to get started with our recommended project, environment, and flag?" | ||
l.SetShowStatusBar(false) | ||
l.SetFilteringEnabled(false) | ||
|
||
return autoCreateModel{ | ||
list: l, | ||
} | ||
} | ||
|
||
func (m autoCreateModel) Init() tea.Cmd { | ||
return nil | ||
} | ||
|
||
func (m autoCreateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
var cmd tea.Cmd | ||
switch msg := msg.(type) { | ||
case tea.KeyMsg: | ||
switch { | ||
case key.Matches(msg, keys.Enter): | ||
i, ok := m.list.SelectedItem().(choice) | ||
if ok { | ||
m.choice = i.Key | ||
} | ||
case key.Matches(msg, keys.Quit): | ||
return m, tea.Quit | ||
default: | ||
m.list, cmd = m.list.Update(msg) | ||
} | ||
} | ||
|
||
return m, cmd | ||
} | ||
|
||
func (m autoCreateModel) View() string { | ||
return "\n" + m.list.View() | ||
} | ||
|
||
type autoCreateDelegate struct{} | ||
|
||
func (d autoCreateDelegate) Height() int { return 1 } | ||
func (d autoCreateDelegate) Spacing() int { return 0 } | ||
func (d autoCreateDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } | ||
func (d autoCreateDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { | ||
i, ok := listItem.(choice) | ||
if !ok { | ||
return | ||
} | ||
|
||
str := i.Name | ||
|
||
fn := choiceStyle.Render | ||
if index == m.Index() { | ||
fn = func(s ...string) string { | ||
return selectedChoiceItemStyle.Render("> " + strings.Join(s, " ")) | ||
} | ||
} | ||
|
||
fmt.Fprint(w, fn(str)) | ||
} | ||
|
||
func choicesToItems(choices []choice) []list.Item { | ||
items := make([]list.Item, len(choices)) | ||
for i, c := range choices { | ||
items[i] = list.Item(c) | ||
} | ||
|
||
return items | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ type sessionState int | |
|
||
// list of steps in the wizard | ||
const ( | ||
initialStep sessionState = iota | ||
autoCreateStep sessionState = iota | ||
projectsStep | ||
environmentsStep | ||
flagsStep | ||
|
@@ -21,27 +21,29 @@ const ( | |
// WizardModel is a high level container model that controls the nested models which each | ||
// represent a step in the setup wizard. | ||
type WizardModel struct { | ||
quitting bool | ||
err error | ||
currStep sessionState | ||
steps []tea.Model | ||
currProjectKey string | ||
currEnvironmentKey string | ||
currFlagKey string | ||
quitting bool | ||
err error | ||
currStep sessionState | ||
steps []tea.Model | ||
useRecommendedResources bool | ||
sunnyguduru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
currProjectKey string | ||
currEnvironmentKey string | ||
currFlagKey string | ||
} | ||
|
||
func NewWizardModel() tea.Model { | ||
steps := []tea.Model{ | ||
// Since there isn't a model for the initial step, the currStep value will always be one ahead of the step in | ||
// this slice. It may be convenient to add a model for the initial step to contain its own view logic and to | ||
// prevent this off-by-one issue. | ||
NewAutoCreate(), | ||
NewProject(), | ||
NewEnvironment(), | ||
Newflag(), | ||
NewFlag(), | ||
} | ||
|
||
return WizardModel{ | ||
currStep: initialStep, | ||
currStep: autoCreateStep, | ||
steps: steps, | ||
} | ||
} | ||
|
@@ -58,38 +60,51 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
switch { | ||
case key.Matches(msg, keys.Enter): | ||
switch m.currStep { | ||
case initialStep: | ||
projModel, _ := m.steps[m.currStep].Update(fetchProjects{}) | ||
// we need to cast this to get the data out of it, but maybe we can create our own interface with | ||
// common values such as Choice() and Err() so we don't have to cast | ||
p, ok := projModel.(projectModel) | ||
case autoCreateStep: | ||
model, _ := m.steps[autoCreateStep].Update(msg) | ||
p, ok := model.(autoCreateModel) | ||
if ok { | ||
if p.err != nil { | ||
m.err = p.err | ||
return m, nil | ||
m.useRecommendedResources = p.choice == "yes" | ||
if !m.useRecommendedResources { | ||
sunnyguduru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
projModel, _ := m.steps[projectsStep].Update(fetchProjects{}) | ||
// we need to cast this to get the data out of it, but maybe we can create our own interface with | ||
// common values such as Choice() and Err() so we don't have to cast | ||
p, ok := projModel.(projectModel) | ||
if ok { | ||
if p.err != nil { | ||
m.err = p.err | ||
return m, nil | ||
} | ||
} | ||
// update projModel with the fetched projects | ||
m.steps[projectsStep] = projModel | ||
// go to the next step | ||
m.currStep += 1 | ||
} else { | ||
// create project, environment, and flag | ||
// go to step after flagsStep | ||
m.currProjectKey = "setup-wizard-project" | ||
m.currEnvironmentKey = "test" | ||
m.currFlagKey = "setup-wizard-flag" | ||
m.currStep = flagsStep + 1 | ||
} | ||
} | ||
|
||
// update the nested model | ||
m.steps[m.currStep] = projModel | ||
// go to the next step | ||
m.currStep += 1 | ||
case projectsStep: | ||
projModel, _ := m.steps[m.currStep-1].Update(msg) | ||
projModel, _ := m.steps[projectsStep].Update(msg) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to just directly use |
||
p, ok := projModel.(projectModel) | ||
if ok { | ||
m.currProjectKey = p.choice | ||
m.currStep += 1 | ||
} | ||
case environmentsStep: | ||
envModel, _ := m.steps[m.currStep-1].Update(msg) | ||
envModel, _ := m.steps[environmentsStep].Update(msg) | ||
p, ok := envModel.(environmentModel) | ||
if ok { | ||
m.currEnvironmentKey = p.choice | ||
m.currStep += 1 | ||
} | ||
case flagsStep: | ||
model, _ := m.steps[m.currStep-1].Update(msg) | ||
model, _ := m.steps[flagsStep].Update(msg) | ||
f, ok := model.(flagModel) | ||
if ok { | ||
m.currFlagKey = f.choice | ||
|
@@ -100,15 +115,15 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
} | ||
case key.Matches(msg, keys.Back): | ||
// only go back if not on the first step | ||
if m.currStep > initialStep { | ||
if m.currStep > autoCreateStep { | ||
m.currStep -= 1 | ||
} | ||
case key.Matches(msg, keys.Quit): | ||
m.quitting = true | ||
return m, tea.Quit | ||
default: | ||
updatedModel, _ := m.steps[m.currStep-1].Update(msg) | ||
m.steps[m.currStep-1] = updatedModel | ||
updatedModel, _ := m.steps[m.currStep].Update(msg) | ||
m.steps[m.currStep] = updatedModel | ||
} | ||
} | ||
|
||
|
@@ -124,15 +139,11 @@ func (m WizardModel) View() string { | |
return fmt.Sprintf("ERROR: %s", m.err) | ||
} | ||
|
||
if m.currStep == initialStep { | ||
return "welcome" | ||
} | ||
|
||
if m.currStep > flagsStep { | ||
return fmt.Sprintf("envKey is %s, projKey is %s, flagKey is %s", m.currEnvironmentKey, m.currProjectKey, m.currFlagKey) | ||
} | ||
|
||
return fmt.Sprintf("\nstep %d of %d\n"+m.steps[m.currStep-1].View(), m.currStep, len(m.steps)) | ||
return fmt.Sprintf("\nstep %d of %d\n"+m.steps[m.currStep].View(), m.currStep+1, len(m.steps)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got a little confused here. But basically we're rendering the current step's view. While showing what step we're on in the header out of the total number of steps. |
||
} | ||
|
||
type keyMap struct { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rename the file
auto_create.go
to be more idiomatic?