1package terminal
2
3import (
4	"bytes"
5	"syscall"
6	"unsafe"
7)
8
9var (
10	dll              = syscall.NewLazyDLL("kernel32.dll")
11	setConsoleMode   = dll.NewProc("SetConsoleMode")
12	getConsoleMode   = dll.NewProc("GetConsoleMode")
13	readConsoleInput = dll.NewProc("ReadConsoleInputW")
14)
15
16const (
17	EVENT_KEY = 0x0001
18
19	// key codes for arrow keys
20	// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
21	VK_DELETE = 0x2E
22	VK_END    = 0x23
23	VK_HOME   = 0x24
24	VK_LEFT   = 0x25
25	VK_UP     = 0x26
26	VK_RIGHT  = 0x27
27	VK_DOWN   = 0x28
28
29	RIGHT_CTRL_PRESSED = 0x0004
30	LEFT_CTRL_PRESSED  = 0x0008
31
32	ENABLE_ECHO_INPUT      uint32 = 0x0004
33	ENABLE_LINE_INPUT      uint32 = 0x0002
34	ENABLE_PROCESSED_INPUT uint32 = 0x0001
35)
36
37type inputRecord struct {
38	eventType uint16
39	padding   uint16
40	event     [16]byte
41}
42
43type keyEventRecord struct {
44	bKeyDown          int32
45	wRepeatCount      uint16
46	wVirtualKeyCode   uint16
47	wVirtualScanCode  uint16
48	unicodeChar       uint16
49	wdControlKeyState uint32
50}
51
52type runeReaderState struct {
53	term uint32
54}
55
56func newRuneReaderState(input FileReader) runeReaderState {
57	return runeReaderState{}
58}
59
60func (rr *RuneReader) Buffer() *bytes.Buffer {
61	return nil
62}
63
64func (rr *RuneReader) SetTermMode() error {
65	r, _, err := getConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(unsafe.Pointer(&rr.state.term)))
66	// windows return 0 on error
67	if r == 0 {
68		return err
69	}
70
71	newState := rr.state.term
72	newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
73	r, _, err = setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState))
74	// windows return 0 on error
75	if r == 0 {
76		return err
77	}
78	return nil
79}
80
81func (rr *RuneReader) RestoreTermMode() error {
82	r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(rr.state.term))
83	// windows return 0 on error
84	if r == 0 {
85		return err
86	}
87	return nil
88}
89
90func (rr *RuneReader) ReadRune() (rune, int, error) {
91	ir := &inputRecord{}
92	bytesRead := 0
93	for {
94		rv, _, e := readConsoleInput.Call(rr.stdio.In.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead)))
95		// windows returns non-zero to indicate success
96		if rv == 0 && e != nil {
97			return 0, 0, e
98		}
99
100		if ir.eventType != EVENT_KEY {
101			continue
102		}
103
104		// the event data is really a c struct union, so here we have to do an usafe
105		// cast to put the data into the keyEventRecord (since we have already verified
106		// above that this event does correspond to a key event
107		key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0]))
108		// we only care about key down events
109		if key.bKeyDown == 0 {
110			continue
111		}
112		if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' {
113			return KeyInterrupt, bytesRead, nil
114		}
115		// not a normal character so look up the input sequence from the
116		// virtual key code mappings (VK_*)
117		if key.unicodeChar == 0 {
118			switch key.wVirtualKeyCode {
119			case VK_DOWN:
120				return KeyArrowDown, bytesRead, nil
121			case VK_LEFT:
122				return KeyArrowLeft, bytesRead, nil
123			case VK_RIGHT:
124				return KeyArrowRight, bytesRead, nil
125			case VK_UP:
126				return KeyArrowUp, bytesRead, nil
127			case VK_DELETE:
128				return SpecialKeyDelete, bytesRead, nil
129			case VK_HOME:
130				return SpecialKeyHome, bytesRead, nil
131			case VK_END:
132				return SpecialKeyEnd, bytesRead, nil
133			default:
134				// not a virtual key that we care about so just continue on to
135				// the next input key
136				continue
137			}
138		}
139		r := rune(key.unicodeChar)
140		return r, bytesRead, nil
141	}
142}
143