Added typing and input validation to network creation popup

This commit is contained in:
2024-11-25 23:49:40 +02:00
parent d12ebef2c5
commit b05e4fcbcb
3 changed files with 137 additions and 27 deletions

View File

@@ -137,7 +137,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.networkCreationPopup = &popup
}
default:
case tea.KeyEscape:
if m.networkCreationPopup != nil {
m.networkCreationPopup = nil
}
case tea.KeyEnter:
if m.networkCreationPopup != nil {
return m, m.networkCreationPopup.Select()
}
default:
if m.networkCreationPopup != nil {
popup, cmd := m.networkCreationPopup.Update(msg)
m.networkCreationPopup = &popup

View File

@@ -1,6 +1,7 @@
package networkcreation
import (
"errors"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -8,11 +9,18 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/kyren223/eko/internal/client/ui/colors"
"github.com/kyren223/eko/internal/client/ui/field"
"github.com/kyren223/eko/internal/client/ui/layouts/flex"
"github.com/kyren223/eko/pkg/assert"
)
var (
style = lipgloss.NewStyle().Border(lipgloss.ThickBorder())
width = 48
style = lipgloss.NewStyle().
Border(lipgloss.ThickBorder()).
Padding(1, 4).
Align(lipgloss.Center, lipgloss.Center)
headerStyle = lipgloss.NewStyle().Foreground(colors.Turquoise)
fieldBlurredStyle = lipgloss.NewStyle().
@@ -23,11 +31,7 @@ var (
BorderForeground(colors.Focus).
Border(lipgloss.ThickBorder())
underlineStyle = func(s string, width int, focus bool) string {
color := colors.Gray
if focus {
color = colors.Focus
}
underlineStyle = func(s string, width int, color lipgloss.Color) string {
underline := lipgloss.NewStyle().Foreground(color).
Render(strings.Repeat(lipgloss.ThickBorder().Bottom, width))
return lipgloss.JoinVertical(lipgloss.Left, s, underline)
@@ -35,6 +39,11 @@ var (
iconHeader = headerStyle.Bold(true).Render(" Icon: ")
colorHeader = headerStyle.Bold(true).Italic(true).Render("# ")
blurredCreate = lipgloss.NewStyle().
Background(colors.Gray).Padding(0, 1).Render("Create Network")
focusedCreate = lipgloss.NewStyle().
Background(colors.Blue).Padding(0, 1).Render("Create Network")
)
const (
@@ -46,49 +55,73 @@ const (
NameField = iota
IconField
ColorField
CreateField
FieldCount
)
type Model struct {
name field.Model
Style lipgloss.Style
precomputedIconStyle lipgloss.Style
icon textinput.Model
color textinput.Model
Width int
Height int
selected int
name field.Model
icon textinput.Model
color textinput.Model
create string
selected int
nameWidth int
}
func New() Model {
name := field.New(30)
name := field.New(width)
name.Header = "Network Name"
name.HeaderStyle = headerStyle
name.FocusedStyle = fieldFocusedStyle
name.BlurredStyle = fieldBlurredStyle
name.Input.CharLimit = 32
name.ErrorStyle = lipgloss.NewStyle().Foreground(colors.Error)
name.Input.CharLimit = width
name.Focus()
name.Input.Validate = func(s string) error {
if strings.TrimSpace(s) == "" {
return errors.New("cannot be empty")
}
if strings.TrimSpace(s) != s {
return errors.New("no leading/trailing spaces")
}
return nil
}
nameWidth := lipgloss.Width(name.View())
icon := textinput.New()
icon.Prompt = ""
icon.CharLimit = MaxIconLength
icon.Placeholder = "ic"
icon.Validate = func(s string) error {
if len(s) == 0 {
return errors.New("err")
}
return nil
}
color := textinput.New()
color.Prompt = ""
color.CharLimit = MaxHexDigits
color.Placeholder = "000000"
color.Validate = func(s string) error {
if len(s) != 0 && len(s) != MaxHexDigits {
return errors.New("err")
}
return nil
}
return Model{
Width: 50,
Height: 10,
name: name,
icon: icon,
color: color,
Style: style,
create: blurredCreate,
precomputedIconStyle: lipgloss.NewStyle().Width(lipgloss.Width(name.View()) / 2),
nameWidth: nameWidth,
precomputedIconStyle: lipgloss.NewStyle().Width(nameWidth / 2),
}
}
@@ -99,17 +132,31 @@ func (m Model) Init() tea.Cmd {
func (m Model) View() string {
name := m.name.View()
iconInput := underlineStyle(m.icon.View(), MaxIconLength, m.selected == IconField)
color := colors.Gray
if m.icon.Err != nil {
color = colors.Error
} else if m.selected == IconField {
color = colors.Focus
}
iconInput := underlineStyle(m.icon.View(), MaxIconLength, color)
iconText := m.precomputedIconStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, iconHeader, iconInput))
colorInput := underlineStyle(m.color.View(), MaxHexDigits, m.selected == ColorField)
color = colors.Gray
if m.color.Err != nil {
color = colors.Error
} else if m.selected == ColorField {
color = colors.Focus
}
colorInput := underlineStyle(m.color.View(), MaxHexDigits, color)
colorText := lipgloss.JoinHorizontal(lipgloss.Top, colorHeader, colorInput)
icon := lipgloss.JoinHorizontal(lipgloss.Top, iconText, colorText)
content := lipgloss.JoinVertical(lipgloss.Left, name, "\n", icon)
popup := lipgloss.NewStyle().Width(m.Width).Height(m.Height).Render(content)
return m.Style.Render(popup)
// create := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.create)
create := m.create
content := flex.NewVertical(name, icon, create).WithGap(1).View()
return style.Render(content)
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
@@ -130,7 +177,25 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case IconField:
m.icon, cmd = m.icon.Update(msg)
case ColorField:
oldValue := m.color.Value()
position := m.color.Position()
m.color, cmd = m.color.Update(msg)
newValue := m.color.Value()
hex := "0123456789abcdefABCDEF"
invalid := false
for _, c := range newValue {
if !strings.ContainsRune(hex, c) {
invalid = true
break
}
}
if invalid {
m.color.SetValue(oldValue)
m.color.SetCursor(position)
}
}
return m, cmd
}
@@ -153,6 +218,7 @@ func (m *Model) updateFocus() tea.Cmd {
m.name.Blur()
m.icon.Blur()
m.color.Blur()
m.create = blurredCreate
switch m.selected {
case NameField:
return m.name.Focus()
@@ -160,8 +226,27 @@ func (m *Model) updateFocus() tea.Cmd {
return m.icon.Focus()
case ColorField:
return m.color.Focus()
case CreateField:
m.create = focusedCreate
return nil
default:
assert.Never("missing switch statement field in update focus", "selected", m.selected)
return nil
}
}
func (m *Model) Select() tea.Cmd {
if m.selected != CreateField {
return nil
}
m.name.Input.Err = m.name.Input.Validate(m.name.Input.Value())
m.icon.Err = m.icon.Validate(m.icon.Value())
m.color.Err = m.color.Validate(m.color.Value())
if m.name.Input.Err != nil || m.icon.Err != nil || m.color.Err != nil {
return nil
}
// TODO: send api request for creating the server
return nil
}

View File

@@ -16,8 +16,18 @@ type Model struct {
Vertical bool
}
func New() Model {
return Model{}
func NewHorizontal(contents ...string) Model {
return Model{
contents: contents,
Vertical: false,
}
}
func NewVertical(contents ...string) Model {
return Model{
contents: contents,
Vertical: true,
}
}
func (m Model) Init() tea.Cmd {
@@ -54,3 +64,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
func (m *Model) SetContents(contents ...string) {
m.contents = contents
}
func (m Model) WithGap(gap int) Model {
m.Gap = gap
return m
}