Skip to content
Merged
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
2 changes: 1 addition & 1 deletion accessibility/accessibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func PromptInt(prompt string, low, high int) int {
//
// Deprecated: use [accessibility.PromptBool] instead.
func PromptBool() bool {
return accessibility.PromptBool(os.Stdout, os.Stdin, false)
return accessibility.PromptBool(os.Stdout, os.Stdin, "Choose [y/N]: ", false)
}

// PromptString prompts a user for a string value and validates it against a
Expand Down
56 changes: 56 additions & 0 deletions examples/accessibility-secure-input/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"errors"
"log"

"github.com/charmbracelet/huh"
)

func validate(s string) error {
if s == "" {
return errors.New("input cannot be empty")
}
return nil
}

func main() {
form := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Welcome!").
Description("This is an accessible form example!"),
huh.NewInput().
Validate(validate).
Title("Name:"),
huh.NewInput().
EchoMode(huh.EchoModePassword).
Validate(validate).
Title("Password:"),
huh.NewMultiSelect[string]().
Options(huh.NewOptions(
"Red",
"Green",
"Yellow",
)...).
Limit(2).
Title("Choose some colors:"),
huh.NewSelect[string]().
Options(huh.NewOptions(
"Red",
"Green",
"Yellow",
)...).
Title("Choose the best color:"),
huh.NewFilePicker().
Title("Which file?"),
huh.NewConfirm().
Title("Send something?"),
),
).WithAccessible(true)

err := form.Run()
if err != nil {
log.Fatal(err)
}
}
4 changes: 2 additions & 2 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ require (
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
github.com/charmbracelet/x/input v0.3.4 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/termios v0.1.0 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
Expand Down
10 changes: 6 additions & 4 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
Expand Down
15 changes: 9 additions & 6 deletions field_confirm.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package huh

import (
"fmt"
"cmp"
"io"
"os"
"strings"
Expand Down Expand Up @@ -308,12 +308,15 @@ func (c *Confirm) Run() error {
// runAccessible runs the confirm field in accessible mode.
func (c *Confirm) runAccessible(w io.Writer, r io.Reader) error {
styles := c.activeStyles()
if c.title.val != "" {
_, _ = fmt.Fprintln(w, styles.Title.Render(c.title.val))
_, _ = fmt.Fprintln(w)
defaultValue := c.GetValue().(bool)
opts := "[y/N]"
if defaultValue {
opts = "[Y/n]"
}
c.accessor.Set(accessibility.PromptBool(w, r, c.GetValue().(bool)))
_, _ = fmt.Fprintln(w, styles.SelectedOption.Render("Chose: "+c.String())+"\n")
prompt := styles.Title.
PaddingRight(1).
Render(cmp.Or(c.title.val, "Choose"), opts)
c.accessor.Set(accessibility.PromptBool(w, r, prompt, defaultValue))
return nil
}

Expand Down
12 changes: 6 additions & 6 deletions field_filepicker.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package huh

import (
"cmp"
"errors"
"fmt"
"io"
"os"
"strings"
Expand Down Expand Up @@ -317,8 +317,9 @@ func (f *FilePicker) Run() error {
// runAccessible runs an accessible file field.
func (f *FilePicker) runAccessible(w io.Writer, r io.Reader) error {
styles := f.activeStyles()
_, _ = fmt.Fprintln(w, styles.Title.Render(f.title))
_, _ = fmt.Fprintln(w)
prompt := styles.Title.
PaddingRight(1).
Render(cmp.Or(f.title, "Choose a file:"))

validateFile := func(s string) error {
// is the string a file?
Expand All @@ -327,7 +328,7 @@ func (f *FilePicker) runAccessible(w io.Writer, r io.Reader) error {
}

// is it one of the allowed types?
valid := false
valid := len(f.picker.AllowedTypes) == 0
for _, ext := range f.picker.AllowedTypes {
if strings.HasSuffix(s, ext) {
valid = true
Expand All @@ -345,11 +346,10 @@ func (f *FilePicker) runAccessible(w io.Writer, r io.Reader) error {
f.accessor.Set(accessibility.PromptString(
w,
r,
"File: ",
prompt,
f.GetValue().(string),
validateFile,
))
_, _ = fmt.Fprintln(w, styles.SelectedOption.Render(f.accessor.Get()+"\n"))
return nil
}

Expand Down
46 changes: 31 additions & 15 deletions field_input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package huh

import (
"cmp"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -429,22 +431,36 @@ func (i *Input) run() error {
// runAccessible runs the input field in accessible mode.
func (i *Input) runAccessible(w io.Writer, r io.Reader) error {
styles := i.activeStyles()
_, _ = fmt.Fprintln(w, styles.Title.Render(i.title.val))
_, _ = fmt.Fprintln(w)
i.accessor.Set(accessibility.PromptString(
w,
r,
"Input: ",
i.GetValue().(string),
func(input string) error {
if i.textinput.CharLimit > 0 && len(input) > i.textinput.CharLimit {
return fmt.Errorf("Input cannot exceed %d characters", i.textinput.CharLimit)
validator := func(input string) error {
if i.textinput.CharLimit > 0 && len(input) > i.textinput.CharLimit {
return fmt.Errorf("Input cannot exceed %d characters", i.textinput.CharLimit)
}
return i.validate(input)
}

//nolint:exhaustive
switch i.textinput.EchoMode {
case textinput.EchoNormal:
prompt := styles.Title.
PaddingRight(1).
Render(cmp.Or(i.title.val, "Input:"))
value := accessibility.PromptString(w, r, prompt, i.GetValue().(string), validator)
i.accessor.Set(value)
return nil
default:
prompt := styles.Title.
PaddingRight(1).
Render(cmp.Or(i.title.val, "Password:"))
if fd, ok := r.(interface{ Fd() uintptr }); ok {
value, err := accessibility.PromptPassword(w, fd.Fd(), prompt, validator)
if err != nil {
return err //nolint:wrapcheck
}
return i.validate(input)
},
))
_, _ = fmt.Fprintln(w, styles.SelectedOption.Render("Input: "+i.accessor.Get()+"\n"))
return nil
i.accessor.Set(value)
return nil
}
return errors.New("password asking needs a tty")
}
}

// WithKeyMap sets the keymap on an input field.
Expand Down
52 changes: 19 additions & 33 deletions field_multiselect.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package huh

import (
"cmp"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -652,21 +653,17 @@ func (m *MultiSelect[T]) View() string {

func (m *MultiSelect[T]) printOptions(w io.Writer) {
styles := m.activeStyles()
maxWidth := m.width - styles.Base.GetHorizontalFrameSize()
var sb strings.Builder
sb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))
sb.WriteString("\n")

for i, option := range m.options.val {
if option.selected {
sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key)))
} else {
sb.WriteString(fmt.Sprintf("%d. %s %s", i+1, " ", option.Key))
sb.WriteString(fmt.Sprintf("%d. %s", i+1, option.Key))
}
sb.WriteString("\n")
}

_, _ = fmt.Fprintln(w, sb.String())
sb.WriteString("0. Confirm selection\n")
_, _ = fmt.Fprint(w, sb.String())
}

// setFilter sets the filter of the select field.
Expand Down Expand Up @@ -711,21 +708,23 @@ func (m *MultiSelect[T]) Run() error {

// runAccessible() runs the multi-select field in accessible mode.
func (m *MultiSelect[T]) runAccessible(w io.Writer, r io.Reader) error {
m.printOptions(w)
styles := m.activeStyles()
title := styles.Title.
PaddingRight(1).
Render(cmp.Or(m.title.val, "Select:"))
_, _ = fmt.Fprintln(w, title)
limit := m.limit
if limit == 0 {
limit = len(m.options.val)
}
_, _ = fmt.Fprintf(w, "Select up to %d options.\n", limit)

var choice int
for {
_, _ = fmt.Fprintf(w, "Select up to %d options. 0 to continue.\n", m.limit)

choice = accessibility.PromptInt(
w,
r,
"Select: ",
0,
len(m.options.val),
nil,
)
m.printOptions(w)

prompt := fmt.Sprintf("Input a number between %d and %d: ", 0, len(m.options.val))
choice = accessibility.PromptInt(w, r, prompt, 0, len(m.options.val), nil)
if choice == 0 {
m.updateValue()
err := m.validate(m.accessor.Get())
Expand All @@ -738,26 +737,13 @@ func (m *MultiSelect[T]) runAccessible(w io.Writer, r io.Reader) error {

if !m.options.val[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit {
_, _ = fmt.Fprintf(w, "You can't select more than %d options.\n", m.limit)
_, _ = fmt.Fprintln(w)
continue
}
m.options.val[choice-1].selected = !m.options.val[choice-1].selected
if m.options.val[choice-1].selected {
_, _ = fmt.Fprintf(w, "Selected: %s\n\n", m.options.val[choice-1].Key)
} else {
_, _ = fmt.Fprintf(w, "Deselected: %s\n\n", m.options.val[choice-1].Key)
}

m.printOptions(w)
}

var values []string
for _, option := range m.options.val {
if option.selected {
values = append(values, option.Key)
}
_, _ = fmt.Fprintln(w)
}

_, _ = fmt.Fprintln(w, styles.SelectedOption.Render("Selected:", strings.Join(values, ", ")+"\n"))
return nil
}

Expand Down
10 changes: 5 additions & 5 deletions field_note.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,13 @@ func (n *Note) Run() error {

// runAccessible runs an accessible note field.
func (n *Note) runAccessible(w io.Writer, _ io.Reader) error {
styles := n.activeStyles()
if n.title.val != "" {
_, _ = fmt.Fprintln(w, n.title.val)
_, _ = fmt.Fprintln(w)
_, _ = fmt.Fprintln(w, styles.Title.Render(n.title.val))
}
if n.description.val != "" {
_, _ = fmt.Fprintln(w, n.description.val)
}

_, _ = fmt.Fprintln(w, n.description.val)
_, _ = fmt.Fprintln(w)
return nil
}

Expand Down
Loading
Loading