diff --git a/complete.go b/complete.go index 6d5bc16..3a7aa4f 100644 --- a/complete.go +++ b/complete.go @@ -183,7 +183,7 @@ func (o *opCompleter) CompleteRefresh() { colWidth = w } } - colNum := getWidth(o.op.cfg.StdoutFd) / (colWidth + o.candidateOff + 2) + colNum := o.op.cfg.FuncGetWidth() / (colWidth + o.candidateOff + 2) o.candidateColNum = colNum buf := bytes.NewBuffer(nil) buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) diff --git a/operation.go b/operation.go index 02cc2e3..e1e8b07 100644 --- a/operation.go +++ b/operation.go @@ -2,10 +2,7 @@ package readline import ( "errors" - "fmt" "io" - - "golang.org/x/crypto/ssh/terminal" ) var ( @@ -58,7 +55,7 @@ func (w *wrapWriter) Write(b []byte) (int, error) { func NewOperation(t *Terminal, cfg *Config) *Operation { op := &Operation{ t: t, - buf: NewRuneBuffer(t, cfg.Prompt, cfg.MaskRune, cfg), + buf: NewRuneBuffer(t, cfg.Prompt, cfg), outchan: make(chan []rune), errchan: make(chan error), } @@ -341,16 +338,7 @@ func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) { } func (o *Operation) Password(prompt string) ([]byte, error) { - w := o.Stdout() - if prompt != "" { - fmt.Fprintf(w, prompt) - } - o.t.EnterRawMode() - defer o.t.ExitRawMode() - - b, err := terminal.ReadPassword(int(o.cfg.Stdin.Fd())) - fmt.Fprint(w, "\r\n") - return b, err + return o.PasswordEx(prompt, nil) } func (o *Operation) SetTitle(t string) { diff --git a/password.go b/password.go index 95a939a..4b07379 100644 --- a/password.go +++ b/password.go @@ -21,7 +21,7 @@ func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) { func (o *opPassword) PasswordConfig() *Config { return &Config{ - MaskRune: '*', + EnableMask: true, InterruptPrompt: "\n", EOFPrompt: "\n", HistoryLimit: -1, diff --git a/readline.go b/readline.go index 0ffc5b4..53755fc 100644 --- a/readline.go +++ b/readline.go @@ -11,11 +11,6 @@ type Instance struct { Operation *Operation } -type FdReader interface { - io.Reader - Fd() uintptr -} - type Config struct { // prompt supports ANSI escape sequence, so we can color some characters even in windows Prompt string @@ -39,12 +34,17 @@ type Config struct { InterruptPrompt string EOFPrompt string - Stdin FdReader + FuncGetWidth func() int + + Stdin io.Reader Stdout io.Writer Stderr io.Writer - MaskRune rune + EnableMask bool + MaskRune rune + // erase the editing line after user submited it + // it often use in IM. UniqueEditLine bool // force use interactive even stdout is not a tty @@ -100,6 +100,10 @@ func (c *Config) Init() error { c.EOFPrompt = "" } + if c.FuncGetWidth == nil { + c.FuncGetWidth = genGetWidthFunc(c.StdoutFd) + } + return nil } diff --git a/runebuf.go b/runebuf.go index bf4c222..a137fb9 100644 --- a/runebuf.go +++ b/runebuf.go @@ -18,7 +18,6 @@ type RuneBuffer struct { idx int prompt []rune w io.Writer - mask rune cleanInScreen bool interactive bool @@ -41,10 +40,9 @@ func (r *RuneBuffer) Restore() { }) } -func NewRuneBuffer(w io.Writer, prompt string, mask rune, cfg *Config) *RuneBuffer { +func NewRuneBuffer(w io.Writer, prompt string, cfg *Config) *RuneBuffer { rb := &RuneBuffer{ w: w, - mask: mask, interactive: cfg.useInteractive(), cfg: cfg, } @@ -58,7 +56,7 @@ func (r *RuneBuffer) SetConfig(cfg *Config) { } func (r *RuneBuffer) SetMask(m rune) { - r.mask = m + r.cfg.MaskRune = m } func (r *RuneBuffer) CurrentWidth(x int) int { @@ -295,7 +293,7 @@ func (r *RuneBuffer) MoveToLineEnd() { } func (r *RuneBuffer) LineCount() int { - return LineCount(r.cfg.StdoutFd, runes.WidthAll(r.buf)+r.PromptLen()) + return LineCount(r.cfg.FuncGetWidth, runes.WidthAll(r.buf)+r.PromptLen()) } func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { @@ -329,7 +327,7 @@ func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { func (r *RuneBuffer) IdxLine() int { totalWidth := runes.WidthAll(r.buf[:r.idx]) + r.PromptLen() - w := getWidth(r.cfg.StdoutFd) + w := r.cfg.FuncGetWidth() if w <= 0 { return -1 } @@ -368,12 +366,12 @@ 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.cfg.EnableMask && len(r.buf) > 0 { + buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1))) if r.buf[len(r.buf)-1] == '\n' { buf.Write([]byte{'\n'}) } else { - buf.Write([]byte(string(r.mask))) + buf.Write([]byte(string(r.cfg.MaskRune))) } } else { buf.Write([]byte(string(r.buf))) diff --git a/search.go b/search.go index fdcab37..3b40ad6 100644 --- a/search.go +++ b/search.go @@ -125,7 +125,7 @@ func (o *opSearch) SearchRefresh(x int) { } x = o.buf.CurrentWidth(x) x += o.buf.PromptLen() - x = x % getWidth(o.cfg.StdoutFd) + x = x % o.cfg.FuncGetWidth() if o.markStart > 0 { o.buf.SetStyle(o.markStart, o.markEnd, "4") diff --git a/std.go b/std.go index 326e64b..5fe2c29 100644 --- a/std.go +++ b/std.go @@ -17,6 +17,7 @@ var ( stdOnce sync.Once ) +// global instance will not submit history automatic func getInstance() *Instance { stdOnce.Do(func() { std, _ = NewEx(&Config{ @@ -26,6 +27,10 @@ func getInstance() *Instance { return std } +// let readline load history from filepath +// and try to persist history into disk +// set fp to "" to prevent readline persisting history to disk +// so the `AddHistory` will return nil error forever. func SetHistoryPath(fp string) { ins := getInstance() cfg := ins.Config.Clone() @@ -33,6 +38,7 @@ func SetHistoryPath(fp string) { ins.SetConfig(cfg) } +// set auto completer to global instance func SetAutoComplete(completer AutoCompleter) { ins := getInstance() cfg := ins.Config.Clone() @@ -40,11 +46,14 @@ func SetAutoComplete(completer AutoCompleter) { ins.SetConfig(cfg) } +// add history to global instance manually +// raise error only if `SetHistoryPath` is set with a non-empty path func AddHistory(content string) error { ins := getInstance() return ins.SaveHistory(content) } +// readline with global configs func Line(prompt string) (string, error) { ins := getInstance() ins.SetPrompt(prompt) diff --git a/terminal.go b/terminal.go index 71a72f6..b65361c 100644 --- a/terminal.go +++ b/terminal.go @@ -36,7 +36,7 @@ func NewTerminal(cfg *Config) (*Terminal, error) { } func (t *Terminal) EnterRawMode() (err error) { - t.state, err = MakeRaw(int(t.cfg.Stdin.Fd())) + t.state, err = MakeRaw(int(t.cfg.StdinFd)) return err } @@ -44,7 +44,7 @@ func (t *Terminal) ExitRawMode() (err error) { if t.state == nil { return } - err = Restore(int(t.cfg.Stdin.Fd()), t.state) + err = Restore(int(t.cfg.StdinFd), t.state) if err == nil { t.state = nil } diff --git a/utils.go b/utils.go index 6be2e38..a270952 100644 --- a/utils.go +++ b/utils.go @@ -87,8 +87,8 @@ func escapeKey(r rune) rune { } // calculate how many lines for N character -func LineCount(stdoutFd int, w int) int { - screenWidth := getWidth(stdoutFd) +func LineCount(getWidth func() int, w int) int { + screenWidth := getWidth() r := w / screenWidth if w%screenWidth != 0 { r++ @@ -116,3 +116,9 @@ func GetInt(s []string, def int) int { } return c } + +func genGetWidthFunc(fd int) func() int { + return func() int { + return getWidth(fd) + } +}