okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Ayman BagabasMerge pull request #32 from charmbracelet/win32-input-mode21397ab

main
3.2 KiB139 linesraw
1//go:build windows
2// +build windows
3
4package uv
5
6import (
7	"fmt"
8	"io"
9	"os"
10	"sync"
11
12	xwindows "github.com/charmbracelet/x/windows"
13	"github.com/muesli/cancelreader"
14	"golang.org/x/sys/windows"
15)
16
17type conInputReader struct {
18	cancelMixin
19	conin        windows.Handle
20	originalMode uint32
21	newMode      uint32
22}
23
24var _ cancelreader.CancelReader = &conInputReader{}
25
26// NewCancelReader creates a new [cancelreader.CancelReader] that provides a
27// cancelable reader interface that can be used to cancel reads.
28func NewCancelReader(r io.Reader) (cancelreader.CancelReader, error) {
29	fallback := func(io.Reader) (cancelreader.CancelReader, error) {
30		return cancelreader.NewReader(r)
31	}
32
33	var dummy uint32
34	if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
35		// If data was piped to the standard input, it does not emit events
36		// anymore. We can detect this if the console mode cannot be set anymore,
37		// in this case, we fallback to the default cancelreader implementation.
38		windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
39		return fallback(r)
40	}
41
42	conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE)
43	if err != nil {
44		return fallback(r)
45	}
46
47	// Discard any pending input events.
48	if err := xwindows.FlushConsoleInputBuffer(conin); err != nil {
49		return fallback(r)
50	}
51
52	modes := []uint32{
53		windows.ENABLE_VIRTUAL_TERMINAL_INPUT,
54		windows.ENABLE_WINDOW_INPUT,
55		windows.ENABLE_EXTENDED_FLAGS,
56	}
57
58	originalMode, newMode, err := prepareConsole(conin, modes...)
59	if err != nil {
60		return nil, fmt.Errorf("failed to prepare console input: %w", err)
61	}
62
63	return &conInputReader{
64		conin:        conin,
65		originalMode: originalMode,
66		newMode:      newMode,
67	}, nil
68}
69
70// Cancel implements cancelreader.CancelReader.
71func (r *conInputReader) Cancel() bool {
72	r.setCanceled()
73
74	return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
75}
76
77// Close implements cancelreader.CancelReader.
78func (r *conInputReader) Close() error {
79	if r.originalMode != 0 {
80		err := windows.SetConsoleMode(r.conin, r.originalMode)
81		if err != nil {
82			return fmt.Errorf("reset console mode: %w", err)
83		}
84	}
85
86	return nil
87}
88
89// Read implements cancelreader.CancelReader.
90func (r *conInputReader) Read(data []byte) (int, error) {
91	if r.isCanceled() {
92		return 0, cancelreader.ErrCanceled
93	}
94
95	var n uint32
96	if err := windows.ReadFile(r.conin, data, &n, nil); err != nil {
97		return int(n), fmt.Errorf("read console input: %w", err)
98	}
99
100	return int(n), nil
101}
102
103func prepareConsole(input windows.Handle, modes ...uint32) (originalMode, newMode uint32, err error) {
104	err = windows.GetConsoleMode(input, &originalMode)
105	if err != nil {
106		return 0, 0, fmt.Errorf("get console mode: %w", err)
107	}
108
109	for _, mode := range modes {
110		newMode |= mode
111	}
112
113	err = windows.SetConsoleMode(input, newMode)
114	if err != nil {
115		return 0, 0, fmt.Errorf("set console mode: %w", err)
116	}
117
118	return originalMode, newMode, nil
119}
120
121// cancelMixin represents a goroutine-safe cancelation status.
122type cancelMixin struct {
123	unsafeCanceled bool
124	lock           sync.Mutex
125}
126
127func (c *cancelMixin) setCanceled() {
128	c.lock.Lock()
129	defer c.lock.Unlock()
130
131	c.unsafeCanceled = true
132}
133
134func (c *cancelMixin) isCanceled() bool {
135	c.lock.Lock()
136	defer c.lock.Unlock()
137
138	return c.unsafeCanceled
139}