Skip to content

Commit

Permalink
feat(textinput): make validation customizable
Browse files Browse the repository at this point in the history
This PR builds upon the excellent work in charmbracelet#167 and charmbracelet#114 and adds a bit
more customizability to the feature.

Currently, the validation API will completely block text input if the
Validate function returns an error. This commit makes a breaking change
to the ValidateFunc by returning an additonal bool that indicates
whether or not input should be blocked.

This is helpful for cases where the user is requested to type an
existing system path, and the Validate function keeps asserting the
existence of the path. With the current implementation such a validation
is not possible.

For example:

    > /
    Err: nil

    > /t
    Err: /t: No such file or directory

    > /tm
    Err: /tm: No such file or directory

    > /tmp
    Err: nil
  • Loading branch information
GabrielNagy committed Sep 4, 2023
1 parent eda8912 commit b0c7209
Showing 1 changed file with 25 additions and 15 deletions.
40 changes: 25 additions & 15 deletions textinput/textinput.go
Expand Up @@ -35,8 +35,9 @@ const (
EchoNone
)

// ValidateFunc is a function that returns an error if the input is invalid.
type ValidateFunc func(string) error
// ValidateFunc is a function that returns an error if the input is invalid and
// a boolean indicating whether text input should be blocked.
type ValidateFunc func(string) (bool, error)

// KeyMap is the key bindings for different actions within the textinput.
type KeyMap struct {
Expand Down Expand Up @@ -180,19 +181,17 @@ func (m *Model) SetValue(s string) {
// Clean up any special characters in the input provided by the
// caller. This avoids bugs due to e.g. tab characters and whatnot.
runes := m.san().Sanitize([]rune(s))
m.setValueInternal(runes)
blockInput, err := m.validateIfDefined(string(runes))
m.setValueInternal(runes, err, blockInput)
}

func (m *Model) setValueInternal(runes []rune) {
if m.Validate != nil {
if err := m.Validate(string(runes)); err != nil {
m.Err = err
return
}
func (m *Model) setValueInternal(runes []rune, err error, blockInput bool) {
m.Err = err
if blockInput {
return
}

empty := len(m.value) == 0
m.Err = nil

if m.CharLimit > 0 && len(runes) > m.CharLimit {
m.value = runes[:m.CharLimit]
Expand Down Expand Up @@ -323,9 +322,10 @@ func (m *Model) insertRunesFromUserInput(v []rune) {

// Put it all back together
value := append(head, tail...)
m.setValueInternal(value)
blockInput, inputErr := m.validateIfDefined(string(value))
m.setValueInternal(value, inputErr, blockInput)

if m.Err != nil {
if blockInput {
m.pos = oldPos
}
}
Expand Down Expand Up @@ -378,6 +378,7 @@ func (m *Model) handleOverflow() {
// deleteBeforeCursor deletes all text before the cursor.
func (m *Model) deleteBeforeCursor() {
m.value = m.value[m.pos:]
_, m.Err = m.validateIfDefined(string(m.value))
m.offset = 0
m.SetCursor(0)
}
Expand All @@ -387,6 +388,7 @@ func (m *Model) deleteBeforeCursor() {
// masked input.
func (m *Model) deleteAfterCursor() {
m.value = m.value[:m.pos]
_, m.Err = m.validateIfDefined(string(m.value))
m.SetCursor(len(m.value))
}

Expand Down Expand Up @@ -432,6 +434,7 @@ func (m *Model) deleteWordBackward() {
} else {
m.value = append(m.value[:m.pos], m.value[oldPos:]...)
}
_, m.Err = m.validateIfDefined(string(m.value))
}

// deleteWordForward deletes the word right to the cursor. If input is masked
Expand Down Expand Up @@ -471,6 +474,7 @@ func (m *Model) deleteWordForward() {
} else {
m.value = append(m.value[:oldPos], m.value[m.pos:]...)
}
_, m.Err = m.validateIfDefined(string(m.value))

m.SetCursor(oldPos)
}
Expand Down Expand Up @@ -575,12 +579,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.Err = nil
m.deleteWordBackward()
case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
m.Err = nil
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
_, m.Err = m.validateIfDefined(string(m.value))
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
Expand All @@ -597,13 +601,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if m.pos < len(m.value) {
m.SetCursor(m.pos + 1)
}
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.deleteWordBackward()
case key.Matches(msg, m.KeyMap.LineStart):
m.CursorStart()
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
_, m.Err = m.validateIfDefined(string(m.value))
}
case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd()
Expand Down Expand Up @@ -859,3 +862,10 @@ func (m *Model) previousSuggestion() {
m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
}
}

func (m Model) validateIfDefined(v string) (bool, error) {
if m.Validate != nil {
return m.Validate(v)
}
return false, nil
}

0 comments on commit b0c7209

Please sign in to comment.