diff --git a/example/main.go b/example/main.go index 20a8650..cc1a32a 100644 --- a/example/main.go +++ b/example/main.go @@ -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 == "" { diff --git a/history.go b/history.go index 6f7e77d..3d1a672 100644 --- a/history.go +++ b/history.go @@ -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) { diff --git a/operation.go b/operation.go index 1e2a2b7..8592de1 100644 --- a/operation.go +++ b/operation.go @@ -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) +} diff --git a/password.go b/password.go new file mode 100644 index 0000000..95a939a --- /dev/null +++ b/password.go @@ -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, + } +} diff --git a/readline.go b/readline.go index 7ab8e45..06b4211 100644 --- a/readline.go +++ b/readline.go @@ -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() +} diff --git a/runebuf.go b/runebuf.go index 69dc378..69722aa 100644 --- a/runebuf.go +++ b/runebuf.go @@ -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:])) } diff --git a/terminal.go b/terminal.go index 1cf4a27..24114bd 100644 --- a/terminal.go +++ b/terminal.go @@ -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 +}