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 }