cancelreader_windows.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. //go:build windows
  2. // +build windows
  3. package input
  4. import (
  5. "fmt"
  6. "io"
  7. "os"
  8. "sync"
  9. xwindows "github.com/charmbracelet/x/windows"
  10. "github.com/muesli/cancelreader"
  11. "golang.org/x/sys/windows"
  12. )
  13. type conInputReader struct {
  14. cancelMixin
  15. conin windows.Handle
  16. originalMode uint32
  17. }
  18. var _ cancelreader.CancelReader = &conInputReader{}
  19. func newCancelreader(r io.Reader, flags int) (cancelreader.CancelReader, error) {
  20. fallback := func(io.Reader) (cancelreader.CancelReader, error) {
  21. return cancelreader.NewReader(r)
  22. }
  23. var dummy uint32
  24. if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
  25. // If data was piped to the standard input, it does not emit events
  26. // anymore. We can detect this if the console mode cannot be set anymore,
  27. // in this case, we fallback to the default cancelreader implementation.
  28. windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
  29. return fallback(r)
  30. }
  31. conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE)
  32. if err != nil {
  33. return fallback(r)
  34. }
  35. // Discard any pending input events.
  36. if err := xwindows.FlushConsoleInputBuffer(conin); err != nil {
  37. return fallback(r)
  38. }
  39. modes := []uint32{
  40. windows.ENABLE_WINDOW_INPUT,
  41. windows.ENABLE_EXTENDED_FLAGS,
  42. }
  43. // Enabling mouse mode implicitly blocks console text selection. Thus, we
  44. // need to enable it only if the mouse mode is requested.
  45. // In order to toggle mouse mode, the caller must recreate the reader with
  46. // the appropriate flag toggled.
  47. if flags&FlagMouseMode != 0 {
  48. modes = append(modes, windows.ENABLE_MOUSE_INPUT)
  49. }
  50. originalMode, err := prepareConsole(conin, modes...)
  51. if err != nil {
  52. return nil, fmt.Errorf("failed to prepare console input: %w", err)
  53. }
  54. return &conInputReader{
  55. conin: conin,
  56. originalMode: originalMode,
  57. }, nil
  58. }
  59. // Cancel implements cancelreader.CancelReader.
  60. func (r *conInputReader) Cancel() bool {
  61. r.setCanceled()
  62. return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
  63. }
  64. // Close implements cancelreader.CancelReader.
  65. func (r *conInputReader) Close() error {
  66. if r.originalMode != 0 {
  67. err := windows.SetConsoleMode(r.conin, r.originalMode)
  68. if err != nil {
  69. return fmt.Errorf("reset console mode: %w", err)
  70. }
  71. }
  72. return nil
  73. }
  74. // Read implements cancelreader.CancelReader.
  75. func (r *conInputReader) Read(data []byte) (int, error) {
  76. if r.isCanceled() {
  77. return 0, cancelreader.ErrCanceled
  78. }
  79. var n uint32
  80. if err := windows.ReadFile(r.conin, data, &n, nil); err != nil {
  81. return int(n), fmt.Errorf("read console input: %w", err)
  82. }
  83. return int(n), nil
  84. }
  85. func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
  86. err = windows.GetConsoleMode(input, &originalMode)
  87. if err != nil {
  88. return 0, fmt.Errorf("get console mode: %w", err)
  89. }
  90. var newMode uint32
  91. for _, mode := range modes {
  92. newMode |= mode
  93. }
  94. err = windows.SetConsoleMode(input, newMode)
  95. if err != nil {
  96. return 0, fmt.Errorf("set console mode: %w", err)
  97. }
  98. return originalMode, nil
  99. }
  100. // cancelMixin represents a goroutine-safe cancelation status.
  101. type cancelMixin struct {
  102. unsafeCanceled bool
  103. lock sync.Mutex
  104. }
  105. func (c *cancelMixin) setCanceled() {
  106. c.lock.Lock()
  107. defer c.lock.Unlock()
  108. c.unsafeCanceled = true
  109. }
  110. func (c *cancelMixin) isCanceled() bool {
  111. c.lock.Lock()
  112. defer c.lock.Unlock()
  113. return c.unsafeCanceled
  114. }