From d47448ac4a6778afc124a955667ce84257962e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D2=88=D2=88=D2=88Luiz=20Branco?= Date: Thu, 2 Nov 2017 09:48:02 -0400 Subject: [PATCH] Prompt improve default value (#21) Now any key press other than will clear the default value for the prompt. --- codes.go | 4 -- prompt.go | 133 +++++++++++++++++++++++-------------------------- prompt_test.go | 43 ---------------- select.go | 10 ++-- select_test.go | 24 ++++----- 5 files changed, 78 insertions(+), 136 deletions(-) delete mode 100644 prompt_test.go diff --git a/codes.go b/codes.go index bb552f0..9db373f 100644 --- a/codes.go +++ b/codes.go @@ -63,10 +63,6 @@ func upLine(n uint) string { return movementCode(n, 'A') } -func downLine(n uint) string { - return movementCode(n, 'B') -} - func movementCode(n uint, code rune) string { return esc + strconv.FormatUint(uint64(n), 10) + string(code) } diff --git a/prompt.go b/prompt.go index 94c7bbb..4013bd9 100644 --- a/prompt.go +++ b/prompt.go @@ -1,16 +1,17 @@ package promptui import ( - "bytes" "fmt" "io" - "os" "strings" "text/template" "github.com/chzyer/readline" + "github.com/manifoldco/promptui/screenbuf" ) +const cursor = "\u258f" + // Prompt represents a single line text field input. type Prompt struct { // Label is the value displayed on the command line prompt. It can be any @@ -100,28 +101,17 @@ func (p *Prompt) Run() (string, error) { c.VimMode = true } - prompt := render(p.Templates.prompt, p.Label) - - c.Prompt = prompt c.HistoryLimit = -1 c.UniqueEditLine = true - firstListen := true - wroteErr := false - caughtup := true - var out string - - if p.Default != "" { - caughtup = false - out = p.Default - c.Stdin = io.MultiReader(bytes.NewBuffer([]byte(out)), os.Stdin) - } - rl, err := readline.NewEx(c) if err != nil { return "", err } + rl.Write([]byte(hideCursor)) + sb := screenbuf.New(rl) + validFn := func(x string) error { return nil } @@ -130,27 +120,36 @@ func (p *Prompt) Run() (string, error) { validFn = p.Validate } + var inputErr error + input := p.Default + eraseDefault := input != "" + c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) { - if key == readline.CharEnter { - return nil, 0, false + if line != nil { + input += string(line) } - if firstListen { - firstListen = false + switch key { + case 0: // empty + case readline.CharEnter: return nil, 0, false - } - - if !caughtup && out != "" { - if string(line) == out { - caughtup = true + case readline.CharBackspace: + if eraseDefault { + eraseDefault = false + input = "" } - if wroteErr { - return nil, 0, false + if len(input) > 0 { + input = input[:len(input)-1] + } + default: + if eraseDefault { + eraseDefault = false + input = string(line) } } - err := validFn(string(line)) - var prompt string + err := validFn(input) + var prompt []byte if err != nil { prompt = render(p.Templates.invalid, p.Label) @@ -161,18 +160,32 @@ func (p *Prompt) Run() (string, error) { } } - rl.SetPrompt(prompt) - rl.Refresh() - wroteErr = false + echo := input + if p.Mask != 0 { + echo = strings.Repeat(string(p.Mask), len(echo)) + } - return nil, 0, false + prompt = append(prompt, []byte(echo+cursor)...) + + sb.Reset() + sb.Write(prompt) + + if inputErr != nil { + validation := render(p.Templates.validation, inputErr) + sb.Write(validation) + inputErr = nil + } + + sb.Flush() + + return nil, 0, true }) for { - out, err = rl.Readline() + _, err = rl.Readline() - oerr := validFn(out) - if oerr == nil { + inputErr = validFn(input) + if inputErr == nil { break } @@ -183,52 +196,42 @@ func (p *Prompt) Run() (string, error) { case io.EOF: err = ErrEOF } - break } - - caughtup = false - - c.Stdin = io.MultiReader(bytes.NewBuffer([]byte(out)), os.Stdin) - rl, _ = readline.NewEx(c) - - firstListen = true - wroteErr = true - - validation := render(p.Templates.validation, oerr) - prompt := render(p.Templates.invalid, p.Label) - - rl.SetPrompt("\n" + validation + upLine(1) + "\r" + prompt) - rl.Refresh() - } - - if wroteErr { - rl.Write([]byte(downLine(1) + clearLine + upLine(1) + "\r")) } if err != nil { if err.Error() == "Interrupt" { err = ErrInterrupt } - rl.Write([]byte("\n")) + sb.Reset() + sb.WriteString("") + sb.Flush() + rl.Write([]byte(showCursor)) + rl.Close() return "", err } - echo := out + echo := input if p.Mask != 0 { echo = strings.Repeat(string(p.Mask), len(echo)) } - prompt = render(p.Templates.valid, p.Label) + prompt := render(p.Templates.valid, p.Label) + prompt = append(prompt, []byte(echo)...) if p.IsConfirm && strings.ToLower(echo) != "y" { prompt = render(p.Templates.invalid, p.Label) err = ErrAbort } - rl.Write([]byte(prompt + render(p.Templates.success, echo) + "\n")) + sb.Reset() + sb.Write(prompt) + sb.Flush() + rl.Write([]byte(showCursor)) + rl.Close() - return out, err + return input, err } func (p *Prompt) prepareTemplates() error { @@ -242,7 +245,6 @@ func (p *Prompt) prepareTemplates() error { } bold := Styler(FGBold) - //faint := Styler(FGFaint) if p.IsConfirm { p.Default = "" @@ -321,12 +323,3 @@ func (p *Prompt) prepareTemplates() error { return nil } - -func render(tpl *template.Template, data interface{}) string { - var buf bytes.Buffer - err := tpl.Execute(&buf, data) - if err != nil { - return fmt.Sprintf("%v", data) - } - return buf.String() -} diff --git a/prompt_test.go b/prompt_test.go deleted file mode 100644 index 73e4f08..0000000 --- a/prompt_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package promptui - -import ( - "bytes" - "testing" -) - -func outputTest(mask rune, input, displayed, output, def string) func(t *testing.T) { - return func(t *testing.T) { - in := bytes.Buffer{} - out := bytes.Buffer{} - p := Prompt{ - Label: "test", - Default: def, - Mask: mask, - stdin: &in, - stdout: &out, - } - - in.Write([]byte(input + "\n")) - res, err := p.Run() - - if err != nil { - t.Errorf("error during prompt: %s", err) - } - - if res != output { - t.Errorf("wrong result: %s != %s", res, output) - } - - expected := "\x1b[1m\x1b[32m✔\x1b[0m \x1b[1mtest\x1b[0m\x1b[1m:\x1b[0m \x1b[2m" + displayed + "\033[0m\n" - if !bytes.Equal(out.Bytes(), []byte(expected)) { - t.Errorf("wrong output: %q != %q", out.Bytes(), expected) - } - - } -} - -func TestPrompt(t *testing.T) { - t.Run("can read input", outputTest(0x0, "hi", "hi", "hi", "")) - t.Run("displays masked values", outputTest('*', "hi", "**", "hi", "")) - t.Run("can use a default", outputTest(0x0, "", "hi", "hi", "hi")) -} diff --git a/select.go b/select.go index 056a676..e5f81b4 100644 --- a/select.go +++ b/select.go @@ -133,7 +133,7 @@ func (s *Select) innerRun(starting int, top rune) (int, string, error) { s.list.PageUp() } - label := renderBytes(s.Templates.label, s.Label) + label := render(s.Templates.label, s.Label) sb.Write(label) items, idx := s.list.Items() @@ -158,9 +158,9 @@ func (s *Select) innerRun(starting int, top rune) (int, string, error) { output := []byte(page + " ") if i == idx { - output = append(output, renderBytes(s.Templates.active, item)...) + output = append(output, render(s.Templates.active, item)...) } else { - output = append(output, renderBytes(s.Templates.inactive, item)...) + output = append(output, render(s.Templates.inactive, item)...) } sb.Write(output) @@ -197,7 +197,7 @@ func (s *Select) innerRun(starting int, top rune) (int, string, error) { items, idx := s.list.Items() item := items[idx] - output := renderBytes(s.Templates.selected, item) + output := render(s.Templates.selected, item) sb.Reset() sb.Write(output) @@ -353,7 +353,7 @@ func (s *Select) detailsOutput(item interface{}) [][]byte { return bytes.Split(output, []byte("\n")) } -func renderBytes(tpl *template.Template, data interface{}) []byte { +func render(tpl *template.Template, data interface{}) []byte { var buf bytes.Buffer err := tpl.Execute(&buf, data) if err != nil { diff --git a/select_test.go b/select_test.go index 28eb145..399d519 100644 --- a/select_test.go +++ b/select_test.go @@ -4,10 +4,6 @@ import ( "testing" ) -type example struct { - start, end, selected, max, size int -} - func TestSelectTemplateRender(t *testing.T) { t.Run("when using default style", func(t *testing.T) { values := []string{"Zero"} @@ -20,25 +16,25 @@ func TestSelectTemplateRender(t *testing.T) { t.Fatalf("Unexpected error preparing templates %v", err) } - result := render(s.Templates.label, s.Label) + result := string(render(s.Templates.label, s.Label)) exp := "\x1b[34m?\x1b[0m Select Number: " if result != exp { t.Errorf("Expected label to eq %q, got %q", exp, result) } - result = render(s.Templates.active, values[0]) + result = string(render(s.Templates.active, values[0])) exp = "\x1b[1m▸\x1b[0m \x1b[4mZero\x1b[0m" if result != exp { t.Errorf("Expected active item to eq %q, got %q", exp, result) } - result = render(s.Templates.inactive, values[0]) + result = string(render(s.Templates.inactive, values[0])) exp = " Zero" if result != exp { t.Errorf("Expected inactive item to eq %q, got %q", exp, result) } - result = render(s.Templates.selected, values[0]) + result = string(render(s.Templates.selected, values[0])) exp = "\x1b[32m\x1b[32m✔\x1b[0m \x1b[2mZero\x1b[0m" if result != exp { t.Errorf("Expected selected item to eq %q, got %q", exp, result) @@ -82,31 +78,31 @@ Description: {{.Description}}`, t.Fatalf("Unexpected error preparing templates %v", err) } - result := render(s.Templates.label, s.Label) + result := string(render(s.Templates.label, s.Label)) exp := "Spicy Level?" if result != exp { t.Errorf("Expected label to eq %q, got %q", exp, result) } - result = render(s.Templates.active, peppers[0]) + result = string(render(s.Templates.active, peppers[0])) exp = "🔥 \x1b[1mBell Pepper\x1b[0m (\x1b[3m\x1b[31m0\x1b[0m)" if result != exp { t.Errorf("Expected active item to eq %q, got %q", exp, result) } - result = render(s.Templates.inactive, peppers[0]) + result = string(render(s.Templates.inactive, peppers[0])) exp = " \x1b[1mBell Pepper\x1b[0m (\x1b[3m\x1b[31m0\x1b[0m)" if result != exp { t.Errorf("Expected inactive item to eq %q, got %q", exp, result) } - result = render(s.Templates.selected, peppers[0]) + result = string(render(s.Templates.selected, peppers[0])) exp = "🔥 \x1b[1m\x1b[31mBell Pepper\x1b[0m" if result != exp { t.Errorf("Expected selected item to eq %q, got %q", exp, result) } - result = render(s.Templates.details, peppers[0]) + result = string(render(s.Templates.details, peppers[0])) exp = "Name: Bell Pepper\nPeppers: 1\nDescription: Not very spicy!" if result != exp { t.Errorf("Expected selected item to eq %q, got %q", exp, result) @@ -145,7 +141,7 @@ Description: {{.Description}}`, t.Fatalf("Unexpected error preparing templates %v", err) } - result := render(s.Templates.label, s.Label) + result := string(render(s.Templates.label, s.Label)) exp := "{Pepper}" if result != exp { t.Errorf("Expected label to eq %q, got %q", exp, result)