rebuild EventForm with bubbletea instead of huh #8

Open
opened 2025-07-27 19:50:32 +00:00 by specCon18 · 3 comments
Owner

this way we can turn it into a proper view and reuse it for editing events

this way we can turn it into a proper view and reuse it for editing events
specCon18 added this to the Event System project 2025-07-27 19:52:03 +00:00
Author
Owner

forms are built in testbed need integrated into project.

forms are built in testbed need integrated into project.
Author
Owner

new-event.go

package main

import (
        "fmt"
        "os"
        "strings"

        "github.com/charmbracelet/bubbles/textinput"
        tea "github.com/charmbracelet/bubbletea"
        "github.com/charmbracelet/lipgloss"
)

type model struct {
        inputs    []textinput.Model
        focusIdx  int
        submitted bool
}

func initialModel() model {
        labels := []string{
                "Title", "Description", "Year", "Month", "Day",
                "Start Hour", "End Hour", "Color",
        }

        inputs := make([]textinput.Model, len(labels))
        for i, label := range labels {
                ti := textinput.New()
                ti.Placeholder = label
                ti.CharLimit = 64
                ti.Width = 40
                if i == 0 {
                        ti.Focus()
                } else {
                        ti.Blur()
                }
                inputs[i] = ti
        }

        return model{
                inputs:    inputs,
                focusIdx:  0,
                submitted: false,
        }
}

func (m model) Init() tea.Cmd {
        return textinput.Blink
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        if m.submitted {
                return m, tea.Quit
        }

        switch msg := msg.(type) {
        case tea.KeyMsg:
                switch msg.String() {
                case "ctrl+c", "esc":
                        return m, tea.Quit

                case "enter", "tab":
                        if m.focusIdx == len(m.inputs)-1 {
                                m.inputs[m.focusIdx].Blur()
                                m.submitted = true
                                return m, tea.Quit
                        }
                        m.inputs[m.focusIdx].Blur()
                        m.focusIdx = (m.focusIdx + 1) % len(m.inputs)
                        m.inputs[m.focusIdx].Focus()
                        return m, nil

                case "shift+tab":
                        m.inputs[m.focusIdx].Blur()
                        if m.focusIdx == 0 {
                                m.focusIdx = len(m.inputs) - 1
                        } else {
                                m.focusIdx--
                        }
                        m.inputs[m.focusIdx].Focus()
                        return m, nil
                }
        }

        cmds := make([]tea.Cmd, len(m.inputs))
        for i := range m.inputs {
                m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
        }

        return m, tea.Batch(cmds...)
}

var (
        baseStyle = lipgloss.NewStyle().
                        Border(lipgloss.NormalBorder()).
                        Padding(0, 1)

        focusedStyle = baseStyle.Copy().
                        BorderForeground(lipgloss.Color("62")) // Cyan for focus

        headerStyle = lipgloss.NewStyle().
                        Bold(true).
                        Underline(true).
                        MarginBottom(1)

        instructionStyle = lipgloss.NewStyle().
                                Foreground(lipgloss.Color("240")).
                                MarginTop(1)
)

func styleInput(input textinput.Model, focused bool) string {
        if focused {
                return focusedStyle.Render(input.View())
        }
        return baseStyle.Render(input.View())
}

func (m model) View() string {
        if m.submitted {
                var b strings.Builder
                fmt.Fprintf(&b, "Submitted Event:\n\n")
                fields := []string{"Title", "Description", "Year", "Month", "Day", "StartHour", "EndHour", "Color"}
                for i, input := range m.inputs {
                        fmt.Fprintf(&b, "%s: %s\n", fields[i], input.Value())
                }
                return b.String()
        }

        inputViews := make([]string, len(m.inputs))
        for i, input := range m.inputs {
                inputViews[i] = styleInput(input, i == m.focusIdx)
        }

        formView := lipgloss.JoinVertical(lipgloss.Left, inputViews...)

        var b strings.Builder
        b.WriteString(headerStyle.Render("Create New Event"))
        b.WriteString("\n")
        b.WriteString(formView)
        b.WriteString(instructionStyle.Render("\n[Tab]/[Enter] → Next • [Shift+Tab] → Prev • [Esc] to Quit\n"))

        return b.String()
}

func main() {
        if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
                fmt.Println("Error running program:", err)
                os.Exit(1)
        }
}

update.go

package main

import (
        "fmt"
        "os"
        "strings"

        "github.com/charmbracelet/bubbles/textinput"
        tea "github.com/charmbracelet/bubbletea"
        "github.com/charmbracelet/lipgloss"
)

type model struct {
        inputs    []textinput.Model
        focusIdx  int
        submitted bool
}

func initialModel() model {
        labels := []string{
                "Title", "Description", "End Hour", "Color",
        }

        inputs := make([]textinput.Model, len(labels))
        for i, label := range labels {
                ti := textinput.New()
                ti.Placeholder = label
                ti.CharLimit = 64
                ti.Width = 40
                if i == 0 {
                        ti.Focus()
                } else {
                        ti.Blur()
                }
                inputs[i] = ti
        }

        return model{
                inputs:    inputs,
                focusIdx:  0,
                submitted: false,
        }
}

func (m model) Init() tea.Cmd {
        return textinput.Blink
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        if m.submitted {
                return m, tea.Quit
        }

        switch msg := msg.(type) {
        case tea.KeyMsg:
                switch msg.String() {
                case "ctrl+c", "esc":
                        return m, tea.Quit

                case "enter", "tab":
                        if m.focusIdx == len(m.inputs)-1 {
                                m.inputs[m.focusIdx].Blur()
                                m.submitted = true
                                return m, tea.Quit
                        }
                        m.inputs[m.focusIdx].Blur()
                        m.focusIdx = (m.focusIdx + 1) % len(m.inputs)
                        m.inputs[m.focusIdx].Focus()
                        return m, nil

                case "shift+tab":
                        m.inputs[m.focusIdx].Blur()
                        if m.focusIdx == 0 {
                                m.focusIdx = len(m.inputs) - 1
                        } else {
                                m.focusIdx--
                        }
                        m.inputs[m.focusIdx].Focus()
                        return m, nil
                }
        }

        cmds := make([]tea.Cmd, len(m.inputs))
        for i := range m.inputs {
                m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
        }

        return m, tea.Batch(cmds...)
}

var (
        baseStyle = lipgloss.NewStyle().
                        Border(lipgloss.NormalBorder()).
                        Padding(0, 1)

        focusedStyle = baseStyle.Copy().
                        BorderForeground(lipgloss.Color("62")) // Cyan focus border

        headerStyle = lipgloss.NewStyle().
                        Bold(true).
                        Underline(true).
                        MarginBottom(1)

        instructionStyle = lipgloss.NewStyle().
                                Foreground(lipgloss.Color("240")).
                                MarginTop(1)
)

func styleInput(input textinput.Model, focused bool) string {
        if focused {
                return focusedStyle.Render(input.View())
        }
        return baseStyle.Render(input.View())
}

func (m model) View() string {
        if m.submitted {
                var b strings.Builder
                fmt.Fprintf(&b, "Edited Event:\n\n")
                fields := []string{"Title", "Description", "EndHour", "Color"}
                for i, input := range m.inputs {
                        fmt.Fprintf(&b, "%s: %s\n", fields[i], input.Value())
                }
                return b.String()
        }

        inputViews := make([]string, len(m.inputs))
        for i, input := range m.inputs {
                inputViews[i] = styleInput(input, i == m.focusIdx)
        }

        formView := lipgloss.JoinVertical(lipgloss.Left, inputViews...)

        var b strings.Builder
        b.WriteString(headerStyle.Render("Edit Event"))
        b.WriteString("\n")
        b.WriteString(formView)
        b.WriteString(instructionStyle.Render("\n[Tab]/[Enter] → Next • [Shift+Tab] → Prev • [Esc] to Quit\n"))

        return b.String()
}

func main() {
        if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
                fmt.Println("Error running program:", err)
                os.Exit(1)
        }
}

delete.go

package main

import (
        "fmt"
        "os"

        tea "github.com/charmbracelet/bubbletea"
        "github.com/charmbracelet/lipgloss"
)

type choice int

const (
        no choice = iota
        yes
)

type model struct {
        cursor    choice
        confirmed bool
}

func initialModel() model {
        return model{
                cursor:    no,
                confirmed: false,
        }
}

func (m model) Init() tea.Cmd {
        return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        if m.confirmed {
                return m, tea.Quit
        }

        switch msg := msg.(type) {
        case tea.KeyMsg:
                switch msg.String() {
                case "ctrl+c", "esc":
                        m.cursor = no
                        m.confirmed = true
                        return m, tea.Quit
                case "left", "shift+tab":
                        m.cursor = (m.cursor - 1 + 2) % 2
                case "right", "tab":
                        m.cursor = (m.cursor + 1) % 2
                case "enter":
                        m.confirmed = true
                        return m, tea.Quit
                }
        }
        return m, nil
}

func (m model) View() string {
        if m.confirmed {
                if m.cursor == yes {
                        return "Event deleted.\n"
                }
                return "Canceled deletion.\n"
        }

        yesStyle := lipgloss.NewStyle().
                Border(lipgloss.NormalBorder()).
                Padding(0, 2).
                Margin(1, 1)

        noStyle := yesStyle.Copy()

        if m.cursor == yes {
                yesStyle = yesStyle.BorderForeground(lipgloss.Color("1")) // red
        } else {
                noStyle = noStyle.BorderForeground(lipgloss.Color("1")) // red
        }

        prompt := lipgloss.NewStyle().
                Bold(true).
                MarginBottom(1).
                Render("Are you sure you want to delete this event?")

        buttons := lipgloss.JoinHorizontal(lipgloss.Top,
                yesStyle.Render("Yes"),
                noStyle.Render("No"),
        )

        return prompt + "\n" + buttons + "\n\n[←/→ or Tab] Select • [Enter] Confirm • [Esc] Cancel"
}

func main() {
        if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
                fmt.Println("Error:", err)
                os.Exit(1)
        }
}
``
**new-event.go** ```go package main import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type model struct { inputs []textinput.Model focusIdx int submitted bool } func initialModel() model { labels := []string{ "Title", "Description", "Year", "Month", "Day", "Start Hour", "End Hour", "Color", } inputs := make([]textinput.Model, len(labels)) for i, label := range labels { ti := textinput.New() ti.Placeholder = label ti.CharLimit = 64 ti.Width = 40 if i == 0 { ti.Focus() } else { ti.Blur() } inputs[i] = ti } return model{ inputs: inputs, focusIdx: 0, submitted: false, } } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.submitted { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit case "enter", "tab": if m.focusIdx == len(m.inputs)-1 { m.inputs[m.focusIdx].Blur() m.submitted = true return m, tea.Quit } m.inputs[m.focusIdx].Blur() m.focusIdx = (m.focusIdx + 1) % len(m.inputs) m.inputs[m.focusIdx].Focus() return m, nil case "shift+tab": m.inputs[m.focusIdx].Blur() if m.focusIdx == 0 { m.focusIdx = len(m.inputs) - 1 } else { m.focusIdx-- } m.inputs[m.focusIdx].Focus() return m, nil } } cmds := make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } return m, tea.Batch(cmds...) } var ( baseStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). Padding(0, 1) focusedStyle = baseStyle.Copy(). BorderForeground(lipgloss.Color("62")) // Cyan for focus headerStyle = lipgloss.NewStyle(). Bold(true). Underline(true). MarginBottom(1) instructionStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). MarginTop(1) ) func styleInput(input textinput.Model, focused bool) string { if focused { return focusedStyle.Render(input.View()) } return baseStyle.Render(input.View()) } func (m model) View() string { if m.submitted { var b strings.Builder fmt.Fprintf(&b, "Submitted Event:\n\n") fields := []string{"Title", "Description", "Year", "Month", "Day", "StartHour", "EndHour", "Color"} for i, input := range m.inputs { fmt.Fprintf(&b, "%s: %s\n", fields[i], input.Value()) } return b.String() } inputViews := make([]string, len(m.inputs)) for i, input := range m.inputs { inputViews[i] = styleInput(input, i == m.focusIdx) } formView := lipgloss.JoinVertical(lipgloss.Left, inputViews...) var b strings.Builder b.WriteString(headerStyle.Render("Create New Event")) b.WriteString("\n") b.WriteString(formView) b.WriteString(instructionStyle.Render("\n[Tab]/[Enter] → Next • [Shift+Tab] → Prev • [Esc] to Quit\n")) return b.String() } func main() { if _, err := tea.NewProgram(initialModel()).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } ``` **update.go** ```go package main import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type model struct { inputs []textinput.Model focusIdx int submitted bool } func initialModel() model { labels := []string{ "Title", "Description", "End Hour", "Color", } inputs := make([]textinput.Model, len(labels)) for i, label := range labels { ti := textinput.New() ti.Placeholder = label ti.CharLimit = 64 ti.Width = 40 if i == 0 { ti.Focus() } else { ti.Blur() } inputs[i] = ti } return model{ inputs: inputs, focusIdx: 0, submitted: false, } } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.submitted { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit case "enter", "tab": if m.focusIdx == len(m.inputs)-1 { m.inputs[m.focusIdx].Blur() m.submitted = true return m, tea.Quit } m.inputs[m.focusIdx].Blur() m.focusIdx = (m.focusIdx + 1) % len(m.inputs) m.inputs[m.focusIdx].Focus() return m, nil case "shift+tab": m.inputs[m.focusIdx].Blur() if m.focusIdx == 0 { m.focusIdx = len(m.inputs) - 1 } else { m.focusIdx-- } m.inputs[m.focusIdx].Focus() return m, nil } } cmds := make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } return m, tea.Batch(cmds...) } var ( baseStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). Padding(0, 1) focusedStyle = baseStyle.Copy(). BorderForeground(lipgloss.Color("62")) // Cyan focus border headerStyle = lipgloss.NewStyle(). Bold(true). Underline(true). MarginBottom(1) instructionStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). MarginTop(1) ) func styleInput(input textinput.Model, focused bool) string { if focused { return focusedStyle.Render(input.View()) } return baseStyle.Render(input.View()) } func (m model) View() string { if m.submitted { var b strings.Builder fmt.Fprintf(&b, "Edited Event:\n\n") fields := []string{"Title", "Description", "EndHour", "Color"} for i, input := range m.inputs { fmt.Fprintf(&b, "%s: %s\n", fields[i], input.Value()) } return b.String() } inputViews := make([]string, len(m.inputs)) for i, input := range m.inputs { inputViews[i] = styleInput(input, i == m.focusIdx) } formView := lipgloss.JoinVertical(lipgloss.Left, inputViews...) var b strings.Builder b.WriteString(headerStyle.Render("Edit Event")) b.WriteString("\n") b.WriteString(formView) b.WriteString(instructionStyle.Render("\n[Tab]/[Enter] → Next • [Shift+Tab] → Prev • [Esc] to Quit\n")) return b.String() } func main() { if _, err := tea.NewProgram(initialModel()).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } ``` **delete.go** ```go package main import ( "fmt" "os" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type choice int const ( no choice = iota yes ) type model struct { cursor choice confirmed bool } func initialModel() model { return model{ cursor: no, confirmed: false, } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.confirmed { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": m.cursor = no m.confirmed = true return m, tea.Quit case "left", "shift+tab": m.cursor = (m.cursor - 1 + 2) % 2 case "right", "tab": m.cursor = (m.cursor + 1) % 2 case "enter": m.confirmed = true return m, tea.Quit } } return m, nil } func (m model) View() string { if m.confirmed { if m.cursor == yes { return "Event deleted.\n" } return "Canceled deletion.\n" } yesStyle := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). Padding(0, 2). Margin(1, 1) noStyle := yesStyle.Copy() if m.cursor == yes { yesStyle = yesStyle.BorderForeground(lipgloss.Color("1")) // red } else { noStyle = noStyle.BorderForeground(lipgloss.Color("1")) // red } prompt := lipgloss.NewStyle(). Bold(true). MarginBottom(1). Render("Are you sure you want to delete this event?") buttons := lipgloss.JoinHorizontal(lipgloss.Top, yesStyle.Render("Yes"), noStyle.Render("No"), ) return prompt + "\n" + buttons + "\n\n[←/→ or Tab] Select • [Enter] Confirm • [Esc] Cancel" } func main() { if _, err := tea.NewProgram(initialModel()).Run(); err != nil { fmt.Println("Error:", err) os.Exit(1) } } ``
Author
Owner

after disscussion with sky new and update need rendered to the right of hourly view by using views compose-ability and auto-focus to switch to and from them delete should be its own view switched to.

after disscussion with sky new and update need rendered to the right of hourly view by using views compose-ability and auto-focus to switch to and from them delete should be its own view switched to.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: SK-Development-Studios/GoCalTui#8
No description provided.