Merge pull request #6 from chzyer/feature/support_windows

support windows
This commit is contained in:
Chzyer 2015-09-29 23:36:46 +08:00
commit 2dbff40748
15 changed files with 635 additions and 107 deletions

View File

@ -1,9 +1,10 @@
language: go language: go
go: go:
- 1.4
- 1.5 - 1.5
before_install: before_install:
- go get golang.org/x/crypto/ssh/terminal - go get golang.org/x/crypto/ssh/terminal
script: script:
- go install github.com/chzyer/readline/example - GOOS=windows go install github.com/chzyer/readline/example
- GOOS=linux go install github.com/chzyer/readline/example
- GOOS=darwin go install github.com/chzyer/readline/example
- go test -v - go test -v

View File

@ -19,7 +19,6 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read
# Todo # Todo
* Add support for windows
* Vim mode * Vim mode
* More funny examples * More funny examples
@ -203,6 +202,7 @@ Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B`
| Mac OS X iTerm2 Tmux | screen | | Mac OS X iTerm2 Tmux | screen |
| Ubuntu Server 14.04 LTS | linux | | Ubuntu Server 14.04 LTS | linux |
| Centos 7 | linux | | Centos 7 | linux |
| Windows 10 | - |
### Notice: ### Notice:
* `Ctrl`+`A` is not working in screen because it used as a control command by default * `Ctrl`+`A` is not working in screen because it used as a control command by default

261
ansi_windows.go Normal file
View File

@ -0,0 +1,261 @@
// +build windows
package readline
import (
"bufio"
"io"
"strconv"
"strings"
"sync"
"unicode/utf8"
"unsafe"
)
const (
_ = uint16(0)
COLOR_FBLUE = 0x0001
COLOR_FGREEN = 0x0002
COLOR_FRED = 0x0004
COLOR_FINTENSITY = 0x0008
COLOR_BBLUE = 0x0010
COLOR_BGREEN = 0x0020
COLOR_BRED = 0x0040
COLOR_BINTENSITY = 0x0080
COMMON_LVB_UNDERSCORE = 0x8000
)
var ColorTableFg = []word{
0, // 30: Black
COLOR_FRED, // 31: Red
COLOR_FGREEN, // 32: Green
COLOR_FRED | COLOR_FGREEN, // 33: Yellow
COLOR_FBLUE, // 34: Blue
COLOR_FRED | COLOR_FBLUE, // 35: Magenta
COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan
COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White
}
var ColorTableBg = []word{
0, // 40: Black
COLOR_BRED, // 41: Red
COLOR_BGREEN, // 42: Green
COLOR_BRED | COLOR_BGREEN, // 43: Yellow
COLOR_BBLUE, // 44: Blue
COLOR_BRED | COLOR_BBLUE, // 45: Magenta
COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan
COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White
}
type ANSIWriter struct {
target io.Writer
ch chan rune
wg sync.WaitGroup
sync.Mutex
}
func NewANSIWriter(w io.Writer) *ANSIWriter {
a := &ANSIWriter{
target: w,
ch: make(chan rune),
}
go a.ioloop()
return a
}
func (a *ANSIWriter) Close() error {
close(a.ch)
a.wg.Wait()
return nil
}
func (a *ANSIWriter) ioloop() {
a.wg.Add(1)
defer a.wg.Done()
var (
ok bool
isEsc bool
isEscSeq bool
char rune
arg []string
target = bufio.NewWriter(a.target)
)
peek := func() rune {
select {
case ch := <-a.ch:
return ch
default:
return 0
}
}
read:
r := char
if char == 0 {
r, ok = <-a.ch
if !ok {
target.Flush()
return
}
} else {
char = 0
}
if isEscSeq {
isEscSeq = a.ioloopEscSeq(target, r, &arg)
goto read
}
switch r {
case CharEsc:
isEsc = true
case '[':
if isEsc {
arg = nil
isEscSeq = true
isEsc = false
break
}
fallthrough
default:
target.WriteRune(r)
char = peek()
if char == 0 || char == CharEsc {
target.Flush()
}
}
goto read
}
func (a *ANSIWriter) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool {
arg := *argptr
var err error
if r >= 'A' && r <= 'D' {
count := short(GetInt(arg, 1))
info, err := GetConsoleScreenBufferInfo()
if err != nil {
return false
}
switch r {
case 'A': // up
info.dwCursorPosition.y -= count
case 'B': // down
info.dwCursorPosition.y += count
case 'C': // right
info.dwCursorPosition.x += count
case 'D': // left
info.dwCursorPosition.x -= count
}
SetConsoleCursorPosition(&info.dwCursorPosition)
return false
}
switch r {
case 'J':
killLines()
case 'K':
eraseLine()
case 'm':
color := word(0)
for _, item := range arg {
var c int
c, err = strconv.Atoi(item)
if err != nil {
w.WriteString("[" + strings.Join(arg, ";") + "m")
break
}
if c >= 30 && c < 40 {
color ^= COLOR_FINTENSITY
color |= ColorTableFg[c-30]
} else if c >= 40 && c < 50 {
color ^= COLOR_BINTENSITY
color |= ColorTableBg[c-40]
} else if c == 4 {
color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
} else { // unknown code treat as reset
color = ColorTableFg[7]
}
}
if err != nil {
break
}
kernel.SetConsoleTextAttribute(stdout, uintptr(color))
case '\007': // set title
case ';':
if len(arg) == 0 || arg[len(arg)-1] != "" {
arg = append(arg, "")
*argptr = arg
}
return true
default:
if len(arg) == 0 {
arg = append(arg, "")
}
arg[len(arg)-1] += string(r)
*argptr = arg
return true
}
*argptr = nil
return false
}
func (a *ANSIWriter) Write(b []byte) (int, error) {
a.Lock()
defer a.Unlock()
off := 0
for len(b) > off {
r, size := utf8.DecodeRune(b[off:])
if size == 0 {
return off, io.ErrShortWrite
}
off += size
a.ch <- r
}
return off, nil
}
func killLines() error {
sbi, err := GetConsoleScreenBufferInfo()
if err != nil {
return err
}
size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x
size += sbi.dwCursorPosition.x
var written int
kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
}
func eraseLine() error {
sbi, err := GetConsoleScreenBufferInfo()
if err != nil {
return err
}
size := sbi.dwSize.x
sbi.dwCursorPosition.x = 0
var written int
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
}

View File

@ -26,7 +26,7 @@ const (
) )
const ( const (
MetaPrev = -iota - 1 MetaPrev rune = -iota - 1
MetaNext MetaNext
MetaDelete MetaDelete
MetaBackspace MetaBackspace

View File

@ -7,6 +7,7 @@ type Operation struct {
t *Terminal t *Terminal
buf *RuneBuffer buf *RuneBuffer
outchan chan []rune outchan chan []rune
w io.Writer
*opHistory *opHistory
*opSearch *opSearch
@ -20,12 +21,18 @@ type wrapWriter struct {
} }
func (w *wrapWriter) Write(b []byte) (int, error) { func (w *wrapWriter) Write(b []byte) (int, error) {
buf := w.r.buf if !w.t.IsReading() {
buf.Clean() return w.target.Write(b)
n, err := w.target.Write(b)
if w.t.IsReading() {
w.r.buf.Refresh(nil)
} }
var (
n int
err error
)
w.r.buf.Refresh(func() {
n, err = w.target.Write(b)
})
if w.r.IsSearchMode() { if w.r.IsSearchMode() {
w.r.SearchRefresh(-1) w.r.SearchRefresh(-1)
} }
@ -43,6 +50,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
outchan: make(chan []rune), outchan: make(chan []rune),
opHistory: newOpHistory(cfg.HistoryFile), opHistory: newOpHistory(cfg.HistoryFile),
} }
op.w = op.buf.w
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory) op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
op.opCompleter = newOpCompleter(op.buf.w, op) op.opCompleter = newOpCompleter(op.buf.w, op)
go op.ioloop() go op.ioloop()
@ -232,6 +240,10 @@ func (o *Operation) Runes() ([]rune, error) {
return r, nil return r, nil
} }
func (o *Operation) SetTitle(t string) {
o.w.Write([]byte("\033[2;" + t + "\007"))
}
func (o *Operation) Slice() ([]byte, error) { func (o *Operation) Slice() ([]byte, error) {
r, err := o.Runes() r, err := o.Runes()
if err != nil { if err != nil {

122
rawreader_windows.go Normal file
View File

@ -0,0 +1,122 @@
// +build windows
package readline
import "unsafe"
const (
VK_CANCEL = 0x03
VK_BACK = 0x08
VK_TAB = 0x09
VK_RETURN = 0x0D
VK_SHIFT = 0x10
VK_CONTROL = 0x11
VK_MENU = 0x12
VK_ESCAPE = 0x1B
VK_LEFT = 0x25
VK_UP = 0x26
VK_RIGHT = 0x27
VK_DOWN = 0x28
VK_DELETE = 0x2E
VK_LSHIFT = 0xA0
VK_RSHIFT = 0xA1
VK_LCONTROL = 0xA2
VK_RCONTROL = 0xA3
)
type RawReader struct {
ctrlKey bool
altKey bool
}
func NewRawReader() *RawReader {
r := new(RawReader)
return r
}
func (r *RawReader) Read(buf []byte) (int, error) {
ir := new(_INPUT_RECORD)
var read int
var err error
next:
err = kernel.ReadConsoleInputW(stdin,
uintptr(unsafe.Pointer(ir)),
1,
uintptr(unsafe.Pointer(&read)),
)
if err != nil {
return 0, err
}
if ir.EventType != EVENT_KEY {
goto next
}
ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0]))
if ker.bKeyDown == 0 { // keyup
if r.ctrlKey || r.altKey {
switch ker.wVirtualKeyCode {
case VK_RCONTROL, VK_LCONTROL:
r.ctrlKey = false
case VK_MENU: //alt
r.altKey = false
}
}
goto next
}
if ker.unicodeChar == 0 {
var target rune
switch ker.wVirtualKeyCode {
case VK_RCONTROL, VK_LCONTROL:
r.ctrlKey = true
case VK_MENU: //alt
r.altKey = true
case VK_LEFT:
target = CharBackward
case VK_RIGHT:
target = CharForward
case VK_UP:
target = CharPrev
case VK_DOWN:
target = CharNext
}
if target != 0 {
return r.write(buf, target)
}
goto next
}
char := rune(ker.unicodeChar)
if r.ctrlKey {
switch char {
case 'A':
char = CharLineStart
case 'E':
char = CharLineEnd
case 'R':
char = CharBckSearch
case 'S':
char = CharFwdSearch
}
} else if r.altKey {
switch char {
case VK_BACK:
char = CharBackspace
}
return r.writeEsc(buf, char)
}
return r.write(buf, char)
}
func (r *RawReader) writeEsc(b []byte, char rune) (int, error) {
b[0] = '\033'
n := copy(b[1:], []byte(string(char)))
return n + 1, nil
}
func (r *RawReader) write(b []byte, char rune) (int, error) {
n := copy(b, []byte(string(char)))
return n, nil
}
func (r *RawReader) Close() error {
return nil
}

View File

@ -1,9 +1,6 @@
package readline package readline
import ( import "io"
"io"
"os"
)
type Instance struct { type Instance struct {
t *Terminal t *Terminal
@ -26,10 +23,10 @@ func (c *Config) Init() error {
} }
c.inited = true c.inited = true
if c.Stdout == nil { if c.Stdout == nil {
c.Stdout = os.Stdout c.Stdout = Stdout
} }
if c.Stderr == nil { if c.Stderr == nil {
c.Stderr = os.Stderr c.Stderr = Stderr
} }
return nil return nil
} }

View File

@ -22,10 +22,6 @@ func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer {
return rb return rb
} }
func (r *RuneBuffer) SetPrompt(prompt string) {
r.prompt = []rune(prompt)
}
func (r *RuneBuffer) CurrentWidth(x int) int { func (r *RuneBuffer) CurrentWidth(x int) int {
return RunesWidth(r.buf[:x]) return RunesWidth(r.buf[:x])
} }
@ -250,7 +246,7 @@ func (r *RuneBuffer) IdxLine() int {
// the cursor will in the first line, otherwise will in the second line // the cursor will in the first line, otherwise will in the second line
// this situation only occurs in golang's Stdout // this situation only occurs in golang's Stdout
// TODO: figure out why // TODO: figure out why
if totalWidth%w == 0 && len(r.buf) == r.idx { if totalWidth%w == 0 && len(r.buf) == r.idx && !isWindows {
line-- line--
} }
@ -280,31 +276,6 @@ func (r *RuneBuffer) output() []byte {
return buf.Bytes() return buf.Bytes()
} }
func (r *RuneBuffer) CleanOutput() []byte {
buf := bytes.NewBuffer(nil)
buf.Write([]byte("\033[J")) // just like ^k :)
idxLine := r.IdxLine()
if idxLine == 0 {
buf.WriteString("\033[2K\r")
return buf.Bytes()
}
for i := 0; i < idxLine; i++ {
buf.WriteString("\033[2K\r\b")
}
buf.WriteString("\033[2K\r")
return buf.Bytes()
}
func (r *RuneBuffer) Clean() {
if r.cleanInScreen {
return
}
r.cleanInScreen = true
r.w.Write(r.CleanOutput())
}
func (r *RuneBuffer) Reset() []rune { func (r *RuneBuffer) Reset() []rune {
ret := r.buf ret := r.buf
r.buf = r.buf[:0] r.buf = r.buf[:0]
@ -331,7 +302,7 @@ func (r *RuneBuffer) SetStyle(start, end int, style string) {
} else { } else {
r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move))) r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move)))
} }
r.w.Write([]byte("\033[" + style)) r.w.Write([]byte("\033[" + style + "m"))
r.w.Write([]byte(string(r.buf[start:end]))) r.w.Write([]byte(string(r.buf[start:end])))
r.w.Write([]byte("\033[0m")) r.w.Write([]byte("\033[0m"))
// TODO: move back // TODO: move back
@ -347,3 +318,32 @@ func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
func (r *RuneBuffer) Set(buf []rune) { func (r *RuneBuffer) Set(buf []rune) {
r.SetWithIdx(len(buf), buf) r.SetWithIdx(len(buf), buf)
} }
func (r *RuneBuffer) SetPrompt(prompt string) {
r.prompt = []rune(prompt)
}
func (r *RuneBuffer) cleanOutput() []byte {
buf := bytes.NewBuffer(nil)
buf.Write([]byte("\033[J")) // just like ^k :)
idxLine := r.IdxLine()
if idxLine == 0 {
buf.WriteString("\033[2K\r")
return buf.Bytes()
}
for i := 0; i < idxLine; i++ {
buf.WriteString("\033[2K\r\033[A")
}
buf.WriteString("\033[2K\r")
return buf.Bytes()
}
func (r *RuneBuffer) Clean() {
if r.cleanInScreen {
return
}
r.cleanInScreen = true
r.w.Write(r.cleanOutput())
}

View File

@ -126,7 +126,7 @@ func (o *opSearch) SearchRefresh(x int) {
x = x % getWidth() x = x % getWidth()
if o.markStart > 0 { if o.markStart > 0 {
o.buf.SetStyle(o.markStart, o.markEnd, "4m") o.buf.SetStyle(o.markStart, o.markEnd, "4")
} }
lineCnt := o.buf.CursorLineCount() lineCnt := o.buf.CursorLineCount()

12
std.go Normal file
View File

@ -0,0 +1,12 @@
package readline
import (
"io"
"os"
)
var (
Stdin io.ReadCloser = os.Stdin
Stdout io.WriteCloser = os.Stdout
Stderr io.WriteCloser = os.Stderr
)

9
std_windows.go Normal file
View File

@ -0,0 +1,9 @@
// +build windows
package readline
func init() {
Stdin = NewRawReader()
Stdout = NewANSIWriter(Stdout)
Stderr = NewANSIWriter(Stderr)
}

View File

@ -3,7 +3,6 @@ package readline
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"os"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -81,7 +80,7 @@ func (t *Terminal) ioloop() {
expectNextChar bool expectNextChar bool
) )
buf := bufio.NewReader(os.Stdin) buf := bufio.NewReader(Stdin)
for { for {
if !expectNextChar { if !expectNextChar {
atomic.StoreInt64(&t.isReading, 0) atomic.StoreInt64(&t.isReading, 0)

View File

@ -1,6 +1,7 @@
package readline package readline
import ( import (
"strconv"
"syscall" "syscall"
"unicode" "unicode"
@ -9,6 +10,7 @@ import (
var ( var (
StdinFd = int(uintptr(syscall.Stdin)) StdinFd = int(uintptr(syscall.Stdin))
isWindows = false
) )
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
@ -231,3 +233,14 @@ func RunesHasPrefix(r, prefix []rune) bool {
} }
return RunesEqual(r[:len(prefix)], prefix) return RunesEqual(r[:len(prefix)], prefix)
} }
func GetInt(s []string, def int) int {
if len(s) == 0 {
return def
}
c, err := strconv.Atoi(s[0])
if err != nil {
return def
}
return c
}

View File

@ -2,65 +2,15 @@
package readline package readline
import ( func init() {
"syscall" isWindows = true
"unsafe"
)
type (
short int16
word uint16
small_rect struct {
left short
top short
right short
bottom short
}
coord struct {
x short
y short
}
console_screen_buffer_info struct {
size coord
cursor_position coord
attributes word
window small_rect
maximum_window_size coord
}
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
tmp_info console_screen_buffer_info
proc_get_console_screen_buffer_info = kernel32.NewProc("GetConsoleScreenBufferInfo")
)
func get_console_screen_buffer_info(h syscall.Handle, info *console_screen_buffer_info) (err error) {
r0, _, e1 := syscall.Syscall(proc_get_console_screen_buffer_info.Addr(),
2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
if int(r0) == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func get_term_size(out syscall.Handle) coord {
err := get_console_screen_buffer_info(out, &tmp_info)
if err != nil {
panic(err)
}
return tmp_info.size
} }
// get width of the terminal // get width of the terminal
func getWidth() int { func getWidth() int {
return int(get_term_size(syscall.Stdout).x) info, _ := GetConsoleScreenBufferInfo()
if info == nil {
return 0
}
return int(info.dwSize.x)
} }

152
windows_api.go Normal file
View File

@ -0,0 +1,152 @@
// +build windows
package readline
import (
"reflect"
"syscall"
"unsafe"
)
var (
kernel = NewKernel()
stdout = uintptr(syscall.Stdout)
stdin = uintptr(syscall.Stdin)
)
type Kernel struct {
SetConsoleCursorPosition,
SetConsoleTextAttribute,
FillConsoleOutputCharacterW,
FillConsoleOutputAttribute,
ReadConsoleInputW,
GetConsoleScreenBufferInfo,
GetConsoleCursorInfo,
GetStdHandle CallFunc
}
type short int16
type word uint16
type dword uint32
type wchar uint16
type _COORD struct {
x short
y short
}
func (c *_COORD) ptr() uintptr {
return uintptr(*(*int32)(unsafe.Pointer(c)))
}
const (
EVENT_KEY = 0x0001
EVENT_MOUSE = 0x0002
EVENT_WINDOW_BUFFER_SIZE = 0x0004
EVENT_MENU = 0x0008
EVENT_FOCUS = 0x0010
)
type _KEY_EVENT_RECORD struct {
bKeyDown int32
wRepeatCount word
wVirtualKeyCode word
wVirtualScanCode word
unicodeChar wchar
dwControlKeyState dword
}
// KEY_EVENT_RECORD KeyEvent;
// MOUSE_EVENT_RECORD MouseEvent;
// WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
// MENU_EVENT_RECORD MenuEvent;
// FOCUS_EVENT_RECORD FocusEvent;
type _INPUT_RECORD struct {
EventType word
Padding uint16
Event [16]byte
}
type _CONSOLE_SCREEN_BUFFER_INFO struct {
dwSize _COORD
dwCursorPosition _COORD
wAttributes word
srWindow _SMALL_RECT
dwMaximumWindowSize _COORD
}
type _SMALL_RECT struct {
left short
top short
right short
bottom short
}
type _CONSOLE_CURSOR_INFO struct {
dwSize dword
bVisible bool
}
type CallFunc func(u ...uintptr) error
func NewKernel() *Kernel {
k := &Kernel{}
kernel32 := syscall.NewLazyDLL("kernel32.dll")
v := reflect.ValueOf(k).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Name
f := kernel32.NewProc(name)
v.Field(i).Set(reflect.ValueOf(k.Wrap(f)))
}
return k
}
func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc {
return func(args ...uintptr) error {
var r0 uintptr
var e1 syscall.Errno
size := uintptr(len(args))
if len(args) <= 3 {
buf := make([]uintptr, 3)
copy(buf, args)
r0, _, e1 = syscall.Syscall(p.Addr(), size,
buf[0], buf[1], buf[2])
} else {
buf := make([]uintptr, 6)
copy(buf, args)
r0, _, e1 = syscall.Syscall6(p.Addr(), size,
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5],
)
}
if int(r0) == 0 {
if e1 != 0 {
return error(e1)
} else {
return syscall.EINVAL
}
}
return nil
}
}
func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) {
t := new(_CONSOLE_SCREEN_BUFFER_INFO)
err := kernel.GetConsoleScreenBufferInfo(
stdout,
uintptr(unsafe.Pointer(t)),
)
return t, err
}
func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) {
t := new(_CONSOLE_CURSOR_INFO)
err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t)))
return t, err
}
func SetConsoleCursorPosition(c *_COORD) error {
return kernel.SetConsoleCursorPosition(stdout, c.ptr())
}