Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(form): add strict group backtracking option #541

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/groupBacktracking/loose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Loose Group Backtracking

<img width="800" src="./loose.gif" />
Binary file added examples/groupBacktracking/loose/loose.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions examples/groupBacktracking/loose/loose.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# VHS documentation
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
# Set Theme <json|string> Set the theme of the terminal
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
# Set BorderRadius <number> Set terminal border radius, in pixels.
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Escape[@<time>] [number] Press the Escape key
# Backspace[@<time>] [number] Press the Backspace key
# Delete[@<time>] [number] Press the Delete key
# Insert[@<time>] [number] Press the Insert key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# PageUp[@<time>] [number] Press the Page Up key
# PageDown[@<time>] [number] Press the Page Down key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output

Output loose.gif

Require go

Set Shell "bash"
Set FontSize 32
Set Width 2000
Set Height 1200

Type "go run ./main.go" Sleep 500ms Enter Sleep 4s
Enter Sleep 4s
Enter Sleep 2s
Type "Loose strictness allows backtracking" Enter Sleep 6s
Shift+Tab Sleep 6s
Shift+Tab Sleep 6s
Down Sleep 2s

Sleep 5s
296 changes: 296 additions & 0 deletions examples/groupBacktracking/loose/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package main

import (
"fmt"
"os"
"strings"

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

const maxWidth = 80

var (
red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"}
indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
)

type Styles struct {
Base,
HeaderText,
Status,
StatusHeader,
Highlight,
ErrorHeaderText,
Help lipgloss.Style
}

func NewStyles(lg *lipgloss.Renderer) *Styles {
s := Styles{}
s.Base = lg.NewStyle().
Padding(1, 4, 0, 1)
s.HeaderText = lg.NewStyle().
Foreground(indigo).
Bold(true).
Padding(0, 1, 0, 2)
s.Status = lg.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(indigo).
PaddingLeft(1).
MarginTop(1)
s.StatusHeader = lg.NewStyle().
Foreground(green).
Bold(true)
s.Highlight = lg.NewStyle().
Foreground(lipgloss.Color("212"))
s.ErrorHeaderText = s.HeaderText.
Foreground(red)
s.Help = lg.NewStyle().
Foreground(lipgloss.Color("240"))
return &s
}

type state int

const (
statusNormal state = iota
stateDone
)

type Model struct {
state state
lg *lipgloss.Renderer
styles *Styles
form *huh.Form
width int
}

func NewModel() Model {
m := Model{width: maxWidth}
m.lg = lipgloss.DefaultRenderer()
m.styles = NewStyles(m.lg)

m.form = huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Key("class").
Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
Title("Choose your class").
Description("This will determine your department"),

huh.NewSelect[string]().
Key("level").
Options(huh.NewOptions("1", "20", "9999")...).
Title("Choose your level").
Description("This will determine your benefits package"),
),
huh.NewGroup(
huh.NewText().
Key("notes").
Title("Additional Notes").
Placeholder("Optional"),

huh.NewConfirm().
Key("done").
Title("All done?").
Validate(func(v bool) error {
if !v {
return fmt.Errorf("Welp, finish up then")
}
return nil
}).
Affirmative("Yep").
Negative("Wait, no"),
),
).
WithWidth(45).
WithShowHelp(false).
WithShowErrors(false).
WithStrictGroupBactracking(false)
return m
}

func (m Model) Init() tea.Cmd {
return m.form.Init()
}

func min(x, y int) int {
if x > y {
return y
}
return x
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize()
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Interrupt
case "esc", "q":
return m, tea.Quit
}
}

var cmds []tea.Cmd

// Process the form
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.form = f
cmds = append(cmds, cmd)
}

if m.form.State == huh.StateCompleted {
// Quit when the form is done.
cmds = append(cmds, tea.Quit)
}

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

func (m Model) View() string {
s := m.styles

switch m.form.State {
case huh.StateCompleted:
title, role := m.getRole()
title = s.Highlight.Render(title)
var b strings.Builder
fmt.Fprintf(&b, "Congratulations, you’re Charm’s newest\n%s!\n\n", title)
fmt.Fprintf(&b, "Your job description is as follows:\n\n%s\n\nPlease proceed to HR immediately.", role)
return s.Status.Margin(0, 1).Padding(1, 2).Width(48).Render(b.String()) + "\n\n"
default:

var class string
if m.form.GetString("class") != "" {
class = "Class: " + m.form.GetString("class")
}

// Form (left side)
v := strings.TrimSuffix(m.form.View(), "\n\n")
form := m.lg.NewStyle().Margin(1, 0).Render(v)

// Status (right side)
var status string
{
var (
buildInfo = "(None)"
role string
jobDescription string
level string
)

if m.form.GetString("level") != "" {
level = "Level: " + m.form.GetString("level")
role, jobDescription = m.getRole()
role = "\n\n" + s.StatusHeader.Render("Projected Role") + "\n" + role
jobDescription = "\n\n" + s.StatusHeader.Render("Duties") + "\n" + jobDescription
}
if m.form.GetString("class") != "" {
buildInfo = fmt.Sprintf("%s\n%s", class, level)
}

const statusWidth = 28
statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()
status = s.Status.
Height(lipgloss.Height(form)).
Width(statusWidth).
MarginLeft(statusMarginLeft).
Render(s.StatusHeader.Render("Current Build") + "\n" +
buildInfo +
role +
jobDescription)
}

errors := m.form.Errors()
header := m.appBoundaryView("Charm Employment Application")
if len(errors) > 0 {
header = m.appErrorBoundaryView(m.errorView())
}
body := lipgloss.JoinHorizontal(lipgloss.Top, form, status)

footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))
if len(errors) > 0 {
footer = m.appErrorBoundaryView("")
}

return s.Base.Render(header + "\n" + body + "\n\n" + footer)
}
}

func (m Model) errorView() string {
var s string
for _, err := range m.form.Errors() {
s += err.Error()
}
return s
}

func (m Model) appBoundaryView(text string) string {
return lipgloss.PlaceHorizontal(
m.width,
lipgloss.Left,
m.styles.HeaderText.Render(text),
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceForeground(indigo),
)
}

func (m Model) appErrorBoundaryView(text string) string {
return lipgloss.PlaceHorizontal(
m.width,
lipgloss.Left,
m.styles.ErrorHeaderText.Render(text),
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceForeground(red),
)
}

func (m Model) getRole() (string, string) {
level := m.form.GetString("level")
switch m.form.GetString("class") {
case "Warrior":
switch level {
case "1":
return "Tank Intern", "Assists with tank-related activities. Paid position."
case "9999":
return "Tank Manager", "Manages tanks and tank-related activities."
default:
return "Tank", "General tank. Does damage, takes damage. Responsible for tanking."
}
case "Mage":
switch level {
case "1":
return "DPS Associate", "Finds DPS deals and passes them on to DPS Manager."
case "9999":
return "DPS Operating Officer", "Oversees all DPS activities."
default:
return "DPS", "Does damage and ideally does not take damage. Logs hours in JIRA."
}
case "Rogue":
switch level {
case "1":
return "Stealth Junior Designer", "Designs rougue-like activities. Reports to Stealth Lead."
case "9999":
return "Stealth Lead", "Lead designer for all things stealth. Some travel required."
default:
return "Sneaky Person", "Sneaks around and does sneaky things. Reports to Stealth Lead."
}
default:
return "", ""
}
}

func main() {
_, err := tea.NewProgram(NewModel()).Run()
if err != nil {
fmt.Println("Oh no:", err)
os.Exit(1)
}
}
3 changes: 3 additions & 0 deletions examples/groupBacktracking/strict/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Strict Group Backtracking

<img width="800" src="./strict.gif" />
Loading