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 validate API to no longer block input if this is the case, thus
handing this responsibility to the clients.

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 Feb 29, 2024
1 parent 6ebf50e commit 3d2b848
Showing 1 changed file with 19 additions and 19 deletions.
38 changes: 19 additions & 19 deletions textinput/textinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,14 @@ 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)
err := m.validateIfDefined(string(runes))
m.setValueInternal(runes, err)
}

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) {
m.Err = err

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 @@ -307,8 +302,6 @@ func (m *Model) insertRunesFromUserInput(v []rune) {
tail := make([]rune, len(tailSrc))
copy(tail, tailSrc)

oldPos := m.pos

// Insert pasted runes
for _, r := range paste {
head = append(head, r)
Expand All @@ -323,11 +316,8 @@ func (m *Model) insertRunesFromUserInput(v []rune) {

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

if m.Err != nil {
m.pos = oldPos
}
inputErr := m.validateIfDefined(string(value))
m.setValueInternal(value, inputErr)
}

// If a max width is defined, perform some logic to treat the visible area
Expand Down Expand Up @@ -378,6 +368,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 +378,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 +424,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 +464,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 +569,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 +591,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 @@ -884,3 +877,10 @@ func (m *Model) previousSuggestion() {
m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
}
}

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

0 comments on commit 3d2b848

Please sign in to comment.