mirror of https://github.com/chzyer/readline.git
add remote mode
This commit is contained in:
parent
14e9df7f3c
commit
b57eccfd02
10
complete.go
10
complete.go
|
@ -22,6 +22,7 @@ type opCompleter struct {
|
|||
w io.Writer
|
||||
op *Operation
|
||||
ac AutoCompleter
|
||||
width int
|
||||
|
||||
inCompleteMode bool
|
||||
inSelectMode bool
|
||||
|
@ -32,11 +33,12 @@ type opCompleter struct {
|
|||
candidateColNum int
|
||||
}
|
||||
|
||||
func newOpCompleter(w io.Writer, op *Operation) *opCompleter {
|
||||
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
||||
return &opCompleter{
|
||||
w: w,
|
||||
op: op,
|
||||
ac: op.cfg.AutoComplete,
|
||||
width: width,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +173,10 @@ func (o *opCompleter) getMatrixSize() int {
|
|||
return line * o.candidateColNum
|
||||
}
|
||||
|
||||
func (o *opCompleter) OnWidthChange(newWidth int) {
|
||||
o.width = newWidth
|
||||
}
|
||||
|
||||
func (o *opCompleter) CompleteRefresh() {
|
||||
if !o.inCompleteMode {
|
||||
return
|
||||
|
@ -183,7 +189,7 @@ func (o *opCompleter) CompleteRefresh() {
|
|||
colWidth = w
|
||||
}
|
||||
}
|
||||
colNum := o.op.cfg.FuncGetWidth() / (colWidth + o.candidateOff + 2)
|
||||
colNum := o.width / (colWidth + o.candidateOff + 2)
|
||||
o.candidateColNum = colNum
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import "github.com/chzyer/readline"
|
||||
|
||||
func main() {
|
||||
if err := readline.DialRemote("tcp", ":12344"); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := &readline.Config{
|
||||
Prompt: "readline-remote: ",
|
||||
}
|
||||
handleFunc := func(rl *readline.Instance) {
|
||||
for {
|
||||
line, err := rl.Readline()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Fprintln(rl.Stdout(), "receive:"+line)
|
||||
}
|
||||
}
|
||||
err := readline.ListenRemote("tcp", ":12344", cfg, handleFunc)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
16
operation.go
16
operation.go
|
@ -53,17 +53,24 @@ func (w *wrapWriter) Write(b []byte) (int, error) {
|
|||
}
|
||||
|
||||
func NewOperation(t *Terminal, cfg *Config) *Operation {
|
||||
width := cfg.FuncGetWidth()
|
||||
op := &Operation{
|
||||
t: t,
|
||||
buf: NewRuneBuffer(t, cfg.Prompt, cfg),
|
||||
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
|
||||
outchan: make(chan []rune),
|
||||
errchan: make(chan error),
|
||||
}
|
||||
op.w = op.buf.w
|
||||
op.SetConfig(cfg)
|
||||
op.opVim = newVimMode(op)
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op)
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||
op.opPassword = newOpPassword(op)
|
||||
op.cfg.FuncOnWidthChanged(func() {
|
||||
newWidth := cfg.FuncGetWidth()
|
||||
op.opCompleter.OnWidthChange(newWidth)
|
||||
op.opSearch.OnWidthChange(newWidth)
|
||||
op.buf.OnWidthChange(newWidth)
|
||||
})
|
||||
go op.ioloop()
|
||||
return op
|
||||
}
|
||||
|
@ -381,11 +388,12 @@ func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
|
|||
op.SetPrompt(cfg.Prompt)
|
||||
op.SetMaskRune(cfg.MaskRune)
|
||||
op.buf.SetConfig(cfg)
|
||||
width := op.cfg.FuncGetWidth()
|
||||
|
||||
if cfg.opHistory == nil {
|
||||
op.SetHistoryPath(cfg.HistoryFile)
|
||||
cfg.opHistory = op.history
|
||||
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg)
|
||||
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width)
|
||||
}
|
||||
op.history = cfg.opHistory
|
||||
|
||||
|
@ -394,7 +402,7 @@ func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
|
|||
op.history.Init()
|
||||
|
||||
if op.cfg.AutoComplete != nil {
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op)
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||
}
|
||||
|
||||
op.opSearch = cfg.opSearch
|
||||
|
|
29
readline.go
29
readline.go
|
@ -45,8 +45,10 @@ type Config struct {
|
|||
UniqueEditLine bool
|
||||
|
||||
// force use interactive even stdout is not a tty
|
||||
StdinFd int
|
||||
StdoutFd int
|
||||
FuncIsTerminal func() bool
|
||||
FuncMakeRaw func() error
|
||||
FuncExitRaw func() error
|
||||
FuncOnWidthChanged func(func())
|
||||
ForceUseInteractive bool
|
||||
|
||||
// private fields
|
||||
|
@ -59,7 +61,7 @@ func (c *Config) useInteractive() bool {
|
|||
if c.ForceUseInteractive {
|
||||
return true
|
||||
}
|
||||
return IsTerminal(c.StdoutFd) && IsTerminal(c.StdinFd)
|
||||
return c.FuncIsTerminal()
|
||||
}
|
||||
|
||||
func (c *Config) Init() error {
|
||||
|
@ -76,12 +78,6 @@ func (c *Config) Init() error {
|
|||
if c.Stderr == nil {
|
||||
c.Stderr = Stderr
|
||||
}
|
||||
if c.StdinFd == 0 {
|
||||
c.StdinFd = StdinFd
|
||||
}
|
||||
if c.StdoutFd == 0 {
|
||||
c.StdoutFd = StdoutFd
|
||||
}
|
||||
if c.HistoryLimit == 0 {
|
||||
c.HistoryLimit = 500
|
||||
}
|
||||
|
@ -98,7 +94,20 @@ func (c *Config) Init() error {
|
|||
}
|
||||
|
||||
if c.FuncGetWidth == nil {
|
||||
c.FuncGetWidth = genGetWidthFunc(c.StdoutFd)
|
||||
c.FuncGetWidth = GetScreenWidth
|
||||
}
|
||||
if c.FuncIsTerminal == nil {
|
||||
c.FuncIsTerminal = DefaultIsTerminal
|
||||
}
|
||||
rm := new(RawMode)
|
||||
if c.FuncMakeRaw == nil {
|
||||
c.FuncMakeRaw = rm.Enter
|
||||
}
|
||||
if c.FuncExitRaw == nil {
|
||||
c.FuncExitRaw = rm.Exit
|
||||
}
|
||||
if c.FuncOnWidthChanged == nil {
|
||||
c.FuncOnWidthChanged = DefaultOnWidthChanged
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
426
remote.go
426
remote.go
|
@ -1,49 +1,409 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
Conn net.Conn
|
||||
Terminal *Terminal
|
||||
runChan chan error
|
||||
type MsgType int16
|
||||
|
||||
const (
|
||||
T_DATA = MsgType(iota)
|
||||
T_WIDTH
|
||||
T_WIDTH_REPORT
|
||||
T_ISTTY_REPORT
|
||||
T_RAW
|
||||
T_ERAW // exit raw
|
||||
)
|
||||
|
||||
type RemoteSvr struct {
|
||||
closed int32
|
||||
width int32
|
||||
reciveChan chan struct{}
|
||||
writeChan chan *writeCtx
|
||||
conn net.Conn
|
||||
isTerminal bool
|
||||
funcWidthChan func()
|
||||
|
||||
dataBufM sync.Mutex
|
||||
dataBuf bytes.Buffer
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, t *Terminal) (*Conn, error) {
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
Terminal: t,
|
||||
runChan: make(chan error),
|
||||
}, nil
|
||||
type writeReply struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *Conn) Run() error {
|
||||
c.Terminal.EnterRawMode()
|
||||
go func() {
|
||||
_, err := io.Copy(c.Conn, os.Stdin)
|
||||
c.runChan <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(os.Stdout, c.Conn)
|
||||
c.runChan <- err
|
||||
}()
|
||||
err := <-c.runChan
|
||||
c.Terminal.ExitRawMode()
|
||||
type writeCtx struct {
|
||||
msg *Message
|
||||
reply chan *writeReply
|
||||
}
|
||||
|
||||
func newWriteCtx(msg *Message) *writeCtx {
|
||||
return &writeCtx{
|
||||
msg: msg,
|
||||
reply: make(chan *writeReply),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
|
||||
rs := &RemoteSvr{
|
||||
width: -1,
|
||||
conn: conn,
|
||||
writeChan: make(chan *writeCtx),
|
||||
reciveChan: make(chan struct{}),
|
||||
}
|
||||
buf := bufio.NewReader(rs.conn)
|
||||
|
||||
if err := rs.init(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go rs.readLoop(buf)
|
||||
go rs.writeLoop()
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) init(buf *bufio.Reader) error {
|
||||
m, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// receive isTerminal
|
||||
if m.Type != T_ISTTY_REPORT {
|
||||
return fmt.Errorf("unexpected init message")
|
||||
}
|
||||
r.GotIsTerminal(m.Data)
|
||||
|
||||
// receive width
|
||||
m, err = ReadMessage(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.Type != T_WIDTH_REPORT {
|
||||
return fmt.Errorf("unexpected init message")
|
||||
}
|
||||
r.GotReportWidth(m.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) HandleConfig(cfg *Config) {
|
||||
cfg.Stderr = r
|
||||
cfg.Stdout = r
|
||||
cfg.Stdin = r
|
||||
cfg.FuncExitRaw = r.ExitRawMode
|
||||
cfg.FuncIsTerminal = r.IsTerminal
|
||||
cfg.FuncMakeRaw = r.EnterRawMode
|
||||
cfg.FuncExitRaw = r.ExitRawMode
|
||||
cfg.FuncGetWidth = r.GetWidth
|
||||
cfg.FuncOnWidthChanged = func(f func()) {
|
||||
r.funcWidthChan = f
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) IsTerminal() bool {
|
||||
return r.isTerminal
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Read(b []byte) (int, error) {
|
||||
r.dataBufM.Lock()
|
||||
n, err := r.dataBuf.Read(b)
|
||||
r.dataBufM.Unlock()
|
||||
if n == 0 && err == io.EOF {
|
||||
<-r.reciveChan
|
||||
r.dataBufM.Lock()
|
||||
n, err = r.dataBuf.Read(b)
|
||||
r.dataBufM.Unlock()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) writeMsg(m *Message) error {
|
||||
ctx := newWriteCtx(m)
|
||||
r.writeChan <- ctx
|
||||
reply := <-ctx.reply
|
||||
return reply.err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Write(b []byte) (int, error) {
|
||||
ctx := newWriteCtx(NewMessage(T_DATA, b))
|
||||
r.writeChan <- ctx
|
||||
reply := <-ctx.reply
|
||||
return reply.n, reply.err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) EnterRawMode() error {
|
||||
return r.writeMsg(NewMessage(T_RAW, nil))
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) ExitRawMode() error {
|
||||
return r.writeMsg(NewMessage(T_ERAW, nil))
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) writeLoop() {
|
||||
defer r.Close()
|
||||
|
||||
for {
|
||||
ctx, ok := <-r.writeChan
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
n, err := ctx.msg.WriteTo(r.conn)
|
||||
ctx.reply <- &writeReply{n, err}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Close() {
|
||||
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
|
||||
close(r.writeChan)
|
||||
r.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
|
||||
defer r.Close()
|
||||
for {
|
||||
m, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch m.Type {
|
||||
case T_DATA:
|
||||
r.dataBufM.Lock()
|
||||
r.dataBuf.Write(m.Data)
|
||||
r.dataBufM.Unlock()
|
||||
select {
|
||||
case r.reciveChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case T_WIDTH_REPORT:
|
||||
r.GotReportWidth(m.Data)
|
||||
case T_ISTTY_REPORT:
|
||||
r.GotIsTerminal(m.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GotIsTerminal(data []byte) {
|
||||
if binary.BigEndian.Uint16(data) == 0 {
|
||||
r.isTerminal = false
|
||||
} else {
|
||||
r.isTerminal = true
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GotReportWidth(data []byte) {
|
||||
atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
|
||||
if r.funcWidthChan != nil {
|
||||
r.funcWidthChan()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GetWidth() int {
|
||||
return int(atomic.LoadInt32(&r.width))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type Message struct {
|
||||
Type MsgType
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func ReadMessage(r io.Reader) (*Message, error) {
|
||||
m := new(Message)
|
||||
var length int32
|
||||
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Data = make([]byte, int(length)-2)
|
||||
if _, err := io.ReadFull(r, m.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewMessage(t MsgType, data []byte) *Message {
|
||||
return &Message{t, data}
|
||||
}
|
||||
|
||||
func (m *Message) WriteTo(w io.Writer) (int, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
|
||||
binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
|
||||
binary.Write(buf, binary.BigEndian, m.Type)
|
||||
buf.Write(m.Data)
|
||||
n, err := buf.WriteTo(w)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type RemoteCli struct {
|
||||
conn net.Conn
|
||||
raw RawMode
|
||||
receiveChan chan struct{}
|
||||
|
||||
data bytes.Buffer
|
||||
dataM sync.Mutex
|
||||
}
|
||||
|
||||
func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
|
||||
r := &RemoteCli{
|
||||
conn: conn,
|
||||
receiveChan: make(chan struct{}),
|
||||
}
|
||||
if err := r.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) init() error {
|
||||
if err := r.reportIsTerminal(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.reportWidth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// register sig for width changed
|
||||
DefaultOnWidthChanged(func() {
|
||||
r.reportWidth()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) writeMsg(m *Message) error {
|
||||
r.dataM.Lock()
|
||||
_, err := m.WriteTo(r.conn)
|
||||
r.dataM.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func Dial(network string, address string) (*Conn, error) {
|
||||
conn, err := net.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg Config
|
||||
t, err := NewTerminal(&cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(conn, t)
|
||||
func (r *RemoteCli) Write(b []byte) (int, error) {
|
||||
m := NewMessage(T_DATA, b)
|
||||
r.dataM.Lock()
|
||||
n, err := m.WriteTo(r.conn)
|
||||
r.dataM.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RemoteCli) reportWidth() error {
|
||||
screenWidth := GetScreenWidth()
|
||||
data := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(data, uint16(screenWidth))
|
||||
msg := NewMessage(T_WIDTH_REPORT, data)
|
||||
|
||||
if err := r.writeMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) reportIsTerminal() error {
|
||||
isTerminal := DefaultIsTerminal()
|
||||
data := make([]byte, 2)
|
||||
if isTerminal {
|
||||
binary.BigEndian.PutUint16(data, 1)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(data, 0)
|
||||
}
|
||||
msg := NewMessage(T_ISTTY_REPORT, data)
|
||||
if err := r.writeMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) readLoop() {
|
||||
buf := bufio.NewReader(r.conn)
|
||||
for {
|
||||
msg, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch msg.Type {
|
||||
case T_ERAW:
|
||||
r.raw.Exit()
|
||||
case T_RAW:
|
||||
r.raw.Enter()
|
||||
case T_DATA:
|
||||
os.Stdout.Write(msg.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteCli) Serve() error {
|
||||
go func() {
|
||||
for {
|
||||
n, _ := io.Copy(r, os.Stdin)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
r.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListenRemote(n, addr string, cfg *Config, h func(*Instance)) error {
|
||||
ln, err := net.Listen(n, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
rl, err := HandleConn(*cfg, conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h(rl)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
|
||||
r, err := NewRemoteSvr(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.HandleConfig(&cfg)
|
||||
|
||||
rl, err := NewEx(&cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
func DialRemote(n, addr string) error {
|
||||
conn, err := net.Dial(n, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cli, err := NewRemoteCli(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Serve()
|
||||
}
|
||||
|
|
68
runebuf.go
68
runebuf.go
|
@ -1,6 +1,7 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -24,9 +25,25 @@ type RuneBuffer struct {
|
|||
interactive bool
|
||||
cfg *Config
|
||||
|
||||
width int
|
||||
|
||||
bck *runeBufferBck
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) OnWidthChange(newWidth int) {
|
||||
oldWidth := r.width
|
||||
if newWidth < oldWidth {
|
||||
sp := SplitByMultiLine(
|
||||
r.PromptLen(), oldWidth, newWidth, r.buf[:r.idx])
|
||||
idxLine := len(sp) - 1
|
||||
r.clean(idxLine)
|
||||
} else {
|
||||
r.Clean()
|
||||
}
|
||||
r.width = newWidth
|
||||
r.print()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Backup() {
|
||||
r.bck = &runeBufferBck{r.buf, r.idx}
|
||||
}
|
||||
|
@ -41,11 +58,12 @@ func (r *RuneBuffer) Restore() {
|
|||
})
|
||||
}
|
||||
|
||||
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config) *RuneBuffer {
|
||||
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer {
|
||||
rb := &RuneBuffer{
|
||||
w: w,
|
||||
interactive: cfg.useInteractive(),
|
||||
cfg: cfg,
|
||||
width: width,
|
||||
}
|
||||
rb.SetPrompt(prompt)
|
||||
return rb
|
||||
|
@ -293,8 +311,11 @@ func (r *RuneBuffer) MoveToLineEnd() {
|
|||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) LineCount() int {
|
||||
return LineCount(r.cfg.FuncGetWidth(),
|
||||
func (r *RuneBuffer) LineCount(width int) int {
|
||||
if width == -1 {
|
||||
width = r.width
|
||||
}
|
||||
return LineCount(width,
|
||||
runes.WidthAll(r.buf)+r.PromptLen())
|
||||
}
|
||||
|
||||
|
@ -327,14 +348,13 @@ func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) IdxLine() int {
|
||||
sw := r.cfg.FuncGetWidth()
|
||||
sp := SplitByLine(r.PromptLen(), sw, r.buf[:r.idx])
|
||||
func (r *RuneBuffer) IdxLine(width int) int {
|
||||
sp := SplitByLine(r.PromptLen(), width, r.buf[:r.idx])
|
||||
return len(sp) - 1
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) CursorLineCount() int {
|
||||
return r.LineCount() - r.IdxLine()
|
||||
return r.LineCount(r.width) - r.IdxLine(r.width)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Refresh(f func()) {
|
||||
|
@ -348,6 +368,10 @@ func (r *RuneBuffer) Refresh(f func()) {
|
|||
if f != nil {
|
||||
f()
|
||||
}
|
||||
r.print()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) print() {
|
||||
r.w.Write(r.output())
|
||||
r.cleanInScreen = false
|
||||
}
|
||||
|
@ -367,8 +391,7 @@ func (r *RuneBuffer) output() []byte {
|
|||
}
|
||||
|
||||
} else {
|
||||
sw := r.cfg.FuncGetWidth()
|
||||
sp := SplitByLine(r.PromptLen(), sw, r.buf)
|
||||
sp := SplitByLine(r.PromptLen(), r.width, r.buf)
|
||||
written := 0
|
||||
idxInLine := 0
|
||||
for idx, s := range sp {
|
||||
|
@ -383,7 +406,7 @@ func (r *RuneBuffer) output() []byte {
|
|||
}
|
||||
}
|
||||
if len(r.buf) > r.idx {
|
||||
targetLine := r.IdxLine()
|
||||
targetLine := r.IdxLine(r.width)
|
||||
currentLine := len(sp) - 1
|
||||
// assert currentLine >= targetLine
|
||||
if targetLine == 0 {
|
||||
|
@ -452,27 +475,30 @@ func (r *RuneBuffer) SetPrompt(prompt string) {
|
|||
r.prompt = []rune(prompt)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) cleanOutput() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
|
||||
buf := bufio.NewWriter(w)
|
||||
buf.Write([]byte("\033[J")) // just like ^k :)
|
||||
|
||||
idxLine := r.IdxLine()
|
||||
|
||||
if idxLine == 0 {
|
||||
buf.WriteString("\033[2K\r")
|
||||
return buf.Bytes()
|
||||
}
|
||||
io.WriteString(buf, "\033[2K\r")
|
||||
} else {
|
||||
for i := 0; i < idxLine; i++ {
|
||||
buf.WriteString("\033[2K\r\033[A")
|
||||
io.WriteString(buf, "\033[2K\r\033[A")
|
||||
}
|
||||
buf.WriteString("\033[2K\r")
|
||||
return buf.Bytes()
|
||||
io.WriteString(buf, "\033[2K\r")
|
||||
}
|
||||
buf.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Clean() {
|
||||
r.clean(r.IdxLine(r.width))
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) clean(idxLine int) {
|
||||
if r.cleanInScreen || !r.interactive {
|
||||
return
|
||||
}
|
||||
r.cleanInScreen = true
|
||||
r.w.Write(r.cleanOutput())
|
||||
r.cleanOutput(r.w, idxLine)
|
||||
}
|
||||
|
|
10
search.go
10
search.go
|
@ -29,17 +29,23 @@ type opSearch struct {
|
|||
cfg *Config
|
||||
markStart int
|
||||
markEnd int
|
||||
width int
|
||||
}
|
||||
|
||||
func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config) *opSearch {
|
||||
func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch {
|
||||
return &opSearch{
|
||||
w: w,
|
||||
buf: buf,
|
||||
cfg: cfg,
|
||||
history: history,
|
||||
width: width,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opSearch) OnWidthChange(newWidth int) {
|
||||
o.width = newWidth
|
||||
}
|
||||
|
||||
func (o *opSearch) IsSearchMode() bool {
|
||||
return o.inMode
|
||||
}
|
||||
|
@ -125,7 +131,7 @@ func (o *opSearch) SearchRefresh(x int) {
|
|||
}
|
||||
x = o.buf.CurrentWidth(x)
|
||||
x += o.buf.PromptLen()
|
||||
x = x % o.cfg.FuncGetWidth()
|
||||
x = x % o.width
|
||||
|
||||
if o.markStart > 0 {
|
||||
o.buf.SetStyle(o.markStart, o.markEnd, "4")
|
||||
|
|
15
terminal.go
15
terminal.go
|
@ -5,13 +5,10 @@ import (
|
|||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type Terminal struct {
|
||||
cfg *Config
|
||||
state *terminal.State
|
||||
outchan chan rune
|
||||
closed int32
|
||||
stopChan chan struct{}
|
||||
|
@ -36,19 +33,11 @@ func NewTerminal(cfg *Config) (*Terminal, error) {
|
|||
}
|
||||
|
||||
func (t *Terminal) EnterRawMode() (err error) {
|
||||
t.state, err = MakeRaw(int(t.cfg.StdinFd))
|
||||
return err
|
||||
return t.cfg.FuncMakeRaw()
|
||||
}
|
||||
|
||||
func (t *Terminal) ExitRawMode() (err error) {
|
||||
if t.state == nil {
|
||||
return
|
||||
}
|
||||
err = Restore(int(t.cfg.StdinFd), t.state)
|
||||
if err == nil {
|
||||
t.state = nil
|
||||
}
|
||||
return err
|
||||
return t.cfg.FuncExitRaw()
|
||||
}
|
||||
|
||||
func (t *Terminal) Write(b []byte) (int, error) {
|
||||
|
|
44
utils.go
44
utils.go
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/chzyer/readline/runes"
|
||||
|
||||
|
@ -12,8 +11,6 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
StdinFd = int(uintptr(syscall.Stdin))
|
||||
StdoutFd = int(uintptr(syscall.Stdout))
|
||||
isWindows = false
|
||||
)
|
||||
|
||||
|
@ -89,6 +86,29 @@ func escapeKey(r rune) rune {
|
|||
return r
|
||||
}
|
||||
|
||||
func SplitByMultiLine(start, oldWidth, newWidth int, rs []rune) []string {
|
||||
var ret []string
|
||||
buf := bytes.NewBuffer(nil)
|
||||
currentWidth := start
|
||||
for _, r := range rs {
|
||||
w := runes.Width(r)
|
||||
currentWidth += w
|
||||
buf.WriteRune(r)
|
||||
if currentWidth == newWidth {
|
||||
ret = append(ret, buf.String())
|
||||
buf.Reset()
|
||||
continue
|
||||
}
|
||||
if currentWidth >= oldWidth {
|
||||
ret = append(ret, buf.String())
|
||||
buf.Reset()
|
||||
currentWidth = 0
|
||||
}
|
||||
}
|
||||
ret = append(ret, buf.String())
|
||||
return ret
|
||||
}
|
||||
|
||||
func SplitByLine(start, screenWidth int, rs []rune) []string {
|
||||
var ret []string
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
@ -137,8 +157,18 @@ func GetInt(s []string, def int) int {
|
|||
return c
|
||||
}
|
||||
|
||||
func genGetWidthFunc(fd int) func() int {
|
||||
return func() int {
|
||||
return getWidth(fd)
|
||||
}
|
||||
type RawMode struct {
|
||||
state *terminal.State
|
||||
}
|
||||
|
||||
func (r *RawMode) Enter() (err error) {
|
||||
r.state, err = MakeRaw(GetStdin())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RawMode) Exit() error {
|
||||
if r.state == nil {
|
||||
return nil
|
||||
}
|
||||
return Restore(GetStdin(), r.state)
|
||||
}
|
||||
|
|
|
@ -1 +1,15 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitByMultiLine(t *testing.T) {
|
||||
rs := []rune("hello!bye!!!!")
|
||||
expected := []string{"hell", "o!", "bye!", "!!", "!"}
|
||||
ret := SplitByMultiLine(0, 6, 4, rs)
|
||||
if !reflect.DeepEqual(ret, expected) {
|
||||
t.Fatal(ret, expected)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
@ -28,3 +31,40 @@ func getWidth(stdoutFd int) int {
|
|||
}
|
||||
return int(ws.Col)
|
||||
}
|
||||
|
||||
func GetScreenWidth() int {
|
||||
return getWidth(syscall.Stdout)
|
||||
}
|
||||
|
||||
func DefaultIsTerminal() bool {
|
||||
return IsTerminal(syscall.Stdin) && IsTerminal(syscall.Stdout)
|
||||
}
|
||||
|
||||
func GetStdin() int {
|
||||
return syscall.Stdin
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
widthChange sync.Once
|
||||
widthChangeCallback func()
|
||||
)
|
||||
|
||||
func DefaultOnWidthChanged(f func()) {
|
||||
widthChangeCallback = f
|
||||
widthChange.Do(func() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGWINCH)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, ok := <-ch
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
widthChangeCallback()
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,15 +2,29 @@
|
|||
|
||||
package readline
|
||||
|
||||
import "syscall"
|
||||
|
||||
func GetStdin() int {
|
||||
return int(syscall.Stdin)
|
||||
}
|
||||
|
||||
func init() {
|
||||
isWindows = true
|
||||
}
|
||||
|
||||
// get width of the terminal
|
||||
func getWidth(fd int) int {
|
||||
func GetScreenWidth() int {
|
||||
info, _ := GetConsoleScreenBufferInfo()
|
||||
if info == nil {
|
||||
return -1
|
||||
}
|
||||
return int(info.dwSize.x)
|
||||
}
|
||||
|
||||
func DefaultIsTerminal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func DefaultOnWidthChanged(func()) {
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue