mirror of
https://github.com/Kyren223/eko.git
synced 2026-07-12 07:19:29 +00:00
Added typing and input validation to network creation popup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user