diff --git a/README.md b/README.md index 032b6e1..744a953 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,7 @@ Easy building a multi-platform binary of the command line tools because built wi ![Keyboard shortcuts](./_resources/keyboard-shortcuts.gif) -go-prompt implements the bash compatible keyboard shortcuts. - -* `Ctrl+A`: Go to the beginning of the line. -* `Ctrl+E`: Go to the end of the line. -* `Ctrl+K`: Cut the part of the line after the cursor. -* etc... +You can customize keyboard shortcuts. More details are available from 'KeyBoard Shortcuts' section in Developer Guide. #### Easy to use @@ -61,7 +56,7 @@ func main() { More practical example is avairable from `_example` directory or [kube-prompt](https://github.com/c-bata/kube-prompt). -#### Flexible customize +#### Flexible customization ![options](./_resources/prompt-options.png) go-prompt has many color options. All options are listed in [Developer Guide](./example/README.md). @@ -75,6 +70,7 @@ go-prompt has many color options. All options are listed in [Developer Guide](./ * If you want to create projects using go-prompt, you might want to look at the [Getting Started](./example/README.md). * If you want to contribute go-prompt, you might want to look at the [Developer Guide](./_tools/README.md). +* If you want to know internal API, you might want to look at the [GoDoc](http://godoc.org/github.com/c-bata/go-prompt). ## Author diff --git a/emacs.go b/emacs.go new file mode 100644 index 0000000..dc5f8a5 --- /dev/null +++ b/emacs.go @@ -0,0 +1,83 @@ +package prompt + +var emacsKeyBindings = []KeyBind { + // Go to the End of the line + { + Key: ControlE, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextAfterCursor()) + buf.CursorRight(len(x)) + return buf + }, + }, + // Go to the beginning of the line + { + Key: ControlA, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextBeforeCursor()) + buf.CursorLeft(len(x)) + return buf + }, + }, + // Cut the Line after the cursor + { + Key: ControlK, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextAfterCursor()) + buf.Delete(len(x)) + return buf + }, + }, + // Cut/delete the Line before the cursor + { + Key: ControlU, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextBeforeCursor()) + buf.DeleteBeforeCursor(len(x)) + return buf + }, + }, + // Delete character under the cursor + { + Key: ControlD, + Fn: func(buf *Buffer) *Buffer { + if buf.Text() == "" { + return buf // This means just exit. + } + buf.Delete(1) + return buf + }, + }, + // Backspace + { + Key: ControlH, + Fn: func(buf *Buffer) *Buffer { + buf.DeleteBeforeCursor(1) + return buf + }, + }, + // Right allow: Forward one character + { + Key: ControlF, + Fn: func(buf *Buffer) *Buffer { + buf.CursorRight(1) + return buf + }, + }, + // Left allow: Backward one character + { + Key: ControlB, + Fn: func(buf *Buffer) *Buffer { + buf.CursorLeft(1) + return buf + }, + }, + // Cut the Word before the cursor. + { + Key: ControlW, + Fn: func(buf *Buffer) *Buffer { + buf.DeleteBeforeCursor(len([]rune(buf.Document().GetWordBeforeCursor()))) + return buf + }, + }, +} diff --git a/key_bind.go b/key_bind.go new file mode 100644 index 0000000..c5e1e6c --- /dev/null +++ b/key_bind.go @@ -0,0 +1,62 @@ +package prompt + +type KeyBindFunc func(*Buffer) *Buffer + +type KeyBind struct { + Key Key + Fn KeyBindFunc +} + +var commonKeyBindings = []KeyBind { + // Go to the End of the line + { + Key: End, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextAfterCursor()) + buf.CursorRight(len(x)) + return buf + }, + }, + // Go to the beginning of the line + { + Key: Home, + Fn: func(buf *Buffer) *Buffer { + x := []rune(buf.Document().TextBeforeCursor()) + buf.CursorLeft(len(x)) + return buf + }, + }, + // Delete character under the cursor + { + Key: Delete, + Fn: func(buf *Buffer) *Buffer { + buf.Delete(1) + return buf + }, + }, + // Backspace + { + Key: Backspace, + Fn: func(buf *Buffer) *Buffer { + buf.DeleteBeforeCursor(1) + return buf + }, + }, + // Right allow: Forward one character + { + Key: Right, + Fn: func(buf *Buffer) *Buffer { + buf.CursorRight(1) + return buf + }, + }, + // Left allow: Backward one character + { + Key: Left, + Fn: func(buf *Buffer) *Buffer { + buf.CursorLeft(1) + return buf + }, + }, + +} diff --git a/option.go b/option.go index 8a89a89..cc23001 100644 --- a/option.go +++ b/option.go @@ -145,6 +145,13 @@ func OptionHistory(x []string) option { } } +func OptionAddKeyBind(b ...KeyBind) option { + return func(p *Prompt) error { + p.keyBindings = append(p.keyBindings, b...) + return nil + } +} + func New(executor Executor, completer Completer, opts ...option) *Prompt { pt := &Prompt{ in: &VT100Parser{fd: syscall.Stdin}, diff --git a/prompt.go b/prompt.go index 831909f..144a533 100644 --- a/prompt.go +++ b/prompt.go @@ -18,12 +18,13 @@ type Executor func(string) type Completer func(string) []Suggest type Prompt struct { - in ConsoleParser - buf *Buffer - renderer *Render - executor Executor - history *History - completion *CompletionManager + in ConsoleParser + buf *Buffer + renderer *Render + executor Executor + history *History + completion *CompletionManager + keyBindings []KeyBind } type Exec struct { @@ -97,7 +98,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { key := p.in.GetKey(b) switch key { - case ControlJ, Enter: + case Enter, ControlJ: if s, ok := p.completion.GetSelectedSuggestion(); ok { w := p.buf.Document().GetWordBeforeCursor() if w != "" { @@ -114,30 +115,12 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { if exec.input != "" { p.history.Add(exec.input) } - case ControlA: - x := []rune(p.buf.Document().TextBeforeCursor()) - p.buf.CursorLeft(len(x)) - case ControlE: - x := []rune(p.buf.Document().TextAfterCursor()) - p.buf.CursorRight(len(x)) - case ControlK: - x := []rune(p.buf.Document().TextAfterCursor()) - p.buf.Delete(len(x)) - case ControlU: - x := []rune(p.buf.Document().TextBeforeCursor()) - p.buf.DeleteBeforeCursor(len(x)) case ControlC: p.renderer.BreakLine(p.buf) p.buf = NewBuffer() p.completion.Reset() p.history.Clear() - case ControlD: - if p.buf.Text() == "" { - shouldExit = true - } else { - p.buf.Delete(1) - } - case Up: + case Up, ControlP: if !p.completion.Completing() { if newBuf, changed := p.history.Older(p.buf); changed { p.buf = newBuf @@ -147,7 +130,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { fallthrough case BackTab: p.completion.Previous() - case Down: + case Down, ControlN: if !p.completion.Completing() { if newBuf, changed := p.history.Newer(p.buf); changed { p.buf = newBuf @@ -157,20 +140,12 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { fallthrough case Tab, ControlI: p.completion.Next() - case Left: - p.buf.CursorLeft(1) - case Right: - p.buf.CursorRight(1) - case Backspace: - if s, ok := p.completion.GetSelectedSuggestion(); ok { - w := p.buf.Document().GetWordBeforeCursor() - if w != "" { - p.buf.DeleteBeforeCursor(len([]rune(w))) - } - p.buf.InsertText(s.Text, false, true) - p.completion.Reset() + case ControlD: + if p.buf.Text() == "" { + shouldExit = true + return } - p.buf.DeleteBeforeCursor(1) + p.completion.Reset() case NotDefined: if s, ok := p.completion.GetSelectedSuggestion(); ok { w := p.buf.Document().GetWordBeforeCursor() @@ -182,8 +157,38 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { p.completion.Reset() p.buf.InsertText(string(b), false, true) default: + if s, ok := p.completion.GetSelectedSuggestion(); ok { + w := p.buf.Document().GetWordBeforeCursor() + if w != "" { + p.buf.DeleteBeforeCursor(len([]rune(w))) + } + p.buf.InsertText(s.Text, false, true) + } p.completion.Reset() } + + for i := range commonKeyBindings { + kb := commonKeyBindings[i] + if kb.Key == key { + p.buf = kb.Fn(p.buf) + } + } + + // All the above assume that bash is running in the default Emacs setting + for i := range emacsKeyBindings { + kb := emacsKeyBindings[i] + if kb.Key == key { + p.buf = kb.Fn(p.buf) + } + } + + // Custom keybindings + for i := range p.keyBindings { + kb := p.keyBindings[i] + if kb.Key == key { + p.buf = kb.Fn(p.buf) + } + } return }