From 194a1982392fdf7ca6199d3c2f47f6e200969792 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 7 Jul 2023 10:40:42 +0200 Subject: [PATCH] Add `ExecuteOnEnterCallback` --- _example/bang-executor/main.go | 26 +++++++++++ _example/http-prompt/main.go | 13 +++--- _example/live-prefix/main.go | 18 +++----- constructor.go | 79 +++++++++++++++++++++------------- prompt.go | 40 +++++++++++------ render.go | 18 +++----- render_test.go | 3 +- shortcut.go | 3 +- 8 files changed, 124 insertions(+), 76 deletions(-) create mode 100644 _example/bang-executor/main.go diff --git a/_example/bang-executor/main.go b/_example/bang-executor/main.go new file mode 100644 index 00000000..0f24d9d2 --- /dev/null +++ b/_example/bang-executor/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "unicode/utf8" + + "github.com/elk-language/go-prompt" +) + +func main() { + p := prompt.New( + executor, + prompt.WithExecuteOnEnterCallback(ExecuteOnEnter), + ) + + p.Run() +} + +func ExecuteOnEnter(input string) bool { + char, _ := utf8.DecodeLastRuneInString(input) + return char == '!' +} + +func executor(s string) { + fmt.Println("You printed: " + s) +} diff --git a/_example/http-prompt/main.go b/_example/http-prompt/main.go index 129b415a..d80e0a25 100644 --- a/_example/http-prompt/main.go +++ b/_example/http-prompt/main.go @@ -92,11 +92,13 @@ var suggestions = []prompt.Suggest{ {"X-XSRF-TOKEN", "Prevent cross-site request forgery"}, } -func livePrefix() (string, bool) { - if ctx.url.Path == "/" { - return "", false +func livePrefix(defaultPrefix string) prompt.PrefixCallback { + return func() string { + if ctx.url.Path == "/" { + return defaultPrefix + } + return ctx.url.String() + "> " } - return ctx.url.String() + "> ", true } func executor(in string) { @@ -183,8 +185,7 @@ func main() { p := prompt.New( executor, - prompt.WithPrefix(u.String()+"> "), - prompt.WithLivePrefix(livePrefix), + prompt.WithPrefixCallback(livePrefix(u.String()+"> ")), prompt.WithTitle("http-prompt"), prompt.WithCompleter(completer), ) diff --git a/_example/live-prefix/main.go b/_example/live-prefix/main.go index 121df1c6..4fe160a5 100644 --- a/_example/live-prefix/main.go +++ b/_example/live-prefix/main.go @@ -6,20 +6,15 @@ import ( prompt "github.com/elk-language/go-prompt" ) -var LivePrefixState struct { - LivePrefix string - IsEnable bool -} +var LivePrefix string = ">>> " func executor(in string) { fmt.Println("Your input: " + in) if in == "" { - LivePrefixState.IsEnable = false - LivePrefixState.LivePrefix = in + LivePrefix = ">>> " return } - LivePrefixState.LivePrefix = in + "> " - LivePrefixState.IsEnable = true + LivePrefix = in + "> " } func completer(in prompt.Document) []prompt.Suggest { @@ -32,15 +27,14 @@ func completer(in prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true) } -func changeLivePrefix() (string, bool) { - return LivePrefixState.LivePrefix, LivePrefixState.IsEnable +func changeLivePrefix() string { + return LivePrefix } func main() { p := prompt.New( executor, - prompt.WithPrefix(">>> "), - prompt.WithLivePrefix(changeLivePrefix), + prompt.WithPrefixCallback(changeLivePrefix), prompt.WithTitle("live-prefix-example"), prompt.WithCompleter(completer), ) diff --git a/constructor.go b/constructor.go index e39f1898..16a8cd77 100644 --- a/constructor.go +++ b/constructor.go @@ -4,6 +4,9 @@ package prompt // prompt.New accepts any number of options (this is functional option pattern). type Option func(prompt *Prompt) error +// Callback function that returns a prompt prefix. +type PrefixCallback func() (prefix string) + // WithCompleter is an option that sets a custom Completer object. func WithCompleter(c Completer) Option { return func(p *Prompt) error { @@ -12,59 +15,59 @@ func WithCompleter(c Completer) Option { } } -// WithReader to set a custom Reader object. An argument should implement Reader interface. -func WithReader(x Reader) Option { +// WithReader can be used to set a custom Reader object. +func WithReader(r Reader) Option { return func(p *Prompt) error { - p.reader = x + p.reader = r return nil } } -// WithWriter to set a custom Writer object. An argument should implement Writer interface. -func WithWriter(x Writer) Option { +// WithWriter can be used to set a custom Writer object. +func WithWriter(w Writer) Option { return func(p *Prompt) error { - registerWriter(x) - p.renderer.out = x + registerWriter(w) + p.renderer.out = w return nil } } -// WithTitle to set title displayed at the header bar of terminal. -func WithTitle(x string) Option { +// WithTitle can be used to set the title displayed at the header bar of the terminal. +func WithTitle(t string) Option { return func(p *Prompt) error { - p.renderer.title = x + p.renderer.title = t return nil } } -// WithPrefix to set prefix string. -func WithPrefix(x string) Option { +// WithPrefix can be used to set a prefix string for the prompt. +func WithPrefix(prefix string) Option { return func(p *Prompt) error { - p.renderer.prefix = x + p.renderer.prefixCallback = func() string { return prefix } return nil } } -// WithInitialBufferText to set the initial buffer text -func WithInitialBufferText(x string) Option { +// WithInitialText can be used to set the initial buffer text. +func WithInitialText(text string) Option { return func(p *Prompt) error { - p.buf.InsertText(x, false, true) + p.buf.InsertText(text, false, true) return nil } } -// WithCompletionWordSeparator to set word separators. Enable only ' ' if empty. -func WithCompletionWordSeparator(x string) Option { +// WithCompletionWordSeparator can be used to set word separators. Enable only ' ' if empty. +func WithCompletionWordSeparator(sep string) Option { return func(p *Prompt) error { - p.completion.wordSeparator = x + p.completion.wordSeparator = sep return nil } } -// WithLivePrefix to change the prefix dynamically by callback function -func WithLivePrefix(f func() (prefix string, useLivePrefix bool)) Option { +// WithPrefixCallback can be used to change the prefix dynamically by a callback function. +func WithPrefixCallback(f PrefixCallback) Option { return func(p *Prompt) error { - p.renderer.livePrefixCallback = f + p.renderer.prefixCallback = f return nil } } @@ -282,6 +285,24 @@ func WithLexer(lex Lexer) Option { } } +// WithExecuteOnEnterCallback can be used to set +// a custom callback function that determines whether an Enter key +// should trigger the Executor or add a newline to the user input buffer. +func WithExecuteOnEnterCallback(fn ExecuteOnEnterCallback) Option { + return func(p *Prompt) error { + p.executeOnEnterCallback = fn + return nil + } +} + +func DefaultExecuteOnEnterCallback(input string) bool { + return true +} + +func DefaultPrefixCallback() string { + return "> " +} + // New returns a Prompt with powerful auto-completion. func New(executor Executor, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() @@ -290,9 +311,8 @@ func New(executor Executor, opts ...Option) *Prompt { pt := &Prompt{ reader: NewStdinReader(), renderer: &Render{ - prefix: "> ", out: defaultWriter, - livePrefixCallback: func() (string, bool) { return "", false }, + prefixCallback: DefaultPrefixCallback, prefixTextColor: Blue, prefixBGColor: DefaultColor, inputTextColor: DefaultColor, @@ -310,11 +330,12 @@ func New(executor Executor, opts ...Option) *Prompt { scrollbarThumbColor: DarkGray, scrollbarBGColor: Cyan, }, - buf: NewBuffer(), - executor: executor, - history: NewHistory(), - completion: NewCompletionManager(6), - keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting + buf: NewBuffer(), + executor: executor, + history: NewHistory(), + completion: NewCompletionManager(6), + executeOnEnterCallback: DefaultExecuteOnEnterCallback, + keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } for _, opt := range opts { diff --git a/prompt.go b/prompt.go index daf4e968..ecc43ed9 100644 --- a/prompt.go +++ b/prompt.go @@ -23,25 +23,33 @@ type Executor func(string) // Exit means exit go-prompt (not the overall Go program) type ExitChecker func(in string, breakline bool) bool +// ExecuteOnEnterCallback is a function that receives +// user input after Enter has been pressed +// and determines whether the input should be executed. +// If this function returns true, the Executor callback will be called +// otherwise a newline will be added to the buffer containing user input. +type ExecuteOnEnterCallback func(input string) bool + // Completer is a function that returns // a slice of suggestions for the given Document. type Completer func(Document) []Suggest // Prompt is a core struct of go-prompt. type Prompt struct { - reader Reader - buf *Buffer - renderer *Render - executor Executor - history *History - lexer Lexer - completion *CompletionManager - keyBindings []KeyBind - ASCIICodeBindings []ASCIICodeBind - keyBindMode KeyBindMode - completionOnDown bool - exitChecker ExitChecker - skipClose bool + reader Reader + buf *Buffer + renderer *Render + executor Executor + history *History + lexer Lexer + completion *CompletionManager + keyBindings []KeyBind + ASCIICodeBindings []ASCIICodeBind + keyBindMode KeyBindMode + completionOnDown bool + exitChecker ExitChecker + executeOnEnterCallback ExecuteOnEnterCallback + skipClose bool } // UserInput is the struct that contains the user input context. @@ -137,8 +145,12 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { switch key { case Enter, ControlJ, ControlM: - p.renderer.BreakLine(p.buf, p.lexer) + if !p.executeOnEnterCallback(p.buf.Text()) { + p.buf.NewLine(false) + break + } + p.renderer.BreakLine(p.buf, p.lexer) userInput = &UserInput{input: p.buf.Text()} p.buf = NewBuffer() if userInput.input != "" { diff --git a/render.go b/render.go index a2d4f0df..97b9d6c4 100644 --- a/render.go +++ b/render.go @@ -10,13 +10,12 @@ import ( // Render to render prompt information from state of Buffer. type Render struct { - out Writer - prefix string - livePrefixCallback func() (prefix string, useLivePrefix bool) - breakLineCallback func(*Document) - title string - row uint16 - col uint16 + out Writer + prefixCallback PrefixCallback + breakLineCallback func(*Document) + title string + row uint16 + col uint16 previousCursor Position @@ -50,10 +49,7 @@ func (r *Render) Setup() { // getCurrentPrefix to get current prefix. // If live-prefix is enabled, return live-prefix. func (r *Render) getCurrentPrefix() string { - if prefix, ok := r.livePrefixCallback(); ok { - return prefix - } - return r.prefix + return r.prefixCallback() } func (r *Render) renderPrefix() { diff --git a/render_test.go b/render_test.go index 14edcd22..81e0f643 100644 --- a/render_test.go +++ b/render_test.go @@ -73,11 +73,10 @@ func TestFormatCompletion(t *testing.T) { func TestBreakLineCallback(t *testing.T) { var i int r := &Render{ - prefix: "> ", out: &PosixWriter{ fd: syscall.Stdin, // "write" to stdin just so we don't mess with the output of the tests }, - livePrefixCallback: func() (string, bool) { return "", false }, + prefixCallback: DefaultPrefixCallback, prefixTextColor: Blue, prefixBGColor: DefaultColor, inputTextColor: DefaultColor, diff --git a/shortcut.go b/shortcut.go index 15b62ecd..4c4c8691 100644 --- a/shortcut.go +++ b/shortcut.go @@ -3,10 +3,9 @@ package prompt func NoopExecutor(in string) {} // Input get the input data from the user and return it. -func Input(prefix string, opts ...Option) string { +func Input(opts ...Option) string { pt := New(NoopExecutor) pt.renderer.prefixTextColor = DefaultColor - pt.renderer.prefix = prefix for _, opt := range opts { if err := opt(pt); err != nil {