From bda9c6f85fc4941e356fbc7761d1b92d9f8f72c1 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 10 Dec 2025 14:58:16 -0800 Subject: [PATCH 1/3] chore: refactor the platform tty/termio logic into separate packages The purpose here is to improve readability and maintainability. --- source/dcell/common.d | 37 +- source/dcell/termio.d | 843 +++++++++++++-------------------------- source/dcell/tty.d | 90 +++++ source/dcell/ttyscreen.d | 30 +- source/dcell/wintty.d | 211 ++++++++++ 5 files changed, 612 insertions(+), 599 deletions(-) create mode 100644 source/dcell/tty.d create mode 100644 source/dcell/wintty.d diff --git a/source/dcell/common.d b/source/dcell/common.d index fd2cbf0..a74c599 100644 --- a/source/dcell/common.d +++ b/source/dcell/common.d @@ -11,36 +11,13 @@ module dcell.common; import dcell.screen; +import dcell.ttyscreen; -version (Posix) -{ - import dcell.ttyscreen; - - /** - * Obtain a new screen. On POSIX this is connected to /dev/tty - * using the $TERM environment variable (or ansi if not set). - */ - Screen newScreen() - { - return newTtyScreen(); - } -} -else version (Windows) -{ - import dcell.ttyscreen; - - Screen newScreen() - { - return newTtyScreen(); - } -} -else +/** + * Obtain a new screen. On POSIX this is connected to /dev/tty + * using the $TERM environment variable. + */ +Screen newScreen() { - Screen newScreen() - { - import std.exception; - - throw new Exception("platform not known"); - return null; - } + return new TtyScreen(); } diff --git a/source/dcell/termio.d b/source/dcell/termio.d index 077cbe9..ae17d88 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -15,6 +15,7 @@ import std.datetime; import std.exception; import std.range.interfaces; import dcell.coord; +import dcell.tty; version (OSX) { @@ -32,648 +33,366 @@ else version (VisionOS) { version = UseSelect; } - -/** - * TtyImpl is the interface that implementations should - * override or supply to support terminal I/O ioctls or - * equivalent functionality. It is provided in this form, as - * some implementations may not be based on actual tty devices. - */ -interface TtyImpl +else { - /** - * Save current tty settings. These can be subsequently - * restored using restore. - */ - void save(); - - /** - * Restore tty settings saved with save(). - */ - void restore(); - - /** - * Make the terminal suitable for raw mode input. - * In this mode the terminal is not suitable for - * typical interactive shell use, but is good if absolute - * control over input is needed. After this, reads - * will block until one character is presented. (Same - * effect as 'blocking(true)'. - */ - void raw(); - - /** - * Read input. May return an empty slice if no data - * is present and blocking is disabled. - */ - string read(Duration dur = Duration.zero); - - /** - * Write output. - */ - void write(string s); - - /** - * Flush output. - */ - void flush(); - - /** - * Get window size. - */ - Coord windowSize(); - - /** - * Stop input scanning. - */ - void stop(); - - /** - * Close the tty device. - */ - void close(); - - /** - * Start termio. This will open the device. - */ - void start(); - - /** - * Resized returns true if the window was resized since last checked. - * Normally resize will force the window into non-blocking mode so - * that the caller can see the resize in a timely fashion. - * This is edge triggered (reading it will clear the value.) - */ - bool resized(); + version = UsePoll; } -version (Posix) +//dfmt off +version (Posix): +//dfmt on + +import core.sys.posix.sys.ioctl; +import core.sys.posix.termios; +import core.sys.posix.unistd; +import core.sys.posix.fcntl; +import std.process; +import std.stdio; + +package class PosixTty : Tty { - import core.sys.posix.sys.ioctl; - import core.sys.posix.termios; - import core.sys.posix.unistd; - import core.sys.posix.fcntl; - import std.process; - import std.stdio; + this(string dev) + { + path = dev; + } - package class PosixTty : TtyImpl + void start() { - this(string dev) + if (!file.isOpen) { - path = dev; + file = File(path, "r+b"); + fd = file.fileno(); } + save(); + watchResize(fd); + } - void start() + void stop() + { + if (file.isOpen()) { - if (!file.isOpen) - { - file = File(path, "r+b"); - fd = file.fileno(); - } - save(); - watchResize(fd); + ignoreResize(fd); + flush(); } + } - void stop() + void close() + { + if (file.isOpen) { - if (file.isOpen()) - { - ignoreResize(fd); - flush(); - } + stop(); + restore(); + file.close(); } + } - void close() - { - if (file.isOpen) - { - stop(); - restore(); - file.close(); - } - } + void save() + { + if (!isatty(fd)) + throw new Exception("not a tty device"); + enforce(tcgetattr(fd, &saved) >= 0, "failed to get termio state"); + } - void save() - { - if (!isatty(fd)) - throw new Exception("not a tty device"); - enforce(tcgetattr(fd, &saved) >= 0, "failed to get termio state"); - } + void restore() + { + enforce(tcsetattr(fd, TCSAFLUSH, &saved) >= 0, "failed to set termio state"); + } - void restore() - { - enforce(tcsetattr(fd, TCSAFLUSH, &saved) >= 0, "failed to set termio state"); - } + void flush() + { + file.flush(); + } - void flush() - { - file.flush(); - } + void raw() + { + termios tio; + enforce(tcgetattr(fd, &tio) >= 0, "failed to get termio state"); + tio.c_iflag &= ~(IGNBRK | BRKINT | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + tio.c_oflag &= ~OPOST; + tio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tio.c_cflag &= ~(CSIZE | PARENB); + tio.c_cflag |= CS8; + tio.c_cc[VMIN] = 1; // at least one character + tio.c_cc[VTIME] = 0; // but block forever + enforce(tcsetattr(fd, TCSANOW, &tio) >= 0, "failed to set termios"); + } - void raw() @trusted + Coord windowSize() + { + // If cores.sys.posix.sys.ioctl had more complete and accurate data... + // this structure is fairly consistent amongst all POSIX variants + struct winSz { - termios tio; - enforce(tcgetattr(fd, &tio) >= 0, "failed to get termio state"); - tio.c_iflag &= ~(IGNBRK | BRKINT | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - tio.c_oflag &= ~OPOST; - tio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); - tio.c_cflag &= ~(CSIZE | PARENB); - tio.c_cflag |= CS8; - tio.c_cc[VMIN] = 1; // at least one character - tio.c_cc[VTIME] = 0; // but block forever - enforce(tcsetattr(fd, TCSANOW, &tio) >= 0, "failed to set termios"); + ushort ws_row; + ushort ws_col; + ushort ws_xpix; + ushort ws_ypix; } - Coord windowSize() + version (linux) { - // If cores.sys.posix.sys.ioctl had more complete and accurate data... - // this structure is fairly consistent amongst all POSIX variants - struct winSz - { - ushort ws_row; - ushort ws_col; - ushort ws_xpix; - ushort ws_ypix; - } - - version (linux) - { - // has TIOCGWINSZ already -- but it might be wrong - // Linux has different values for TIOCGWINSZ depending - // on architecture - // SPARC, PPC, and MIPS use legacy BSD based values. - // Others use a newer // value. - version (SPARC64) - enum TIOCGWINSZ = 0x40087468; - else version (SPARC) - enum TIOCGWINSZ = 0x40087468; - else version (PPC) - enum TIOCGWINSZ = 0x40087468; - else version (PPC64) - enum TIOCGWINSZ = 0x40087468; - else version (MIPS32) - enum TIOCGWINSZ = 0x40087468; - else version (MIPS64) - enum TIOCGWINSZ = 0x40087468; - else - enum TIOCGWINSZ = 0x5413; // everything else - } - else version (Apple) + // has TIOCGWINSZ already -- but it might be wrong + // Linux has different values for TIOCGWINSZ depending + // on architecture + // SPARC, PPC, and MIPS use legacy BSD based values. + // Others use a newer // value. + version (SPARC64) enum TIOCGWINSZ = 0x40087468; - else version (Solaris) - enum TIOCGWINSZ = 0x5468; - else version (OpenBSD) + else version (SPARC) enum TIOCGWINSZ = 0x40087468; - else version (DragonFlyBSD) + else version (PPC) enum TIOCGWINSZ = 0x40087468; - else version (NetBSD) + else version (PPC64) enum TIOCGWINSZ = 0x40087468; - else version (FreeBSD) + else version (MIPS32) enum TIOCGWINSZ = 0x40087468; - else version (AIX) + else version (MIPS64) enum TIOCGWINSZ = 0x40087468; - - winSz wsz; - enforce(ioctl(fd, TIOCGWINSZ, &wsz) >= 0); - return Coord(wsz.ws_col, wsz.ws_row); - } - - version (UseSelect) - { - // On macOS, we have to use a select() based implementation because poll() - // does not work reasonably on /dev/tty. (This was very astonishing when first - // we discovered it -- POLLNVAL for device files.) - string read(Duration dur = Duration.zero) - { - // this has to use the underlying read system call - import unistd = core.sys.posix.unistd; - import core.sys.posix.sys.select; // Or similar module for select bindings - - fd_set readFds; - timeval timeout; - timeval* tvp; - - FD_ZERO(&readFds); - FD_SET(fd, &readFds); - FD_SET(sigRfd, &readFds); - - if (dur.isNegative || dur == Duration.max) - { - tvp = null; - } - else - { - auto usecs = dur.total!"usecs"; - - timeout.tv_sec = cast(typeof(timeout.tv_sec)) usecs / 1_000_000; - timeout.tv_usec = cast(typeof(timeout.tv_usec)) usecs % 1_000_000; - tvp = &timeout; - } - - import std.algorithm : max; - - int num = select(max(fd, sigRfd) + 1, &readFds, null, null, tvp); - - if (num < 1) - { - return ""; - } - - string result; - - if (FD_ISSET(fd, &readFds)) - { - ubyte[128] buf; - auto nread = unistd.read(fd, cast(void*) buf.ptr, buf.length); - if (nread > 0) - { - result = cast(string)(buf[0 .. nread]).dup; - } - } - if (FD_ISSET(sigRfd, &readFds)) - { - ubyte[1] buf; - // this can fail, we're just clearning the signaled state - unistd.read(sigRfd, buf.ptr, 1); - } - return result; - } - } - else - { - string read(Duration dur = Duration.zero) - { - // this has to use the underlying read system call - import unistd = core.sys.posix.unistd; - import core.sys.posix.poll; - import core.sys.posix.fcntl; - - pollfd[2] pfd; - pfd[0].fd = fd; - pfd[0].events = POLLRDNORM; - pfd[0].revents = 0; - - pfd[1].fd = sigRfd; - pfd[1].events = POLLRDNORM; - pfd[1].revents = 0; - - int dly; - if (dur.isNegative || dur == Duration.max) - { - dly = -1; - } - else - { - dly = cast(int)(dur.total!"msecs"); - } - - string result; - - long rv = poll(pfd.ptr, 2, dly); - if (rv < 1) - { - return result; - } - if (pfd[0].revents & POLLRDNORM) - { - ubyte[128] buf; - auto nread = unistd.read(fd, cast(void*) buf.ptr, buf.length); - if (nread > 0) - { - result = cast(string)(buf[0 .. nread]).dup; - } - } - if (pfd[1].revents & POLLRDNORM) - { - ubyte[1] buf; - // this can fail, and its fine (just clearing the signaled state) - unistd.read(sigRfd, buf.ptr, 1); - } - import std.format; - - return result; - } - } - - void write(string s) - { - file.write(s); - } - - bool resized() - { - // NB: resized is edge triggered. - return wasResized(fd); + else + enum TIOCGWINSZ = 0x5413; // everything else } - - private: - string path; - File file; - int fd; - termios saved; - bool block; + else version (Apple) + enum TIOCGWINSZ = 0x40087468; + else version (Solaris) + enum TIOCGWINSZ = 0x5468; + else version (OpenBSD) + enum TIOCGWINSZ = 0x40087468; + else version (DragonFlyBSD) + enum TIOCGWINSZ = 0x40087468; + else version (NetBSD) + enum TIOCGWINSZ = 0x40087468; + else version (FreeBSD) + enum TIOCGWINSZ = 0x40087468; + else version (AIX) + enum TIOCGWINSZ = 0x40087468; + + winSz wsz; + enforce(ioctl(fd, TIOCGWINSZ, &wsz) >= 0); + return Coord(wsz.ws_col, wsz.ws_row); } - TtyImpl newDevTty(string dev = "/dev/tty") + // On macOS, we have to use a select() based implementation because poll() + // does not work reasonably on /dev/tty. (This was very astonishing when first + // we discovered it -- POLLNVAL for device files.) + version (UseSelect) string read(Duration dur = Duration.zero) { - return new PosixTty(dev); - } + // this has to use the underlying read system call + import unistd = core.sys.posix.unistd; + import core.sys.posix.sys.select; // Or similar module for select bindings -} -else version (Windows) -{ - TtyImpl newDevTty() - { - return new WinTty(); - } -} -else -{ - TtyImpl newDevTty(string _ = "/dev/tty") - { - throw new Exception("not supported"); - } -} + fd_set readFds; + timeval timeout; + timeval* tvp; -version (Posix) -{ - import core.atomic; - import core.sys.posix.signal; + FD_ZERO(&readFds); + FD_SET(fd, &readFds); + FD_SET(sigRfd, &readFds); - private __gshared int sigRaised = 0; - private __gshared int sigFd = -1; - private __gshared Pipe sigPipe; - private __gshared int sigWfd; - private __gshared int sigRfd; + if (dur.isNegative || dur == Duration.max) + { + tvp = null; + } + else + { + auto usecs = dur.total!"usecs"; - private extern (C) void handleSigWinCh(int _) nothrow - { - atomicStore(sigRaised, 1); + timeout.tv_sec = cast(typeof(timeout.tv_sec)) usecs / 1_000_000; + timeout.tv_usec = cast(typeof(timeout.tv_usec)) usecs % 1_000_000; + tvp = &timeout; + } - // wake any reader so it can see the update - // this is crummy but its the best way to get this noticed. - ubyte[1] buf; - import unistd = core.sys.posix.unistd; + import std.algorithm : max; - // we do not care if this fails - unistd.write(sigWfd, buf.ptr, 1); - } + int num = select(max(fd, sigRfd) + 1, &readFds, null, null, tvp); - // We don't have a standard definition of SIGWINCH - version (linux) - { - // Legacy Linux is not even self-compatible ick. - version (MIPS_Any) - enum SIGWINCH = 20; - else - enum SIGWINCH = 28; - } - else version (Solaris) - enum SIGWINCH = 20; - else version (OSX) - enum SIGWINCH = 28; - else version (FreeBSD) - enum SIGWINCH = 28; - else version (NetBSD) - enum SIGWINCH = 28; - else version (DragonFlyBSD) - enum SIGWINCH = 28; - else version (OpenBSD) - enum SIGWINCH = 28; - else version (AIX) - enum SIGWINCH = 28; - else - static assert(0, "no version"); - - void watchResize(int fd) - { - import std.process; - import core.sys.posix.fcntl; - - if (atomicLoad(sigFd) == -1) + if (num < 1) { - // create the pipe for notifications if not already done so. - sigPipe = pipe(); - sigWfd = sigPipe.writeEnd.fileno(); - sigRfd = sigPipe.readEnd.fileno(); - fcntl(sigWfd, F_SETFL, O_NONBLOCK); - fcntl(sigRfd, F_SETFL, O_NONBLOCK); - - sigFd = fd; - sigaction_t sa; - sa.sa_handler = &handleSigWinCh; - sigaction(SIGWINCH, &sa, null); + return ""; } - } - void ignoreResize(int fd) - { - if (atomicLoad(sigFd) == fd) - { - sigaction_t sa; - sa.sa_handler = SIG_IGN; - sigaction(SIGWINCH, &sa, null); - sigFd = -1; - sigPipe.close(); - } - } + string result; - bool wasResized(int fd) - { - if (fd == atomicLoad(sigFd) && fd != -1) + if (FD_ISSET(fd, &readFds)) { - return atomicExchange(&sigRaised, 0) != 0; + ubyte[128] buf; + auto nread = unistd.read(fd, cast(void*) buf.ptr, buf.length); + if (nread > 0) + { + result = cast(string)(buf[0 .. nread]).dup; + } } - else + if (FD_ISSET(sigRfd, &readFds)) { - return false; + ubyte[1] buf; + // this can fail, we're just clearning the signaled state + unistd.read(sigRfd, buf.ptr, 1); } + return result; } -} - -version (Windows) -{ - import core.sys.windows.windows; - - // Kernel32.dll functions - extern (Windows) + version (UsePoll) string read(Duration dur = Duration.zero) { - 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); + // this has to use the underlying read system call + import unistd = core.sys.posix.unistd; + import core.sys.posix.poll; + import core.sys.posix.fcntl; - BOOL CloseHandle(HANDLE hObject); - } + pollfd[2] pfd; + pfd[0].fd = fd; + pfd[0].events = POLLRDNORM; + pfd[0].revents = 0; - // WindowsTty use ReadConsoleInput, as that is the only - // way to get window resize events. - package class WinTty : TtyImpl - { + pfd[1].fd = sigRfd; + pfd[1].events = POLLRDNORM; + pfd[1].revents = 0; - this() + int dly; + if (dur.isNegative || dur == Duration.max) { - input = GetStdHandle(STD_INPUT_HANDLE); - output = GetStdHandle(STD_OUTPUT_HANDLE); - eventH = CreateEventW(null, true, false, null); + dly = -1; } - - void save() + else { - - GetConsoleMode(output, &omode); - GetConsoleMode(input, &imode); + dly = cast(int)(dur.total!"msecs"); } - void restore() + string result; + + long rv = poll(pfd.ptr, 2, dly); + if (rv < 1) { - SetConsoleMode(output, omode); - SetConsoleMode(input, imode); + return result; } - - void start() + if (pfd[0].revents & POLLRDNORM) { - save(); - if (!started) + ubyte[128] buf; + auto nread = unistd.read(fd, cast(void*) buf.ptr, buf.length); + if (nread > 0) { - started = true; - FlushConsoleInputBuffer(input); + result = cast(string)(buf[0 .. nread]).dup; } } - - void stop() + if (pfd[1].revents & POLLRDNORM) { - SetEvent(eventH); + ubyte[1] buf; + // this can fail, and its fine (just clearing the signaled state) + unistd.read(sigRfd, buf.ptr, 1); } + import std.format; - void close() - { - CloseHandle(input); - CloseHandle(output); - CloseHandle(eventH); - } + return result; + } - void raw() - { - SetConsoleMode(input, ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS); - SetConsoleMode(output, - ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + void write(string s) + { + file.write(s); + } - } + bool resized() + { + // NB: resized is edge triggered. + return wasResized(fd); + } - void flush() - { - } +private: + string path; + File file; + int fd; + termios saved; + bool block; +} - /** - * 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; +import core.atomic; +import core.sys.posix.signal; - DWORD dly; - if (dur.isNegative || dur == Duration.max) - { - dly = INFINITE; - } - else - { - dly = cast(DWORD)(dur.total!"msecs"); - } +private: - auto rv = WaitForMultipleObjects(2, handles.ptr, false, dly); - string result = null; +__gshared int sigRaised = 0; +__gshared int sigFd = -1; +__gshared Pipe sigPipe; +__gshared int sigWfd; +__gshared int sigRfd; - // 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: - if (ev.KeyEvent.bKeyDown && ev.KeyEvent.AsciiChar != 0) - { - 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; - } - } +extern (C) void handleSigWinCh(int _) nothrow +{ + atomicStore(sigRaised, 1); - /** - * Write output. - */ - void write(string s) - { - import std.utf; + // wake any reader so it can see the update + // this is crummy but its the best way to get this noticed. + ubyte[1] buf; + import unistd = core.sys.posix.unistd; - wstring w = toUTF16(s); + // we do not care if this fails + unistd.write(sigWfd, buf.ptr, 1); +} - WriteConsoleW(output, w.ptr, cast(uint) w.length, null, null); - } +// We don't have a standard definition of SIGWINCH +version (linux) +{ + // Legacy Linux is not even self-compatible ick. + version (MIPS_Any) + enum SIGWINCH = 20; + else + enum SIGWINCH = 28; +} +else version (Solaris) + enum SIGWINCH = 20; +else version (OSX) + enum SIGWINCH = 28; +else version (FreeBSD) + enum SIGWINCH = 28; +else version (NetBSD) + enum SIGWINCH = 28; +else version (DragonFlyBSD) + enum SIGWINCH = 28; +else version (OpenBSD) + enum SIGWINCH = 28; +else version (AIX) + enum SIGWINCH = 28; +else + static assert(0, "no version"); - Coord windowSize() - { - CONSOLE_SCREEN_BUFFER_INFO info; - GetConsoleScreenBufferInfo(output, &info); - return Coord(info.srWindow.Right - info.srWindow.Left + 1, - info.srWindow.Bottom - info.srWindow.Top + 1); - } +void watchResize(int fd) +{ + import std.process; + import core.sys.posix.fcntl; - bool resized() - { - bool result = wasResized; - wasResized = false; - return result; - } + if (atomicLoad(sigFd) == -1) + { + // create the pipe for notifications if not already done so. + sigPipe = pipe(); + sigWfd = sigPipe.writeEnd.fileno(); + sigRfd = sigPipe.readEnd.fileno(); + fcntl(sigWfd, F_SETFL, O_NONBLOCK); + fcntl(sigRfd, F_SETFL, O_NONBLOCK); + + sigFd = fd; + sigaction_t sa; + sa.sa_handler = &handleSigWinCh; + sigaction(SIGWINCH, &sa, null); + } +} + +void ignoreResize(int fd) +{ + if (atomicLoad(sigFd) == fd) + { + sigaction_t sa; + sa.sa_handler = SIG_IGN; + sigaction(SIGWINCH, &sa, null); + sigFd = -1; + sigPipe.close(); + } +} - private: - HANDLE output; - HANDLE input; - HANDLE eventH; - DWORD omode; - DWORD imode; - bool started; - bool wasResized; +bool wasResized(int fd) +{ + if (fd == atomicLoad(sigFd) && fd != -1) + { + return atomicExchange(&sigRaised, 0) != 0; + } + else + { + return false; } } diff --git a/source/dcell/tty.d b/source/dcell/tty.d new file mode 100644 index 0000000..e02924b --- /dev/null +++ b/source/dcell/tty.d @@ -0,0 +1,90 @@ +/** + * Tty module for dcell defines the interface that platforms must implement + * for exchanging data for display, and key strokes and other events, between + * the specific terminal / pty subsystem, and the VT common core. + * + * Copyright: Copyright 2025 Garrett D'Amore + * Authors: Garrett D'Amore + * License: + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt) + * SPDX-License-Identifier: BSL-1.0 + */ +module dcell.tty; + +import std.datetime; +import dcell.coord; + +/** + * Tty is the interface that implementations should + * override or supply to support terminal I/O ioctls or + * equivalent functionality. It is provided in this form, as + * some implementations may not be based on actual tty devices. + */ +interface Tty +{ + /** + * Save current tty settings. These can be subsequently + * restored using restore. + */ + void save(); + + /** + * Restore tty settings saved with save(). + */ + void restore(); + + /** + * Make the terminal suitable for raw mode input. + * In this mode the terminal is not suitable for + * typical interactive shell use, but is good if absolute + * control over input is needed. After this, reads + * will block until one character is presented. (Same + * effect as 'blocking(true)'. + */ + void raw(); + + /** + * Read input. May return an empty slice if no data + * is present and blocking is disabled. + */ + string read(Duration dur = Duration.zero); + + /** + * Write output. + */ + void write(string s); + + /** + * Flush output. + */ + void flush(); + + /** + * Get window size. + */ + Coord windowSize(); + + /** + * Stop input scanning. + */ + void stop(); + + /** + * Close the tty device. + */ + void close(); + + /** + * Start termio. This will open the device. + */ + void start(); + + /** + * Resized returns true if the window was resized since last checked. + * Normally resize will force the window into non-blocking mode so + * that the caller can see the resize in a timely fashion. + * This is edge triggered (reading it will clear the value.) + */ + bool resized(); +} diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index 2d66ada..6b7bbae 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -32,6 +32,7 @@ import dcell.termio; import dcell.screen; import dcell.event; import dcell.parser; +import dcell.tty; class TtyScreen : Screen { @@ -123,7 +124,27 @@ class TtyScreen : Screen // requestWindowSize = "\x1b[18t" // For modern terminals } - this(TtyImpl tt, string term = "") + this() + { + version (Posix) + { + import dcell.termio : PosixTty; + + this(new PosixTty("/dev/tty"), ""); + } + else version (Windows) + { + import dcell.wintty : WinTty; + + this(new WinTty()); + } + else + { + throw new Exception("no default TTY for platform"); + } + } + + this(Tty tt, string term = "") { ti = tt; ti.start(); @@ -455,7 +476,7 @@ private: Cursor cursorShape; MouseEnable mouseEn; // saved state for suspend/resume bool pasteEn; // saved state for suspend/resume - TtyImpl ti; + Tty ti; OutBuffer ob; bool started; bool legacy; // legacy terminals don't have support for OSC, APC, DSC, etc. @@ -755,8 +776,3 @@ private: flush(); } } - -Screen newTtyScreen() -{ - return new TtyScreen(newDevTty()); -} diff --git a/source/dcell/wintty.d b/source/dcell/wintty.d new file mode 100644 index 0000000..0e07071 --- /dev/null +++ b/source/dcell/wintty.d @@ -0,0 +1,211 @@ +/** + * Windows TTY support for dcell. + * + * Copyright: Copyright 2025 Garrett D'Amore + * Authors: Garrett D'Amore + * License: + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt) + * SPDX-License-Identifier: BSL-1.0 + */ +module dcell.wintty; + +// dfmt off +version (Windows): +// dfmt on + +import core.sys.windows.windows; +import std.datetime; +import std.exception; +import std.range.interfaces; +import dcell.coord; +import dcell.tty; + +// Kernel32.dll functions +extern (Windows) @nogc nothrow +{ + 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); +} + +@nogc: +nothrow: + +// WindowsTty use ReadConsoleInput, as that is the only +// way to get window resize events. +class WinTty : Tty +{ + + 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_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + } + + void flush() + { + } + + 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: + if (ev.KeyEvent.bKeyDown && ev.KeyEvent.AsciiChar != 0) + { + 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) @nogc nothrow + { + import std.utf; + + wchar[128] buf; + uint l = 0; + foreach (wc; s.byWchar) + { + buf[l++] = wc; + if (l == buf.length) + { + WriteConsoleW(output, buf.ptr, l, null, null); + l = 0; + } + } + if (l != 0) + { + WriteConsoleW(output, buf.ptr, l, null, null); + } + } + + Coord windowSize() + { + CONSOLE_SCREEN_BUFFER_INFO info; + GetConsoleScreenBufferInfo(output, &info); + return Coord(info.srWindow.Right - info.srWindow.Left + 1, + info.srWindow.Bottom - info.srWindow.Top + 1); + } + + bool resized() + { + bool result = wasResized; + wasResized = false; + return result; + } + +private: + HANDLE output; + HANDLE input; + HANDLE eventH; + DWORD omode; + DWORD imode; + bool started; + bool wasResized; +} From e4efbc32c2b2d2c634f71b17f10897574b1b77ab Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 10 Dec 2025 15:04:07 -0800 Subject: [PATCH 2/3] doc: clarifying comments and increased doc comments. Also this makes the PosixTty and WinTty public, which can allow others to make use of them connected to different files, etc. --- source/dcell/termio.d | 7 ++++++- source/dcell/ttyscreen.d | 6 +++++- source/dcell/wintty.d | 7 +++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/source/dcell/termio.d b/source/dcell/termio.d index ae17d88..33d4489 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -49,8 +49,13 @@ import core.sys.posix.fcntl; import std.process; import std.stdio; -package class PosixTty : Tty +/** + * PosixTty implements the TTY interface for POSIX systems, using a normal + * file descriptor and the termio facility found on such systems. + */ +class PosixTty : Tty { + /// Create a Tty device on a given device path. The usual path is "/dev/tty". this(string dev) { path = dev; diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index 6b7bbae..6c22db8 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -1,5 +1,9 @@ /** - * TtyScreen module implements POSIX style terminals (ala XTerm). + * TtyScreen module implements VT style terminals (ala XTerm). + * These are terminals that work by sending escape sequences over + * a single byte stream. Historically this would be a serial port, + * but modern systems likely use SSH, or a pty (pseudo-terminal). + * Modern Windows has adopted this form of API as well. * * Copyright: Copyright 2025 Garrett D'Amore * Authors: Garrett D'Amore diff --git a/source/dcell/wintty.d b/source/dcell/wintty.d index 0e07071..bf32ea2 100644 --- a/source/dcell/wintty.d +++ b/source/dcell/wintty.d @@ -50,8 +50,11 @@ extern (Windows) @nogc nothrow @nogc: nothrow: -// WindowsTty use ReadConsoleInput, as that is the only -// way to get window resize events. +/** + * WinTty impleements the Tty using the VT input mode and the Win32 ReadConsoleInput and WriteConsole APIs. + * We use this instead of ReadFile/WriteFile in order to obtain resize events, and access to the screen size. + * The terminal is expected to be connected the the process' STD_INPUT_HANDLE and STD_OUTPUT_HANDLE. + */ class WinTty : Tty { From f8fee367bec2dfd0ec8053caca43cd602c72a254 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 10 Dec 2025 16:05:39 -0800 Subject: [PATCH 3/3] fix: windows - do not close standard input or output --- source/dcell/wintty.d | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/dcell/wintty.d b/source/dcell/wintty.d index bf32ea2..353de29 100644 --- a/source/dcell/wintty.d +++ b/source/dcell/wintty.d @@ -95,8 +95,7 @@ class WinTty : Tty void close() { - CloseHandle(input); - CloseHandle(output); + // NB: We do not close the standard input and output handles. CloseHandle(eventH); }