mirror of https://github.com/chzyer/readline.git
add password support
This commit is contained in:
parent
ee4d466b62
commit
71e9536f4b
|
@ -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 == "" {
|
||||
|
|
24
history.go
24
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) {
|
||||
|
|
99
operation.go
99
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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
49
readline.go
49
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
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
18
runebuf.go
18
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,
|
||||
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))
|
||||
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:]))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue