Skip to content

Commit 2ca4d6f

Browse files
authored
Merge pull request #2931 from weaveworks/2870-replace-text-inputs-with-checkboxes-in-bootstrap-wizard-ui
Replace text inputs with checkboxes in the bootstrap wizard UI
2 parents 3b1059d + b6e6ca0 commit 2ca4d6f

File tree

2 files changed

+163
-75
lines changed

2 files changed

+163
-75
lines changed

pkg/run/bootstrap/bootstrap_wizard.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,7 @@ const (
300300
branchPrefix = "refs/heads/"
301301
)
302302

303-
type (
304-
GitProvider int32
305-
)
303+
type GitProvider int32
306304

307305
const (
308306
GitProviderUnknown GitProvider = 0

pkg/run/bootstrap/bootstrap_wizard_ui.go

Lines changed: 162 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bootstrap
22

33
import (
44
"fmt"
5+
"strconv"
56
"strings"
67

78
"github.com/charmbracelet/bubbles/table"
@@ -19,7 +20,39 @@ type preWizardModel struct {
1920
msgChan chan GitProvider
2021
}
2122

22-
const flagSeparator = " - "
23+
type wizardModel struct {
24+
windowIsReady bool
25+
viewport viewport.Model
26+
inputs []*bootstrapWizardInput
27+
msgChan chan BootstrapCmdOptions
28+
cursorMode textinput.CursorMode
29+
focusIndex int
30+
errorMsg string
31+
}
32+
33+
type checkbox struct {
34+
checked bool
35+
}
36+
37+
type bootstrapWizardInputType int32
38+
39+
const (
40+
bootstrapWizardInputTypeTextInput bootstrapWizardInputType = 0
41+
bootstrapWizardInputTypeCheckbox bootstrapWizardInputType = 1
42+
)
43+
44+
type bootstrapWizardInput struct {
45+
inputType bootstrapWizardInputType
46+
flagName string
47+
prompt string
48+
textInput textinput.Model
49+
checkboxInput *checkbox
50+
}
51+
52+
const (
53+
flagSeparator = " - "
54+
buttonText = "Submit"
55+
)
2356

2457
// UI styling
2558
var (
@@ -37,8 +70,8 @@ var (
3770
helpStyle = blurredStyle.Copy()
3871
cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
3972

40-
focusedButton = focusedStyle.Copy().Render("[ Submit ]")
41-
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit"))
73+
focusedButton = focusedStyle.Render(fmt.Sprintf("[ %s ]", buttonText))
74+
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render(buttonText))
4275
)
4376

4477
func makeViewport(width int, height int, content string) viewport.Model {
@@ -162,62 +195,96 @@ func (m preWizardModel) getContent() string {
162195
) + m.textInput.View()
163196
}
164197

165-
type wizardModel struct {
166-
windowIsReady bool
167-
viewport viewport.Model
168-
textInputs []textinput.Model
169-
prompts []string
170-
msgChan chan BootstrapCmdOptions
171-
cursorMode textinput.CursorMode
172-
focusIndex int
173-
errorMsg string
198+
func makeInput(task *BootstrapWizardTask, isFocused bool) *bootstrapWizardInput {
199+
var inputType bootstrapWizardInputType
200+
201+
if task.isBoolean {
202+
inputType = bootstrapWizardInputTypeCheckbox
203+
} else {
204+
inputType = bootstrapWizardInputTypeTextInput
205+
}
206+
207+
flagName := task.flagName
208+
209+
prompt := task.flagName + flagSeparator + task.flagDescription
210+
211+
ti := textinput.Model{}
212+
213+
var cb *checkbox
214+
215+
if inputType == bootstrapWizardInputTypeCheckbox {
216+
cb = &checkbox{
217+
checked: task.flagValue == "true",
218+
}
219+
} else {
220+
ti = textinput.New()
221+
ti.CursorStyle = cursorStyle
222+
ti.CharLimit = 100
223+
224+
ti.SetValue(task.flagValue)
225+
ti.Placeholder = task.flagDescription
226+
227+
if task.isPassword {
228+
ti.EchoMode = textinput.EchoPassword
229+
}
230+
231+
if isFocused {
232+
ti.Focus()
233+
ti.PromptStyle = focusedStyle
234+
ti.TextStyle = focusedStyle
235+
}
236+
}
237+
238+
return &bootstrapWizardInput{
239+
inputType: inputType,
240+
flagName: flagName,
241+
prompt: prompt,
242+
textInput: ti,
243+
checkboxInput: cb,
244+
}
174245
}
175246

176-
func makeTextInput(task *BootstrapWizardTask, isFocused bool) textinput.Model {
177-
ti := textinput.New()
178-
ti.CursorStyle = cursorStyle
179-
ti.CharLimit = 100
247+
func (input *bootstrapWizardInput) getView(isFocused bool) string {
248+
if input.inputType == bootstrapWizardInputTypeTextInput {
249+
return input.textInput.View()
250+
}
251+
252+
var checkmark string
180253

181-
ti.SetValue(task.flagValue)
182-
ti.Placeholder = task.flagDescription
254+
if input.checkboxInput.checked {
255+
checkmark = "x"
183256

184-
if task.isPassword {
185-
ti.EchoMode = textinput.EchoPassword
257+
if isFocused {
258+
checkmark = focusedStyle.Render(checkmark)
259+
}
260+
} else {
261+
checkmark = blurredStyle.Render("_")
186262
}
187263

264+
open := "["
265+
close := "]"
266+
188267
if isFocused {
189-
ti.Focus()
190-
ti.PromptStyle = focusedStyle
191-
ti.TextStyle = focusedStyle
268+
open = focusedStyle.Render(open)
269+
close = focusedStyle.Render(close)
192270
}
193271

194-
return ti
272+
return fmt.Sprintf("%s%s%s %s", open, checkmark, close, input.flagName)
195273
}
196274

197275
func initialWizardModel(tasks []*BootstrapWizardTask, msgChan chan BootstrapCmdOptions) wizardModel {
198276
numInputs := len(tasks)
199277

200-
inputs := make([]textinput.Model, numInputs)
278+
inputs := make([]*bootstrapWizardInput, numInputs)
201279

202280
for i := range inputs {
203-
task := tasks[i]
204-
205-
ti := makeTextInput(task, i == 0)
206-
207-
inputs[i] = ti
208-
}
209-
210-
prompts := []string{}
211-
212-
for _, task := range tasks {
213-
prompts = append(prompts, task.flagName+flagSeparator+task.flagDescription)
281+
inputs[i] = makeInput(tasks[i], i == 0)
214282
}
215283

216284
return wizardModel{
217-
textInputs: inputs,
218-
errorMsg: "",
219-
prompts: prompts,
220-
msgChan: msgChan,
285+
inputs: inputs,
286+
errorMsg: "",
287+
msgChan: msgChan,
221288
}
222289
}
223290

@@ -241,37 +308,43 @@ func (m wizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
241308
m.cursorMode = textinput.CursorBlink
242309
}
243310

244-
cmdsTextInputs := make([]tea.Cmd, len(m.textInputs))
311+
cmdsTextInputs := make([]tea.Cmd, len(m.inputs))
245312

246-
for i := range m.textInputs {
247-
cmdsTextInputs[i] = m.textInputs[i].SetCursorMode(m.cursorMode)
313+
for i, input := range m.inputs {
314+
if input.inputType == bootstrapWizardInputTypeTextInput {
315+
cmdsTextInputs[i] = input.textInput.SetCursorMode(m.cursorMode)
316+
}
248317
}
249318

250319
cmds = append(cmds, cmdsTextInputs...)
251320
case tea.KeyTab, tea.KeyShiftTab, tea.KeyEnter:
252321
t := msg.Type
253322

254-
if t == tea.KeyEnter && m.focusIndex == len(m.textInputs) {
323+
if t == tea.KeyEnter && m.focusIndex == len(m.inputs) {
255324
options := make(BootstrapCmdOptions)
256325

257-
for i, input := range m.textInputs {
258-
prompt := m.prompts[i]
326+
for _, input := range m.inputs {
327+
var value string
259328

260-
value := strings.TrimSpace(input.Value())
329+
if input.inputType == bootstrapWizardInputTypeTextInput {
330+
value = strings.TrimSpace(input.textInput.Value())
261331

262-
if value == "" {
263-
m.errorMsg = "Missing value in " + input.Placeholder
332+
if value == "" {
333+
m.errorMsg = "Missing value in " + input.textInput.Placeholder
264334

265-
m.viewport.SetContent(m.getContent())
335+
m.viewport.SetContent(m.getContent())
266336

267-
var cmdViewport tea.Cmd
337+
var cmdViewport tea.Cmd
268338

269-
m.viewport, cmdViewport = m.viewport.Update(msg)
339+
m.viewport, cmdViewport = m.viewport.Update(msg)
270340

271-
return m, cmdViewport
341+
return m, cmdViewport
342+
}
343+
} else {
344+
value = strconv.FormatBool(input.checkboxInput.checked)
272345
}
273346

274-
options[prompt[:strings.Index(prompt, flagSeparator)]] = value
347+
options[input.flagName] = value
275348
}
276349

277350
go func() { m.msgChan <- options }()
@@ -285,29 +358,41 @@ func (m wizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
285358
m.focusIndex++
286359
}
287360

288-
if m.focusIndex > len(m.textInputs) {
361+
if m.focusIndex > len(m.inputs) {
289362
m.focusIndex = 0
290363
} else if m.focusIndex < 0 {
291-
m.focusIndex = len(m.textInputs)
364+
m.focusIndex = len(m.inputs)
292365
}
293366

294-
cmdsTextInputs := make([]tea.Cmd, len(m.textInputs))
367+
cmdsTextInputs := []tea.Cmd{}
368+
369+
for i, input := range m.inputs[:len(m.inputs)-1] {
370+
if input.inputType == bootstrapWizardInputTypeCheckbox {
371+
continue
372+
}
295373

296-
for i := 0; i <= len(m.textInputs)-1; i++ {
297374
if i == m.focusIndex {
298-
cmdsTextInputs[i] = m.textInputs[i].Focus()
299-
m.textInputs[i].PromptStyle = focusedStyle
300-
m.textInputs[i].TextStyle = focusedStyle
375+
cmdsTextInputs = append(cmdsTextInputs, input.textInput.Focus())
376+
input.textInput.PromptStyle = focusedStyle
377+
input.textInput.TextStyle = focusedStyle
301378

302379
continue
303380
}
304381

305-
m.textInputs[i].Blur()
306-
m.textInputs[i].PromptStyle = noStyle
307-
m.textInputs[i].TextStyle = noStyle
382+
input.textInput.Blur()
383+
input.textInput.PromptStyle = noStyle
384+
input.textInput.TextStyle = noStyle
308385
}
309386

310387
cmds = append(cmds, cmdsTextInputs...)
388+
case tea.KeySpace:
389+
input := m.inputs[m.focusIndex]
390+
391+
if m.focusIndex == len(m.inputs) || input.inputType != bootstrapWizardInputTypeCheckbox {
392+
break
393+
}
394+
395+
input.checkboxInput.checked = !input.checkboxInput.checked
311396
}
312397
case tea.WindowSizeMsg:
313398
if !m.windowIsReady {
@@ -338,12 +423,16 @@ func (m wizardModel) View() string {
338423
}
339424

340425
func (m *wizardModel) updateInputs(msg tea.Msg) tea.Cmd {
341-
cmds := make([]tea.Cmd, len(m.textInputs))
426+
cmds := make([]tea.Cmd, len(m.inputs))
342427

343428
// Only text inputs with Focus() set will respond, so it's safe to simply
344429
// update all of them here without any further logic.
345-
for i := range m.textInputs {
346-
m.textInputs[i], cmds[i] = m.textInputs[i].Update(msg)
430+
for i, input := range m.inputs {
431+
if input.inputType == bootstrapWizardInputTypeCheckbox {
432+
continue
433+
}
434+
435+
input.textInput, cmds[i] = input.textInput.Update(msg)
347436
}
348437

349438
return tea.Batch(cmds...)
@@ -354,21 +443,22 @@ func (m wizardModel) getContent() string {
354443

355444
b.WriteString("Please enter the following values" + "\n" +
356445
"(Tab and Shift+Tab to move input selection," + "\n" +
446+
"(Space to toggle the currently focused checkbox," + "\n" +
357447
"Enter to move to the next input or submit the form, " + "\n" +
358448
"up and down arrows to scroll the view, Ctrl+C twice to quit):" + "\n\n\n")
359449

360-
for i := range m.textInputs {
361-
b.WriteString(m.prompts[i])
450+
for i, input := range m.inputs {
451+
b.WriteString(input.prompt)
362452
b.WriteRune('\n')
363-
b.WriteString(m.textInputs[i].View())
453+
b.WriteString(input.getView(i == m.focusIndex))
364454

365-
if i < len(m.textInputs)-1 {
455+
if i < len(m.inputs)-1 {
366456
b.WriteRune('\n')
367457
}
368458
}
369459

370460
button := &blurredButton
371-
if m.focusIndex == len(m.textInputs) {
461+
if m.focusIndex == len(m.inputs) {
372462
button = &focusedButton
373463
}
374464

0 commit comments

Comments
 (0)