Skip to content

Commit

Permalink
Layouts (#274)
Browse files Browse the repository at this point in the history
* feat: multicolumn layout forms

* feat: form layouts

* fix: linting errors

* feat: grid layout

* fix: blur causes validation

* fix: lint

---------

Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
  • Loading branch information
maaslalani and adamdottv committed Jun 5, 2024
1 parent 4cb2312 commit 463dcbc
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 7 deletions.
25 changes: 25 additions & 0 deletions examples/layout/columns/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import "github.com/charmbracelet/huh"

func main() {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("First"),
huh.NewInput().Title("Second"),
huh.NewInput().Title("Third"),
),
huh.NewGroup(
huh.NewInput().Title("Fourth"),
huh.NewInput().Title("Fifth"),
huh.NewInput().Title("Sixth"),
),
huh.NewGroup(
huh.NewInput().Title("Seventh"),
huh.NewInput().Title("Eigth"),
huh.NewInput().Title("Nineth"),
huh.NewInput().Title("Tenth"),
),
).WithLayout(huh.LayoutColumns(2))
form.Run()
}
25 changes: 25 additions & 0 deletions examples/layout/default/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import "github.com/charmbracelet/huh"

func main() {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("First"),
huh.NewInput().Title("Second"),
huh.NewInput().Title("Third"),
),
huh.NewGroup(
huh.NewInput().Title("Fourth"),
huh.NewInput().Title("Fifth"),
huh.NewInput().Title("Sixth"),
),
huh.NewGroup(
huh.NewInput().Title("Seventh"),
huh.NewInput().Title("Eigth"),
huh.NewInput().Title("Nineth"),
huh.NewInput().Title("Tenth"),
),
)
form.Run()
}
30 changes: 30 additions & 0 deletions examples/layout/grid/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import "github.com/charmbracelet/huh"

func main() {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("First"),
huh.NewInput().Title("Second"),
huh.NewInput().Title("Third"),
),
huh.NewGroup(
huh.NewInput().Title("Fourth"),
huh.NewInput().Title("Fifth"),
huh.NewInput().Title("Sixth"),
),
huh.NewGroup(
huh.NewInput().Title("Seventh"),
huh.NewInput().Title("Eigth"),
huh.NewInput().Title("Nineth"),
huh.NewInput().Title("Tenth"),
),
huh.NewGroup(
huh.NewInput().Title("Eleventh"),
huh.NewInput().Title("Twelveth"),
huh.NewInput().Title("Thirteenth"),
),
).WithLayout(huh.LayoutGrid(2, 2))
form.Run()
}
25 changes: 25 additions & 0 deletions examples/layout/stack/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import "github.com/charmbracelet/huh"

func main() {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("First"),
huh.NewInput().Title("Second"),
huh.NewInput().Title("Third"),
),
huh.NewGroup(
huh.NewInput().Title("Fourth"),
huh.NewInput().Title("Fifth"),
huh.NewInput().Title("Sixth"),
),
huh.NewGroup(
huh.NewInput().Title("Seventh"),
huh.NewInput().Title("Eigth"),
huh.NewInput().Title("Nineth"),
huh.NewInput().Title("Tenth"),
),
).WithLayout(huh.LayoutStack)
form.Run()
}
2 changes: 1 addition & 1 deletion field_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (t *Text) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, t.keymap.Editor):
ext := strings.TrimPrefix(t.editorExtension, ".")
tmpFile, _ := os.CreateTemp(os.TempDir(), "*."+ext)
cmd := exec.Command(t.editorCmd, append(t.editorArgs, tmpFile.Name())...)
cmd := exec.Command(t.editorCmd, append(t.editorArgs, tmpFile.Name())...) //nolint

Check failure on line 218 in field_text.go

View workflow job for this annotation

GitHub Actions / lint-soft

directive `//nolint` is unused (nolintlint)
_ = os.WriteFile(tmpFile.Name(), []byte(t.textarea.Value()), 0600)
cmds = append(cmds, tea.ExecProcess(cmd, func(error) tea.Msg {
content, _ := os.ReadFile(tmpFile.Name())
Expand Down
22 changes: 20 additions & 2 deletions form.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type Form struct {
height int
keymap *KeyMap
teaOptions []tea.ProgramOption

layout Layout
}

// NewForm returns a form with the given groups and default themes and
Expand All @@ -78,6 +80,7 @@ func NewForm(groups ...*Group) *Form {
paginator: p,
keymap: NewDefaultKeyMap(),
results: make(map[string]any),
layout: LayoutDefault,
teaOptions: []tea.ProgramOption{
tea.WithOutput(os.Stderr),
},
Expand Down Expand Up @@ -266,6 +269,7 @@ func (f *Form) WithWidth(width int) *Form {
}
f.width = width
for _, group := range f.groups {
width := f.layout.GroupWidth(f, group, width)
group.WithWidth(width)
}
return f
Expand Down Expand Up @@ -301,6 +305,14 @@ func (f *Form) WithProgramOptions(opts ...tea.ProgramOption) *Form {
return f
}

// WithLayout sets the layout on a form.
//
// This allows customization of the form group layout.
func (f *Form) WithLayout(layout Layout) *Form {
f.layout = layout
return f
}

// UpdateFieldPositions sets the position on all the fields.
func (f *Form) UpdateFieldPositions() *Form {
firstGroup := 0
Expand Down Expand Up @@ -431,6 +443,9 @@ func (f *Form) PrevField() tea.Cmd {
func (f *Form) Init() tea.Cmd {
cmds := make([]tea.Cmd, len(f.groups))
for i, group := range f.groups {
if i == 0 {
group.active = true
}
cmds[i] = group.Init()
}

Expand All @@ -457,7 +472,8 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
break
}
for _, group := range f.groups {
group.WithWidth(msg.Width)
width := f.layout.GroupWidth(f, group, msg.Width)
group.WithWidth(width)
}
if f.height > 0 {
break
Expand Down Expand Up @@ -507,6 +523,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return submit()
}
}
f.groups[f.paginator.Page].active = true
return f, f.groups[f.paginator.Page].Init()

case prevGroupMsg:
Expand All @@ -521,6 +538,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

f.groups[f.paginator.Page].active = true
return f, f.groups[f.paginator.Page].Init()
}

Expand Down Expand Up @@ -551,7 +569,7 @@ func (f *Form) View() string {
return ""
}

return f.groups[f.paginator.Page].View()
return f.layout.View(f)
}

// Run runs the form.
Expand Down
31 changes: 27 additions & 4 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Group struct {
height int
keymap *KeyMap
hide func() bool
active bool
}

// NewGroup returns a new group with the given fields.
Expand All @@ -53,6 +54,7 @@ func NewGroup(fields ...Field) *Group {
help: help.New(),
showHelp: true,
showErrors: true,
active: false,
}

height := group.fullHeight()
Expand Down Expand Up @@ -190,8 +192,10 @@ func (g *Group) Init() tea.Cmd {
return tea.Batch(cmds...)
}

cmd := g.fields[g.paginator.Page].Focus()
cmds = append(cmds, cmd)
if g.active {
cmd := g.fields[g.paginator.Page].Focus()
cmds = append(cmds, cmd)
}
g.buildView()
return tea.Batch(cmds...)
}
Expand Down Expand Up @@ -261,7 +265,7 @@ func (g *Group) fullHeight() int {
return height
}

func (g *Group) buildView() {
func (g *Group) getContent() (int, string) {
var fields strings.Builder
offset := 0
gap := "\n\n"
Expand All @@ -282,14 +286,33 @@ func (g *Group) buildView() {
}
}

g.viewport.SetContent(fields.String() + "\n")
return offset, fields.String() + "\n"
}

func (g *Group) buildView() {
offset, content := g.getContent()

g.viewport.SetContent(content)
g.viewport.SetYOffset(offset)
}

// View renders the group.
func (g *Group) View() string {
var view strings.Builder
view.WriteString(g.viewport.View())
view.WriteString(g.Footer())
return view.String()
}

// Content renders the group's content only (no footer).
func (g *Group) Content() string {
_, content := g.getContent()
return content
}

// Footer renders the group's footer only (no content).
func (g *Group) Footer() string {
var view strings.Builder
view.WriteRune('\n')
errors := g.Errors()
if g.showHelp && len(errors) <= 0 {
Expand Down
Loading

0 comments on commit 463dcbc

Please sign in to comment.