diff --git a/operation.go b/operation.go index 8c4a758..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 @@ -49,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() @@ -238,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/runebuf.go b/runebuf.go index 2ea40f4..d5116bb 100644 --- a/runebuf.go +++ b/runebuf.go @@ -302,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 @@ -334,9 +334,11 @@ func (r *RuneBuffer) cleanOutput() []byte { } for i := 0; i < idxLine; i++ { - buf.WriteString("\033[2K\r\b") + buf.WriteString("\033[2K\r") + if i != idxLine-1 { + buf.WriteByte('\b') + } } - buf.WriteString("\033[2K\r") return buf.Bytes() } 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 index a5fade5..6a3f876 100644 --- a/std.go +++ b/std.go @@ -6,6 +6,6 @@ import ( ) var ( - Stdout io.Writer = os.Stdout - Stderr io.Writer = os.Stderr + Stdout io.WriteCloser = os.Stdout + Stderr io.WriteCloser = os.Stderr ) diff --git a/std_ansi.go b/std_ansi.go new file mode 100644 index 0000000..783b61c --- /dev/null +++ b/std_ansi.go @@ -0,0 +1,217 @@ +// +build windows + +package readline + +import ( + "io" + "strconv" + "strings" + "sync" + "unicode/utf8" + "unsafe" + + "gopkg.in/bufio.v1" +) + +func init() { + Stdout = NewANSIWriter(Stdout) + Stderr = NewANSIWriter(Stderr) +} + +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, 1024), + } + 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 + switch r { + case 'J': + eraseLine() + 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 |= ColorTableFg[c-30] + } else if c == 0 { + color = ColorTableFg[7] + } + } + if err != nil { + break + } + kernel.SetConsoleTextAttribute(stdout, uintptr(color)) + case 'A': + case 'B': + case 'C': + case 'D': + case '\007': + case ';': + if len(arg) == 0 || arg[len(arg)-1] != "" { + arg = append(arg, "") + } + fallthrough + 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 eraseLine() error { + sbi, err := GetConsoleScreenBufferInfo() + if err != nil { + return err + } + + var written int + return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '), + uintptr(sbi.dwSize.x-sbi.dwCursorPosition.x), + sbi.dwCursorPosition.ptr(), + uintptr(unsafe.Pointer(&written)), + ) +} + +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 +) + +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 +} diff --git a/std_windows.go b/std_windows.go deleted file mode 100644 index 985fb24..0000000 --- a/std_windows.go +++ /dev/null @@ -1,62 +0,0 @@ -// +build windows - -package readline - -import ( - "io" - - "gopkg.in/bufio.v1" -) - -func init() { - Stdout = NewANSIWriter(Stdout) - Stderr = NewANSIWriter(Stderr) -} - -type ANSIWriter struct { - io.Writer -} - -func NewANSIWriter(w io.Writer) *ANSIWriter { - a := &ANSIWriter{ - Writer: w, - } - return a -} - -func (a *ANSIWriter) Write(b []byte) (int, error) { - var ( - isEsc bool - isEscSeq bool - ) - buf := bufio.NewBuffer(nil) - for i := 0; i < len(b); i++ { - if isEscSeq { - isEsc = false - isEscSeq = false - continue - } - if isEsc { - isEsc = false - continue - } - - switch b[i] { - case CharEsc: - isEsc = true - continue - case '[': - if isEsc { - isEscSeq = true - continue - } - fallthrough - default: - } - } - n, err := buf.WriteTo(a.Writer) - if err != nil { - return n, err - } - return len(b), nil -} diff --git a/utils_windows.go b/utils_windows.go index 859908c..55b8eea 100644 --- a/utils_windows.go +++ b/utils_windows.go @@ -2,65 +2,11 @@ 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 -} - // 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..5f65eef --- /dev/null +++ b/windows_api.go @@ -0,0 +1,103 @@ +// +build windows + +package readline + +import ( + "reflect" + "syscall" + "unsafe" +) + +var ( + kernel = NewKernel() + stdout = uintptr(syscall.Stdout) +) + +type Kernel struct { + SetConsoleCursorPosition, + SetConsoleTextAttribute, + GetConsoleScreenBufferInfo, + FillConsoleOutputCharacterW, + GetStdHandle CallFunc +} + +type short int16 +type word uint16 + +type _COORD struct { + x short + y short +} + +func (c *_COORD) ptr() uintptr { + return uintptr(*(*int32)(unsafe.Pointer(c))) +} + +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 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 +}