add password support

This commit is contained in:
Cheney 2015-11-20 20:56:42 +08:00
parent ee4d466b62
commit 71e9536f4b
7 changed files with 239 additions and 17 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"io"
"log"
"strconv"
@ -30,6 +31,7 @@ var completer = readline.NewPrefixCompleter(
readline.PcItem("bye"),
),
readline.PcItem("setprompt"),
readline.PcItem("setpassword"),
readline.PcItem("bye"),
readline.PcItem("help"),
readline.PcItem("go",
@ -45,18 +47,27 @@ var completer = readline.NewPrefixCompleter(
)
func main() {
l, err := readline.NewEx(&readline.Config{
cfg := &readline.Config{
Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp",
AutoComplete: completer,
InterruptPrompt: "\nInterrupt, Press Ctrl+D to exit",
EOFPrompt: "exit",
})
}
l, err := readline.NewEx(cfg)
if err != nil {
panic(err)
}
defer l.Close()
setPasswordCfg := l.GenPasswordConfig()
setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line)))
l.Refresh()
return nil, 0, false
})
log.SetOutput(l.Stderr())
for {
line, err := l.Readline()
@ -88,6 +99,11 @@ func main() {
println("you enter:", strconv.Quote(string(pswd)))
case line == "help":
usage(l.Stderr())
case line == "setpassword":
pswd, err := l.ReadPasswordWithConfig(setPasswordCfg)
if err == nil {
println("you set:", strconv.Quote(string(pswd)))
}
case strings.HasPrefix(line, "setprompt"):
prompt := line[10:]
if prompt == "" {

View File

@ -33,10 +33,23 @@ func newOpHistory(cfg *Config) (o *opHistory) {
cfg: cfg,
history: list.New(),
}
return o
}
func (o *opHistory) IsHistoryClosed() bool {
return o.fd.Fd() == ^(uintptr(0))
}
func (o *opHistory) InitHistory() {
if o.IsHistoryClosed() {
o.initHistory()
}
}
func (o *opHistory) initHistory() {
if o.cfg.HistoryFile != "" {
o.historyUpdatePath(o.cfg.HistoryFile)
}
return
}
// only called by newOpHistory
@ -65,7 +78,7 @@ func (o *opHistory) historyUpdatePath(path string) {
}
func (o *opHistory) CompactHistory() {
for o.history.Len() > o.cfg.HistoryLimit {
for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 {
o.history.Remove(o.history.Front())
}
}
@ -225,9 +238,15 @@ func (o *opHistory) NewHistory(current []rune) {
o.PushHistory(nil)
}
func (o *opHistory) RevertHistory() {
o.historyVer++
o.current = o.history.Back()
}
func (o *opHistory) UpdateHistory(s []rune, commit bool) {
if o.current == nil {
o.PushHistory(s)
o.CompactHistory()
return
}
r := o.current.Value.(*hisItem)
@ -242,6 +261,7 @@ func (o *opHistory) UpdateHistory(s []rune, commit bool) {
r.Tmp = append(r.Tmp[:0], s...)
}
o.current.Value = r
o.CompactHistory()
}
func (o *opHistory) PushHistory(s []rune) {

View File

@ -24,6 +24,7 @@ type Operation struct {
*opHistory
*opSearch
*opCompleter
*opPassword
*opVim
}
@ -57,17 +58,16 @@ func (w *wrapWriter) Write(b []byte) (int, error) {
func NewOperation(t *Terminal, cfg *Config) *Operation {
op := &Operation{
cfg: cfg,
t: t,
buf: NewRuneBuffer(t, cfg.Prompt),
buf: NewRuneBuffer(t, cfg.Prompt, cfg.MaskRune),
outchan: make(chan []rune),
errchan: make(chan error),
}
op.SetHistoryPath(cfg.HistoryFile)
op.opVim = newVimMode(op)
op.w = op.buf.w
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
op.SetConfig(cfg)
op.opVim = newVimMode(op)
op.opCompleter = newOpCompleter(op.buf.w, op)
op.opPassword = newOpPassword(op)
go op.ioloop()
return op
}
@ -76,11 +76,16 @@ func (o *Operation) SetPrompt(s string) {
o.buf.SetPrompt(s)
}
func (o *Operation) SetMaskRune(r rune) {
o.buf.SetMask(r)
}
func (o *Operation) ioloop() {
for {
keepInSearchMode := false
keepInCompleteMode := false
r := o.t.ReadRune()
isUpdateHistory := true
if o.IsInCompleteSelectMode() {
keepInCompleteMode = o.HandleCompleteSelect(r)
@ -205,6 +210,8 @@ func (o *Operation) ioloop() {
// treat as EOF
o.buf.WriteString(o.cfg.EOFPrompt + "\n")
o.buf.Reset()
isUpdateHistory = false
o.RevertHistory()
o.errchan <- io.EOF
case CharInterrupt:
if o.IsSearchMode() {
@ -222,6 +229,8 @@ func (o *Operation) ioloop() {
o.buf.Refresh(nil)
o.buf.WriteString(o.cfg.InterruptPrompt + "\n")
o.buf.Reset()
isUpdateHistory = false
o.RevertHistory()
o.errchan <- ErrInterrupt
default:
if o.IsSearchMode() {
@ -236,19 +245,26 @@ func (o *Operation) ioloop() {
}
}
if o.cfg.Listener != nil {
newLine, newPos, ok := o.cfg.Listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
if ok {
o.buf.SetWithIdx(newPos, newLine)
}
}
if !keepInSearchMode && o.IsSearchMode() {
o.ExitSearchMode(false)
o.buf.Refresh(nil)
} else if o.IsInCompleteMode() {
if !keepInCompleteMode {
o.ExitCompleteMode(false)
o.buf.Refresh(nil)
o.Refresh()
} else {
o.buf.Refresh(nil)
o.CompleteRefresh()
}
}
if !o.IsSearchMode() {
if isUpdateHistory && !o.IsSearchMode() {
o.UpdateHistory(o.buf.Runes(), false)
}
}
@ -274,6 +290,9 @@ func (o *Operation) Runes() ([]rune, error) {
o.t.EnterRawMode()
defer o.t.ExitRawMode()
if o.cfg.Listener != nil {
o.cfg.Listener.OnChange(nil, 0, 0)
}
o.buf.Refresh(nil) // print prompt
o.t.KickRead()
select {
@ -284,6 +303,23 @@ func (o *Operation) Runes() ([]rune, error) {
}
}
func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) {
cfg := o.GenPasswordConfig()
cfg.Prompt = prompt
cfg.Listener = l
return o.PasswordWithConfig(cfg)
}
func (o *Operation) GenPasswordConfig() *Config {
return o.opPassword.PasswordConfig()
}
func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) {
o.opPassword.EnterPasswordMode(cfg)
defer o.opPassword.ExitPasswordMode()
return o.Slice()
}
func (o *Operation) Password(prompt string) ([]byte, error) {
w := o.Stdout()
if prompt != "" {
@ -324,3 +360,52 @@ func (o *Operation) SetHistoryPath(path string) {
func (o *Operation) IsNormalMode() bool {
return !o.IsInCompleteMode() && !o.IsSearchMode()
}
func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
if op.cfg == cfg {
return op.cfg, nil
}
if err := cfg.Init(); err != nil {
return op.cfg, err
}
old := op.cfg
op.cfg = cfg
op.SetPrompt(cfg.Prompt)
op.SetMaskRune(cfg.MaskRune)
if cfg.opHistory == nil {
op.SetHistoryPath(cfg.HistoryFile)
cfg.opHistory = op.opHistory
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
}
op.opHistory = cfg.opHistory
// SetHistoryPath will close opHistory which already exists
// so if we use it next time, we need to reopen it by `InitHistory()`
op.opHistory.InitHistory()
op.opSearch = cfg.opSearch
return old, nil
}
func (o *Operation) Refresh() {
if o.t.IsReading() {
o.buf.Refresh(nil)
}
}
func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener {
return &DumpListener{f: f}
}
type DumpListener struct {
f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
}
func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
return d.f(line, pos, key)
}
type Listener interface {
OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
}

32
password.go Normal file
View File

@ -0,0 +1,32 @@
package readline
type opPassword struct {
o *Operation
backupCfg *Config
}
func newOpPassword(o *Operation) *opPassword {
return &opPassword{o: o}
}
func (o *opPassword) ExitPasswordMode() {
o.o.SetConfig(o.backupCfg)
o.backupCfg = nil
}
func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) {
o.backupCfg, err = o.o.SetConfig(cfg)
return
}
func (o *opPassword) PasswordConfig() *Config {
return &Config{
MaskRune: '*',
InterruptPrompt: "\n",
EOFPrompt: "\n",
HistoryLimit: -1,
Stdout: o.o.cfg.Stdout,
Stderr: o.o.cfg.Stderr,
}
}

View File

@ -14,12 +14,16 @@ type Config struct {
// readline will persist historys to file where HistoryFile specified
HistoryFile string
// specify the max length of historys, it's 500 by default
// specify the max length of historys, it's 500 by default, set it to -1 to disable history
HistoryLimit int
// AutoCompleter will called once user press TAB
AutoComplete AutoCompleter
// Any key press will pass to Listener
// NOTE: Listener will be triggered by (nil, 0, 0) immediately
Listener Listener
// If VimMode is true, readline will in vim.insert mode by default
VimMode bool
@ -29,7 +33,12 @@ type Config struct {
Stdout io.Writer
Stderr io.Writer
inited bool
MaskRune rune
// private fields
inited bool
opHistory *opHistory
opSearch *opSearch
}
func (c *Config) Init() error {
@ -43,7 +52,7 @@ func (c *Config) Init() error {
if c.Stderr == nil {
c.Stderr = Stderr
}
if c.HistoryLimit <= 0 {
if c.HistoryLimit == 0 {
c.HistoryLimit = 500
}
@ -61,6 +70,10 @@ func (c *Config) Init() error {
return nil
}
func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) {
c.Listener = FuncListener(f)
}
func NewEx(cfg *Config) (*Instance, error) {
t, err := NewTerminal(cfg)
if err != nil {
@ -82,6 +95,10 @@ func (i *Instance) SetPrompt(s string) {
i.Operation.SetPrompt(s)
}
func (i *Instance) SetMaskRune(r rune) {
i.Operation.SetMaskRune(r)
}
// change hisotry persistence in runtime
func (i *Instance) SetHistoryPath(p string) {
i.Operation.SetHistoryPath(p)
@ -106,6 +123,19 @@ func (i *Instance) IsVimMode() bool {
return i.Operation.IsEnableVimMode()
}
func (i *Instance) GenPasswordConfig() *Config {
return i.Operation.GenPasswordConfig()
}
// we can generate a config by `i.GenPasswordConfig()`
func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) {
return i.Operation.PasswordWithConfig(cfg)
}
func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) {
return i.Operation.PasswordEx(prompt, l)
}
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
return i.Operation.Password(prompt)
}
@ -127,3 +157,18 @@ func (i *Instance) Close() error {
i.Operation.Close()
return nil
}
func (i *Instance) SetConfig(cfg *Config) *Config {
if i.Config == cfg {
return cfg
}
old := i.Config
i.Config = cfg
i.Operation.SetConfig(cfg)
i.Terminal.SetConfig(cfg)
return old
}
func (i *Instance) Refresh() {
i.Operation.Refresh()
}

View File

@ -3,6 +3,7 @@ package readline
import (
"bytes"
"io"
"strings"
"github.com/chzyer/readline/runes"
)
@ -17,6 +18,7 @@ type RuneBuffer struct {
idx int
prompt []rune
w io.Writer
mask rune
cleanInScreen bool
@ -37,14 +39,19 @@ func (r *RuneBuffer) Restore() {
})
}
func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer {
func NewRuneBuffer(w io.Writer, prompt string, mask rune) *RuneBuffer {
rb := &RuneBuffer{
w: w,
w: w,
mask: mask,
}
rb.SetPrompt(prompt)
return rb
}
func (r *RuneBuffer) SetMask(m rune) {
r.mask = m
}
func (r *RuneBuffer) CurrentWidth(x int) int {
return runes.WidthAll(r.buf[:x])
}
@ -336,7 +343,16 @@ func (r *RuneBuffer) Refresh(f func()) {
func (r *RuneBuffer) output() []byte {
buf := bytes.NewBuffer(nil)
buf.WriteString(string(r.prompt))
buf.Write([]byte(string(r.buf)))
if r.mask != 0 && len(r.buf) > 0 {
buf.Write([]byte(strings.Repeat(string(r.mask), len(r.buf)-1)))
if r.buf[len(r.buf)-1] == '\n' {
buf.Write([]byte{'\n'})
} else {
buf.Write([]byte(string(r.mask)))
}
} else {
buf.Write([]byte(string(r.buf)))
}
if len(r.buf) > r.idx {
buf.Write(runes.Backspace(r.buf[r.idx:]))
}

View File

@ -150,3 +150,11 @@ func (t *Terminal) Close() error {
t.wg.Wait()
return t.ExitRawMode()
}
func (t *Terminal) SetConfig(c *Config) error {
if err := c.Init(); err != nil {
return err
}
t.cfg = c
return nil
}