cuuAndEd
Vladimir Bauer
7 years ago
| 6 | 6 | "io" |
| 7 | 7 | "os" |
| 8 | 8 | |
| 9 | isatty "github.com/mattn/go-isatty" | |
| 10 | 9 | "golang.org/x/crypto/ssh/terminal" |
| 11 | 10 | ) |
| 12 | 11 | |
| 13 | // ESC is the ASCII code for escape character | |
| 14 | const ESC = 27 | |
| 15 | ||
| 16 | 12 | var NotATTY = errors.New("not a terminal") |
| 17 | 13 | |
| 18 | var ( | |
| 19 | cursorUp = fmt.Sprintf("%c[%dA", ESC, 1) | |
| 20 | clearLine = fmt.Sprintf("%c[2K\r", ESC) | |
| 21 | clearCursorAndLine = cursorUp + clearLine | |
| 22 | ) | |
| 14 | var cuuAndEd = fmt.Sprintf("%c[%%dA%[1]c[J", 27) | |
| 23 | 15 | |
| 24 | // Writer is a buffered the writer that updates the terminal. The | |
| 16 | // Writer is a buffered the writer that updates the terminal. The | |
| 25 | 17 | // contents of writer will be flushed when Flush is called. |
| 26 | 18 | type Writer struct { |
| 27 | 19 | out io.Writer |
| 28 | 20 | buf bytes.Buffer |
| 21 | lineCount int | |
| 22 | fd uintptr | |
| 29 | 23 | isTerminal bool |
| 30 | fd int | |
| 31 | lineCount int | |
| 32 | 24 | } |
| 33 | 25 | |
| 34 | 26 | // New returns a new Writer with defaults |
| 35 | 27 | func New(out io.Writer) *Writer { |
| 36 | 28 | w := &Writer{out: out} |
| 37 | 29 | if f, ok := out.(*os.File); ok { |
| 38 | fd := f.Fd() | |
| 39 | w.isTerminal = isatty.IsTerminal(fd) | |
| 40 | w.fd = int(fd) | |
| 30 | w.fd = f.Fd() | |
| 31 | w.isTerminal = terminal.IsTerminal(int(w.fd)) | |
| 41 | 32 | } |
| 42 | 33 | return w |
| 43 | 34 | } |
| 44 | 35 | |
| 45 | 36 | // Flush flushes the underlying buffer |
| 46 | func (w *Writer) Flush(lineCount int) error { | |
| 47 | err := w.clearLines() | |
| 37 | func (w *Writer) Flush(lineCount int) (err error) { | |
| 38 | w.clearLines() | |
| 48 | 39 | w.lineCount = lineCount |
| 49 | // WriteTo takes care of w.buf.Reset | |
| 50 | if _, e := w.buf.WriteTo(w.out); err == nil { | |
| 51 | err = e | |
| 52 | } | |
| 53 | return err | |
| 40 | _, err = w.buf.WriteTo(w.out) | |
| 41 | return | |
| 54 | 42 | } |
| 55 | 43 | |
| 56 | 44 | // Write appends the contents of p to the underlying buffer |
| 72 | 60 | // GetWidth returns width of underlying terminal. |
| 73 | 61 | func (w *Writer) GetWidth() (int, error) { |
| 74 | 62 | if w.isTerminal { |
| 75 | tw, _, err := terminal.GetSize(w.fd) | |
| 63 | tw, _, err := terminal.GetSize(int(w.fd)) | |
| 76 | 64 | return tw, err |
| 77 | 65 | } |
| 78 | 66 | return -1, NotATTY |
| 1 | 1 | |
| 2 | 2 | package cwriter |
| 3 | 3 | |
| 4 | import ( | |
| 5 | "io" | |
| 6 | "strings" | |
| 7 | ) | |
| 4 | import "fmt" | |
| 8 | 5 | |
| 9 | func (w *Writer) clearLines() error { | |
| 10 | _, err := io.WriteString(w.out, strings.Repeat(clearCursorAndLine, w.lineCount)) | |
| 11 | return err | |
| 6 | func (w *Writer) clearLines() { | |
| 7 | fmt.Fprintf(w.out, cuuAndEd, w.lineCount) | |
| 12 | 8 | } |
| 0 | // +build !windows | |
| 1 | ||
| 2 | package cwriter_test | |
| 3 | ||
| 4 | import ( | |
| 5 | "bytes" | |
| 6 | "testing" | |
| 7 | ||
| 8 | . "github.com/vbauerster/mpb/v4/cwriter" | |
| 9 | ) | |
| 10 | ||
| 11 | // TestWriterPosix by writing and flushing many times. The output buffer | |
| 12 | // must contain the clearCursor and clearLine sequences. | |
| 13 | func TestWriterPosix(t *testing.T) { | |
| 14 | out := new(bytes.Buffer) | |
| 15 | w := New(out) | |
| 16 | ||
| 17 | for _, tcase := range []struct { | |
| 18 | input, expectedOutput string | |
| 19 | }{ | |
| 20 | {input: "foo\n", expectedOutput: "foo\n"}, | |
| 21 | {input: "bar\n", expectedOutput: "foo\n" + ClearCursorAndLine + "bar\n"}, | |
| 22 | {input: "fizz\n", expectedOutput: "foo\n" + ClearCursorAndLine + "bar\n" + ClearCursorAndLine + "fizz\n"}, | |
| 23 | } { | |
| 24 | t.Run(tcase.input, func(t *testing.T) { | |
| 25 | s := []byte(tcase.input) | |
| 26 | lc := bytes.Count(s, []byte("\n")) | |
| 27 | w.Write(s) | |
| 28 | w.Flush(lc) | |
| 29 | output := out.String() | |
| 30 | if output != tcase.expectedOutput { | |
| 31 | t.Fatalf("want %q, got %q", tcase.expectedOutput, output) | |
| 32 | } | |
| 33 | }) | |
| 34 | } | |
| 35 | } |
| 2 | 2 | package cwriter |
| 3 | 3 | |
| 4 | 4 | import ( |
| 5 | "io" | |
| 6 | "strings" | |
| 5 | "fmt" | |
| 7 | 6 | "syscall" |
| 8 | 7 | "unsafe" |
| 9 | ||
| 10 | isatty "github.com/mattn/go-isatty" | |
| 11 | 8 | ) |
| 12 | 9 | |
| 13 | 10 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") |
| 19 | 16 | procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") |
| 20 | 17 | ) |
| 21 | 18 | |
| 22 | type ( | |
| 23 | short int16 | |
| 24 | word uint16 | |
| 25 | dword uint32 | |
| 19 | type short int16 | |
| 20 | type dword uint32 | |
| 21 | type word uint16 | |
| 26 | 22 | |
| 27 | coord struct { | |
| 28 | x short | |
| 29 | y short | |
| 30 | } | |
| 31 | smallRect struct { | |
| 32 | left short | |
| 33 | top short | |
| 34 | right short | |
| 35 | bottom short | |
| 36 | } | |
| 37 | consoleScreenBufferInfo struct { | |
| 38 | size coord | |
| 39 | cursorPosition coord | |
| 40 | attributes word | |
| 41 | window smallRect | |
| 42 | maximumWindowSize coord | |
| 43 | } | |
| 44 | ) | |
| 45 | ||
| 46 | // FdWriter is a writer with a file descriptor. | |
| 47 | type FdWriter interface { | |
| 48 | io.Writer | |
| 49 | Fd() uintptr | |
| 23 | type coord struct { | |
| 24 | x short | |
| 25 | y short | |
| 50 | 26 | } |
| 51 | 27 | |
| 52 | func (w *Writer) clearLines() error { | |
| 53 | f, ok := w.out.(FdWriter) | |
| 54 | if ok && !isatty.IsTerminal(f.Fd()) { | |
| 55 | _, err := io.WriteString(w.out, strings.Repeat(clearCursorAndLine, w.lineCount)) | |
| 56 | return err | |
| 28 | type smallRect struct { | |
| 29 | left short | |
| 30 | top short | |
| 31 | right short | |
| 32 | bottom short | |
| 33 | } | |
| 34 | ||
| 35 | type consoleScreenBufferInfo struct { | |
| 36 | size coord | |
| 37 | cursorPosition coord | |
| 38 | attributes word | |
| 39 | window smallRect | |
| 40 | maximumWindowSize coord | |
| 41 | } | |
| 42 | ||
| 43 | func (w *Writer) clearLines() { | |
| 44 | if !w.isTerminal { | |
| 45 | fmt.Fprintf(w.out, cuuAndEd, w.lineCount) | |
| 57 | 46 | } |
| 58 | fd := f.Fd() | |
| 59 | 47 | var info consoleScreenBufferInfo |
| 60 | procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info))) | |
| 48 | procGetConsoleScreenBufferInfo.Call(w.fd, uintptr(unsafe.Pointer(&info))) | |
| 61 | 49 | |
| 62 | 50 | for i := 0; i < w.lineCount; i++ { |
| 63 | 51 | // move the cursor up |
| 64 | 52 | info.cursorPosition.y-- |
| 65 | procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&info.cursorPosition)))) | |
| 53 | procSetConsoleCursorPosition.Call(w.fd, uintptr(*(*int32)(unsafe.Pointer(&info.cursorPosition)))) | |
| 66 | 54 | // clear the line |
| 67 | 55 | cursor := coord{ |
| 68 | 56 | x: info.window.left, |
| 69 | 57 | y: info.window.top + info.cursorPosition.y, |
| 70 | 58 | } |
| 71 | var count, w dword | |
| 59 | var count, dw dword | |
| 72 | 60 | count = dword(info.size.x) |
| 73 | procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) | |
| 61 | procFillConsoleOutputCharacter.Call(w.fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&dw))) | |
| 74 | 62 | } |
| 75 | return nil | |
| 76 | 63 | } |