From 7fcb15c95eaadcd5f544cfaeb96458ea709a47d2 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Tue, 9 Dec 2025 23:23:01 -0800 Subject: [PATCH 1/3] feat: Add support for Windows (fixes #14) --- source/dcell/common.d | 7 +- source/dcell/termio.d | 194 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/source/dcell/common.d b/source/dcell/common.d index 161c071..fd2cbf0 100644 --- a/source/dcell/common.d +++ b/source/dcell/common.d @@ -27,12 +27,11 @@ version (Posix) } else version (Windows) { + import dcell.ttyscreen; + Screen newScreen() { - import std.exception; - - throw new Exception("windows not supported yet"); - return null; + return newTtyScreen(); } } else diff --git a/source/dcell/termio.d b/source/dcell/termio.d index ebcad4f..47bf938 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -399,6 +399,13 @@ version (Posix) } } +else version (Windows) +{ + TtyImpl newDevTty() + { + return new WinTty(); + } +} else { TtyImpl newDevTty(string _ = "/dev/tty") @@ -502,3 +509,190 @@ version (Posix) } } } + +version (Windows) +{ + + import core.sys.windows.windows; + + // Kernel32.dll functions + extern (Windows) + { + BOOL ReadConsoleInputW(HANDLE hConsoleInput, INPUT_RECORD* lpBuffer, DWORD nLength, DWORD* lpNumEventsRead); + + BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, DWORD* lpcNumberOfEvents); + + BOOL FlushConsoleInputBuffer(HANDLE hConsoleInput); + + DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds); + + BOOL SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode); + + BOOL GetConsoleMode(HANDLE hConsoleHandle, DWORD* lpMode); + + BOOL GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO* lpConsoleScreenBufferInfo); + + HANDLE CreateEventW(SECURITY_ATTRIBUTES* secAttr, BOOL bManualReset, BOOL bInitialState, LPCWSTR lpName); + + BOOL SetEvent(HANDLE hEvent); + + BOOL WriteConsoleW(HANDLE hFile, LPCVOID buf, DWORD nNumBytesToWrite, LPDWORD lpNumBytesWritten, LPVOID rsvd); + + BOOL CloseHandle(HANDLE hObject); + } + + // WindowsTty use ReadConsoleInput, as that is the only + // way to get window resize events. + package class WinTty : TtyImpl + { + + this() + { + input = GetStdHandle(STD_INPUT_HANDLE); + output = GetStdHandle(STD_OUTPUT_HANDLE); + eventH = CreateEventW(null, true, false, null); + } + + void save() + { + + GetConsoleMode(output, &omode); + GetConsoleMode(input, &imode); + } + + void restore() + { + SetConsoleMode(output, omode); + SetConsoleMode(input, imode); + } + + void start() + { + save(); + if (!started) + { + started = true; + FlushConsoleInputBuffer(input); + } + } + + void stop() + { + SetEvent(eventH); + } + + void close() + { + CloseHandle(input); + CloseHandle(output); + CloseHandle(eventH); + } + + void raw() + { + SetConsoleMode(input, ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS); + SetConsoleMode(output, // ENABLE_LVB_GRID_WORLDWIDE | + ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + + } + + void flush() + { + } + + void blocking(bool b) + { + } + + /** + * Read input. May return an empty slice if no data + * is present and blocking is disabled. + */ + string read(Duration dur = Duration.zero) + { + HANDLE[2] handles; + handles[0] = input; + handles[1] = eventH; + + DWORD dly; + if (dur.isNegative || dur == Duration.max) + { + dly = INFINITE; + } + else + { + dly = cast(DWORD)(dur.total!"msecs"); + } + + auto rv = WaitForMultipleObjects(2, handles.ptr, false, dly); + string result = null; + + // WaitForMultipleObjects returns WAIT_OBJECT_0 + the index. + switch (rv) + { + case WAIT_OBJECT_0 + 1: // w.cancelFlag + return result; + case WAIT_OBJECT_0: // input + INPUT_RECORD[128] recs; + DWORD nrec; + ReadConsoleInput(input, recs.ptr, 128, &nrec); + + foreach (ev; recs[0 .. nrec]) + { + switch (ev.EventType) + { + case KEY_EVENT: + auto chr = ev.KeyEvent.AsciiChar; + result ~= chr; + break; + case WINDOW_BUFFER_SIZE_EVENT: + wasResized = true; + break; + default: // we could process focus, etc. here, but we already + // get them inline via VT sequences + break; + } + } + + return result; + default: + return result; + } + } + + /** + * Write output. + */ + void write(string s) + { + import std.utf; + + wstring w = toUTF16(s); + + WriteConsoleW(output, w.ptr, cast(uint) w.length, null, null); + } + + Coord windowSize() + { + GetConsoleScreenBufferInfo(output, &oscreen); + return Coord(oscreen.dwSize.X, oscreen.dwSize.Y); + } + + bool resized() + { + bool result = wasResized; + wasResized = false; + return result; + } + + private: + HANDLE output; + HANDLE input; + HANDLE eventH; + DWORD omode; + DWORD imode; + CONSOLE_SCREEN_BUFFER_INFO oscreen; + bool started; + bool wasResized; + } +} From f78c872cfee055e64c70c7c6a95353c29975b78f Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 10 Dec 2025 07:38:24 -0800 Subject: [PATCH 2/3] chore!: remove unused blocking API from TTY. --- source/dcell/termio.d | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/source/dcell/termio.d b/source/dcell/termio.d index 47bf938..94b1b41 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -62,14 +62,6 @@ interface TtyImpl */ void raw(); - /** - * Make input blocking or non-blocking. Blocking input - * will cause reads against the terminal to block forever - * until at least one character is returned. Otherwise it - * will return in at most - */ - void blocking(bool b); - /** * Read input. May return an empty slice if no data * is present and blocking is disabled. @@ -178,17 +170,6 @@ version (Posix) file.flush(); } - void blocking(bool b) @trusted - { - termios tio; - enforce(tcgetattr(fd, &tio) >= 0); - tio.c_cc[VMIN] = b ? 1 : 0; - tio.c_cc[VTIME] = 0; - - enforce(tcsetattr(fd, TCSANOW, &tio) >= 0); - block = b; - } - void raw() @trusted { termios tio; @@ -591,7 +572,7 @@ version (Windows) void raw() { SetConsoleMode(input, ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS); - SetConsoleMode(output, // ENABLE_LVB_GRID_WORLDWIDE | + SetConsoleMode(output, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); } @@ -600,10 +581,6 @@ version (Windows) { } - void blocking(bool b) - { - } - /** * Read input. May return an empty slice if no data * is present and blocking is disabled. From 192496c9dea8039b9eee7112432ceb28bd5e0e7f Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 10 Dec 2025 07:49:41 -0800 Subject: [PATCH 3/3] fix: fix control key reporting (needed especially for win32-input-mode) --- source/dcell/parser.d | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/source/dcell/parser.d b/source/dcell/parser.d index fddd88c..96bdc6e 100644 --- a/source/dcell/parser.d +++ b/source/dcell/parser.d @@ -1014,7 +1014,6 @@ private: else if (chr < ' ' && p0 >= 0x41 && p0 <= 0x5a) { key = cast(Key) p0; - chr = 0; } else if (key == 0x11 || key == 0x13 || key == 0x14) { @@ -1110,6 +1109,36 @@ private: mod |= Modifiers.alt; escaped = false; } + if (dch < ' ' && k < Key.rune) + { + switch (cast(int) k) + { + case 0xd, 0xa: + k = Key.enter; + break; + case 0x9: + k = Key.tab; + break; + case 0x8: + k = Key.backspace; + break; + case 0x1b: + k = Key.esc; + break; + case 0: // control-space + k = Key.rune; + mod |= Modifiers.ctrl; + dch = ' '; + break; + default: + // most likely entered with a CTRL keypress + k = Key.rune; + mod |= Modifiers.ctrl; + dch = dch + '\x60'; + break; + } + } + Event ev = { type: EventType.key, when: MonoTime.currTime(), key: { key: k, ch: dch, mod: mod