diff --git a/_example/build.sh b/_example/build.sh index 4c552afc..7b71a4f5 100755 --- a/_example/build.sh +++ b/_example/build.sh @@ -10,3 +10,4 @@ go build -o ${BIN_DIR}/http-prompt ${DIR}/http-prompt/main.go go build -o ${BIN_DIR}/live-prefix ${DIR}/live-prefix/main.go go build -o ${BIN_DIR}/simple-echo ${DIR}/simple-echo/main.go go build -o ${BIN_DIR}/simple-echo-cjk-cyrillic ${DIR}/simple-echo/cjk-cyrillic/main.go +go build -o ${BIN_DIR}/even-lexer ${DIR}/even-lexer/main.go diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go new file mode 100644 index 00000000..836ac57d --- /dev/null +++ b/_example/even-lexer/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "github.com/c-bata/go-prompt" + "strings" +) + +func main() { + p := prompt.New( + executor, + completer, + prompt.OptionSetLexer(lexer), + ) + + p.Run() +} + +func lexer(line string) []prompt.LexerElement { + var elements []prompt.LexerElement + + strArr := strings.Split(line, "") + + for k, v := range strArr { + element := prompt.LexerElement{ + Text: v, + } + + // every even char must be green. + if k%2 == 0 { + element.Color = prompt.Green + } else { + element.Color = prompt.White + } + + elements = append(elements, element) + } + + return elements +} + +func completer(in prompt.Document) []prompt.Suggest { + return []prompt.Suggest{} +} + +func executor(s string) { + fmt.Println("You printed: " + s) +} diff --git a/lexer.go b/lexer.go new file mode 100644 index 00000000..3f5cd76a --- /dev/null +++ b/lexer.go @@ -0,0 +1,34 @@ +package prompt + +// LexerFunc is a callback from render. +type LexerFunc = func(line string) []LexerElement + +// LexerElement is a element of lexer. +type LexerElement struct { + Color Color + Text string +} + +// Lexer is a struct with lexer param and function. +type Lexer struct { + IsEnabled bool + fn LexerFunc +} + +// NewLexer returns new Lexer. +func NewLexer() *Lexer { + return &Lexer{ + IsEnabled: false, + } +} + +// SetLexerFunction in lexer struct. +func (l *Lexer) SetLexerFunction(fn LexerFunc) { + l.IsEnabled = true + l.fn = fn +} + +// Process line with a custom function. +func (l *Lexer) Process(line string) []LexerElement { + return l.fn(line) +} diff --git a/option.go b/option.go index 9fbd6806..dbb877bf 100644 --- a/option.go +++ b/option.go @@ -266,6 +266,14 @@ func OptionSetExitCheckerOnInput(fn ExitChecker) Option { } } +// OptionSetLexer set lexer function and enable it. +func OptionSetLexer(fn LexerFunc) Option { + return func(p *Prompt) error { + p.lexer.SetLexerFunction(fn) + return nil + } +} + // New returns a Prompt with powerful auto-completion. func New(executor Executor, completer Completer, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() @@ -297,6 +305,7 @@ func New(executor Executor, completer Completer, opts ...Option) *Prompt { buf: NewBuffer(), executor: executor, history: NewHistory(), + lexer: NewLexer(), completion: NewCompletionManager(completer, 6), keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } diff --git a/prompt.go b/prompt.go index 173cd517..9fa70c3b 100644 --- a/prompt.go +++ b/prompt.go @@ -28,6 +28,7 @@ type Prompt struct { renderer *Render executor Executor history *History + lexer *Lexer completion *CompletionManager keyBindings []KeyBind ASCIICodeBindings []ASCIICodeBind @@ -54,7 +55,7 @@ func (p *Prompt) Run() { p.completion.Update(*p.buf.Document()) } - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) @@ -69,7 +70,7 @@ func (p *Prompt) Run() { select { case b := <-bufCh: if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) stopReadBufCh <- struct{}{} stopHandleSignalCh <- struct{}{} return @@ -85,7 +86,7 @@ func (p *Prompt) Run() { p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) if p.exitChecker != nil && p.exitChecker(e.input, true) { p.skipTearDown = true @@ -97,13 +98,13 @@ func (p *Prompt) Run() { go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh) } else { p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) } case w := <-winSizeCh: p.renderer.UpdateWinSize(w) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) case code := <-exitCh: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) p.tearDown() os.Exit(code) default: @@ -121,7 +122,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { switch key { case Enter, ControlJ, ControlM: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) exec = &Exec{input: p.buf.Text()} p.buf = NewBuffer() @@ -129,7 +130,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { p.history.Add(exec.input) } case ControlC: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) p.buf = NewBuffer() p.history.Clear() case Up, ControlP: @@ -240,7 +241,7 @@ func (p *Prompt) Input() string { p.completion.Update(*p.buf.Document()) } - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) go p.readBuffer(bufCh, stopReadBufCh) @@ -249,7 +250,7 @@ func (p *Prompt) Input() string { select { case b := <-bufCh: if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) stopReadBufCh <- struct{}{} return "" } else if e != nil { @@ -258,7 +259,7 @@ func (p *Prompt) Input() string { return e.input } else { p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) } default: time.Sleep(10 * time.Millisecond) diff --git a/render.go b/render.go index 299ac48b..bb9b681e 100644 --- a/render.go +++ b/render.go @@ -2,6 +2,7 @@ package prompt import ( "runtime" + "strings" "github.com/c-bata/go-prompt/internal/debug" runewidth "github.com/mattn/go-runewidth" @@ -167,7 +168,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { } // Render renders to the console. -func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { +func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer *Lexer) { // In situations where a pseudo tty is allocated (e.g. within a docker container), // window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions. if r.col == 0 { @@ -194,9 +195,25 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { defer r.out.ShowCursor() r.renderPrefix() - r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(line) + + if lexer.IsEnabled { + processed := lexer.Process(line) + var s = line + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.SetColor(r.inputTextColor, r.inputBGColor, false) + r.out.WriteStr(line) + } + r.out.SetColor(DefaultColor, DefaultColor, false) + r.lineWrap(cursor) r.out.EraseDown() @@ -213,7 +230,25 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { cursor += runewidth.StringWidth(suggest.Text) rest := buffer.Document().TextAfterCursor() - r.out.WriteStr(rest) + + if lexer.IsEnabled { + processed := lexer.Process(rest) + + var s = rest + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.WriteStr(rest) + } + + r.out.SetColor(DefaultColor, DefaultColor, false) + cursor += runewidth.StringWidth(rest) r.lineWrap(cursor) @@ -223,14 +258,32 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { } // BreakLine to break line. -func (r *Render) BreakLine(buffer *Buffer) { +func (r *Render) BreakLine(buffer *Buffer, lexer *Lexer) { // Erasing and Render cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix()) r.clear(cursor) + r.renderPrefix() - r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(buffer.Document().Text + "\n") + + if lexer.IsEnabled { + processed := lexer.Process(buffer.Document().Text + "\n") + + var s = buffer.Document().Text + "\n" + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.SetColor(r.inputTextColor, r.inputBGColor, false) + r.out.WriteStr(buffer.Document().Text + "\n") + } + r.out.SetColor(DefaultColor, DefaultColor, false) + debug.AssertNoError(r.out.Flush()) if r.breakLineCallback != nil { r.breakLineCallback(buffer.Document()) diff --git a/render_test.go b/render_test.go index 0d9fa8a3..302f6e19 100644 --- a/render_test.go +++ b/render_test.go @@ -96,7 +96,8 @@ func TestBreakLineCallback(t *testing.T) { col: 1, } b := NewBuffer() - r.BreakLine(b) + l := NewLexer() + r.BreakLine(b, l) if i != 0 { t.Errorf("i should initially be 0, before applying a break line callback") @@ -105,9 +106,9 @@ func TestBreakLineCallback(t *testing.T) { r.breakLineCallback = func(doc *Document) { i++ } - r.BreakLine(b) - r.BreakLine(b) - r.BreakLine(b) + r.BreakLine(b, l) + r.BreakLine(b, l) + r.BreakLine(b, l) if i != 3 { t.Errorf("BreakLine callback not called, i should be 3")