Package list golang-github-go-kit-kit / a62210c
Added msys2/cygwin terminal detection support Julien Kauffmann 4 years ago
3 changed file(s) with 148 addition(s) and 6 deletion(s). Raw diff Collapse all Expand all
2424 // platform support for ANSI color codes. If w is not a terminal it is
2525 // returned unmodified.
2626 func NewColorWriter(w io.Writer) io.Writer {
27 if !IsTerminal(w) {
27 if !IsConsole(w) {
2828 return w
2929 }
3030
77 package term
88
99 import (
10 "encoding/binary"
1011 "io"
12 "regexp"
1113 "syscall"
1214 "unsafe"
1315 )
1517 var kernel32 = syscall.NewLazyDLL("kernel32.dll")
1618
1719 var (
18 procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
20 procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
21 msysPipeNameRegex = regexp.MustCompile(`\\(cygwin|msys)-\w+-pty\d?-(to|from)-master`)
22 )
23
24 const (
25 fileNameInfo = 0x02
1926 )
2027
2128 // IsTerminal returns true if w writes to a terminal.
2229 func IsTerminal(w io.Writer) bool {
23 fw, ok := w.(fder)
24 if !ok {
30 return IsConsole(w) || IsMSYSTerminal(w)
31 }
32
33 // IsConsole returns true if w writes to a Windows console.
34 func IsConsole(w io.Writer) bool {
35 var handle syscall.Handle
36
37 if fw, ok := w.(fder); ok {
38 handle = syscall.Handle(fw.Fd())
39 } else {
40 // The writer has no file-descriptor and so can't be a terminal.
2541 return false
2642 }
43
2744 var st uint32
28 r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fw.Fd(), uintptr(unsafe.Pointer(&st)), 0)
29 return r != 0 && e == 0
45 err := syscall.GetConsoleMode(handle, &st)
46
47 // If the handle is attached to a terminal, GetConsoleMode returns a
48 // non-zero value containing the console mode flags. We don't care about
49 // the specifics of flags, just that it is not zero.
50 return (err == nil && st != 0)
3051 }
52
53 // IsMSYSTerminal returns true if w writes to a MSYS/MSYS2 terminal.
54 func IsMSYSTerminal(w io.Writer) bool {
55 var handle syscall.Handle
56
57 if fw, ok := w.(fder); ok {
58 handle = syscall.Handle(fw.Fd())
59 } else {
60 // The writer has no file-descriptor and so can't be a terminal.
61 return false
62 }
63
64 // MSYS(2) terminal reports as a pipe for STDIN/STDOUT/STDERR. If it isn't
65 // a pipe, it can't be a MSYS(2) terminal.
66 filetype, err := syscall.GetFileType(handle)
67
68 if filetype != syscall.FILE_TYPE_PIPE || err != nil {
69 return false
70 }
71
72 // MSYS2/Cygwin terminal's name looks like: \msys-dd50a72ab4668b33-pty2-to-master
73 data := make([]byte, 256, 256)
74
75 r, _, e := syscall.Syscall6(
76 procGetFileInformationByHandleEx.Addr(),
77 4,
78 uintptr(handle),
79 uintptr(fileNameInfo),
80 uintptr(unsafe.Pointer(&data[0])),
81 uintptr(len(data)),
82 0,
83 0,
84 )
85
86 if r != 0 && e == 0 {
87 // The first 4 bytes of the buffer are the size of the UTF16 name, in bytes.
88 unameLen := binary.LittleEndian.Uint32(data[:4]) / 2
89 uname := make([]uint16, unameLen, unameLen)
90
91 for i := uint32(0); i < unameLen; i++ {
92 uname[i] = binary.LittleEndian.Uint16(data[i*2+4 : i*2+2+4])
93 }
94
95 name := syscall.UTF16ToString(uname)
96
97 return msysPipeNameRegex.MatchString(name)
98 }
99
100 return false
101 }
0 package term
1
2 import (
3 "fmt"
4 "syscall"
5 "testing"
6 )
7
8 // +build windows
9
10 type myWriter struct {
11 fd uintptr
12 }
13
14 func (w *myWriter) Write(p []byte) (int, error) {
15 return 0, fmt.Errorf("not implemented")
16 }
17
18 func (w *myWriter) Fd() uintptr {
19 return w.fd
20 }
21
22 var procGetStdHandle = kernel32.NewProc("GetStdHandle")
23
24 const stdOutputHandle = ^uintptr(0) - 11 + 1
25
26 func getConsoleHandle() syscall.Handle {
27 ptr, err := syscall.UTF16PtrFromString("CONOUT$")
28
29 if err != nil {
30 panic(err)
31 }
32
33 handle, err := syscall.CreateFile(ptr, syscall.GENERIC_READ|syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, 0, 0)
34
35 if err != nil {
36 panic(err)
37 }
38
39 return handle
40 }
41
42 func TestIsTerminal(t *testing.T) {
43 // This is necessary because depending on whether `go test` is called with
44 // the `-v` option, stdout will or will not be bound, changing the behavior
45 // of the test. So we refer to it directly to avoid flakyness.
46 handle := getConsoleHandle()
47
48 writer := &myWriter{
49 fd: uintptr(handle),
50 }
51
52 if !IsTerminal(writer) {
53 t.Errorf("output is supposed to be a terminal")
54 }
55 }
56
57 func TestIsConsole(t *testing.T) {
58 // This is necessary because depending on whether `go test` is called with
59 // the `-v` option, stdout will or will not be bound, changing the behavior
60 // of the test. So we refer to it directly to avoid flakyness.
61 handle := getConsoleHandle()
62
63 writer := &myWriter{
64 fd: uintptr(handle),
65 }
66
67 if !IsConsole(writer) {
68 t.Errorf("output is supposed to be a console")
69 }
70 }