update:wip
This commit is contained in:
3
internal/pages/add_server.go
Normal file
3
internal/pages/add_server.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package pages
|
||||
|
||||
type AddServerPageMsg struct{}
|
||||
12
internal/pages/config.go
Normal file
12
internal/pages/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package pages
|
||||
|
||||
type ConfigPageMsg struct{}
|
||||
|
||||
func ConfigMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Add Server", Key: "add"},
|
||||
{Label: "Edit Server", Key: "edit", RequiresServers: true},
|
||||
{Label: "Remove Server", Key: "remove", RequiresServers: true},
|
||||
{Label: "Back", Key: "back"},
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ package pages
|
||||
type HomePageMsg struct{}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Key string
|
||||
Label string
|
||||
Key string
|
||||
RequiresServers bool
|
||||
}
|
||||
|
||||
func HomeMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Select Server", Key: "server"},
|
||||
{Label: "Select Server", Key: "server", RequiresServers: true},
|
||||
{Label: "Config", Key: "config"},
|
||||
{Label: "Exit", Key: "exit"},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -54,3 +55,24 @@ func NewConfigService() (*ConfigService, error) {
|
||||
func (c *ConfigService) Servers() map[string]Server {
|
||||
return c.servers
|
||||
}
|
||||
|
||||
func (c *ConfigService) HasServer(name string) bool {
|
||||
_, ok := c.servers[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *ConfigService) AddServer(name string, s Server) error {
|
||||
if c.HasServer(name) {
|
||||
return fmt.Errorf("server %q already exists", name)
|
||||
}
|
||||
c.servers[name] = s
|
||||
return c.flush()
|
||||
}
|
||||
|
||||
func (c *ConfigService) flush() error {
|
||||
data, err := json.MarshalIndent(c.servers, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(c.path, data, 0o600)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,50 @@ var (
|
||||
menuItemInactive = menuItemBase.
|
||||
Foreground(lipgloss.Color("245"))
|
||||
|
||||
menuItemDisabled = menuItemBase.
|
||||
Foreground(lipgloss.Color("240")).
|
||||
PaddingLeft(4)
|
||||
|
||||
// Form fields
|
||||
fieldLabelRequired = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("75")).
|
||||
Bold(true).
|
||||
MarginTop(1)
|
||||
|
||||
fieldLabelOptional = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("245")).
|
||||
MarginTop(1)
|
||||
|
||||
FieldLegendStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Italic(true).
|
||||
MarginTop(1)
|
||||
|
||||
// Buttons
|
||||
buttonActive = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("232")).
|
||||
Background(lipgloss.Color("75")).
|
||||
Bold(true).
|
||||
Padding(0, 2).
|
||||
MarginTop(1)
|
||||
|
||||
buttonInactive = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("232")).
|
||||
Background(lipgloss.Color("240")).
|
||||
Padding(0, 2).
|
||||
MarginTop(1)
|
||||
|
||||
buttonLocked = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("238")).
|
||||
Background(lipgloss.Color("235")).
|
||||
Padding(0, 2).
|
||||
MarginTop(1)
|
||||
|
||||
// Status lines
|
||||
StatusOKStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("86")).
|
||||
MarginTop(1)
|
||||
|
||||
StatusWarnStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("221")).
|
||||
MarginTop(1)
|
||||
@@ -57,10 +100,33 @@ var (
|
||||
FooterDescStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
||||
)
|
||||
|
||||
// MenuItemStyle returns the appropriate style for a menu row.
|
||||
func MenuItemStyle(active bool) lipgloss.Style {
|
||||
if active {
|
||||
func MenuItemStyle(active, disabled bool) lipgloss.Style {
|
||||
switch {
|
||||
case disabled:
|
||||
return menuItemDisabled
|
||||
case active:
|
||||
return menuItemActive
|
||||
default:
|
||||
return menuItemInactive
|
||||
}
|
||||
}
|
||||
|
||||
func FieldLabelStyle(required bool) lipgloss.Style {
|
||||
if required {
|
||||
return fieldLabelRequired
|
||||
}
|
||||
return fieldLabelOptional
|
||||
}
|
||||
|
||||
// ButtonStyle returns the style for a button.
|
||||
// focused: cursor is on this button. enabled: button is interactive.
|
||||
func ButtonStyle(focused, enabled bool) lipgloss.Style {
|
||||
switch {
|
||||
case !enabled:
|
||||
return buttonLocked
|
||||
case focused:
|
||||
return buttonActive
|
||||
default:
|
||||
return buttonInactive
|
||||
}
|
||||
return menuItemInactive
|
||||
}
|
||||
|
||||
78
internal/tui/form.go
Normal file
78
internal/tui/form.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldName = iota
|
||||
fieldHost
|
||||
fieldUser
|
||||
fieldPrivateKey
|
||||
fieldPort
|
||||
fieldSave
|
||||
fieldBack
|
||||
fieldCount
|
||||
)
|
||||
|
||||
type addServerForm struct {
|
||||
inputs [5]textinput.Model
|
||||
focused int // 0–6: inputs 0-4, save 5, back 6
|
||||
}
|
||||
|
||||
func newAddServerForm() addServerForm {
|
||||
mkInput := func(placeholder string, limit int) textinput.Model {
|
||||
ti := textinput.New()
|
||||
ti.Prompt = ""
|
||||
ti.CharLimit = limit
|
||||
ti.SetWidth(40)
|
||||
ti.Placeholder = placeholder
|
||||
return ti
|
||||
}
|
||||
|
||||
f := addServerForm{}
|
||||
f.inputs[fieldName] = mkInput("production-web", 64)
|
||||
f.inputs[fieldHost] = mkInput("192.168.1.1 or example.com", 253)
|
||||
f.inputs[fieldUser] = mkInput("deploy", 64)
|
||||
f.inputs[fieldPrivateKey] = mkInput("~/.ssh/id_rsa", 512)
|
||||
f.inputs[fieldPort] = mkInput("22 (optional)", 5)
|
||||
f.inputs[fieldName].Focus()
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *addServerForm) focusField(i int) {
|
||||
for j := range f.inputs {
|
||||
f.inputs[j].Blur()
|
||||
}
|
||||
if i < len(f.inputs) {
|
||||
f.inputs[i].Focus()
|
||||
}
|
||||
f.focused = i
|
||||
}
|
||||
|
||||
func (f addServerForm) canSave() bool {
|
||||
return strings.TrimSpace(f.inputs[fieldName].Value()) != "" &&
|
||||
strings.TrimSpace(f.inputs[fieldHost].Value()) != "" &&
|
||||
strings.TrimSpace(f.inputs[fieldUser].Value()) != "" &&
|
||||
strings.TrimSpace(f.inputs[fieldPrivateKey].Value()) != ""
|
||||
}
|
||||
|
||||
func (f addServerForm) focusNext() addServerForm {
|
||||
next := f.focused + 1
|
||||
if next >= fieldCount {
|
||||
next = fieldCount - 1
|
||||
}
|
||||
f.focusField(next)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f addServerForm) focusPrev() addServerForm {
|
||||
prev := f.focused - 1
|
||||
if prev < 0 {
|
||||
prev = 0
|
||||
}
|
||||
f.focusField(prev)
|
||||
return f
|
||||
}
|
||||
@@ -2,16 +2,10 @@ package tui
|
||||
|
||||
import (
|
||||
"filepass/internal/pages"
|
||||
"filepass/internal/services"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
type configLoadedMsg struct {
|
||||
servers map[string]services.Server
|
||||
err error
|
||||
}
|
||||
|
||||
func (m TUIInterface) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
func() tea.Msg { return pages.HomePageMsg{} },
|
||||
|
||||
@@ -5,13 +5,25 @@ import (
|
||||
"filepass/internal/services"
|
||||
)
|
||||
|
||||
type page int
|
||||
|
||||
const (
|
||||
pageHome page = iota
|
||||
pageConfig
|
||||
pageAddServer
|
||||
)
|
||||
|
||||
type TUIInterface struct {
|
||||
Services *services.ServicesStore
|
||||
Page page
|
||||
MenuItems []pages.MenuItem
|
||||
Selected int
|
||||
Servers map[string]services.Server
|
||||
NoServers bool
|
||||
InitErr error
|
||||
FlashMsg string
|
||||
Form addServerForm
|
||||
FormErr string // inline field error (e.g. duplicate name)
|
||||
Quitting bool
|
||||
WindowWidth int
|
||||
WindowHeight int
|
||||
@@ -20,6 +32,7 @@ type TUIInterface struct {
|
||||
func NewTUIInterface(store *services.ServicesStore) TUIInterface {
|
||||
return TUIInterface{
|
||||
Services: store,
|
||||
Page: pageHome,
|
||||
MenuItems: pages.HomeMenuItems(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,69 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filepass/internal/pages"
|
||||
"filepass/internal/services"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
type configLoadedMsg struct {
|
||||
servers map[string]services.Server
|
||||
err error
|
||||
}
|
||||
|
||||
type serverAddedMsg struct {
|
||||
name string
|
||||
servers map[string]services.Server
|
||||
}
|
||||
|
||||
type clearFlashMsg struct{}
|
||||
|
||||
func clearFlashAfter(d time.Duration) tea.Cmd {
|
||||
return tea.Tick(d, func(time.Time) tea.Msg {
|
||||
return clearFlashMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
// isDisabled reports whether a menu item is non-interactive given current state.
|
||||
func (m TUIInterface) isDisabled(i int) bool {
|
||||
return m.MenuItems[i].RequiresServers && m.NoServers
|
||||
}
|
||||
|
||||
// nextSelectable finds the next non-disabled index in direction (+1 or -1).
|
||||
func (m TUIInterface) nextSelectable(from, dir int) int {
|
||||
i := from + dir
|
||||
for i >= 0 && i < len(m.MenuItems) {
|
||||
if !m.isDisabled(i) {
|
||||
return i
|
||||
}
|
||||
i += dir
|
||||
}
|
||||
return from
|
||||
}
|
||||
|
||||
func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case pages.HomePageMsg:
|
||||
m.Page = pageHome
|
||||
m.MenuItems = pages.HomeMenuItems()
|
||||
m.Selected = 0
|
||||
return m, nil
|
||||
|
||||
case pages.ConfigPageMsg:
|
||||
m.Page = pageConfig
|
||||
m.MenuItems = pages.ConfigMenuItems()
|
||||
m.Selected = 0
|
||||
return m, nil
|
||||
|
||||
case pages.AddServerPageMsg:
|
||||
m.Page = pageAddServer
|
||||
m.Form = newAddServerForm()
|
||||
m.FormErr = ""
|
||||
return m, nil
|
||||
|
||||
case configLoadedMsg:
|
||||
@@ -20,28 +75,58 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.NoServers = len(msg.servers) == 0
|
||||
return m, nil
|
||||
|
||||
case serverAddedMsg:
|
||||
m.Servers = msg.servers
|
||||
m.NoServers = len(msg.servers) == 0
|
||||
m.Page = pageConfig
|
||||
m.MenuItems = pages.ConfigMenuItems()
|
||||
m.Selected = 0
|
||||
m.FlashMsg = "✓ \"" + msg.name + "\" added successfully."
|
||||
return m, clearFlashAfter(2 * time.Second)
|
||||
|
||||
case clearFlashMsg:
|
||||
m.FlashMsg = ""
|
||||
return m, nil
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.WindowWidth = msg.Width
|
||||
m.WindowHeight = msg.Height
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
// add server form has its own key handling
|
||||
if m.Page == pageAddServer {
|
||||
return m.updateAddServer(msg)
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.Selected > 0 {
|
||||
m.Selected--
|
||||
}
|
||||
m.Selected = m.nextSelectable(m.Selected, -1)
|
||||
case "down", "j":
|
||||
if m.Selected < len(m.MenuItems)-1 {
|
||||
m.Selected++
|
||||
}
|
||||
m.Selected = m.nextSelectable(m.Selected, +1)
|
||||
case "enter":
|
||||
if m.MenuItems[m.Selected].Key == "exit" {
|
||||
if m.isDisabled(m.Selected) {
|
||||
return m, nil
|
||||
}
|
||||
switch m.MenuItems[m.Selected].Key {
|
||||
case "exit":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
case "config":
|
||||
return m, func() tea.Msg { return pages.ConfigPageMsg{} }
|
||||
case "back":
|
||||
return m, func() tea.Msg { return pages.HomePageMsg{} }
|
||||
case "add":
|
||||
return m, func() tea.Msg { return pages.AddServerPageMsg{} }
|
||||
// TODO: "server", "edit", "remove"
|
||||
}
|
||||
case "ctrl+c":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.Page == pageConfig {
|
||||
return m, func() tea.Msg { return pages.HomePageMsg{} }
|
||||
}
|
||||
// TODO: dispatch to server/config pages
|
||||
case "ctrl+c", "esc":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -49,3 +134,83 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m TUIInterface) updateAddServer(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
f := m.Form
|
||||
|
||||
switch msg.String() {
|
||||
case "tab", "down":
|
||||
m.Form = f.focusNext()
|
||||
return m, nil
|
||||
|
||||
case "shift+tab", "up":
|
||||
m.Form = f.focusPrev()
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
// on an input field, advance to next
|
||||
if f.focused < fieldSave {
|
||||
m.Form = f.focusNext()
|
||||
return m, nil
|
||||
}
|
||||
// on save button
|
||||
if f.focused == fieldSave {
|
||||
return m.submitAddServer()
|
||||
}
|
||||
// on back button
|
||||
if f.focused == fieldBack {
|
||||
return m, func() tea.Msg { return pages.ConfigPageMsg{} }
|
||||
}
|
||||
|
||||
case "ctrl+c":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
case "esc":
|
||||
return m, func() tea.Msg { return pages.ConfigPageMsg{} }
|
||||
}
|
||||
|
||||
// route keystrokes to the focused input
|
||||
if f.focused < len(f.inputs) {
|
||||
var cmd tea.Cmd
|
||||
f.inputs[f.focused], cmd = f.inputs[f.focused].Update(msg)
|
||||
// clear duplicate-name error when user edits the name field
|
||||
if f.focused == fieldName {
|
||||
m.FormErr = ""
|
||||
}
|
||||
m.Form = f
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m TUIInterface) submitAddServer() (tea.Model, tea.Cmd) {
|
||||
f := m.Form
|
||||
name := strings.TrimSpace(f.inputs[fieldName].Value())
|
||||
|
||||
if !f.canSave() {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.Services.Config.HasServer(name) {
|
||||
m.FormErr = "✗ \"" + name + "\" already exists."
|
||||
return m, nil
|
||||
}
|
||||
|
||||
s := services.Server{
|
||||
Host: strings.TrimSpace(f.inputs[fieldHost].Value()),
|
||||
User: strings.TrimSpace(f.inputs[fieldUser].Value()),
|
||||
PrivateKey: strings.TrimSpace(f.inputs[fieldPrivateKey].Value()),
|
||||
Port: strings.TrimSpace(f.inputs[fieldPort].Value()),
|
||||
}
|
||||
|
||||
if err := m.Services.Config.AddServer(name, s); err != nil {
|
||||
m.FormErr = "✗ " + err.Error()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, func() tea.Msg {
|
||||
return serverAddedMsg{name: name, servers: m.Services.Config.Servers()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,17 @@ func footerSep() string {
|
||||
return styles.FooterSepStyle.Render(" · ")
|
||||
}
|
||||
|
||||
func (m TUIInterface) subtitle() string {
|
||||
switch m.Page {
|
||||
case pageConfig:
|
||||
return "Configuration"
|
||||
case pageAddServer:
|
||||
return "Add Server"
|
||||
default:
|
||||
return "Secure file transfer"
|
||||
}
|
||||
}
|
||||
|
||||
func (m TUIInterface) View() tea.View {
|
||||
if m.Quitting {
|
||||
return tea.NewView("")
|
||||
@@ -31,49 +42,41 @@ func (m TUIInterface) View() tea.View {
|
||||
h = 24
|
||||
}
|
||||
|
||||
// menu rows
|
||||
var menuRows []string
|
||||
for i, item := range m.MenuItems {
|
||||
menuRows = append(menuRows, styles.MenuItemStyle(i == m.Selected).Render(item.Label))
|
||||
}
|
||||
menu := lipgloss.JoinVertical(lipgloss.Left, menuRows...)
|
||||
|
||||
// status line — error takes priority over no-servers hint
|
||||
var statusLine string
|
||||
switch {
|
||||
case m.InitErr != nil:
|
||||
statusLine = styles.StatusErrStyle.Render("✗ " + m.InitErr.Error())
|
||||
case m.NoServers:
|
||||
statusLine = styles.StatusWarnStyle.Render("⚠ No servers configured. Select Config to add one.")
|
||||
var body string
|
||||
switch m.Page {
|
||||
case pageAddServer:
|
||||
body = m.viewAddServer()
|
||||
default:
|
||||
body = m.viewMenu()
|
||||
}
|
||||
|
||||
// top content
|
||||
innerRows := []string{
|
||||
header := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.CardTitleStyle.Render("✦ filepass"),
|
||||
styles.CardSubtitleStyle.Render("Secure file transfer"),
|
||||
menu,
|
||||
}
|
||||
if statusLine != "" {
|
||||
innerRows = append(innerRows, statusLine)
|
||||
}
|
||||
topContent := styles.CardInnerStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left, innerRows...),
|
||||
styles.CardSubtitleStyle.Render(m.subtitle()),
|
||||
)
|
||||
|
||||
// footer
|
||||
hints := footerHint("↑↓", "navigate") +
|
||||
footerSep() +
|
||||
footerHint("enter", "select") +
|
||||
footerSep() +
|
||||
footerHint("esc", "quit")
|
||||
footer := styles.FooterStyle.Render(hints)
|
||||
topContent := styles.CardInnerStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left, header, body),
|
||||
)
|
||||
|
||||
var footerStr string
|
||||
if m.Page == pageAddServer {
|
||||
footerStr = footerHint("tab/↑↓", "navigate") +
|
||||
footerSep() +
|
||||
footerHint("enter", "confirm") +
|
||||
footerSep() +
|
||||
footerHint("esc", "back")
|
||||
} else {
|
||||
footerStr = footerHint("↑↓", "navigate") +
|
||||
footerSep() +
|
||||
footerHint("enter", "select") +
|
||||
footerSep() +
|
||||
footerHint("esc", "quit")
|
||||
}
|
||||
footer := styles.FooterStyle.Render(footerStr)
|
||||
|
||||
// card
|
||||
card := styles.CardStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left,
|
||||
topContent,
|
||||
footer,
|
||||
),
|
||||
lipgloss.JoinVertical(lipgloss.Left, topContent, footer),
|
||||
)
|
||||
|
||||
cardHeight := lipgloss.Height(card)
|
||||
@@ -89,3 +92,63 @@ func (m TUIInterface) View() tea.View {
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m TUIInterface) viewMenu() string {
|
||||
var menuRows []string
|
||||
for i, item := range m.MenuItems {
|
||||
disabled := m.isDisabled(i)
|
||||
menuRows = append(menuRows, styles.MenuItemStyle(i == m.Selected, disabled).Render(item.Label))
|
||||
}
|
||||
menu := lipgloss.JoinVertical(lipgloss.Left, menuRows...)
|
||||
|
||||
var statusLine string
|
||||
switch {
|
||||
case m.InitErr != nil:
|
||||
statusLine = styles.StatusErrStyle.Render("✗ " + m.InitErr.Error())
|
||||
case m.NoServers && m.Page == pageHome:
|
||||
statusLine = styles.StatusWarnStyle.Render("⚠ No servers configured. Select Config to add one.")
|
||||
case m.FlashMsg != "" && m.Page == pageConfig:
|
||||
statusLine = styles.StatusOKStyle.Render(m.FlashMsg)
|
||||
}
|
||||
|
||||
if statusLine != "" {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, menu, statusLine)
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
func (m TUIInterface) viewAddServer() string {
|
||||
f := m.Form
|
||||
labels := []string{"Name", "Host", "User", "Private Key Path", "Port"}
|
||||
required := []bool{true, true, true, true, false}
|
||||
|
||||
var rows []string
|
||||
for i, label := range labels {
|
||||
lbl := styles.FieldLabelStyle(required[i]).Render(label)
|
||||
input := f.inputs[i].View()
|
||||
rows = append(rows, lipgloss.JoinVertical(lipgloss.Left, lbl, input))
|
||||
}
|
||||
form := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
|
||||
// required legend
|
||||
legend := styles.FieldLegendStyle.Render("* required")
|
||||
|
||||
// form error (duplicate name, etc.)
|
||||
var errLine string
|
||||
if m.FormErr != "" {
|
||||
errLine = styles.StatusErrStyle.Render(m.FormErr)
|
||||
}
|
||||
|
||||
// save / back buttons
|
||||
saveBtn := styles.ButtonStyle(f.focused == fieldSave, f.canSave()).Render("Save")
|
||||
backBtn := styles.ButtonStyle(f.focused == fieldBack, true).Render("Back")
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Top, saveBtn, " ", backBtn)
|
||||
|
||||
parts := []string{form, legend}
|
||||
if errLine != "" {
|
||||
parts = append(parts, errLine)
|
||||
}
|
||||
parts = append(parts, buttons)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user