main
Raw Download raw file
  1//go:build windows
  2// +build windows
  3
  4package tea
  5
  6import (
  7	"fmt"
  8	"io"
  9	"os"
 10	"sync"
 11
 12	"github.com/charmbracelet/x/term"
 13	"github.com/erikgeiser/coninput"
 14	"github.com/muesli/cancelreader"
 15	"golang.org/x/sys/windows"
 16)
 17
 18type conInputReader struct {
 19	cancelMixin
 20
 21	conin windows.Handle
 22
 23	originalMode uint32
 24}
 25
 26var _ cancelreader.CancelReader = &conInputReader{}
 27
 28func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, error) {
 29	fallback := func(io.Reader) (cancelreader.CancelReader, error) {
 30		return cancelreader.NewReader(r)
 31	}
 32	if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() {
 33		return fallback(r)
 34	}
 35
 36	conin, err := coninput.NewStdinHandle()
 37	if err != nil {
 38		return fallback(r)
 39	}
 40
 41	modes := []uint32{
 42		windows.ENABLE_WINDOW_INPUT,
 43		windows.ENABLE_EXTENDED_FLAGS,
 44	}
 45
 46	// Since we have options to enable mouse events, [WithMouseCellMotion],
 47	// [WithMouseAllMotion], and [EnableMouseCellMotion],
 48	// [EnableMouseAllMotion], and [DisableMouse], we need to check if the user
 49	// has enabled mouse events and add the appropriate mode accordingly.
 50	// Otherwise, mouse events will be enabled all the time.
 51	if enableMouse {
 52		modes = append(modes, windows.ENABLE_MOUSE_INPUT)
 53	}
 54
 55	originalMode, err := prepareConsole(conin, modes...)
 56	if err != nil {
 57		return nil, fmt.Errorf("failed to prepare console input: %w", err)
 58	}
 59
 60	return &conInputReader{
 61		conin:        conin,
 62		originalMode: originalMode,
 63	}, nil
 64}
 65
 66// Cancel implements cancelreader.CancelReader.
 67func (r *conInputReader) Cancel() bool {
 68	r.setCanceled()
 69
 70	return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
 71}
 72
 73// Close implements cancelreader.CancelReader.
 74func (r *conInputReader) Close() error {
 75	if r.originalMode != 0 {
 76		err := windows.SetConsoleMode(r.conin, r.originalMode)
 77		if err != nil {
 78			return fmt.Errorf("reset console mode: %w", err)
 79		}
 80	}
 81
 82	return nil
 83}
 84
 85// Read implements cancelreader.CancelReader.
 86func (r *conInputReader) Read(_ []byte) (n int, err error) {
 87	if r.isCanceled() {
 88		err = cancelreader.ErrCanceled
 89	}
 90	return
 91}
 92
 93func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
 94	err = windows.GetConsoleMode(input, &originalMode)
 95	if err != nil {
 96		return 0, fmt.Errorf("get console mode: %w", err)
 97	}
 98
 99	newMode := coninput.AddInputModes(0, modes...)
100
101	err = windows.SetConsoleMode(input, newMode)
102	if err != nil {
103		return 0, fmt.Errorf("set console mode: %w", err)
104	}
105
106	return originalMode, nil
107}
108
109// cancelMixin represents a goroutine-safe cancelation status.
110type cancelMixin struct {
111	unsafeCanceled bool
112	lock           sync.Mutex
113}
114
115func (c *cancelMixin) setCanceled() {
116	c.lock.Lock()
117	defer c.lock.Unlock()
118
119	c.unsafeCanceled = true
120}
121
122func (c *cancelMixin) isCanceled() bool {
123	c.lock.Lock()
124	defer c.lock.Unlock()
125
126	return c.unsafeCanceled
127}