add remote mode

This commit is contained in:
chzyer 2016-03-13 18:32:48 +08:00
parent 14e9df7f3c
commit b57eccfd02
13 changed files with 637 additions and 100 deletions

View File

@ -19,9 +19,10 @@ type AutoCompleter interface {
} }
type opCompleter struct { type opCompleter struct {
w io.Writer w io.Writer
op *Operation op *Operation
ac AutoCompleter ac AutoCompleter
width int
inCompleteMode bool inCompleteMode bool
inSelectMode bool inSelectMode bool
@ -32,11 +33,12 @@ type opCompleter struct {
candidateColNum int candidateColNum int
} }
func newOpCompleter(w io.Writer, op *Operation) *opCompleter { func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
return &opCompleter{ return &opCompleter{
w: w, w: w,
op: op, op: op,
ac: op.cfg.AutoComplete, ac: op.cfg.AutoComplete,
width: width,
} }
} }
@ -171,6 +173,10 @@ func (o *opCompleter) getMatrixSize() int {
return line * o.candidateColNum return line * o.candidateColNum
} }
func (o *opCompleter) OnWidthChange(newWidth int) {
o.width = newWidth
}
func (o *opCompleter) CompleteRefresh() { func (o *opCompleter) CompleteRefresh() {
if !o.inCompleteMode { if !o.inCompleteMode {
return return
@ -183,7 +189,7 @@ func (o *opCompleter) CompleteRefresh() {
colWidth = w colWidth = w
} }
} }
colNum := o.op.cfg.FuncGetWidth() / (colWidth + o.candidateOff + 2) colNum := o.width / (colWidth + o.candidateOff + 2)
o.candidateColNum = colNum o.candidateColNum = colNum
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) buf.Write(bytes.Repeat([]byte("\n"), lineCnt))

View File

@ -0,0 +1,9 @@
package main
import "github.com/chzyer/readline"
func main() {
if err := readline.DialRemote("tcp", ":12344"); err != nil {
println(err.Error())
}
}

View File

@ -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())
}
}

View File

@ -53,17 +53,24 @@ func (w *wrapWriter) Write(b []byte) (int, error) {
} }
func NewOperation(t *Terminal, cfg *Config) *Operation { func NewOperation(t *Terminal, cfg *Config) *Operation {
width := cfg.FuncGetWidth()
op := &Operation{ op := &Operation{
t: t, t: t,
buf: NewRuneBuffer(t, cfg.Prompt, cfg), buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
outchan: make(chan []rune), outchan: make(chan []rune),
errchan: make(chan error), errchan: make(chan error),
} }
op.w = op.buf.w op.w = op.buf.w
op.SetConfig(cfg) op.SetConfig(cfg)
op.opVim = newVimMode(op) op.opVim = newVimMode(op)
op.opCompleter = newOpCompleter(op.buf.w, op) op.opCompleter = newOpCompleter(op.buf.w, op, width)
op.opPassword = newOpPassword(op) 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() go op.ioloop()
return op return op
} }
@ -381,11 +388,12 @@ func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
op.SetPrompt(cfg.Prompt) op.SetPrompt(cfg.Prompt)
op.SetMaskRune(cfg.MaskRune) op.SetMaskRune(cfg.MaskRune)
op.buf.SetConfig(cfg) op.buf.SetConfig(cfg)
width := op.cfg.FuncGetWidth()
if cfg.opHistory == nil { if cfg.opHistory == nil {
op.SetHistoryPath(cfg.HistoryFile) op.SetHistoryPath(cfg.HistoryFile)
cfg.opHistory = op.history 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 op.history = cfg.opHistory
@ -394,7 +402,7 @@ func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
op.history.Init() op.history.Init()
if op.cfg.AutoComplete != nil { if op.cfg.AutoComplete != nil {
op.opCompleter = newOpCompleter(op.buf.w, op) op.opCompleter = newOpCompleter(op.buf.w, op, width)
} }
op.opSearch = cfg.opSearch op.opSearch = cfg.opSearch

View File

@ -45,8 +45,10 @@ type Config struct {
UniqueEditLine bool UniqueEditLine bool
// force use interactive even stdout is not a tty // force use interactive even stdout is not a tty
StdinFd int FuncIsTerminal func() bool
StdoutFd int FuncMakeRaw func() error
FuncExitRaw func() error
FuncOnWidthChanged func(func())
ForceUseInteractive bool ForceUseInteractive bool
// private fields // private fields
@ -59,7 +61,7 @@ func (c *Config) useInteractive() bool {
if c.ForceUseInteractive { if c.ForceUseInteractive {
return true return true
} }
return IsTerminal(c.StdoutFd) && IsTerminal(c.StdinFd) return c.FuncIsTerminal()
} }
func (c *Config) Init() error { func (c *Config) Init() error {
@ -76,12 +78,6 @@ func (c *Config) Init() error {
if c.Stderr == nil { if c.Stderr == nil {
c.Stderr = Stderr c.Stderr = Stderr
} }
if c.StdinFd == 0 {
c.StdinFd = StdinFd
}
if c.StdoutFd == 0 {
c.StdoutFd = StdoutFd
}
if c.HistoryLimit == 0 { if c.HistoryLimit == 0 {
c.HistoryLimit = 500 c.HistoryLimit = 500
} }
@ -98,7 +94,20 @@ func (c *Config) Init() error {
} }
if c.FuncGetWidth == nil { 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 return nil

426
remote.go
View File

@ -1,49 +1,409 @@
package readline package readline
import ( import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io" "io"
"net" "net"
"os" "os"
"sync"
"sync/atomic"
) )
type Conn struct { type MsgType int16
Conn net.Conn
Terminal *Terminal const (
runChan chan error 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) { type writeReply struct {
return &Conn{ n int
Conn: conn, err error
Terminal: t,
runChan: make(chan error),
}, nil
} }
func (c *Conn) Run() error { type writeCtx struct {
c.Terminal.EnterRawMode() msg *Message
go func() { reply chan *writeReply
_, err := io.Copy(c.Conn, os.Stdin) }
c.runChan <- err
}() func newWriteCtx(msg *Message) *writeCtx {
go func() { return &writeCtx{
_, err := io.Copy(os.Stdout, c.Conn) msg: msg,
c.runChan <- err reply: make(chan *writeReply),
}() }
err := <-c.runChan }
c.Terminal.ExitRawMode()
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 return err
} }
func Dial(network string, address string) (*Conn, error) { func (r *RemoteCli) Write(b []byte) (int, error) {
conn, err := net.Dial(network, address) m := NewMessage(T_DATA, b)
if err != nil { r.dataM.Lock()
return nil, err n, err := m.WriteTo(r.conn)
} r.dataM.Unlock()
var cfg Config return n, err
t, err := NewTerminal(&cfg) }
if err != nil {
return nil, err func (r *RemoteCli) reportWidth() error {
} screenWidth := GetScreenWidth()
return NewConn(conn, t) 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()
} }

View File

@ -1,6 +1,7 @@
package readline package readline
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
@ -24,9 +25,25 @@ type RuneBuffer struct {
interactive bool interactive bool
cfg *Config cfg *Config
width int
bck *runeBufferBck 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() { func (r *RuneBuffer) Backup() {
r.bck = &runeBufferBck{r.buf, r.idx} 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{ rb := &RuneBuffer{
w: w, w: w,
interactive: cfg.useInteractive(), interactive: cfg.useInteractive(),
cfg: cfg, cfg: cfg,
width: width,
} }
rb.SetPrompt(prompt) rb.SetPrompt(prompt)
return rb return rb
@ -293,8 +311,11 @@ func (r *RuneBuffer) MoveToLineEnd() {
}) })
} }
func (r *RuneBuffer) LineCount() int { func (r *RuneBuffer) LineCount(width int) int {
return LineCount(r.cfg.FuncGetWidth(), if width == -1 {
width = r.width
}
return LineCount(width,
runes.WidthAll(r.buf)+r.PromptLen()) runes.WidthAll(r.buf)+r.PromptLen())
} }
@ -327,14 +348,13 @@ func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
return return
} }
func (r *RuneBuffer) IdxLine() int { func (r *RuneBuffer) IdxLine(width int) int {
sw := r.cfg.FuncGetWidth() sp := SplitByLine(r.PromptLen(), width, r.buf[:r.idx])
sp := SplitByLine(r.PromptLen(), sw, r.buf[:r.idx])
return len(sp) - 1 return len(sp) - 1
} }
func (r *RuneBuffer) CursorLineCount() int { 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()) { func (r *RuneBuffer) Refresh(f func()) {
@ -348,6 +368,10 @@ func (r *RuneBuffer) Refresh(f func()) {
if f != nil { if f != nil {
f() f()
} }
r.print()
}
func (r *RuneBuffer) print() {
r.w.Write(r.output()) r.w.Write(r.output())
r.cleanInScreen = false r.cleanInScreen = false
} }
@ -367,8 +391,7 @@ func (r *RuneBuffer) output() []byte {
} }
} else { } else {
sw := r.cfg.FuncGetWidth() sp := SplitByLine(r.PromptLen(), r.width, r.buf)
sp := SplitByLine(r.PromptLen(), sw, r.buf)
written := 0 written := 0
idxInLine := 0 idxInLine := 0
for idx, s := range sp { for idx, s := range sp {
@ -383,7 +406,7 @@ func (r *RuneBuffer) output() []byte {
} }
} }
if len(r.buf) > r.idx { if len(r.buf) > r.idx {
targetLine := r.IdxLine() targetLine := r.IdxLine(r.width)
currentLine := len(sp) - 1 currentLine := len(sp) - 1
// assert currentLine >= targetLine // assert currentLine >= targetLine
if targetLine == 0 { if targetLine == 0 {
@ -452,27 +475,30 @@ func (r *RuneBuffer) SetPrompt(prompt string) {
r.prompt = []rune(prompt) r.prompt = []rune(prompt)
} }
func (r *RuneBuffer) cleanOutput() []byte { func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
buf := bytes.NewBuffer(nil) buf := bufio.NewWriter(w)
buf.Write([]byte("\033[J")) // just like ^k :) buf.Write([]byte("\033[J")) // just like ^k :)
idxLine := r.IdxLine()
if idxLine == 0 { if idxLine == 0 {
buf.WriteString("\033[2K\r") io.WriteString(buf, "\033[2K\r")
return buf.Bytes() } else {
for i := 0; i < idxLine; i++ {
io.WriteString(buf, "\033[2K\r\033[A")
}
io.WriteString(buf, "\033[2K\r")
} }
for i := 0; i < idxLine; i++ { buf.Flush()
buf.WriteString("\033[2K\r\033[A") return
}
buf.WriteString("\033[2K\r")
return buf.Bytes()
} }
func (r *RuneBuffer) Clean() { func (r *RuneBuffer) Clean() {
r.clean(r.IdxLine(r.width))
}
func (r *RuneBuffer) clean(idxLine int) {
if r.cleanInScreen || !r.interactive { if r.cleanInScreen || !r.interactive {
return return
} }
r.cleanInScreen = true r.cleanInScreen = true
r.w.Write(r.cleanOutput()) r.cleanOutput(r.w, idxLine)
} }

View File

@ -29,17 +29,23 @@ type opSearch struct {
cfg *Config cfg *Config
markStart int markStart int
markEnd 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{ return &opSearch{
w: w, w: w,
buf: buf, buf: buf,
cfg: cfg, cfg: cfg,
history: history, history: history,
width: width,
} }
} }
func (o *opSearch) OnWidthChange(newWidth int) {
o.width = newWidth
}
func (o *opSearch) IsSearchMode() bool { func (o *opSearch) IsSearchMode() bool {
return o.inMode return o.inMode
} }
@ -125,7 +131,7 @@ func (o *opSearch) SearchRefresh(x int) {
} }
x = o.buf.CurrentWidth(x) x = o.buf.CurrentWidth(x)
x += o.buf.PromptLen() x += o.buf.PromptLen()
x = x % o.cfg.FuncGetWidth() x = x % o.width
if o.markStart > 0 { if o.markStart > 0 {
o.buf.SetStyle(o.markStart, o.markEnd, "4") o.buf.SetStyle(o.markStart, o.markEnd, "4")

View File

@ -5,13 +5,10 @@ import (
"fmt" "fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
"golang.org/x/crypto/ssh/terminal"
) )
type Terminal struct { type Terminal struct {
cfg *Config cfg *Config
state *terminal.State
outchan chan rune outchan chan rune
closed int32 closed int32
stopChan chan struct{} stopChan chan struct{}
@ -36,19 +33,11 @@ func NewTerminal(cfg *Config) (*Terminal, error) {
} }
func (t *Terminal) EnterRawMode() (err error) { func (t *Terminal) EnterRawMode() (err error) {
t.state, err = MakeRaw(int(t.cfg.StdinFd)) return t.cfg.FuncMakeRaw()
return err
} }
func (t *Terminal) ExitRawMode() (err error) { func (t *Terminal) ExitRawMode() (err error) {
if t.state == nil { return t.cfg.FuncExitRaw()
return
}
err = Restore(int(t.cfg.StdinFd), t.state)
if err == nil {
t.state = nil
}
return err
} }
func (t *Terminal) Write(b []byte) (int, error) { func (t *Terminal) Write(b []byte) (int, error) {

View File

@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"strconv" "strconv"
"syscall"
"github.com/chzyer/readline/runes" "github.com/chzyer/readline/runes"
@ -12,8 +11,6 @@ import (
) )
var ( var (
StdinFd = int(uintptr(syscall.Stdin))
StdoutFd = int(uintptr(syscall.Stdout))
isWindows = false isWindows = false
) )
@ -89,6 +86,29 @@ func escapeKey(r rune) rune {
return r 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 { func SplitByLine(start, screenWidth int, rs []rune) []string {
var ret []string var ret []string
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
@ -137,8 +157,18 @@ func GetInt(s []string, def int) int {
return c return c
} }
func genGetWidthFunc(fd int) func() int { type RawMode struct {
return func() int { state *terminal.State
return getWidth(fd) }
}
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)
} }

View File

@ -1 +1,15 @@
package readline 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)
}
}

View File

@ -3,6 +3,9 @@
package readline package readline
import ( import (
"os"
"os/signal"
"sync"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -28,3 +31,40 @@ func getWidth(stdoutFd int) int {
} }
return int(ws.Col) 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()
}
}()
})
}

View File

@ -2,15 +2,29 @@
package readline package readline
import "syscall"
func GetStdin() int {
return int(syscall.Stdin)
}
func init() { func init() {
isWindows = true isWindows = true
} }
// get width of the terminal // get width of the terminal
func getWidth(fd int) int { func GetScreenWidth() int {
info, _ := GetConsoleScreenBufferInfo() info, _ := GetConsoleScreenBufferInfo()
if info == nil { if info == nil {
return -1 return -1
} }
return int(info.dwSize.x) return int(info.dwSize.x)
} }
func DefaultIsTerminal() bool {
return true
}
func DefaultOnWidthChanged(func()) {
}