Skip to content

Commit

Permalink
feat: Use bubbletea for user input
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Sep 15, 2022
1 parent b4ab2ee commit df4f35e
Show file tree
Hide file tree
Showing 16 changed files with 738 additions and 274 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ linters-settings:
- anon
- empty
- error
- github.com/charmbracelet/bubbletea\.Model
- github.com/go-git/go-git/v5/plumbing/format/diff\.File
- github.com/go-git/go-git/v5/plumbing/format/diff\.Patch
- github.com/mitchellh/mapstructure\.DecodeHookFunc
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.22
github.com/bmatcuk/doublestar/v4 v4.2.0
github.com/bradenhilton/mozillainstallhash v1.0.0
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.22.1
github.com/charmbracelet/glamour v0.5.0
github.com/coreos/go-semver v0.3.0
github.com/fsnotify/fsnotify v1.5.4
Expand Down Expand Up @@ -56,6 +58,7 @@ require (
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21 // indirect
Expand All @@ -68,7 +71,9 @@ require (
github.com/aws/smithy-go v1.13.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bradenhilton/cityhash v1.0.0 // indirect
github.com/charmbracelet/lipgloss v0.5.0 // indirect
github.com/cloudflare/circl v1.2.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
Expand All @@ -91,11 +96,14 @@ require (
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/microcosm-cc/bluemonday v1.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.12.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
Expand Down
123 changes: 27 additions & 96 deletions go.sum

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions pkg/chezmoibubbles/boolinputmodel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package chezmoibubbles

import (
"strconv"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"

"github.com/twpayne/chezmoi/v2/pkg/chezmoi"
)

type BoolInputModel struct {
textInput textinput.Model
defaultValue *bool
canceled bool
}

func NewBoolInputModel(prompt string, defaultValue *bool) BoolInputModel {
textInput := textinput.New()
textInput.Prompt = prompt + "? "
textInput.Placeholder = "bool"
if defaultValue != nil {
textInput.Placeholder += ", default " + strconv.FormatBool(*defaultValue)
}
textInput.Validate = func(value string) error {
if value == "" && defaultValue != nil {
return nil
}
_, err := chezmoi.ParseBool(value)
return err
}
textInput.Focus()
return BoolInputModel{
textInput: textInput,
defaultValue: defaultValue,
}
}

func (m BoolInputModel) Canceled() bool {
return m.canceled
}

func (m BoolInputModel) Init() tea.Cmd {
return textinput.Blink
}

func (m BoolInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//nolint:gocritic
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
m.canceled = true
return m, tea.Quit
case tea.KeyEnter:
if m.defaultValue != nil {
m.textInput.SetValue(strconv.FormatBool(*m.defaultValue))
return m, tea.Quit
}
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
if _, err := chezmoi.ParseBool(m.textInput.Value()); err == nil {
return m, tea.Quit
}
return m, cmd
}

func (m BoolInputModel) Value() bool {
valueStr := m.textInput.Value()
if valueStr == "" && m.defaultValue != nil {
return *m.defaultValue
}
value, _ := chezmoi.ParseBool(valueStr)
return value
}

func (m BoolInputModel) View() string {
return m.textInput.View()
}
1 change: 1 addition & 0 deletions pkg/chezmoibubbles/chezmoibubbles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package chezmoibubbles
81 changes: 81 additions & 0 deletions pkg/chezmoibubbles/choiceinputmodel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package chezmoibubbles

import (
"errors"
"strings"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"

"github.com/twpayne/chezmoi/v2/pkg/chezmoi"
)

type ChoiceInputModel struct {
textInput textinput.Model
abbreviations map[string]string
defaultValue *string
canceled bool
}

func NewChoiceInputModel(prompt string, choices []string, defaultValue *string) ChoiceInputModel {
textInput := textinput.New()
textInput.Prompt = prompt + "? "
textInput.Placeholder = strings.Join(choices, "/")
if defaultValue != nil {
textInput.Placeholder += ", default " + *defaultValue
}
abbreviations := chezmoi.UniqueAbbreviations(choices)
textInput.Validate = func(s string) error {
if s == "" && defaultValue != nil {
return nil
}
if _, ok := abbreviations[s]; ok {
return nil
}
return errors.New("unknown or ambiguous choice")
}
textInput.Focus()
return ChoiceInputModel{
textInput: textInput,
abbreviations: abbreviations,
defaultValue: defaultValue,
}
}

func (m ChoiceInputModel) Canceled() bool {
return m.canceled
}

func (m ChoiceInputModel) Init() tea.Cmd {
return textinput.Blink
}

func (m ChoiceInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//nolint:gocritic
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
m.canceled = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
if _, ok := m.abbreviations[m.textInput.Value()]; ok {
return m, tea.Quit
}
return m, cmd
}

func (m ChoiceInputModel) Value() string {
value := m.textInput.Value()
if value == "" && m.defaultValue != nil {
return *m.defaultValue
}
return m.abbreviations[value]
}

func (m ChoiceInputModel) View() string {
return m.textInput.View()
}
76 changes: 76 additions & 0 deletions pkg/chezmoibubbles/intinputmodel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package chezmoibubbles

import (
"strconv"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)

type IntInputModel struct {
textInput textinput.Model
defaultValue *int64
canceled bool
}

func NewIntInputModel(prompt string, defaultValue *int64) IntInputModel {
textInput := textinput.New()
textInput.Prompt = prompt + "? "
textInput.Placeholder = "int"
if defaultValue != nil {
textInput.Placeholder += ", default " + strconv.FormatInt(*defaultValue, 10)
}
textInput.Validate = func(value string) error {
if value == "" && defaultValue != nil {
return nil
}
_, err := strconv.ParseInt(value, 10, 64)
return err
}
textInput.Focus()
return IntInputModel{
textInput: textInput,
defaultValue: defaultValue,
}
}

func (m IntInputModel) Canceled() bool {
return m.canceled
}

func (m IntInputModel) Init() tea.Cmd {
return textinput.Blink
}

func (m IntInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//nolint:gocritic
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
m.canceled = true
return m, tea.Quit
case tea.KeyEnter:
if m.defaultValue != nil {
m.textInput.SetValue(strconv.FormatInt(*m.defaultValue, 10))
return m, tea.Quit
}
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}

func (m IntInputModel) Value() int64 {
valueStr := m.textInput.Value()
if valueStr == "" && m.defaultValue != nil {
return *m.defaultValue
}
value, _ := strconv.ParseInt(valueStr, 10, 64)
return value
}

func (m IntInputModel) View() string {
return m.textInput.View()
}
55 changes: 55 additions & 0 deletions pkg/chezmoibubbles/passwordinputmodel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package chezmoibubbles

import (
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)

type PasswordInputModel struct {
textInput textinput.Model
canceled bool
}

func NewPasswordInputModel(prompt string) PasswordInputModel {
textInput := textinput.New()
textInput.Prompt = prompt
textInput.Placeholder = "password"
textInput.EchoMode = textinput.EchoNone
textInput.Focus()
return PasswordInputModel{
textInput: textInput,
}
}

func (m PasswordInputModel) Canceled() bool {
return m.canceled
}

func (m PasswordInputModel) Init() tea.Cmd {
return textinput.Blink
}

func (m PasswordInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//nolint:gocritic
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
return m, tea.Quit
case tea.KeyCtrlC, tea.KeyEsc:
m.canceled = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}

func (m PasswordInputModel) Value() string {
return m.textInput.Value()
}

func (m PasswordInputModel) View() string {
return m.textInput.View()
}
Loading

0 comments on commit df4f35e

Please sign in to comment.