diff --git a/README.md b/README.md index 545dd28..72154aa 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,15 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read # Todo -* Vim mode * More funny examples * Support dumb/eterm-color terminal in emacs +# Features +* Support emacs/vi mode, almost all basic features that GNU-Readline is supported +* zsh-style backward/forward history search +* zsh-style completion +* Readline auto refresh when others write to Stdout while editing(it needs specify the Stdout/Stderr provided by *readline.Instance to others). + # Usage * Simplest example @@ -139,7 +144,7 @@ for { # Shortcut `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. * Shortcut in normal mode diff --git a/char.go b/char.go index 1293e16..7885da3 100644 --- a/char.go +++ b/char.go @@ -7,7 +7,7 @@ const ( CharDelete = 4 CharLineEnd = 5 CharForward = 6 - CharCancel = 7 + CharBell = 7 CharCtrlH = 8 CharTab = 9 CharCtrlJ = 10 @@ -26,8 +26,8 @@ const ( ) const ( - MetaPrev rune = -iota - 1 - MetaNext + MetaBackward rune = -iota - 1 + MetaForward MetaDelete MetaBackspace MetaTranspose diff --git a/complete.go b/complete.go index 9101752..122779a 100644 --- a/complete.go +++ b/complete.go @@ -127,7 +127,7 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool { next = false case CharTab, CharForward: o.doSelect() - case CharCancel, CharInterrupt: + case CharBell, CharInterrupt: o.ExitCompleteMode(true) next = false case CharNext: diff --git a/example/main.go b/example/main.go index 534bfb8..e550e95 100644 --- a/example/main.go +++ b/example/main.go @@ -20,6 +20,10 @@ bye } var completer = readline.NewPrefixCompleter( + readline.PcItem("mode", + readline.PcItem("vi"), + readline.PcItem("emacs"), + ), readline.PcItem("login"), readline.PcItem("say", readline.PcItem("hello"), @@ -52,8 +56,23 @@ func main() { if err != nil { break } - + line = strings.TrimSpace(line) switch { + case strings.HasPrefix(line, "mode "): + switch line[5:] { + case "vi": + l.SetVimMode(true) + case "emacs": + l.SetVimMode(false) + default: + println("invalid mode:", line[5:]) + } + case line == "mode": + if l.IsVimMode() { + println("current mode: vim") + } else { + println("current mode: emacs") + } case line == "login": pswd, err := l.ReadPassword("please enter your password: ") if err != nil { diff --git a/operation.go b/operation.go index b1d77e8..bd2247c 100644 --- a/operation.go +++ b/operation.go @@ -18,6 +18,7 @@ type Operation struct { *opHistory *opSearch *opCompleter + *opVim } type wrapWriter struct { @@ -56,6 +57,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation { outchan: make(chan []rune), opHistory: newOpHistory(cfg.HistoryFile), } + op.opVim = newVimMode(op) op.w = op.buf.w op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory) op.opCompleter = newOpCompleter(op.buf.w, op) @@ -87,13 +89,20 @@ func (o *Operation) ioloop() { case CharInterrupt: o.t.KickRead() fallthrough - case CharCancel: + case CharBell: + continue + } + } + + if o.IsEnableVimMode() { + r = o.HandleVim(r, o.t.ReadRune) + if r == 0 { continue } } switch r { - case CharCancel: + case CharBell: if o.IsSearchMode() { o.ExitSearchMode(true) o.buf.Refresh(nil) @@ -119,11 +128,11 @@ func (o *Operation) ioloop() { case CharKill: o.buf.Kill() keepInCompleteMode = true - case MetaNext: + case MetaForward: o.buf.MoveToNextWord() case CharTranspose: o.buf.Transpose() - case MetaPrev: + case MetaBackward: o.buf.MoveToPrevWord() case MetaDelete: o.buf.DeleteWord() @@ -132,7 +141,9 @@ func (o *Operation) ioloop() { case CharLineEnd: o.buf.MoveToLineEnd() case CharDelete: - o.buf.Delete() + if !o.buf.Delete() { + o.t.Bell() + } case CharBackspace, CharCtrlH: if o.IsSearchMode() { o.SearchBackspace() @@ -141,6 +152,7 @@ func (o *Operation) ioloop() { } if o.buf.Len() == 0 { + o.t.Bell() break } o.buf.Backspace() @@ -167,11 +179,15 @@ func (o *Operation) ioloop() { buf := o.PrevHistory() if buf != nil { o.buf.Set(buf) + } else { + o.t.Bell() } case CharNext: buf, ok := o.NextHistory() if ok { o.buf.Set(buf) + } else { + o.t.Bell() } case CharInterrupt: if o.IsSearchMode() { diff --git a/readline.go b/readline.go index 8bdebab..e91d6ae 100644 --- a/readline.go +++ b/readline.go @@ -11,6 +11,7 @@ type Config struct { Prompt string HistoryFile string AutoComplete AutoCompleter + VimMode bool Stdout io.Writer Stderr io.Writer @@ -59,6 +60,14 @@ func (i *Instance) Stderr() io.Writer { return i.o.Stderr() } +func (i *Instance) SetVimMode(on bool) { + i.o.SetVimMode(on) +} + +func (i *Instance) IsVimMode() bool { + return i.o.IsEnableVimMode() +} + func (i *Instance) ReadPassword(prompt string) ([]byte, error) { return i.o.Password(prompt) } diff --git a/runebuf.go b/runebuf.go index d680628..4b0f4aa 100644 --- a/runebuf.go +++ b/runebuf.go @@ -5,6 +5,11 @@ import ( "io" ) +type runeBufferBck struct { + buf []rune + idx int +} + type RuneBuffer struct { buf []rune idx int @@ -12,6 +17,22 @@ type RuneBuffer struct { w io.Writer cleanInScreen bool + + bck *runeBufferBck +} + +func (r *RuneBuffer) Backup() { + r.bck = &runeBufferBck{r.buf, r.idx} +} + +func (r *RuneBuffer) Restore() { + r.Refresh(func() { + if r.bck == nil { + return + } + r.buf = r.bck.buf + r.idx = r.bck.idx + }) } func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer { @@ -98,13 +119,22 @@ func (r *RuneBuffer) MoveForward() { }) } -func (r *RuneBuffer) Delete() { +func (r *RuneBuffer) Erase() { + r.Refresh(func() { + r.idx = 0 + r.buf = r.buf[:0] + }) +} + +func (r *RuneBuffer) Delete() (success bool) { r.Refresh(func() { if r.idx == len(r.buf) { return } r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) + success = true }) + return } func (r *RuneBuffer) DeleteWord() { @@ -126,7 +156,7 @@ func (r *RuneBuffer) DeleteWord() { r.Kill() } -func (r *RuneBuffer) MoveToPrevWord() { +func (r *RuneBuffer) MoveToPrevWord() (success bool) { r.Refresh(func() { if r.idx == 0 { return @@ -135,11 +165,14 @@ func (r *RuneBuffer) MoveToPrevWord() { for i := r.idx - 1; i > 0; i-- { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { r.idx = i + success = true return } } r.idx = 0 + success = true }) + return } func (r *RuneBuffer) KillFront() { @@ -237,6 +270,35 @@ func (r *RuneBuffer) LineCount() int { 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 { totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen() w := getWidth() diff --git a/terminal.go b/terminal.go index 12ac640..7afb025 100644 --- a/terminal.go +++ b/terminal.go @@ -113,6 +113,10 @@ func (t *Terminal) ioloop() { expectNextChar = true switch r { case CharEsc: + if t.cfg.VimMode { + t.outchan <- r + break + } isEscape = true case CharInterrupt, CharEnter, CharCtrlJ: expectNextChar = false @@ -123,6 +127,10 @@ func (t *Terminal) ioloop() { } } +func (t *Terminal) Bell() { + fmt.Fprintf(t, "%c", CharBell) +} + func (t *Terminal) Close() error { if atomic.SwapInt64(&t.closed, 1) != 0 { return nil diff --git a/utils.go b/utils.go index f3b3eb6..6e83ea2 100644 --- a/utils.go +++ b/utils.go @@ -57,9 +57,9 @@ func escapeExKey(r rune) rune { func escapeKey(r rune) rune { switch r { case 'b': - r = MetaPrev + r = MetaBackward case 'f': - r = MetaNext + r = MetaForward case 'd': r = MetaDelete case CharTranspose: diff --git a/vim.go b/vim.go new file mode 100644 index 0000000..f587b6c --- /dev/null +++ b/vim.go @@ -0,0 +1,149 @@ +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_INSERT +} + +func (o *opVim) IsEnableVimMode() bool { + return o.cfg.VimMode +} + +func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { + rb := o.op.buf + handled = true + switch r { + case 'h': + t = CharBackward + case 'j': + t = CharNext + case 'k': + t = CharPrev + case 'l': + t = CharForward + case '0', '^': + rb.MoveToLineStart() + case '$': + rb.MoveToLineEnd() + case 'b', 'B': + rb.MoveToPrevWord() + case 'w', 'W', 'e', 'E': + rb.MoveToNextWord() + case 'f', 'F', 't', 'T': + next := readNext() + prevChar := r == 't' || r == 'T' + reverse := r == 'F' || r == 'T' + switch next { + case CharEsc: + default: + rb.MoveTo(next, prevChar, reverse) + } + default: + return r, false + } + return t, true +} + +func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { + rb := o.op.buf + handled = true + switch r { + case 'i': + case 'I': + rb.MoveToLineStart() + case 'a': + rb.MoveForward() + case 'A': + rb.MoveToLineEnd() + case 's': + rb.Delete() + case 'S': + rb.Erase() + case 'c': + next := readNext() + switch next { + case 'c': + rb.Erase() + case 'w': + rb.DeleteWord() + } + default: + return r, false + } + + o.EnterVimInsertMode() + return +} + +func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { + switch r { + case CharEnter, CharInterrupt: + o.ExitVimMode() + return r + } + + if r, handled := o.handleVimNormalMovement(r, readNext); handled { + return r + } + + if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { + return r + } + + // invalid operation + o.op.t.Bell() + return 0 +} + +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 { + if o.vimMode == VIM_NORMAL { + return o.HandleVimNormal(r, readNext) + } + if r == CharEsc { + o.ExitVimInsertMode() + return 0 + } + + switch o.vimMode { + case VIM_INSERT: + return r + case VIM_VISUAL: + } + return r +}