diff --git a/demos/colors/source/colors.d b/demos/colors/source/colors.d index c52c9b0..615e19a 100644 --- a/demos/colors/source/colors.d +++ b/demos/colors/source/colors.d @@ -12,7 +12,6 @@ module color; import std.stdio; import std.string; -import std.concurrency; import std.random; import std.range; import dcell; @@ -137,7 +136,7 @@ void main() while (!done) { cb.makeBox(s); - auto ev = s.receiveEvent(msecs(50)); + auto ev = s.waitEvent(msecs(50)); switch (ev.type) { case EventType.key: diff --git a/demos/hello/source/hello.d b/demos/hello/source/hello.d index db43e2b..d4e3424 100644 --- a/demos/hello/source/hello.d +++ b/demos/hello/source/hello.d @@ -1,7 +1,7 @@ /** * Hello world demo for Dcell. * - * Copyright: Copyright 2022 Garrett D'Amore + * Copyright: Copyright 2025 Garrett D'Amore * Authors: Garrett D'Amore * License: * Distributed under the Boost Software License, Version 1.0. @@ -12,77 +12,75 @@ module hello; import std.stdio; import std.string; -import std.concurrency; import dcell; void emitStr(Screen s, int x, int y, Style style, string str) { - // NB: this naively assumes only ASCII - while (str != "") - { - s[x, y] = Cell(str[0], style); - str = str[1..$]; - x += 1; - } + // NB: this naively assumes only ASCII + while (str != "") + { + s[x, y] = Cell(str[0], style); + str = str[1 .. $]; + x += 1; + } } void displayHelloWorld(Screen s) { - auto size = s.size(); - Style def; - def.bg = Color.silver; - def.fg = Color.black; - s.setStyle(def); - s.clear(); - Style style = {fg: Color.red, bg: Color.papayaWhip}; - emitStr(s, size.x / 2 - 9, size.y / 2 - 1, style, " Hello, World! "); - emitStr(s, size.x / 2 - 11, size.y / 2 + 1, def, " Press ESC to exit. "); + auto size = s.size(); + Style def; + def.bg = Color.silver; + def.fg = Color.black; + s.setStyle(def); + s.clear(); + Style style = {fg: Color.red, bg: Color.papayaWhip}; + emitStr(s, size.x / 2 - 9, size.y / 2 - 1, style, " Hello, World! "); + emitStr(s, size.x / 2 - 11, size.y / 2 + 1, def, " Press ESC to exit. "); - // this demonstrates a different method. - // it places a red X in the center of the screen. - s[$/2, $/2].text = "X"; - s[$/2, $/2].style.fg = Color.white; - s[$/2, $/2].style.bg = Color.red; - s.show(); + // this demonstrates a different method. + // it places a red X in the center of the screen. + s[$ / 2, $ / 2].text = "X"; + s[$ / 2, $ / 2].style.fg = Color.white; + s[$ / 2, $ / 2].style.bg = Color.red; + s.show(); } void handleEvent(Screen ts, Event ev) { - import core.stdc.stdlib : exit; + import core.stdc.stdlib : exit; - switch (ev.type) - { - case EventType.key: - if (ev.key.key == Key.esc || ev.key.key == Key.f1) - { - ts.stop(); - exit(0); - } - break; - case EventType.resize: - ts.resize(); - displayHelloWorld(ts); - ts.sync(); - break; - default: - break; - } + switch (ev.type) + { + case EventType.key: + if (ev.key.key == Key.esc || ev.key.key == Key.f1) + { + ts.stop(); + exit(0); + } + break; + case EventType.resize: + ts.resize(); + displayHelloWorld(ts); + ts.sync(); + break; + default: + break; + } } void main() { - import std.stdio; + import std.stdio; - auto ts = newScreen(); - assert(ts !is null); + auto ts = newScreen(); + assert(ts !is null); - ts.start(thisTid()); - displayHelloWorld(ts); - for (;;) - { - receive( - (Event ev) { handleEvent(ts, ev); } - ); - } + ts.start(); + displayHelloWorld(ts); + for (;;) + { + Event ev = ts.waitEvent(); + handleEvent(ts, ev); + } } diff --git a/demos/mouse/source/mouse.d b/demos/mouse/source/mouse.d index 1b1ef79..2fe6a3b 100644 --- a/demos/mouse/source/mouse.d +++ b/demos/mouse/source/mouse.d @@ -12,7 +12,6 @@ module mouse; import std.stdio; import std.string; -import std.concurrency; import core.stdc.stdlib; import dcell; @@ -105,7 +104,7 @@ void main() dstring kStr = ""; dstring pStr = ""; - s.start(thisTid()); + s.start(); s.showCursor(Cursor.hidden); s.enableMouse(MouseEnable.all); s.enablePaste(true); @@ -140,10 +139,7 @@ void main() emitStr(s, pos, white, format(pasteFmt, pStr.length, ps)); s.show(); bStr = ""; - Event ev; - receive( - (Event ee) { ev = ee; } - ); + Event ev = s.waitEvent(); Style st; st.bg = Color.red; Style up; diff --git a/source/dcell/evqueue.d b/source/dcell/evqueue.d deleted file mode 100644 index 0e48870..0000000 --- a/source/dcell/evqueue.d +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Event queue module for dcell. - * - * Copyright: Copyright 2022 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.evqueue; - -import core.sync.condition; -import core.sync.mutex; -import core.time; -import std.concurrency; - -import dcell.event; - -package class EventQueue -{ - this() - { - mx = new Mutex(); - cv = new Condition(mx); - } - - Event receive(this Q)(Duration dur) - if (is(Q == EventQueue) || is(Q == shared EventQueue)) - { - Event ev; - synchronized (mx) - { - while ((q.length == 0) && !closed) - { - if (!cv.wait(dur)) - { - return (ev); - } - } - - if (closed) - { - return Event(EventType.closed); - } - ev = q[0]; - q = q[1 .. $]; - } - - return ev; - } - - Event receive(this Q)() if (is(Q == EventQueue) || is(Q == shared EventQueue)) - { - Event ev; - synchronized (mx) - { - while ((q.length == 0) && !closed) - { - cv.wait(); - } - if (closed) - { - return Event(EventType.closed); - } - - ev = q[0]; - q = q[1 .. $]; - } - - return ev; - } - - void close(this Q)() if (is(Q == EventQueue) || is(Q == shared EventQueue)) - { - synchronized (mx) - { - closed = true; - cv.notifyAll(); - } - } - - void send(this Q)(Event ev) if (is(Q == EventQueue) || is(Q == shared EventQueue)) - { - synchronized (mx) - { - if (!closed) // cannot send any more events after close - { - q ~= ev; - cv.notify(); - } - } - } - -private: - - Mutex mx; - Condition cv; - Event[] q; - bool closed; -} - -unittest -{ - import core.thread; - auto eq = new EventQueue(); - - eq.send(Event(EventType.key)); - assert(eq.receive().type == EventType.key); - - assert(eq.receive(msecs(10)).type == EventType.none); - - spawn(function(shared EventQueue eq){ - Thread.sleep(msecs(50)); - eq.send(Event(EventType.mouse)); - }, cast(shared EventQueue)eq); - assert(eq.receive().type == EventType.mouse); - eq.close(); - assert(eq.receive().type == EventType.closed); -} diff --git a/source/dcell/parser.d b/source/dcell/parser.d index 57b8291..fddd88c 100644 --- a/source/dcell/parser.d +++ b/source/dcell/parser.d @@ -317,6 +317,7 @@ class Parser return cast(Event[]) res; } + // Parse the supplied content, returns true if data is fully parsed. bool parse(string b) { buf ~= b; @@ -360,7 +361,6 @@ private: Event[] evs; int utfLen; // how many UTF bytes are expected ubyte escChar; // character immediately following escape (zero if none) - const KeyCode[string] keyCodes; bool partial; // record partially parsed sequences MonoTime keyStart; // when the timer started Duration seqTime = msecs(50); // time to fully decode a partial sequence diff --git a/source/dcell/screen.d b/source/dcell/screen.d index 7690921..65932ca 100644 --- a/source/dcell/screen.d +++ b/source/dcell/screen.d @@ -11,7 +11,6 @@ module dcell.screen; import core.time; -import std.concurrency; public import dcell.cell; public import dcell.cursor; @@ -94,21 +93,11 @@ interface Screen Coord size(); /** - * If start was called without a Tid to send to, then events - * are delivered into a queue that can be polled via this API. - * This function is thread safe. - * Params: - * dur = maximum time to wait, if no event is available then EventType.none is returned. - * Returns: - * The event, which will be EventType.none if it times out, or EventType.closed if it is stopped. + * Wait for an event, up to the given duration. This is used + * to rescan for changes as well, so it should be called as + * frequently. */ - Event receiveEvent(Duration dur); - - /** - * Receive events, without a timeout. This only works if start - * was called without a tid. - */ - Event receiveEvent(); + Event waitEvent(Duration dur = msecs(100)); /** * Enable bracketed paste mode mode. Bracketed paste mode @@ -211,20 +200,17 @@ interface Screen void resize(); /** - * Start should be called to start processing. Once this begins, - * events (see event.d) will be delivered to the caller via - * std.concurrency.send(). Additionally, this may change terminal + * Start sets up the terminal. This changes terminal * settings to put the input stream into raw mode, etc. */ void start(); - void start(Tid); /** * Stop is called to stop processing on the screen. The terminal - * settings will be restored, and the screen may be cleared. Input - * events will no longer be delivered. This should be called when - * the program is exited, or if suspending (to run a sub-shell process - * interactively for example). + * settings will be restored, and the screen may be cleared. + * + * This should be called when the program is exited, or if suspending + * (to run a sub-shell process interactively for example). */ void stop(); } diff --git a/source/dcell/termio.d b/source/dcell/termio.d index 8f39681..2e72d9d 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -2,7 +2,7 @@ * Termio module for dcell contains code associated iwth managing terminal settings such as * non-blocking mode. * - * Copyright: Copyright 2022 Garrett D'Amore + * Copyright: Copyright 2025 Garrett D'Amore * Authors: Garrett D'Amore * License: * Distributed under the Boost Software License, Version 1.0. @@ -11,10 +11,28 @@ */ module dcell.termio; +import std.datetime; import std.exception; import std.range.interfaces; import dcell.coord; +version (OSX) +{ + version = UseSelect; +} +else version (iOS) +{ + version = UseSelect; +} +else version (tvOS) +{ + version = UseSelect; +} +else version (VisionOS) +{ + version = UseSelect; +} + /** * TtyImpl is the interface that implementations should * override or supply to support terminal I/O ioctls or @@ -56,7 +74,7 @@ interface TtyImpl * Read input. May return an empty slice if no data * is present and blocking is disabled. */ - string read(); + string read(Duration dur = Duration.zero); /** * Write output. @@ -92,6 +110,7 @@ interface TtyImpl * 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(); } @@ -101,6 +120,8 @@ version (Posix) 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 : TtyImpl @@ -216,7 +237,7 @@ version (Posix) else enum TIOCGWINSZ = 0x5413; // everything else } - else version (Darwin) + else version (Apple) enum TIOCGWINSZ = 0x40087468; else version (Solaris) enum TIOCGWINSZ = 0x5468; @@ -236,16 +257,121 @@ version (Posix) return Coord(wsz.ws_col, wsz.ws_row); } - string read() + version (UseSelect) { - // this has to use the underlying read system call - import unistd = core.sys.posix.unistd; - - ubyte[] buf = new ubyte[128]; - auto rv = unistd.read(fd, cast(void*) buf.ptr, buf.length); - if (rv < 0) - return ""; - return cast(string) buf[0 .. rv]; + // 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) + { + 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 == dur.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) @@ -255,6 +381,7 @@ version (Posix) bool resized() { + // NB: resized is edge triggered. return wasResized(fd); } @@ -287,19 +414,21 @@ version (Posix) private __gshared int sigRaised = 0; private __gshared int sigFd = -1; + private __gshared Pipe sigPipe; + private __gshared int sigWfd; + private __gshared int sigRfd; + private extern (C) void handleSigWinCh(int _) nothrow { - int fd = sigFd; atomicStore(sigRaised, 1); - termios tio; - // wake the input loop so it can see the signal + + // wake any reader so it can see the update // this is crummy but its the best way to get this noticed. - if (tcgetattr(fd, &tio) >= 0) - { - tio.c_cc[VMIN] = 0; - tio.c_cc[VTIME] = 1; - tcsetattr(fd, TCSANOW, &tio); - } + ubyte[1] buf; + import unistd = core.sys.posix.unistd; + + // we do not care if this fails + unistd.write(sigWfd, buf.ptr, 1); } // We don't have a stanrdard definition of SIGWINCH @@ -330,8 +459,18 @@ version (Posix) void watchResize(int fd) { + import std.process; + import core.sys.posix.fcntl; + 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; @@ -347,6 +486,7 @@ version (Posix) sa.sa_handler = SIG_IGN; sigaction(SIGWINCH, &sa, null); sigFd = -1; + sigPipe.close(); } } diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index 0f90568..fb13c7b 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -16,7 +16,7 @@ import core.atomic; import core.time; import std.algorithm : canFind; import std.format; -import std.concurrency; +import std.datetime; import std.exception; import std.outbuffer; import std.process; @@ -26,14 +26,12 @@ import std.string; import dcell.cell; import dcell.cursor; -import dcell.evqueue; import dcell.key; import dcell.mouse; import dcell.termio; import dcell.screen; import dcell.event; import dcell.parser; -import dcell.turnstile; class TtyScreen : Screen { @@ -131,7 +129,6 @@ class TtyScreen : Screen ti.start(); cells = new CellBuffer(ti.windowSize()); ob = new OutBuffer(); - stopping = new Turnstile(); defStyle.bg = Color.reset; defStyle.fg = Color.reset; @@ -227,11 +224,12 @@ class TtyScreen : Screen ti.close(); } - private void start(Tid tid, EventQueue eq) + void start() { if (started) return; - stopping.set(false); + + parser = new Parser(); // if we are restarting, this discards the old one ti.save(); ti.raw(); puts(vt.hideCursor); @@ -243,20 +241,8 @@ class TtyScreen : Screen resize(); draw(); - spawn(&inputLoop, cast(shared TtyImpl) ti, tid, - cast(shared EventQueue) eq, cast(shared Turnstile) stopping); - started = true; - } - void start(Tid tid) - { - start(tid, null); - } - - void start() - { - eq = new EventQueue(); - start(Tid(), eq); + started = true; } void stop() @@ -277,11 +263,7 @@ class TtyScreen : Screen puts(vt.disableFocus); puts(vt.disableCsiU); flush(); - stopping.set(true); - puts(vt.requestDA); // request DA to wake up the reader - flush(); ti.stop(); - stopping.wait(false); ti.restore(); started = false; } @@ -408,23 +390,62 @@ class TtyScreen : Screen flush(); } - Event receiveEvent(Duration dur) + Event waitEvent(Duration dur = msecs(100)) { - if (eq is null) + // naive polling loop for now. + MonoTime expire; + + if (!dur.isNegative) { - return Event(EventType.error); + expire = MonoTime.currTime() + dur; + } + else + { + // we check dur.isNegative, but this adds a safeguard to make sure + // we won't misconstrue it as a small value. + expire = MonoTime.max; } - return eq.receive(dur); - } - /** This variant of receiveEvent blocks forever until an event is available. */ - Event receiveEvent() - { - if (eq is null) + // residual tracks whether we are waiting for the rest of + // a partial escape sequence in the parser. + bool residual = false; + + for (;;) { - return Event(EventType.error); + events ~= parser.events(); + if (ti.resized()) + { + Event rev; + rev.type = EventType.resize; + rev.when = MonoTime.currTime(); + events ~= rev; + } + if (!events.empty) + { + auto event = events[0]; + events = events[1 .. $]; + return event; + } + + // if we have partial data in the parser, we need to use + // a shorter wakeup, so we can create an event in case the + // escape sequence is not completed (e.g. lone ESC.) + if (residual || !dur.isNegative) + { + auto now = MonoTime.currTime(); + if (expire <= now) + { + // expired + return Event(EventType.none); + } + auto interval = residual ? msecs(1) : (expire - now); + residual = !parser.parse(ti.read(interval)); + } + else + { + residual = !parser.parse(ti.read()); + } } - return eq.receive(); } private: @@ -445,11 +466,11 @@ private: bool pasteEn; // saved state for suspend/resume TtyImpl ti; OutBuffer ob; - Turnstile stopping; bool started; bool legacy; // legacy terminals don't have support for OSC, APC, DSC, etc. - EventQueue eq; Vt vt; + Event[] events; + Parser parser; void puts(string s) { @@ -742,61 +763,6 @@ private: puts(b ? Vt.enablePaste : Vt.disablePaste); flush(); } - - static void inputLoop(shared TtyImpl tin, Tid tid, - shared EventQueue eq, shared Turnstile stopping) - { - TtyImpl f = cast(TtyImpl) tin; - Parser p = new Parser(); - - f.blocking(true); - - for (;;) - { - string s; - try - { - s = f.read(); - } - catch (Exception e) - { - } - bool finished = p.parse(s); - auto evs = p.events(); - if (f.resized()) - { - Event ev; - ev.type = EventType.resize; - ev.when = MonoTime.currTime(); - evs ~= ev; - } - foreach (_, ev; evs) - { - if (eq is null) - send(ownerTid(), ev); - else - { - eq.send(ev); - } - } - - if (stopping.get()) - { - stopping.set(false); - return; - } - - if (!p.empty() || !finished) - { - f.blocking(false); - } - else - { - // No data, so we can sleep until some arrives. - f.blocking(true); - } - } - } } Screen newTtyScreen() diff --git a/source/dcell/turnstile.d b/source/dcell/turnstile.d deleted file mode 100644 index c716e3c..0000000 --- a/source/dcell/turnstile.d +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Private turnstile implementation. - * - * Copyright: Copyright 2022 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.turnstile; - -import core.sync.condition; - -package: - -/** - * Turnstile implements a thread safe primitive -- applications can - * set or wait for a condition. - */ -class Turnstile -{ - private Mutex m; - private Condition c; - private bool val; - - this() - { - m = new Mutex(); - c = new Condition(m); - } - - void set(this T)(bool b) if ((is(T == Turnstile) || is(T == shared Turnstile))) - { - synchronized (m) - { - val = b; - c.notifyAll(); - } - } - - bool get(this T)() if ((is(T == Turnstile) || is(T == shared Turnstile))) - { - bool b; - synchronized (m) - { - b = val; - } - return b; - } - - void wait(this T)(bool b) if (is(T == Turnstile) || is(T == shared Turnstile)) - { - synchronized (m) - { - while (val != b) - { - c.wait(); - } - } - } -}