Skip to content

Commit

Permalink
Validate TUI inputs using bubbles API
Browse files Browse the repository at this point in the history
  • Loading branch information
GabrielNagy committed Jul 6, 2022
1 parent e298b39 commit 1f819dc
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 39 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,5 @@ require (
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 // indirect
)

replace github.com/charmbracelet/bubbles => github.com/gabrielnagy/bubbles v0.12.1-0.20220705151529-bac247074adc
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charithe/durationcheck v0.0.9 h1:mPP4ucLrf/rKZiIG/a9IPXHGlh8p4CzgpyTy6EEutYk=
github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg=
github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=
github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc=
github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
Expand Down Expand Up @@ -227,6 +225,8 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV
github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
github.com/fzipp/gocyclo v0.5.1 h1:L66amyuYogbxl0j2U+vGqJXusPF2IkduvXLnYD5TFgw=
github.com/fzipp/gocyclo v0.5.1/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gabrielnagy/bubbles v0.12.1-0.20220705151529-bac247074adc h1:MySi8bx/F2rQMg9XdDPxQP5YctI9fjKtP6NVBk2VR2Q=
github.com/gabrielnagy/bubbles v0.12.1-0.20220705151529-bac247074adc/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-critic/go-critic v0.6.3 h1:abibh5XYBTASawfTQ0rA7dVtQT+6KzpGqb/J+DxRDaw=
github.com/go-critic/go-critic v0.6.3/go.mod h1:c6b3ZP1MQ7o6lPR7Rv3lEf7pYQUmAcx8ABHgdZCQt/k=
Expand Down
80 changes: 43 additions & 37 deletions internal/watchdtui/watchdtui.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ var (
focusedStyle = boldStyle.Copy().Foreground(lipgloss.Color("#E95420")) // Ubuntu orange
)

const (
noDirectoriesMsg = "please enter at least one directory"
)

type model struct {
focusIndex int
inputs []textinput.Model
Expand Down Expand Up @@ -159,10 +163,10 @@ func initialModel(configFile string, prevConfigFile string, isDefaultConfig bool

var t textinput.Model
for i := range m.inputs {
t = newStyledTextInput()

switch i {
case 0:
t = newStyledTextInput(configInputValidator)
t.Placeholder = fmt.Sprintf(i18n.G("Config file location (leave blank for default: %s)"), m.defaultConfig)
t.Prompt = i18n.G("Config file: ")
t.PromptStyle = boldStyle
Expand All @@ -174,7 +178,11 @@ func initialModel(configFile string, prevConfigFile string, isDefaultConfig bool
t.SetValue(configFile)
}
case 1:
t = newStyledTextInput(m.dirInputValidator)
t.Placeholder = i18n.G("Directory to watch (one per line)")
if len(previousDirs) == 0 {
t.Err = errors.New(i18n.G(noDirectoriesMsg))
}
}

m.inputs[i] = t
Expand All @@ -183,17 +191,20 @@ func initialModel(configFile string, prevConfigFile string, isDefaultConfig bool
// If we managed to read directories from the "previous" config file,
// prefill them
for index, dir := range previousDirs {
m.inputs[index+1] = newStyledTextInput(m.dirInputValidator)
m.inputs[index+1].SetValue(dir)
}

return m
}

// newStyledTextInput returns a new text input with the default style.
func newStyledTextInput() textinput.Model {
func newStyledTextInput(validateFunc textinput.ValidateFunc) textinput.Model {
t := textinput.New()
t.CursorStyle = cursorStyle
t.CharLimit = 1024
t.Validate = validateFunc
t.ValidateAction = textinput.AllowInput
t.SetCursorMode(textinput.CursorStatic)
return t
}
Expand Down Expand Up @@ -328,7 +339,7 @@ func (m model) Update(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
}
// add a new input where we are and move focus to it
m.focusIndex++
m.inputs = slices.Insert(m.inputs, m.focusIndex, newStyledTextInput())
m.inputs = slices.Insert(m.inputs, m.focusIndex, newStyledTextInput(m.dirInputValidator))
}
}
}
Expand Down Expand Up @@ -384,79 +395,74 @@ func (m *model) updateInputs(msg tea.Msg) tea.Cmd {

// Update input style/error separately for config and directories
if m.focusIndex > 0 {
m.updateDirInputErrorAndStyle(i)
} else {
updateConfigInputError(&m.inputs[0])
m.updateDirInputStyle(i)
}
cmds = append(cmds, update)
}

return tea.Batch(cmds...)
}

// updateConfigInputError updates the error state of the config input.
func updateConfigInputError(input *textinput.Model) {
value := input.Value()
// configInputValidator validates the config file input.
func configInputValidator(s string) error {
// If the config input is empty, clean up the error message
if value == "" {
input.Err = nil
return
if s == "" {
return nil
}

absPath, _ := filepath.Abs(value)
stat, err := os.Stat(value)
absPath, _ := filepath.Abs(s)
stat, err := os.Stat(s)

// If the config file does not exist, we're good
if errors.Is(err, os.ErrNotExist) {
input.Err = nil
if !filepath.IsAbs(value) {
input.Err = fmt.Errorf(i18n.G("%s will be the absolute path"), absPath)
if !filepath.IsAbs(s) {
return fmt.Errorf(i18n.G("%s will be the absolute path"), absPath)
}
return
return nil
}

// If we got another error, display it
if err != nil {
input.Err = err
return
return err
}

if stat.IsDir() {
input.Err = fmt.Errorf(i18n.G("%s is a directory; will create %s.yaml inside"), absPath, watchdconfig.CmdName)
return
return fmt.Errorf(i18n.G("%s is a directory; will create %s.yaml inside"), absPath, watchdconfig.CmdName)
}

if stat.Mode().IsRegular() {
input.Err = fmt.Errorf(i18n.G("%s: file already exists and will be overwritten"), absPath)
return
return fmt.Errorf(i18n.G("%s: file already exists and will be overwritten"), absPath)
}

input.Err = nil
return nil
}

// updateDirInputErrorAndStyle updates the error message and style of the given directory input.
func (m *model) updateDirInputErrorAndStyle(i int) {
func (m *model) dirInputValidator(s string) error {
// We consider an empty string to be valid, so users are allowed to press
// enter on it.
if m.inputs[i].Value() == "" {
m.inputs[i].Err = nil
if s == "" {
if len(m.inputs) == 2 {
m.inputs[i].Err = errors.New(i18n.G("please enter at least one directory"))
return errors.New(i18n.G(noDirectoriesMsg))
}
return
return nil
}

// Check to see if the input exists, and if it is a directory
absPath, _ := filepath.Abs(m.inputs[i].Value())

m.inputs[i].Err = nil
m.inputs[i].TextStyle = successStyle
absPath, _ := filepath.Abs(s)

if stat, err := os.Stat(absPath); err != nil {
m.inputs[i].Err = fmt.Errorf(i18n.G("%s: directory does not exist, please enter a valid path"), absPath)
m.inputs[i].TextStyle = noStyle
return fmt.Errorf(i18n.G("%s: directory does not exist, please enter a valid path"), absPath)
} else if !stat.IsDir() {
m.inputs[i].Err = fmt.Errorf(i18n.G("%s: is not a directory"), absPath)
return fmt.Errorf(i18n.G("%s: is not a directory"), absPath)
}

return nil
}

func (m *model) updateDirInputStyle(i int) {
m.inputs[i].TextStyle = successStyle
if m.inputs[i].Err != nil {
m.inputs[i].TextStyle = noStyle
}
}
Expand Down

0 comments on commit 1f819dc

Please sign in to comment.