Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative readline #418

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
/*.pc

.vscode/
.idea/
6 changes: 3 additions & 3 deletions drivers/completer/completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package completer

import (
"fmt"
"github.com/xo/usql/rline"
"log"
"os"
"path/filepath"
"sort"
"strings"
"unicode"

"github.com/gohxs/readline"
"github.com/xo/usql/drivers/metadata"
"github.com/xo/usql/env"
"github.com/xo/usql/text"
Expand Down Expand Up @@ -111,7 +111,7 @@ var (
}
)

func NewDefaultCompleter(opts ...Option) readline.AutoCompleter {
func NewDefaultCompleter(opts ...Option) rline.Completer {
c := completer{
// an empty struct satisfies the metadata.Reader interface, because it is actually empty
reader: struct{}{},
Expand Down Expand Up @@ -277,7 +277,7 @@ type logger interface {
Println(...interface{})
}

func (c completer) Do(line []rune, start int) (newLine [][]rune, length int) {
func (c completer) Complete(line []rune, start int) (newLine [][]rune, length int) {
var i int
for i = start - 1; i > 0; i-- {
if strings.ContainsRune(WORD_BREAKS, line[i]) {
Expand Down
6 changes: 3 additions & 3 deletions drivers/drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"github.com/xo/usql/rline"
"io"
"reflect"
"strings"
Expand All @@ -15,7 +16,6 @@ import (

"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/gohxs/readline"
"github.com/xo/dburl"
"github.com/xo/usql/drivers/completer"
"github.com/xo/usql/drivers/metadata"
Expand Down Expand Up @@ -101,7 +101,7 @@ type Driver struct {
// NewMetadataWriter returns a db metadata printer.
NewMetadataWriter func(db DB, w io.Writer, opts ...metadata.ReaderOption) metadata.Writer
// NewCompleter returns a db auto-completer.
NewCompleter func(db DB, opts ...completer.Option) readline.AutoCompleter
NewCompleter func(db DB, opts ...completer.Option) rline.Completer
// Copy rows into the database table
Copy func(ctx context.Context, db *sql.DB, rows *sql.Rows, table string) (int64, error)
}
Expand Down Expand Up @@ -477,7 +477,7 @@ func NewMetadataWriter(ctx context.Context, u *dburl.URL, db DB, w io.Writer, op

// NewCompleter creates a metadata completer for a driver and database
// connection.
func NewCompleter(ctx context.Context, u *dburl.URL, db DB, readerOpts []metadata.ReaderOption, opts ...completer.Option) readline.AutoCompleter {
func NewCompleter(ctx context.Context, u *dburl.URL, db DB, readerOpts []metadata.ReaderOption, opts ...completer.Option) rline.Completer {
d, ok := drivers[u.Driver]
if !ok {
return nil
Expand Down
4 changes: 2 additions & 2 deletions drivers/metadata/mysql/metadata.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package mysql

import (
"github.com/xo/usql/rline"
"time"

"github.com/gohxs/readline"
"github.com/xo/usql/drivers"
"github.com/xo/usql/drivers/completer"
"github.com/xo/usql/drivers/metadata"
Expand All @@ -30,7 +30,7 @@ var (
infos.WithUsagePrivileges(false),
)
// NewCompleter for MySQL databases
NewCompleter = func(db drivers.DB, opts ...completer.Option) readline.AutoCompleter {
NewCompleter = func(db drivers.DB, opts ...completer.Option) rline.Completer {
readerOpts := []metadata.ReaderOption{
// this needs to be relatively low, since autocomplete is very interactive
metadata.WithTimeout(3 * time.Second),
Expand Down
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
69 changes: 1 addition & 68 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1325,41 +1325,7 @@ func (h *Handler) Include(path string, relative bool) error {
}
defer f.Close()
r := bufio.NewReader(f)
// setup rline
l := &rline.Rline{
N: func() ([]rune, error) {
buf := new(bytes.Buffer)
var b []byte
var isPrefix bool
var err error
for {
// read
b, isPrefix, err = r.ReadLine()
// when not EOF
if err != nil && err != io.EOF {
return nil, err
}
// append
if _, werr := buf.Write(b); werr != nil {
return nil, werr
}
// end of line
if !isPrefix || err != nil {
break
}
}
// peek and read possible line ending \n or \r\n
if err != io.EOF {
if err := peekEnding(buf, r); err != nil {
return nil, err
}
}
return []rune(buf.String()), err
},
Out: h.l.Stdout(),
Err: h.l.Stderr(),
Pw: h.l.Password,
}
l := rline.NewFromReader(r, h.l.Stdout(), h.l.Stderr(), h.l.Password)
p := New(l, h.user, filepath.Dir(path), h.nopw)
p.db, p.u = h.db, h.u
drivers.ConfigStmt(p.u, p.buf)
Expand Down Expand Up @@ -1408,39 +1374,6 @@ func readerOpts() []metadata.ReaderOption {
return opts
}

// peekEnding peeks to see if the next successive bytes in r is \n or \r\n,
// writing to w if it is. Does not advance r if the next bytes are not \n or
// \r\n.
func peekEnding(w io.Writer, r *bufio.Reader) error {
// peek first byte
buf, err := r.Peek(1)
switch {
case err != nil && err != io.EOF:
return err
case err == nil && buf[0] == '\n':
if _, rerr := r.ReadByte(); err != nil && err != io.EOF {
return rerr
}
_, werr := w.Write([]byte{'\n'})
return werr
case err == nil && buf[0] != '\r':
return nil
}
// peek second byte
buf, err = r.Peek(1)
switch {
case err != nil && err != io.EOF:
return err
case err == nil && buf[0] != '\n':
return nil
}
if _, rerr := r.ReadByte(); err != nil && err != io.EOF {
return rerr
}
_, werr := w.Write([]byte{'\n'})
return werr
}

// grab grabs i from r, or returns 0 if i >= end.
func grab(r []rune, i, end int) rune {
if i < end {
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
}
Loading
Loading