Skip to content

Commit

Permalink
Use alternative readline library
Browse files Browse the repository at this point in the history
  • Loading branch information
nineinchnick committed Nov 27, 2023
1 parent 5b1c526 commit 174fe58
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 36 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/nakagami/firebirdsql v0.9.6
github.com/ory/dockertest/v3 v3.10.0
github.com/prestodb/presto-go-client v0.0.0-20230524183650-a1a0bac0f63e
github.com/reeflective/readline v1.0.8
github.com/sijms/go-ora/v2 v2.7.22
github.com/sirupsen/logrus v1.9.3
github.com/snowflakedb/gosnowflake v1.6.25
Expand All @@ -56,6 +57,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
github.com/yookoala/realpath v1.0.0
github.com/ziutek/mymysql v1.5.4
golang.org/x/term v0.14.0
gorm.io/driver/bigquery v1.2.0
modernc.org/ql v1.4.7
modernc.org/sqlite v1.27.0
Expand Down Expand Up @@ -260,7 +262,6 @@ require (
golang.org/x/oauth2 v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.4.0 // indirect
golang.org/x/tools v0.15.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,8 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/reeflective/readline v1.0.8 h1:VuDGI82lAwl1H5by+hpW4OQgM+9ikh6EuOySQUGP3sI=
github.com/reeflective/readline v1.0.8/go.mod h1:5JgnHb/ZCvp/6RUA59HEansPBxWTkyBO4hJ5LL9Fp1Y=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
Expand Down
172 changes: 172 additions & 0 deletions rline/new_readline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//go:build new_readline

package rline

import (
"github.com/reeflective/readline/inputrc"
"golang.org/x/term"
"io"
"os"
"os/signal"
"syscall"

"github.com/mattn/go-isatty"
"github.com/reeflective/readline"
)

var (
// ErrInterrupt is the interrupt error.
ErrInterrupt = readline.ErrInterrupt
)

// baseRline should be embedded in a struct implementing the IO interface,
// as it keeps implementation specific state.
type baseRline struct {
instance *readline.Shell
prompt string
}

// Prompt sets the prompt for the next interactive line read.
func (l *rline) Prompt(s string) {
l.prompt = s
}

// Completer sets the auto-completer.
func (l *rline) Completer(a Completer) {
l.instance.Completer = func(line []rune, cursor int) readline.Completions {
candidates, _ := a.Complete(line, cursor)
values := make([]string, len(candidates))
for candidate := range candidates {
values = append(values, string(candidate))
}
return readline.CompleteValues(values...)
}
}

// SetOutput sets the output format func.
func (l *rline) SetOutput(f func(string) string) {
l.instance.SyntaxHighlighter = func(line []rune) string {
return f(string(line))
}
}

// New readline input/output handler.
func New(forceNonInteractive bool, out, histfile string) (IO, error) {
// determine if interactive
interactive, cygwin := false, false
if !forceNonInteractive {
interactive = isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd())
cygwin = isatty.IsCygwinTerminal(os.Stdout.Fd()) && isatty.IsCygwinTerminal(os.Stdin.Fd())
}
var stdout io.WriteCloser
var closers []func() error
switch {
case out != "":
var err error
stdout, err = os.OpenFile(out, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
}
closers = append(closers, stdout.Close)
interactive = false
default:
stdout = os.Stdout
}
// configure stderr
var stderr io.Writer = os.Stderr
// TODO handle interrupts?
options := []inputrc.Option{inputrc.WithName("usql")}
/*
&readline.Config{
HistoryFile: histfile,
DisableAutoSaveHistory: true,
InterruptPrompt: "^C",
HistorySearchFold: true,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
FuncIsTerminal: func() bool {
return interactive || cygwin
},
FuncFilterInputRune: func(r rune) (rune, bool) {
if r == readline.CharCtrlZ {
return r, false
}
return r, true
},
}
*/
// create readline instance
shell := readline.NewShell(options...)
var history readline.History
if histfile != "" {
history, err := readline.NewHistoryFromFile(histfile)
if err != nil {
return nil, err
}
shell.History.Add("default", history)
}

n := func() ([]rune, error) {
line, err := shell.Readline()
return []rune(line), err
}
pw := func(prompt string) (string, error) {
_, err := shell.Printf(prompt)
if err != nil {
return "", err
}
return readPassword()
}
if forceNonInteractive {
n, pw = nil, nil
}
result := &rline{
baseRline: baseRline{instance: shell},
nextLine: n,
close: func() error {
for _, f := range closers {
_ = f()
}
return nil
},
stdout: stdout,
stderr: stderr,
isInteractive: interactive || cygwin,
passwordPrompt: pw,
}
shell.Prompt.Primary(func() string {
return result.prompt
})
if history != nil {
result.saveHistory = func(input string) error {
_, err := history.Write(input)
return err
}
}
return result, nil
}

func readPassword() (string, error) {
stdin := syscall.Stdin
oldState, err := term.GetState(stdin)
if err != nil {
return "", err
}
defer term.Restore(stdin, oldState)

sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
go func() {
for _ = range sigch {
term.Restore(stdin, oldState)
os.Exit(1)
}
}()

password, err := term.ReadPassword(stdin)
if err != nil {
return "", err
}
return string(password), nil
}
48 changes: 29 additions & 19 deletions rline/readline.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,29 @@ var (
// baseRline should be embedded in a struct implementing the IO interface,
// as it keeps implementation specific state.
type baseRline struct {
instance *readline.Instance
instance *readline.Instance
prompt func(string)
completer func(Completer)
}

// Prompt sets the prompt for the next interactive line read.
func (l *rline) Prompt(s string) {
l.instance.SetPrompt(s)
}

// Completer sets the auto-completer.
func (l *rline) Completer(a Completer) {
cfg := l.instance.Config.Clone()
cfg.AutoComplete = readlineCompleter{c: a}
l.instance.SetConfig(cfg)
}

type readlineCompleter struct {
c Completer
}

func (r readlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
return r.c.Complete(line, pos)
}

// SetOutput sets the output format func.
Expand Down Expand Up @@ -102,32 +124,20 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) {
n, pw = nil, nil
}
return &rline{
instance: l,
baseRline: baseRline{
instance: l,
},
nextLine: n,
close: func() error {
for _, f := range closers {
_ = f()
}
return nil
},
stdout: stdout,
stderr: stderr,
isInteractive: interactive || cygwin,
prompt: l.SetPrompt,
completer: func(a Completer) {
cfg := l.Config.Clone()
cfg.AutoComplete = readlineCompleter{c: a}
l.SetConfig(cfg)
},
stdout: stdout,
stderr: stderr,
isInteractive: interactive || cygwin,
saveHistory: l.SaveHistory,
passwordPrompt: pw,
}, nil
}

type readlineCompleter struct {
c Completer
}

func (r readlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
return r.c.Complete(line, pos)
}
16 changes: 0 additions & 16 deletions rline/rline.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ type rline struct {
stdout io.Writer
stderr io.Writer
isInteractive bool
prompt func(string)
completer func(Completer)
saveHistory func(string) error
passwordPrompt passwordPrompt
}
Expand Down Expand Up @@ -90,20 +88,6 @@ func (l *rline) Interactive() bool {
return l.isInteractive
}

// Prompt sets the prompt for the next interactive line read.
func (l *rline) Prompt(s string) {
if l.prompt != nil {
l.prompt(s)
}
}

// Completer sets the auto-completer.
func (l *rline) Completer(a Completer) {
if l.completer != nil {
l.completer(a)
}
}

// Save saves a line of history.
func (l *rline) Save(s string) error {
if l.saveHistory != nil {
Expand Down

0 comments on commit 174fe58

Please sign in to comment.