From b57eccfd02fe666e4feb3cff230ee08b916853ab Mon Sep 17 00:00:00 2001 From: chzyer <0@0xdf.com> Date: Sun, 13 Mar 2016 18:32:48 +0800 Subject: [PATCH] add remote mode --- complete.go | 22 +- .../readline-remote-client/client.go | 9 + .../readline-remote-server/server.go | 26 ++ operation.go | 16 +- readline.go | 29 +- remote.go | 426 ++++++++++++++++-- runebuf.go | 70 ++- search.go | 10 +- terminal.go | 15 +- utils.go | 44 +- utils_test.go | 14 + utils_unix.go | 40 ++ utils_windows.go | 16 +- 13 files changed, 637 insertions(+), 100 deletions(-) create mode 100644 example/readline-remote/readline-remote-client/client.go create mode 100644 example/readline-remote/readline-remote-server/server.go diff --git a/complete.go b/complete.go index 3a7aa4f..8776504 100644 --- a/complete.go +++ b/complete.go @@ -19,9 +19,10 @@ type AutoCompleter interface { } type opCompleter struct { - w io.Writer - op *Operation - ac AutoCompleter + 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, + 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)) diff --git a/example/readline-remote/readline-remote-client/client.go b/example/readline-remote/readline-remote-client/client.go new file mode 100644 index 0000000..3b7ff31 --- /dev/null +++ b/example/readline-remote/readline-remote-client/client.go @@ -0,0 +1,9 @@ +package main + +import "github.com/chzyer/readline" + +func main() { + if err := readline.DialRemote("tcp", ":12344"); err != nil { + println(err.Error()) + } +} diff --git a/example/readline-remote/readline-remote-server/server.go b/example/readline-remote/readline-remote-server/server.go new file mode 100644 index 0000000..38abc7d --- /dev/null +++ b/example/readline-remote/readline-remote-server/server.go @@ -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()) + } +} diff --git a/operation.go b/operation.go index e1e8b07..c896fda 100644 --- a/operation.go +++ b/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 diff --git a/readline.go b/readline.go index c960f2d..a814cf4 100644 --- a/readline.go +++ b/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 diff --git a/remote.go b/remote.go index 87d35da..6685ce0 100644 --- a/remote.go +++ b/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() } diff --git a/runebuf.go b/runebuf.go index 1d06a31..307ffc8 100644 --- a/runebuf.go +++ b/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++ { + io.WriteString(buf, "\033[2K\r\033[A") + } + io.WriteString(buf, "\033[2K\r") } - for i := 0; i < idxLine; i++ { - buf.WriteString("\033[2K\r\033[A") - } - buf.WriteString("\033[2K\r") - return buf.Bytes() + 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) } diff --git a/search.go b/search.go index 3b40ad6..2f20eb2 100644 --- a/search.go +++ b/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") diff --git a/terminal.go b/terminal.go index b65361c..b1e278b 100644 --- a/terminal.go +++ b/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) { diff --git a/utils.go b/utils.go index 01488a7..de228a9 100644 --- a/utils.go +++ b/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) } diff --git a/utils_test.go b/utils_test.go index 96037df..71cba80 100644 --- a/utils_test.go +++ b/utils_test.go @@ -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) + } +} diff --git a/utils_unix.go b/utils_unix.go index e862507..f0f0da6 100644 --- a/utils_unix.go +++ b/utils_unix.go @@ -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() + } + }() + }) +} diff --git a/utils_windows.go b/utils_windows.go index 016bb05..f88d038 100644 --- a/utils_windows.go +++ b/utils_windows.go @@ -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()) { + +}