mirror of
https://github.com/wader/fq.git
synced 2024-12-01 19:12:34 +03:00
48a19cb82c
Also refactor readline and eval args into option struct and partinally start addressing some side effects during completion.
259 lines
5.4 KiB
Go
259 lines
5.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/wader/fq/pkg/interp"
|
|
"github.com/wader/fq/pkg/registry"
|
|
|
|
"github.com/wader/readline"
|
|
)
|
|
|
|
func maybeLogFile() {
|
|
// used during dev to redirect log to file, useful when debugging repl etc
|
|
if lf := os.Getenv("LOGFILE"); lf != "" {
|
|
if f, err := os.Create(lf); err == nil {
|
|
log.SetOutput(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
type autoCompleterFn func(line []rune, pos int) (newLine [][]rune, length int)
|
|
|
|
func (a autoCompleterFn) Do(line []rune, pos int) (newLine [][]rune, length int) {
|
|
return a(line, pos)
|
|
}
|
|
|
|
type stdOS struct {
|
|
rl *readline.Instance
|
|
closeChan chan struct{}
|
|
interruptChan chan struct{}
|
|
}
|
|
|
|
func newStandardOS() *stdOS {
|
|
closeChan := make(chan struct{})
|
|
interruptChan := make(chan struct{}, 1)
|
|
|
|
// this more or less converts a os signal chan to just a struct{} chan that
|
|
// ignores signals if forwarding it would block, also this makes sure interp
|
|
// does not know about os.
|
|
go func() {
|
|
interruptSignalChan := make(chan os.Signal, 1)
|
|
signal.Notify(interruptSignalChan, os.Interrupt)
|
|
defer func() {
|
|
signal.Stop(interruptSignalChan)
|
|
close(interruptSignalChan)
|
|
close(interruptChan)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-interruptSignalChan:
|
|
// ignore if interruptChan is full
|
|
select {
|
|
case interruptChan <- struct{}{}:
|
|
default:
|
|
}
|
|
case <-closeChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &stdOS{
|
|
closeChan: closeChan,
|
|
interruptChan: interruptChan,
|
|
}
|
|
}
|
|
|
|
func (stdOS) Platform() interp.Platform {
|
|
return interp.Platform{
|
|
OS: runtime.GOOS,
|
|
Arch: runtime.GOARCH,
|
|
}
|
|
}
|
|
|
|
type fdTerminal uintptr
|
|
|
|
func (fd fdTerminal) Size() (int, int) {
|
|
w, h, _ := readline.GetSize(int(fd))
|
|
return w, h
|
|
}
|
|
func (fd fdTerminal) IsTerminal() bool {
|
|
return readline.IsTerminal(int(fd))
|
|
}
|
|
|
|
type stdinInput struct {
|
|
fdTerminal
|
|
fs.File
|
|
}
|
|
|
|
func (o *stdOS) Stdin() interp.Input {
|
|
return stdinInput{
|
|
fdTerminal: fdTerminal(os.Stdin.Fd()),
|
|
File: interp.FileReader{
|
|
R: os.Stdin,
|
|
FileInfo: interp.FixedFileInfo{
|
|
FName: "stdin",
|
|
FMode: fs.ModeIrregular,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type stdoutOutput struct {
|
|
fdTerminal
|
|
os *stdOS
|
|
}
|
|
|
|
func (o stdoutOutput) Write(p []byte) (n int, err error) {
|
|
// Let write go thru readline if it has been used. This to have ansi color emulation
|
|
// on windows thru readlins:s stdout rewriter
|
|
// TODO: check if tty instead? else only color when repl
|
|
if o.os.rl != nil {
|
|
return o.os.rl.Write(p)
|
|
}
|
|
return os.Stdout.Write(p)
|
|
}
|
|
|
|
func (o *stdOS) Stdout() interp.Output {
|
|
return stdoutOutput{fdTerminal: fdTerminal(os.Stdout.Fd()), os: o}
|
|
}
|
|
|
|
type stderrOutput struct {
|
|
fdTerminal
|
|
}
|
|
|
|
func (o stderrOutput) Write(p []byte) (n int, err error) { return os.Stderr.Write(p) }
|
|
|
|
func (o *stdOS) Stderr() interp.Output { return stderrOutput{fdTerminal: fdTerminal(os.Stderr.Fd())} }
|
|
|
|
func (o *stdOS) InterruptChan() chan struct{} { return o.interruptChan }
|
|
|
|
func (*stdOS) Args() []string { return os.Args }
|
|
|
|
func (*stdOS) Environ() []string { return os.Environ() }
|
|
|
|
func (*stdOS) ConfigDir() (string, error) {
|
|
p, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(p, "fq"), nil
|
|
}
|
|
|
|
type stdOSFS struct{}
|
|
|
|
func (stdOSFS) Open(name string) (fs.File, error) { return os.Open(name) }
|
|
|
|
func (*stdOS) FS() fs.FS { return stdOSFS{} }
|
|
|
|
func (o *stdOS) Readline(opts interp.ReadlineOpts) (string, error) {
|
|
if o.rl == nil {
|
|
var err error
|
|
|
|
var historyFile string
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
historyFile = filepath.Join(cacheDir, "fq/history")
|
|
_ = os.MkdirAll(filepath.Dir(historyFile), 0700)
|
|
|
|
o.rl, err = readline.NewEx(&readline.Config{
|
|
HistoryFile: historyFile,
|
|
HistorySearchFold: true,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if opts.CompleteFn != nil {
|
|
o.rl.Config.AutoComplete = autoCompleterFn(func(line []rune, pos int) (newLine [][]rune, length int) {
|
|
names, shared := opts.CompleteFn(string(line), pos)
|
|
var runeNames [][]rune
|
|
for _, name := range names {
|
|
runeNames = append(runeNames, []rune(name[shared:]))
|
|
}
|
|
|
|
return runeNames, shared
|
|
})
|
|
}
|
|
|
|
o.rl.SetPrompt(opts.Prompt)
|
|
line, err := o.rl.Readline()
|
|
if errors.Is(err, readline.ErrInterrupt) {
|
|
return "", interp.ErrInterrupt
|
|
} else if errors.Is(err, io.EOF) {
|
|
return "", interp.ErrEOF
|
|
} else if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return line, nil
|
|
}
|
|
|
|
func (o *stdOS) History() ([]string, error) {
|
|
// TODO: refactor history handling to use internal fs?
|
|
r, err := os.Open(o.rl.Config.HistoryFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
var hs []string
|
|
lineScanner := bufio.NewScanner(r)
|
|
for lineScanner.Scan() {
|
|
hs = append(hs, lineScanner.Text())
|
|
}
|
|
if err := lineScanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return hs, nil
|
|
}
|
|
|
|
func (o *stdOS) Close() error {
|
|
// only close if is terminal otherwise ansi reset will write
|
|
// to stdout and mess up raw output
|
|
if o.rl != nil {
|
|
o.rl.Close()
|
|
}
|
|
close(o.closeChan)
|
|
return nil
|
|
}
|
|
|
|
func Main(r *registry.Registry, version string) {
|
|
os.Exit(func() int {
|
|
defer maybeProfile()()
|
|
maybeLogFile()
|
|
|
|
sos := newStandardOS()
|
|
defer sos.Close()
|
|
i, err := interp.New(sos, r)
|
|
defer i.Stop()
|
|
if err != nil {
|
|
fmt.Fprintln(sos.Stderr(), err)
|
|
return 1
|
|
}
|
|
|
|
if err := i.Main(context.Background(), sos.Stdout(), version); err != nil {
|
|
if ex, ok := err.(interp.Exiter); ok { //nolint:errorlint
|
|
return ex.ExitCode()
|
|
}
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}())
|
|
}
|