main
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}