output_windows.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package terminal
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "syscall"
  9. "unsafe"
  10. "github.com/mattn/go-isatty"
  11. )
  12. var (
  13. cursorFunctions = map[rune]func(c *Cursor) func(int){
  14. 'A': func(c *Cursor) func(int) { return c.Up },
  15. 'B': func(c *Cursor) func(int) { return c.Down },
  16. 'C': func(c *Cursor) func(int) { return c.Forward },
  17. 'D': func(c *Cursor) func(int) { return c.Back },
  18. 'E': func(c *Cursor) func(int) { return c.NextLine },
  19. 'F': func(c *Cursor) func(int) { return c.PreviousLine },
  20. 'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
  21. }
  22. )
  23. const (
  24. foregroundBlue = 0x1
  25. foregroundGreen = 0x2
  26. foregroundRed = 0x4
  27. foregroundIntensity = 0x8
  28. foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
  29. backgroundBlue = 0x10
  30. backgroundGreen = 0x20
  31. backgroundRed = 0x40
  32. backgroundIntensity = 0x80
  33. backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
  34. )
  35. type Writer struct {
  36. out FileWriter
  37. handle syscall.Handle
  38. orgAttr word
  39. }
  40. func NewAnsiStdout(out FileWriter) io.Writer {
  41. var csbi consoleScreenBufferInfo
  42. if !isatty.IsTerminal(out.Fd()) {
  43. return out
  44. }
  45. handle := syscall.Handle(out.Fd())
  46. procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
  47. return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
  48. }
  49. func NewAnsiStderr(out FileWriter) io.Writer {
  50. var csbi consoleScreenBufferInfo
  51. if !isatty.IsTerminal(out.Fd()) {
  52. return out
  53. }
  54. handle := syscall.Handle(out.Fd())
  55. procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
  56. return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
  57. }
  58. func (w *Writer) Write(data []byte) (n int, err error) {
  59. r := bytes.NewReader(data)
  60. for {
  61. ch, size, err := r.ReadRune()
  62. if err != nil {
  63. break
  64. }
  65. n += size
  66. switch ch {
  67. case '\x1b':
  68. size, err = w.handleEscape(r)
  69. n += size
  70. if err != nil {
  71. break
  72. }
  73. default:
  74. fmt.Fprint(w.out, string(ch))
  75. }
  76. }
  77. return
  78. }
  79. func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
  80. buf := make([]byte, 0, 10)
  81. buf = append(buf, "\x1b"...)
  82. // Check '[' continues after \x1b
  83. ch, size, err := r.ReadRune()
  84. if err != nil {
  85. fmt.Fprint(w.out, string(buf))
  86. return
  87. }
  88. n += size
  89. if ch != '[' {
  90. fmt.Fprint(w.out, string(buf))
  91. return
  92. }
  93. // Parse escape code
  94. var code rune
  95. argBuf := make([]byte, 0, 10)
  96. for {
  97. ch, size, err = r.ReadRune()
  98. if err != nil {
  99. fmt.Fprint(w.out, string(buf))
  100. return
  101. }
  102. n += size
  103. if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
  104. code = ch
  105. break
  106. }
  107. argBuf = append(argBuf, string(ch)...)
  108. }
  109. w.applyEscapeCode(buf, string(argBuf), code)
  110. return
  111. }
  112. func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
  113. c := &Cursor{Out: w.out}
  114. switch arg + string(code) {
  115. case "?25h":
  116. c.Show()
  117. return
  118. case "?25l":
  119. c.Hide()
  120. return
  121. }
  122. if f, ok := cursorFunctions[code]; ok {
  123. if n, err := strconv.Atoi(arg); err == nil {
  124. f(c)(n)
  125. return
  126. }
  127. }
  128. switch code {
  129. case 'm':
  130. w.applySelectGraphicRendition(arg)
  131. default:
  132. buf = append(buf, string(code)...)
  133. fmt.Fprint(w.out, string(buf))
  134. }
  135. }
  136. // Original implementation: https://github.com/mattn/go-colorable
  137. func (w *Writer) applySelectGraphicRendition(arg string) {
  138. if arg == "" {
  139. procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
  140. return
  141. }
  142. var csbi consoleScreenBufferInfo
  143. procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
  144. attr := csbi.attributes
  145. for _, param := range strings.Split(arg, ";") {
  146. n, err := strconv.Atoi(param)
  147. if err != nil {
  148. continue
  149. }
  150. switch {
  151. case n == 0 || n == 100:
  152. attr = w.orgAttr
  153. case 1 <= n && n <= 5:
  154. attr |= foregroundIntensity
  155. case 30 <= n && n <= 37:
  156. attr = (attr & backgroundMask)
  157. if (n-30)&1 != 0 {
  158. attr |= foregroundRed
  159. }
  160. if (n-30)&2 != 0 {
  161. attr |= foregroundGreen
  162. }
  163. if (n-30)&4 != 0 {
  164. attr |= foregroundBlue
  165. }
  166. case 40 <= n && n <= 47:
  167. attr = (attr & foregroundMask)
  168. if (n-40)&1 != 0 {
  169. attr |= backgroundRed
  170. }
  171. if (n-40)&2 != 0 {
  172. attr |= backgroundGreen
  173. }
  174. if (n-40)&4 != 0 {
  175. attr |= backgroundBlue
  176. }
  177. case 90 <= n && n <= 97:
  178. attr = (attr & backgroundMask)
  179. attr |= foregroundIntensity
  180. if (n-90)&1 != 0 {
  181. attr |= foregroundRed
  182. }
  183. if (n-90)&2 != 0 {
  184. attr |= foregroundGreen
  185. }
  186. if (n-90)&4 != 0 {
  187. attr |= foregroundBlue
  188. }
  189. case 100 <= n && n <= 107:
  190. attr = (attr & foregroundMask)
  191. attr |= backgroundIntensity
  192. if (n-100)&1 != 0 {
  193. attr |= backgroundRed
  194. }
  195. if (n-100)&2 != 0 {
  196. attr |= backgroundGreen
  197. }
  198. if (n-100)&4 != 0 {
  199. attr |= backgroundBlue
  200. }
  201. }
  202. }
  203. procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
  204. }