github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/logger/output_windows.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 //go:build windows 5 // +build windows 6 7 // Windows 10 has a new terminal that can do ANSI codes by itself, so all this 8 // other stuff is legacy - EXCEPT that the colors are not right! If they ever 9 // get it right, a capable terminal can be identified by 10 // calling GetConsoleMode and checking for 11 // ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 12 13 package logger 14 15 import ( 16 "bytes" 17 "fmt" 18 "io" 19 "os" 20 "sync" 21 "syscall" 22 23 "unsafe" 24 25 "golang.org/x/sys/windows" 26 ) 27 28 var ( 29 kernel32DLL = windows.NewLazySystemDLL("kernel32.dll") 30 setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute") 31 getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") 32 stdOutMutex sync.Mutex 33 stdErrMutex sync.Mutex 34 consoleMode WORD 35 ) 36 37 type ( 38 SHORT int16 39 WORD uint16 40 41 SMALL_RECT struct { 42 Left SHORT 43 Top SHORT 44 Right SHORT 45 Bottom SHORT 46 } 47 48 COORD struct { 49 X SHORT 50 Y SHORT 51 } 52 53 CONSOLE_SCREEN_BUFFER_INFO struct { 54 Size COORD 55 CursorPosition COORD 56 Attributes WORD 57 Window SMALL_RECT 58 MaximumWindowSize COORD 59 } 60 ) 61 62 const ( 63 // Character attributes 64 // Note: 65 // -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan). 66 // Clearing all foreground or background colors results in black; setting all creates white. 67 // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes. 68 fgBlack WORD = 0x0000 69 fgBlue WORD = 0x0001 70 fgGreen WORD = 0x0002 71 fgCyan WORD = 0x0003 72 fgRed WORD = 0x0004 73 fgMagenta WORD = 0x0005 74 fgYellow WORD = 0x0006 75 fgWhite WORD = 0x0007 76 fgIntensity WORD = 0x0008 77 78 bgBlack WORD = 0x0000 79 bgBlue WORD = 0x0010 80 bgGreen WORD = 0x0020 81 bgCyan WORD = 0x0030 82 bgRed WORD = 0x0040 83 bgMagenta WORD = 0x0050 84 bgYellow WORD = 0x0060 85 bgWhite WORD = 0x0070 86 bgIntensity WORD = 0x0080 87 ) 88 89 var codesWin = map[byte]WORD{ 90 0: fgWhite | bgBlack, // "reset": 91 1: fgIntensity, //CpBold = CodePair{1, 22} 92 22: fgWhite, // UnBold: // Just assume this means reset to white fg 93 39: fgWhite, // "resetfg": 94 49: fgWhite, // "resetbg": // Just assume this means reset to white fg 95 30: fgBlack, // "black": 96 31: fgRed, // "red": 97 32: fgGreen, // "green": 98 33: fgYellow, // "yellow": 99 34: fgBlue, // "blue": 100 35: fgMagenta, // "magenta": 101 36: fgCyan, // "cyan": 102 37: fgWhite, // "white": 103 90: fgBlack | fgIntensity, // "grey": 104 91: fgRed | fgIntensity, // "red": 105 92: fgGreen | fgIntensity, // "green": 106 93: fgYellow | fgIntensity, // "yellow": 107 94: fgBlue | fgIntensity, // "blue": 108 95: fgMagenta | fgIntensity, // "magenta": 109 96: fgCyan | fgIntensity, // "cyan": 110 97: fgWhite | fgIntensity, // "white": 111 40: bgBlack, // "bgBlack": 112 41: bgRed, // "bgRed": 113 42: bgGreen, // "bgGreen": 114 43: bgYellow, // "bgYellow": 115 44: bgBlue, // "bgBlue": 116 45: bgMagenta, // "bgMagenta": 117 46: bgCyan, // "bgCyan": 118 47: bgWhite, // "bgWhite": 119 100: bgBlack | bgIntensity, // "bgBlack": 120 101: bgRed | bgIntensity, // "bgRed": 121 102: bgGreen | bgIntensity, // "bgGreen": 122 103: bgYellow | bgIntensity, // "bgYellow": 123 104: bgBlue | bgIntensity, // "bgBlue": 124 105: bgMagenta | bgIntensity, // "bgMagenta": 125 106: bgCyan | bgIntensity, // "bgCyan": 126 107: bgWhite | bgIntensity, // "bgWhite": 127 } 128 129 // Return our writer so we can override Write() 130 func OutputWriterFromFile(out *os.File) io.Writer { 131 return &ColorWriter{w: out, fd: out.Fd(), mutex: &stdOutMutex, lastFgColor: fgWhite} 132 } 133 134 // Return our writer so we can override Write() 135 func OutputWriter() io.Writer { 136 return OutputWriterFromFile(os.Stdout) 137 } 138 139 // Return our writer so we can override Write() 140 func ErrorWriter() io.Writer { 141 return &ColorWriter{w: os.Stderr, fd: os.Stderr.Fd(), mutex: &stdErrMutex, lastFgColor: fgWhite} 142 } 143 144 type ColorWriter struct { 145 w io.Writer 146 fd uintptr 147 mutex *sync.Mutex 148 lastFgColor WORD 149 bold bool 150 } 151 152 // Fd returns the underlying file descriptor. This is mainly to 153 // support checking whether it's a terminal or not. 154 func (cw *ColorWriter) Fd() uintptr { 155 return cw.fd 156 } 157 158 // Rough emulation of Ansi terminal codes. 159 // Parse the string, pick out the codes, and convert to 160 // calls to SetConsoleTextAttribute. 161 // 162 // This function must mix calls to SetConsoleTextAttribute 163 // with separate Write() calls, so to prevent pieces of colored 164 // strings from being interleaved by unsynchronized goroutines, 165 // we unfortunately must lock a mutex. 166 // 167 // Notice this is only necessary for what is now called 168 // "legacy" terminal mode: 169 // https://technet.microsoft.com/en-us/library/mt427362.aspx?f=255&MSPPError=-2147217396 170 func (cw *ColorWriter) Write(p []byte) (n int, err error) { 171 cw.mutex.Lock() 172 defer cw.mutex.Unlock() 173 174 var totalWritten = len(p) 175 ctlStart := []byte{0x1b, '['} 176 177 for nextIndex := 0; len(p) > 0; { 178 179 // search for the next control code 180 nextIndex = bytes.Index(p, ctlStart) 181 if nextIndex == -1 { 182 nextIndex = len(p) 183 } 184 185 cw.w.Write(p[0:nextIndex]) 186 if nextIndex+2 < len(p) { 187 // Skip past the control code 188 nextIndex += 2 189 } 190 191 p = p[nextIndex:] 192 193 if len(p) > 0 { 194 // The control code is written as separate ascii digits. usually 2, 195 // ending with 'm'. 196 // Stop at 4 as a sanity check. 197 if '0' <= p[0] && p[0] <= '9' { 198 p = cw.parseColorControl(p) 199 } else { 200 p = cw.parseControlCode(p) 201 } 202 } 203 204 } 205 return totalWritten, nil 206 } 207 208 func (cw *ColorWriter) parseColorControl(p []byte) []byte { 209 210 var controlIndex int 211 controlCode := p[controlIndex] - '0' 212 controlIndex++ 213 for i := 0; controlIndex < len(p) && p[controlIndex] != 'm' && i < 4; i++ { 214 if '0' <= p[controlIndex] && p[controlIndex] <= '9' { 215 controlCode *= 10 216 controlCode += p[controlIndex] - '0' 217 } 218 controlIndex++ 219 } 220 221 if code, ok := codesWin[controlCode]; ok { 222 if controlCode == 1 { 223 // intensity 224 cw.bold = true 225 } else if controlCode == 0 || controlCode == 22 || controlCode == 39 { 226 // reset 227 code = fgWhite 228 cw.bold = false 229 } 230 if code >= fgBlue && code <= fgWhite { 231 cw.lastFgColor = code 232 } else { 233 code = cw.lastFgColor 234 } 235 if cw.bold { 236 code = code | fgIntensity 237 } 238 setConsoleTextAttribute(cw.fd, code) 239 } 240 if controlIndex+1 <= len(p) { 241 controlIndex++ 242 } 243 244 return p[controlIndex:] 245 } 246 247 // parseControlCode is for absorbing backspaces, which 248 // caused junk to come out on the console, and whichever 249 // other control code we're probably unprepared for 250 func (cw *ColorWriter) parseControlCode(p []byte) []byte { 251 if p[0] == 'D' { 252 cw.w.Write([]byte(fmt.Sprintf("\b"))) 253 } 254 return p[1:] 255 } 256 257 // setConsoleTextAttribute sets the attributes of characters written to the 258 // console screen buffer by the WriteFile or WriteConsole function. 259 // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx. 260 func setConsoleTextAttribute(handle uintptr, attribute WORD) error { 261 r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0) 262 use(attribute) 263 return checkError(r1, r2, err) 264 } 265 266 // GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. 267 // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 268 func getConsoleTextAttribute(handle uintptr) (WORD, error) { 269 var info CONSOLE_SCREEN_BUFFER_INFO 270 if err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { 271 return 0, err 272 } 273 return info.Attributes, nil 274 } 275 276 // SaveConsoleMode records the current text attributes in a global, so 277 // it can be restored later, in case nonstandard colors are expected. 278 func SaveConsoleMode() error { 279 var err error 280 consoleMode, err = getConsoleTextAttribute(os.Stdout.Fd()) 281 return err 282 } 283 284 // RestoreConsoleMode restores the current text attributes from a global, 285 // in case nonstandard colors are expected. 286 func RestoreConsoleMode() { 287 if consoleMode != 0 { 288 setConsoleTextAttribute(os.Stdout.Fd(), consoleMode) 289 } 290 } 291 292 // checkError evaluates the results of a Windows API call and returns the error if it failed. 293 func checkError(r1, r2 uintptr, err error) error { 294 // Windows APIs return non-zero to indicate success 295 if r1 != 0 { 296 return nil 297 } 298 299 // Return the error if provided, otherwise default to EINVAL 300 if err != nil { 301 return err 302 } 303 return syscall.EINVAL 304 } 305 306 // use is a no-op, but the compiler cannot see that it is. 307 // Calling use(p) ensures that p is kept live until that point. 308 func use(p interface{}) {}