forked from mirror/readline
add simple vim mode
This commit is contained in:
parent
390f0ebb6b
commit
79d1bf27b4
14
README.md
14
README.md
|
@ -24,7 +24,7 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read
|
||||||
|
|
||||||
# Todo
|
# Todo
|
||||||
|
|
||||||
* Vim mode
|
* Vim mode (WIP)
|
||||||
* More funny examples
|
* More funny examples
|
||||||
* Support dumb/eterm-color terminal in emacs
|
* Support dumb/eterm-color terminal in emacs
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ for {
|
||||||
# Shortcut
|
# Shortcut
|
||||||
|
|
||||||
`Meta`+`B` means press `Esc` and `n` separately.
|
`Meta`+`B` means press `Esc` and `n` separately.
|
||||||
Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B`
|
Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B`
|
||||||
Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
|
Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
|
||||||
|
|
||||||
* Shortcut in normal mode
|
* Shortcut in normal mode
|
||||||
|
@ -199,6 +199,16 @@ Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
|
||||||
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode |
|
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode |
|
||||||
| Other | Exit Complete Select Mode |
|
| Other | Exit Complete Select Mode |
|
||||||
|
|
||||||
|
* Vim Mode (set Config.VimMode to true)
|
||||||
|
|
||||||
|
| Mode | Shortcut | Comment |
|
||||||
|
|--------+----------+---------------------------------------------|
|
||||||
|
| Normal | `j` | Next line (in history) |
|
||||||
|
| | `k` | Prev line (in history) |
|
||||||
|
| | `h` | Move Backward |
|
||||||
|
| | `l` | Move Forward |
|
||||||
|
|
||||||
|
|
||||||
# Tested with
|
# Tested with
|
||||||
|
|
||||||
| Environment | $TERM |
|
| Environment | $TERM |
|
||||||
|
|
6
char.go
6
char.go
|
@ -7,7 +7,7 @@ const (
|
||||||
CharDelete = 4
|
CharDelete = 4
|
||||||
CharLineEnd = 5
|
CharLineEnd = 5
|
||||||
CharForward = 6
|
CharForward = 6
|
||||||
CharCancel = 7
|
CharBell = 7
|
||||||
CharCtrlH = 8
|
CharCtrlH = 8
|
||||||
CharTab = 9
|
CharTab = 9
|
||||||
CharCtrlJ = 10
|
CharCtrlJ = 10
|
||||||
|
@ -26,8 +26,8 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MetaPrev rune = -iota - 1
|
MetaBackward rune = -iota - 1
|
||||||
MetaNext
|
MetaForward
|
||||||
MetaDelete
|
MetaDelete
|
||||||
MetaBackspace
|
MetaBackspace
|
||||||
MetaTranspose
|
MetaTranspose
|
||||||
|
|
|
@ -127,7 +127,7 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||||||
next = false
|
next = false
|
||||||
case CharTab, CharForward:
|
case CharTab, CharForward:
|
||||||
o.doSelect()
|
o.doSelect()
|
||||||
case CharCancel, CharInterrupt:
|
case CharBell, CharInterrupt:
|
||||||
o.ExitCompleteMode(true)
|
o.ExitCompleteMode(true)
|
||||||
next = false
|
next = false
|
||||||
case CharNext:
|
case CharNext:
|
||||||
|
|
|
@ -40,6 +40,7 @@ func main() {
|
||||||
Prompt: "\033[31m»\033[0m ",
|
Prompt: "\033[31m»\033[0m ",
|
||||||
HistoryFile: "/tmp/readline.tmp",
|
HistoryFile: "/tmp/readline.tmp",
|
||||||
AutoComplete: completer,
|
AutoComplete: completer,
|
||||||
|
// VimMode: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
27
operation.go
27
operation.go
|
@ -18,6 +18,7 @@ type Operation struct {
|
||||||
*opHistory
|
*opHistory
|
||||||
*opSearch
|
*opSearch
|
||||||
*opCompleter
|
*opCompleter
|
||||||
|
*opVim
|
||||||
}
|
}
|
||||||
|
|
||||||
type wrapWriter struct {
|
type wrapWriter struct {
|
||||||
|
@ -56,6 +57,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
|
||||||
outchan: make(chan []rune),
|
outchan: make(chan []rune),
|
||||||
opHistory: newOpHistory(cfg.HistoryFile),
|
opHistory: newOpHistory(cfg.HistoryFile),
|
||||||
}
|
}
|
||||||
|
op.opVim = newVimMode(op)
|
||||||
op.w = op.buf.w
|
op.w = op.buf.w
|
||||||
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
|
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
|
||||||
op.opCompleter = newOpCompleter(op.buf.w, op)
|
op.opCompleter = newOpCompleter(op.buf.w, op)
|
||||||
|
@ -87,13 +89,21 @@ func (o *Operation) ioloop() {
|
||||||
case CharInterrupt:
|
case CharInterrupt:
|
||||||
o.t.KickRead()
|
o.t.KickRead()
|
||||||
fallthrough
|
fallthrough
|
||||||
case CharCancel:
|
case CharBell:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.IsEnableVimMode() {
|
||||||
|
var ok bool
|
||||||
|
r, ok = o.HandleVim(r, o.t.ReadRune)
|
||||||
|
if ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r {
|
switch r {
|
||||||
case CharCancel:
|
case CharBell:
|
||||||
if o.IsSearchMode() {
|
if o.IsSearchMode() {
|
||||||
o.ExitSearchMode(true)
|
o.ExitSearchMode(true)
|
||||||
o.buf.Refresh(nil)
|
o.buf.Refresh(nil)
|
||||||
|
@ -119,11 +129,11 @@ func (o *Operation) ioloop() {
|
||||||
case CharKill:
|
case CharKill:
|
||||||
o.buf.Kill()
|
o.buf.Kill()
|
||||||
keepInCompleteMode = true
|
keepInCompleteMode = true
|
||||||
case MetaNext:
|
case MetaForward:
|
||||||
o.buf.MoveToNextWord()
|
o.buf.MoveToNextWord()
|
||||||
case CharTranspose:
|
case CharTranspose:
|
||||||
o.buf.Transpose()
|
o.buf.Transpose()
|
||||||
case MetaPrev:
|
case MetaBackward:
|
||||||
o.buf.MoveToPrevWord()
|
o.buf.MoveToPrevWord()
|
||||||
case MetaDelete:
|
case MetaDelete:
|
||||||
o.buf.DeleteWord()
|
o.buf.DeleteWord()
|
||||||
|
@ -132,7 +142,9 @@ func (o *Operation) ioloop() {
|
||||||
case CharLineEnd:
|
case CharLineEnd:
|
||||||
o.buf.MoveToLineEnd()
|
o.buf.MoveToLineEnd()
|
||||||
case CharDelete:
|
case CharDelete:
|
||||||
o.buf.Delete()
|
if !o.buf.Delete() {
|
||||||
|
o.t.Bell()
|
||||||
|
}
|
||||||
case CharBackspace, CharCtrlH:
|
case CharBackspace, CharCtrlH:
|
||||||
if o.IsSearchMode() {
|
if o.IsSearchMode() {
|
||||||
o.SearchBackspace()
|
o.SearchBackspace()
|
||||||
|
@ -141,6 +153,7 @@ func (o *Operation) ioloop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.buf.Len() == 0 {
|
if o.buf.Len() == 0 {
|
||||||
|
o.t.Bell()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
o.buf.Backspace()
|
o.buf.Backspace()
|
||||||
|
@ -167,11 +180,15 @@ func (o *Operation) ioloop() {
|
||||||
buf := o.PrevHistory()
|
buf := o.PrevHistory()
|
||||||
if buf != nil {
|
if buf != nil {
|
||||||
o.buf.Set(buf)
|
o.buf.Set(buf)
|
||||||
|
} else {
|
||||||
|
o.t.Bell()
|
||||||
}
|
}
|
||||||
case CharNext:
|
case CharNext:
|
||||||
buf, ok := o.NextHistory()
|
buf, ok := o.NextHistory()
|
||||||
if ok {
|
if ok {
|
||||||
o.buf.Set(buf)
|
o.buf.Set(buf)
|
||||||
|
} else {
|
||||||
|
o.t.Bell()
|
||||||
}
|
}
|
||||||
case CharInterrupt:
|
case CharInterrupt:
|
||||||
if o.IsSearchMode() {
|
if o.IsSearchMode() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Config struct {
|
||||||
Prompt string
|
Prompt string
|
||||||
HistoryFile string
|
HistoryFile string
|
||||||
AutoComplete AutoCompleter
|
AutoComplete AutoCompleter
|
||||||
|
VimMode bool
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
|
@ -59,6 +60,10 @@ func (i *Instance) Stderr() io.Writer {
|
||||||
return i.o.Stderr()
|
return i.o.Stderr()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Instance) SetVimMode(on bool) {
|
||||||
|
i.o.SetVimMode(on)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
|
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
|
||||||
return i.o.Password(prompt)
|
return i.o.Password(prompt)
|
||||||
}
|
}
|
||||||
|
|
38
runebuf.go
38
runebuf.go
|
@ -98,13 +98,15 @@ func (r *RuneBuffer) MoveForward() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuneBuffer) Delete() {
|
func (r *RuneBuffer) Delete() (success bool) {
|
||||||
r.Refresh(func() {
|
r.Refresh(func() {
|
||||||
if r.idx == len(r.buf) {
|
if r.idx == len(r.buf) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
||||||
|
success = true
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuneBuffer) DeleteWord() {
|
func (r *RuneBuffer) DeleteWord() {
|
||||||
|
@ -126,7 +128,7 @@ func (r *RuneBuffer) DeleteWord() {
|
||||||
r.Kill()
|
r.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuneBuffer) MoveToPrevWord() {
|
func (r *RuneBuffer) MoveToPrevWord() (success bool) {
|
||||||
r.Refresh(func() {
|
r.Refresh(func() {
|
||||||
if r.idx == 0 {
|
if r.idx == 0 {
|
||||||
return
|
return
|
||||||
|
@ -135,11 +137,14 @@ func (r *RuneBuffer) MoveToPrevWord() {
|
||||||
for i := r.idx - 1; i > 0; i-- {
|
for i := r.idx - 1; i > 0; i-- {
|
||||||
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||||
r.idx = i
|
r.idx = i
|
||||||
|
success = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.idx = 0
|
r.idx = 0
|
||||||
|
success = true
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuneBuffer) KillFront() {
|
func (r *RuneBuffer) KillFront() {
|
||||||
|
@ -237,6 +242,35 @@ func (r *RuneBuffer) LineCount() int {
|
||||||
return LineCount(RunesWidth(r.buf) + r.PromptLen())
|
return LineCount(RunesWidth(r.buf) + r.PromptLen())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if reverse {
|
||||||
|
for i := r.idx - 1; i >= 0; i-- {
|
||||||
|
if r.buf[i] == ch {
|
||||||
|
r.idx = i
|
||||||
|
if prevChar {
|
||||||
|
r.idx++
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||||
|
if r.buf[i] == ch {
|
||||||
|
r.idx = i
|
||||||
|
if prevChar {
|
||||||
|
r.idx--
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RuneBuffer) IdxLine() int {
|
func (r *RuneBuffer) IdxLine() int {
|
||||||
totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen()
|
totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen()
|
||||||
w := getWidth()
|
w := getWidth()
|
||||||
|
|
|
@ -113,6 +113,10 @@ func (t *Terminal) ioloop() {
|
||||||
expectNextChar = true
|
expectNextChar = true
|
||||||
switch r {
|
switch r {
|
||||||
case CharEsc:
|
case CharEsc:
|
||||||
|
if t.cfg.VimMode {
|
||||||
|
t.outchan <- r
|
||||||
|
break
|
||||||
|
}
|
||||||
isEscape = true
|
isEscape = true
|
||||||
case CharInterrupt, CharEnter, CharCtrlJ:
|
case CharInterrupt, CharEnter, CharCtrlJ:
|
||||||
expectNextChar = false
|
expectNextChar = false
|
||||||
|
@ -123,6 +127,10 @@ func (t *Terminal) ioloop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Bell() {
|
||||||
|
fmt.Fprintf(t, "%c", CharBell)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) Close() error {
|
func (t *Terminal) Close() error {
|
||||||
if atomic.SwapInt64(&t.closed, 1) != 0 {
|
if atomic.SwapInt64(&t.closed, 1) != 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
4
utils.go
4
utils.go
|
@ -57,9 +57,9 @@ func escapeExKey(r rune) rune {
|
||||||
func escapeKey(r rune) rune {
|
func escapeKey(r rune) rune {
|
||||||
switch r {
|
switch r {
|
||||||
case 'b':
|
case 'b':
|
||||||
r = MetaPrev
|
r = MetaBackward
|
||||||
case 'f':
|
case 'f':
|
||||||
r = MetaNext
|
r = MetaForward
|
||||||
case 'd':
|
case 'd':
|
||||||
r = MetaDelete
|
r = MetaDelete
|
||||||
case CharTranspose:
|
case CharTranspose:
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
const (
|
||||||
|
VIM_NORMAL = iota
|
||||||
|
VIM_INSERT
|
||||||
|
VIM_VISUAL
|
||||||
|
)
|
||||||
|
|
||||||
|
type opVim struct {
|
||||||
|
cfg *Config
|
||||||
|
op *Operation
|
||||||
|
vimMode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVimMode(op *Operation) *opVim {
|
||||||
|
ov := &opVim{
|
||||||
|
cfg: op.cfg,
|
||||||
|
op: op,
|
||||||
|
}
|
||||||
|
ov.SetVimMode(ov.cfg.VimMode)
|
||||||
|
return ov
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) SetVimMode(on bool) {
|
||||||
|
if o.cfg.VimMode && !on { // turn off
|
||||||
|
o.ExitVimMode()
|
||||||
|
}
|
||||||
|
o.cfg.VimMode = on
|
||||||
|
o.vimMode = VIM_INSERT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) ExitVimMode() {
|
||||||
|
o.vimMode = VIM_NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) IsEnableVimMode() bool {
|
||||||
|
return o.cfg.VimMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune, handle bool) {
|
||||||
|
switch r {
|
||||||
|
case CharEnter, CharInterrupt:
|
||||||
|
return r, false
|
||||||
|
}
|
||||||
|
rb := o.op.buf
|
||||||
|
handled := true
|
||||||
|
{
|
||||||
|
switch r {
|
||||||
|
case 'h':
|
||||||
|
t = CharBackward
|
||||||
|
case 'j':
|
||||||
|
t = CharNext
|
||||||
|
case 'k':
|
||||||
|
t = CharPrev
|
||||||
|
case 'l':
|
||||||
|
t = CharForward
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
return t, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // to insert
|
||||||
|
handled = true
|
||||||
|
switch r {
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
rb.MoveToLineStart()
|
||||||
|
case 'a':
|
||||||
|
rb.MoveForward()
|
||||||
|
case 'A':
|
||||||
|
rb.MoveToLineEnd()
|
||||||
|
case 's':
|
||||||
|
rb.Delete()
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
o.EnterVimInsertMode()
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // movement
|
||||||
|
handled = true
|
||||||
|
switch r {
|
||||||
|
case '0', '^':
|
||||||
|
rb.MoveToLineStart()
|
||||||
|
case '$':
|
||||||
|
rb.MoveToLineEnd()
|
||||||
|
case 'b':
|
||||||
|
rb.MoveToPrevWord()
|
||||||
|
case 'w':
|
||||||
|
rb.MoveToNextWord()
|
||||||
|
case 'f', 'F', 't', 'T':
|
||||||
|
next := readNext()
|
||||||
|
prevChar := r == 't' || r == 'T'
|
||||||
|
reverse := r == 'F' || r == 'T'
|
||||||
|
switch next {
|
||||||
|
case CharEsc:
|
||||||
|
default:
|
||||||
|
if rb.MoveTo(next, prevChar, reverse) {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid operation
|
||||||
|
o.op.t.Bell()
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) EnterVimInsertMode() {
|
||||||
|
o.vimMode = VIM_INSERT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) ExitVimInsertMode() {
|
||||||
|
o.vimMode = VIM_NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) HandleVim(r rune, readNext func() rune) (rune, bool) {
|
||||||
|
if o.vimMode == VIM_NORMAL {
|
||||||
|
return o.HandleVimNormal(r, readNext)
|
||||||
|
}
|
||||||
|
if r == CharEsc {
|
||||||
|
o.ExitVimInsertMode()
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.vimMode {
|
||||||
|
case VIM_INSERT:
|
||||||
|
return r, false
|
||||||
|
case VIM_VISUAL:
|
||||||
|
}
|
||||||
|
return r, false
|
||||||
|
}
|
Loading…
Reference in New Issue