1 /**
2  * Windows TTY support for dcell.
3  *
4  * Copyright: Copyright 2025 Garrett D'Amore
5  * Authors: Garrett D'Amore
6  * License:
7  *   Distributed under the Boost Software License, Version 1.0.
8  *   (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt)
9  *   SPDX-License-Identifier: BSL-1.0
10  */
11 module dcell.wintty;
12 
13 // dfmt off
14 version (Windows):
15 // dfmt on
16 
17 import core.sys.windows.windows;
18 import std.datetime;
19 import std.exception;
20 import std.range.interfaces;
21 import dcell.coord;
22 import dcell.tty;
23 
24 // Kernel32.dll functions
25 extern (Windows) @nogc nothrow
26 {
27     BOOL ReadConsoleInputW(HANDLE hConsoleInput, INPUT_RECORD* lpBuffer, DWORD nLength, DWORD* lpNumEventsRead);
28 
29     BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, DWORD* lpcNumberOfEvents);
30 
31     BOOL FlushConsoleInputBuffer(HANDLE hConsoleInput);
32 
33     DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
34 
35     BOOL SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode);
36 
37     BOOL GetConsoleMode(HANDLE hConsoleHandle, DWORD* lpMode);
38 
39     BOOL GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO* lpConsoleScreenBufferInfo);
40 
41     HANDLE CreateEventW(SECURITY_ATTRIBUTES* secAttr, BOOL bManualReset, BOOL bInitialState, LPCWSTR lpName);
42 
43     BOOL SetEvent(HANDLE hEvent);
44 
45     BOOL WriteConsoleW(HANDLE hFile, LPCVOID buf, DWORD nNumBytesToWrite, LPDWORD lpNumBytesWritten, LPVOID rsvd);
46 
47     BOOL CloseHandle(HANDLE hObject);
48 }
49 
50 /**
51  * WinTty impleements the Tty using the VT input mode and the Win32 ReadConsoleInput and WriteConsole APIs.
52  * We use this instead of ReadFile/WriteFile in order to obtain resize events, and access to the screen size.
53  * The terminal is expected to be connected the the process' STD_INPUT_HANDLE and STD_OUTPUT_HANDLE.
54  */
55 class WinTty : Tty
56 {
57     /**
58      * Default constructor.
59      * This expects the terminal to be connected to STD_INPUT_HANDLE and STD_OUTPUT_HANDLE.
60      */
61     this() @trusted
62     {
63         input = GetStdHandle(STD_INPUT_HANDLE);
64         output = GetStdHandle(STD_OUTPUT_HANDLE);
65         eventH = CreateEventW(null, true, false, null);
66     }
67 
68     void save() @trusted
69     {
70 
71         GetConsoleMode(output, &omode);
72         GetConsoleMode(input, &imode);
73     }
74 
75     void restore() @trusted
76     {
77         SetConsoleMode(output, omode);
78         SetConsoleMode(input, imode);
79     }
80 
81     void start() @trusted
82     {
83         save();
84         if (!started)
85         {
86             started = true;
87             FlushConsoleInputBuffer(input);
88         }
89     }
90 
91     void stop() @trusted
92     {
93         SetEvent(eventH);
94     }
95 
96     void close() @trusted
97     {
98         // NB: We do not close the standard input and output handles.
99         CloseHandle(eventH);
100     }
101 
102     void raw() @trusted
103     {
104         SetConsoleMode(input, ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS);
105         SetConsoleMode(output,
106             ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
107     }
108 
109     void flush() @safe
110     {
111     }
112 
113     string read(Duration dur = Duration.zero) @trusted
114     {
115         HANDLE[2] handles;
116         handles[0] = input;
117         handles[1] = eventH;
118 
119         DWORD dly;
120         if (dur.isNegative || dur == Duration.max)
121         {
122             dly = INFINITE;
123         }
124         else
125         {
126             dly = cast(DWORD)(dur.total!"msecs");
127         }
128 
129         auto rv = WaitForMultipleObjects(2, handles.ptr, false, dly);
130         string result = null;
131 
132         // WaitForMultipleObjects returns WAIT_OBJECT_0 + the index.
133         switch (rv)
134         {
135         case WAIT_OBJECT_0 + 1: // w.cancelFlag
136             return result;
137         case WAIT_OBJECT_0: // input
138             INPUT_RECORD[128] recs;
139             DWORD nrec;
140             ReadConsoleInput(input, recs.ptr, 128, &nrec);
141 
142             foreach (ev; recs[0 .. nrec])
143             {
144                 switch (ev.EventType)
145                 {
146                 case KEY_EVENT:
147                     if (ev.KeyEvent.bKeyDown && ev.KeyEvent.AsciiChar != 0)
148                     {
149                         auto chr = ev.KeyEvent.AsciiChar;
150                         result ~= chr;
151                     }
152                     break;
153                 case WINDOW_BUFFER_SIZE_EVENT:
154                     wasResized = true;
155                     break;
156                 default: // we could process focus, etc. here, but we already
157                     // get them inline via VT sequences
158                     break;
159                 }
160             }
161 
162             return result;
163         default:
164             return result;
165         }
166     }
167 
168     // Write output
169     void write(string s) @trusted
170     {
171         import std.utf;
172 
173         wchar[128] buf;
174         uint l = 0;
175         foreach (wc; s.byWchar)
176         {
177             buf[l++] = wc;
178             if (l == buf.length)
179             {
180                 WriteConsoleW(output, buf.ptr, l, null, null);
181                 l = 0;
182             }
183         }
184         if (l != 0)
185         {
186             WriteConsoleW(output, buf.ptr, l, null, null);
187         }
188     }
189 
190     Coord windowSize() @trusted
191     {
192         CONSOLE_SCREEN_BUFFER_INFO info;
193         GetConsoleScreenBufferInfo(output, &info);
194         return Coord(info.srWindow.Right - info.srWindow.Left + 1,
195             info.srWindow.Bottom - info.srWindow.Top + 1);
196     }
197 
198     bool resized() @safe
199     {
200         bool result = wasResized;
201         wasResized = false;
202         return result;
203     }
204 
205     void wakeUp() @trusted
206     {
207         SetEvent(eventH);
208     }
209 
210 private:
211     HANDLE output;
212     HANDLE input;
213     HANDLE eventH;
214     DWORD omode;
215     DWORD imode;
216     bool started;
217     bool wasResized;
218 }