diff --git a/.travis.yml b/.travis.yml index 9a9779a..ce3b11b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: go go: - - 1.4 - 1.5 before_install: - go get golang.org/x/crypto/ssh/terminal 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 diff --git a/README.md b/README.md index 544f300..fffb994 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read # Todo -* Add support for windows * Vim mode * 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 | | Ubuntu Server 14.04 LTS | linux | | Centos 7 | linux | +| Windows 10 | - | ### Notice: * `Ctrl`+`A` is not working in screen because it used as a control command by default diff --git a/ansi_windows.go b/ansi_windows.go new file mode 100644 index 0000000..68c79b8 --- /dev/null +++ b/ansi_windows.go @@ -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)), + ) +} diff --git a/char.go b/char.go index ec6cd1d..1293e16 100644 --- a/char.go +++ b/char.go @@ -26,7 +26,7 @@ const ( ) const ( - MetaPrev = -iota - 1 + MetaPrev rune = -iota - 1 MetaNext MetaDelete MetaBackspace diff --git a/operation.go b/operation.go index 7c98b80..df38e3c 100644 --- a/operation.go +++ b/operation.go @@ -7,6 +7,7 @@ type Operation struct { t *Terminal buf *RuneBuffer outchan chan []rune + w io.Writer *opHistory *opSearch @@ -20,12 +21,18 @@ type wrapWriter struct { } func (w *wrapWriter) Write(b []byte) (int, error) { - buf := w.r.buf - buf.Clean() - n, err := w.target.Write(b) - if w.t.IsReading() { - w.r.buf.Refresh(nil) + if !w.t.IsReading() { + return w.target.Write(b) } + + var ( + n int + err error + ) + w.r.buf.Refresh(func() { + n, err = w.target.Write(b) + }) + if w.r.IsSearchMode() { w.r.SearchRefresh(-1) } @@ -43,6 +50,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation { outchan: make(chan []rune), opHistory: newOpHistory(cfg.HistoryFile), } + op.w = op.buf.w op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory) op.opCompleter = newOpCompleter(op.buf.w, op) go op.ioloop() @@ -232,6 +240,10 @@ func (o *Operation) Runes() ([]rune, error) { return r, nil } +func (o *Operation) SetTitle(t string) { + o.w.Write([]byte("\033[2;" + t + "\007")) +} + func (o *Operation) Slice() ([]byte, error) { r, err := o.Runes() if err != nil { diff --git a/rawreader_windows.go b/rawreader_windows.go new file mode 100644 index 0000000..5e7b873 --- /dev/null +++ b/rawreader_windows.go @@ -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 +} diff --git a/readline.go b/readline.go index e4032ad..0501ff3 100644 --- a/readline.go +++ b/readline.go @@ -1,9 +1,6 @@ package readline -import ( - "io" - "os" -) +import "io" type Instance struct { t *Terminal @@ -26,10 +23,10 @@ func (c *Config) Init() error { } c.inited = true if c.Stdout == nil { - c.Stdout = os.Stdout + c.Stdout = Stdout } if c.Stderr == nil { - c.Stderr = os.Stderr + c.Stderr = Stderr } return nil } diff --git a/runebuf.go b/runebuf.go index d310bc7..39c2b39 100644 --- a/runebuf.go +++ b/runebuf.go @@ -22,10 +22,6 @@ func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer { return rb } -func (r *RuneBuffer) SetPrompt(prompt string) { - r.prompt = []rune(prompt) -} - func (r *RuneBuffer) CurrentWidth(x int) int { 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 // this situation only occurs in golang's Stdout // TODO: figure out why - if totalWidth%w == 0 && len(r.buf) == r.idx { + if totalWidth%w == 0 && len(r.buf) == r.idx && !isWindows { line-- } @@ -280,31 +276,6 @@ func (r *RuneBuffer) output() []byte { 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 { ret := r.buf r.buf = r.buf[:0] @@ -331,7 +302,7 @@ func (r *RuneBuffer) SetStyle(start, end int, style string) { } else { 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("\033[0m")) // TODO: move back @@ -347,3 +318,32 @@ func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) { func (r *RuneBuffer) Set(buf []rune) { 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()) +} diff --git a/search.go b/search.go index 450aeb6..4c09ba1 100644 --- a/search.go +++ b/search.go @@ -126,7 +126,7 @@ func (o *opSearch) SearchRefresh(x int) { x = x % getWidth() if o.markStart > 0 { - o.buf.SetStyle(o.markStart, o.markEnd, "4m") + o.buf.SetStyle(o.markStart, o.markEnd, "4") } lineCnt := o.buf.CursorLineCount() diff --git a/std.go b/std.go new file mode 100644 index 0000000..3baf5a7 --- /dev/null +++ b/std.go @@ -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 +) diff --git a/std_windows.go b/std_windows.go new file mode 100644 index 0000000..b10f91b --- /dev/null +++ b/std_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package readline + +func init() { + Stdin = NewRawReader() + Stdout = NewANSIWriter(Stdout) + Stderr = NewANSIWriter(Stderr) +} diff --git a/terminal.go b/terminal.go index 23bb3a1..12ac640 100644 --- a/terminal.go +++ b/terminal.go @@ -3,7 +3,6 @@ package readline import ( "bufio" "fmt" - "os" "sync" "sync/atomic" @@ -81,7 +80,7 @@ func (t *Terminal) ioloop() { expectNextChar bool ) - buf := bufio.NewReader(os.Stdin) + buf := bufio.NewReader(Stdin) for { if !expectNextChar { atomic.StoreInt64(&t.isReading, 0) diff --git a/utils.go b/utils.go index c880be3..f3b3eb6 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,7 @@ package readline import ( + "strconv" "syscall" "unicode" @@ -8,7 +9,8 @@ import ( ) var ( - StdinFd = int(uintptr(syscall.Stdin)) + StdinFd = int(uintptr(syscall.Stdin)) + isWindows = false ) // 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) } + +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 +} diff --git a/utils_windows.go b/utils_windows.go index 859908c..4cee4b6 100644 --- a/utils_windows.go +++ b/utils_windows.go @@ -2,65 +2,15 @@ package readline -import ( - "syscall" - "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 +func init() { + isWindows = true } // get width of the terminal func getWidth() int { - return int(get_term_size(syscall.Stdout).x) + info, _ := GetConsoleScreenBufferInfo() + if info == nil { + return 0 + } + return int(info.dwSize.x) } diff --git a/windows_api.go b/windows_api.go new file mode 100644 index 0000000..63f4f7b --- /dev/null +++ b/windows_api.go @@ -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()) +}