diff --git a/demos/colors/source/colors.d b/demos/colors/source/colors.d index 094c12e..db0665d 100644 --- a/demos/colors/source/colors.d +++ b/demos/colors/source/colors.d @@ -10,6 +10,7 @@ */ module color; +import core.time; import std.stdio; import std.string; import std.random; @@ -29,13 +30,15 @@ class ColorBoxes Random rng; enum inc = 8; int cnt; + bool done; + Screen s; bool flip() { return (rng.uniform!ubyte() & 0x1) != 0; } - this() + this(Screen scr) { rng = rndGen(); r = rng.uniform!ubyte(); @@ -44,9 +47,10 @@ class ColorBoxes ri = inc; gi = inc / 4; // humans are very sensitive to green bi = inc; + s = scr; } - void makeBox(Screen s) + void makeBox() { Coord wsz = s.size(); dchar dc = ' '; @@ -117,31 +121,9 @@ class ColorBoxes } s.show(); } -} - -void main() -{ - import std.stdio; - import core.time; - import core.stdc.stdlib; - - auto s = newScreen(); - assert(s !is null); - scope (exit) - { - s.stop(); - } - - ColorBoxes cb = new ColorBoxes(); - auto now = MonoTime.currTime(); - - s.start(); - bool done = false; - while (!done) + void handleEvent(Event ev) { - cb.makeBox(s); - auto ev = s.waitEvent(msecs(50)); switch (ev.type) { case EventType.key: @@ -173,8 +155,68 @@ void main() default: } } + + void run() + { + s.start(); + scope (exit) + { + s.stop(); + } + done = false; + makeBox(); + while (!done) + { + loop: foreach (ev; s.events()) + { + switch (ev.type) + { + case EventType.key: + switch (ev.key.key) + { + case Key.esc, Key.enter: + done = true; + break loop; + case Key.graph: + // Ctrl-L (without other modifiers) used to force a redraw. + if (ev.key.ch == 'l' && ev.key.mod == Modifiers.ctrl) + { + s.resize(); + s.sync(); + } + break; + default: + } + break; + case EventType.resize: + s.resize(); + break; + case EventType.closed: + done = true; + break loop; + case EventType.error: + assert(0, "error received"); + default: + } + } + makeBox(); + s.position = Coord(1, 1); + s.waitForEvent(msecs(50)); + } + } +} + +void main() +{ + import std.stdio; + import core.time; + import core.stdc.stdlib; + + ColorBoxes cb = new ColorBoxes(newScreen()); + + auto now = MonoTime.currTime(); + cb.run(); auto end = MonoTime.currTime(); - s.stop(); writefln("Drew %d boxes in %s.", cb.cnt, end - now); writefln("Average per box %s.", (end - now) / cb.cnt); } diff --git a/demos/hello/source/hello.d b/demos/hello/source/hello.d index 02acf12..a99c19a 100644 --- a/demos/hello/source/hello.d +++ b/demos/hello/source/hello.d @@ -29,7 +29,7 @@ void displayHelloWorld(Screen s) def.bg = Color.silver; def.fg = Color.black; s.clear(); - Style style = {fg: Color.red, bg: Color.papayaWhip}; + Style style = {fg: Color.navy, bg: Color.papayaWhip}; centerStr(s, size.y / 2 - 1, style, " Hello There! "); centerStr(s, size.y / 2 + 1, def, " Press ESC to exit. "); @@ -79,7 +79,10 @@ void main() displayHelloWorld(ts); for (;;) { - Event ev = ts.waitEvent(); - handleEvent(ts, ev); + ts.waitForEvent(); + foreach (ev; ts.events()) + { + handleEvent(ts, ev); + } } } diff --git a/demos/mouse/source/mouse.d b/demos/mouse/source/mouse.d index 3aa2378..bc7bcb0 100644 --- a/demos/mouse/source/mouse.d +++ b/demos/mouse/source/mouse.d @@ -10,7 +10,7 @@ */ module mouse; -import std.stdio; +import std.range; import std.string; import core.stdc.stdlib; @@ -120,7 +120,7 @@ void main() Coord oldTop = Coord(-1, -1); Coord oldBot = Coord(-1, -1); int esc = 0; - dchar lb; + dchar lb = 0; bool focused = true; for (;;) @@ -142,7 +142,7 @@ void main() pos.y++; emitStr(s, pos, white, format(pasteFmt, pStr.length, ps)); s.show(); - Event ev = s.waitEvent(); + s.waitForEvent(); Style st; st.bg = Color.red; Style up; @@ -157,139 +157,141 @@ void main() pos.x--; pos.y--; - switch (ev.type) + foreach (ev; s.events()) { - case EventType.resize: - s.resize(); - s.sync(); - break; - case EventType.paste: - pStr = ev.paste.content; - break; - case EventType.key: - pStr = ""; - s[pos] = Cell('K', st); - switch (ev.key.key) + switch (ev.type) { - case Key.esc: - esc++; - if (esc > 1) - { - s.stop(); - exit(0); - } + case EventType.resize: + s.resize(); + s.sync(); break; - case Key.graph: - if (ev.key.ch == 'C' || ev.key.ch == 'c') - { - s.clear(); - } - // Ctrl-L (without other modifiers) is used to redraw (UNIX convention) - else if (ev.key.ch == 'l' && ev.key.mod == Modifiers.ctrl) + case EventType.paste: + pStr = ev.paste.content; + break; + case EventType.key: + pStr = ""; + s[pos] = Cell('K', st); + switch (ev.key.key) { - s.sync(); + case Key.esc: + esc++; + if (esc > 1) + { + s.stop(); + exit(0); + } + break; + case Key.graph: + if (ev.key.ch == 'C' || ev.key.ch == 'c') + { + s.clear(); + } + // Ctrl-L (without other modifiers) is used to redraw (UNIX convention) + else if (ev.key.ch == 'l' && ev.key.mod == Modifiers.ctrl) + { + s.sync(); + } + esc = 0; + s[pos] = Cell('R', st); + break; + default: + break; } - esc = 0; - s[pos] = Cell('R', st); + kStr = ev.key.toString(); break; - default: - break; - } - kStr = ev.key.toString(); - break; - case EventType.mouse: - bStr = ""; + case EventType.mouse: + bStr = ""; - if (ev.mouse.mod & Modifiers.shift) - bStr ~= " S"; - if (ev.mouse.mod & Modifiers.ctrl) - bStr ~= " C"; - if (ev.mouse.mod & Modifiers.alt) - bStr ~= " A"; - if (ev.mouse.mod & Modifiers.meta) - bStr ~= " M"; - if (ev.mouse.mod & Modifiers.hyper) - bStr ~= " H"; - if (ev.mouse.btn & Buttons.wheelUp) - bStr ~= " WU"; - if (ev.mouse.btn & Buttons.wheelDown) - bStr ~= " WD"; - if (ev.mouse.btn & Buttons.wheelLeft) - bStr ~= " WL"; - if (ev.mouse.btn & Buttons.wheelRight) - bStr ~= " WR"; - // we only want buttons, not wheel events - auto button = ev.mouse.btn; - button &= 0xff; + if (ev.mouse.mod & Modifiers.shift) + bStr ~= " S"; + if (ev.mouse.mod & Modifiers.ctrl) + bStr ~= " C"; + if (ev.mouse.mod & Modifiers.alt) + bStr ~= " A"; + if (ev.mouse.mod & Modifiers.meta) + bStr ~= " M"; + if (ev.mouse.mod & Modifiers.hyper) + bStr ~= " H"; + if (ev.mouse.btn & Buttons.wheelUp) + bStr ~= " WU"; + if (ev.mouse.btn & Buttons.wheelDown) + bStr ~= " WD"; + if (ev.mouse.btn & Buttons.wheelLeft) + bStr ~= " WL"; + if (ev.mouse.btn & Buttons.wheelRight) + bStr ~= " WR"; + // we only want buttons, not wheel events + auto button = ev.mouse.btn; + button &= 0xff; - if ((button != Buttons.none) && (oldTop.x < 0)) - { - oldTop = ev.mouse.pos; - } + if ((button != Buttons.none) && (oldTop.x < 0)) + { + oldTop = ev.mouse.pos; + } - // NB: this does is the unmasked value! - // It also does not support chording mouse buttons - switch (ev.mouse.btn) - { - case Buttons.none: - if (oldTop.x >= 0) + // NB: this does is the unmasked value! + // It also does not support chording mouse buttons + switch (ev.mouse.btn) { - Style ns = up; - ns.bg = (cast(Color)(lb - '0')); - drawBox(s, oldTop, ev.mouse.pos, ns, lb); - oldTop = Coord(-1, -1); - oldBot = Coord(-1, -1); + case Buttons.none: + if (oldTop.x >= 0) + { + Style ns = up; + ns.bg = (cast(Color)(lb - '0')); + drawBox(s, oldTop, ev.mouse.pos, ns, lb); + oldTop = Coord(-1, -1); + oldBot = Coord(-1, -1); + } + break; + case Buttons.button1: + lb = '1'; + bStr ~= " B1"; + break; + case Buttons.button2: + lb = '2'; + bStr ~= " B2"; + break; + case Buttons.button3: + lb = '3'; + bStr ~= " B3"; + break; + case Buttons.button4: + lb = '4'; + bStr ~= " B4"; + break; + case Buttons.button5: + lb = '5'; + bStr ~= " B5"; + break; + case Buttons.button6: + lb = '6'; + bStr ~= " B6"; + break; + case Buttons.button7: + lb = '7'; + bStr ~= " B7"; + break; + case Buttons.button8: + lb = '8'; + bStr ~= " B8"; + break; + default: + lb = '?'; + break; } + if (button != Buttons.none) + oldBot = ev.mouse.pos; + + mousePos = ev.mouse.pos; + s[pos] = Cell('M', st); break; - case Buttons.button1: - lb = '1'; - bStr ~= " B1"; - break; - case Buttons.button2: - lb = '2'; - bStr ~= " B2"; - break; - case Buttons.button3: - lb = '3'; - bStr ~= " B3"; - break; - case Buttons.button4: - lb = '4'; - bStr ~= " B4"; - break; - case Buttons.button5: - lb = '5'; - bStr ~= " B5"; - break; - case Buttons.button6: - lb = '6'; - bStr ~= " B6"; - break; - case Buttons.button7: - lb = '7'; - bStr ~= " B7"; - break; - case Buttons.button8: - lb = '8'; - bStr ~= " B8"; + case EventType.focus: + focused = ev.focus.focused; break; default: - lb = '?'; + s[pos] = Cell('X', st); break; } - // mousePos = ev.mouse.pos; - if (button != Buttons.none) - oldBot = ev.mouse.pos; - - mousePos = ev.mouse.pos; - s[pos] = Cell('M', st); - break; - case EventType.focus: - focused = ev.focus.focused; - break; - default: - s[pos] = Cell('X', st); - break; } if (oldTop.x >= 0 && oldBot.x >= 0) { diff --git a/demos/styles/source/styles.d b/demos/styles/source/styles.d index d3a6c11..62f092d 100644 --- a/demos/styles/source/styles.d +++ b/demos/styles/source/styles.d @@ -170,7 +170,10 @@ void main() displayStyles(ts); for (;;) { - Event ev = ts.waitEvent(); - handleEvent(ts, ev); + ts.waitForEvent(); + foreach (ev; ts.events()) + { + handleEvent(ts, ev); + } } } diff --git a/source/dcell/event.d b/source/dcell/event.d index 15a966d..e28ea8f 100644 --- a/source/dcell/event.d +++ b/source/dcell/event.d @@ -11,6 +11,7 @@ module dcell.event; import core.time; +import std.range; import dcell.key; import dcell.mouse; @@ -69,3 +70,35 @@ struct FocusEvent { bool focused; } + +/** + * EventQ is both an input and output range of Events that behaves as a FIFO. + * When adding to the output range it will wake up any reader using the + * delegate that was passed to it at construction. + */ +class EventQ +{ + + void put(Event ev) + { + events ~= ev; + } + + final Event front() const nothrow pure @property @nogc @safe + { + return events.front; + } + + final bool empty() const nothrow pure @property @nogc @safe + { + return events.empty; + } + + final void popFront() nothrow pure @nogc @safe + { + events.popFront(); + } + +private: + Event[] events; +} diff --git a/source/dcell/screen.d b/source/dcell/screen.d index 797bb3f..29e828c 100644 --- a/source/dcell/screen.d +++ b/source/dcell/screen.d @@ -93,11 +93,37 @@ interface Screen Coord size(); /** - * 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. + * Wait for at least one event to be posted, for up to the given time. + * Params: + * timeout = maximum duration to wait for an event to arrive + * resched = if no event was posted, the caller should make another + * attempt no later than this in order to handle incompletely parsed events + * + * Returns: true if at least one event is available, false otherwise. + */ + bool waitForEvent(Duration timeout, ref Duration resched); + + /** + * Wait for at least one event to be posted. + * This simpler version can be used when the caller is in a simple poll/handle + * loop (typical for some simple applications.) + * + * Params: + * timeout = maximum duration to wait for an event to arrive + * + * Returns: True if at least one event is available, false otherwise. + */ + final bool waitForEvent(Duration timeout = Duration.max) + { + Duration resched; + return waitForEvent(timeout, resched); + } + + /** + * Obtain the list of events. The returned value is both an input range of `Event`, + * (for receiving events), and an output range of `Event`. */ - Event waitEvent(Duration dur = msecs(100)); + EventQ events() @safe; /** * Enable bracketed paste mode mode. Bracketed paste mode diff --git a/source/dcell/termio.d b/source/dcell/termio.d index 33d4489..0f804f3 100644 --- a/source/dcell/termio.d +++ b/source/dcell/termio.d @@ -61,6 +61,25 @@ class PosixTty : Tty path = dev; } + /** + * Create a Tty device form a given file. This should support termios. + * One reason to do this is so that an explictly created file on /dev/tty + * can be used together with poll, select, epoll, and so forth. It must + * pass the `isatty` check. + * + * Caution: on macOS the tty device does _not_ support the standard + * `poll` or `kqueue` APIs, but does support `select`. + * + * For more advanced use cases, consider implementing the tty interface + * directly. + */ + this(File f) + { + enforce(f.isOpen, "file is not open"); + file = f; + fd = file.fileno(); + } + void start() { if (!file.isOpen) @@ -193,7 +212,7 @@ class PosixTty : Tty FD_SET(fd, &readFds); FD_SET(sigRfd, &readFds); - if (dur.isNegative || dur == Duration.max) + if (dur == Duration.max || dur.isNegative) { tvp = null; } @@ -209,7 +228,6 @@ class PosixTty : Tty import std.algorithm : max; int num = select(max(fd, sigRfd) + 1, &readFds, null, null, tvp); - if (num < 1) { return ""; @@ -252,7 +270,7 @@ class PosixTty : Tty pfd[1].revents = 0; int dly; - if (dur.isNegative || dur == Duration.max) + if (dur == Duration.max) { dly = -1; } @@ -299,6 +317,16 @@ class PosixTty : Tty return wasResized(fd); } + void wakeUp() nothrow + { + import unistd = core.sys.posix.unistd; + + ubyte[1] buf; + + // we do not care if this fails + unistd.write(sigWfd, buf.ptr, 1); + } + private: string path; File file; diff --git a/source/dcell/tty.d b/source/dcell/tty.d index e02924b..331690c 100644 --- a/source/dcell/tty.d +++ b/source/dcell/tty.d @@ -87,4 +87,9 @@ interface Tty * This is edge triggered (reading it will clear the value.) */ bool resized(); + + /** + * Wake up any reader blocked in read(). + */ + void wakeUp(); } diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index 4231810..cf4dcb4 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -157,6 +157,7 @@ class TtyScreen : Screen ti = tt; ti.start(); cells = new CellBuffer(ti.windowSize()); + evq = new TtyEventQ(); ob = new OutBuffer(); cells.style.bg = Color.reset; cells.style.fg = Color.reset; @@ -445,64 +446,66 @@ class TtyScreen : Screen } } - Event waitEvent(Duration dur = msecs(100)) + bool waitForEvent(Duration timeout, ref Duration resched) { - // naive polling loop for now. - MonoTime expire; - - if (!dur.isNegative) - { - 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; - } + // expire for a time when we will timeout, safeguard against obvious overflow. + MonoTime expire = (timeout == Duration.max) ? MonoTime.max : MonoTime.currTime() + timeout; // residual tracks whether we are waiting for the rest of // a partial escape sequence in the parser. bool residual = false; + bool readOnce = false; for (;;) { - events ~= parser.events(); + evq ~= parser.events(); if (ti.resized()) { Event rev; rev.type = EventType.resize; rev.when = MonoTime.currTime(); - events ~= rev; + evq ~= rev; } - if (!events.empty) + if (!evq.empty) { - auto event = events[0]; - events = events[1 .. $]; - return event; + return true; } - // 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) + MonoTime now = MonoTime.currTime(); + + // if we expired, and we haven't at least called the + // read function once, then return. + Duration interval = expire - now; + + if (expire < now) { - auto now = MonoTime.currTime(); - if (expire <= now) + if (readOnce) { - // expired - return Event(EventType.none); + resched = residual ? msecs(25) : Duration.max; + return false; } - auto interval = residual ? msecs(1) : (expire - now); - residual = !parser.parse(ti.read(interval)); + interval = msecs(0); // just do a polling read } - else + + readOnce = true; + + // 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 && interval > msecs(5)) { - residual = !parser.parse(ti.read()); + interval = msecs(5); } + + residual = !parser.parse(ti.read(interval)); } } + EventQ events() nothrow @safe @nogc + { + return evq; + } + // This is the default style we use when writing content using // put and similar APIs. @property ref Style style() @safe @@ -551,6 +554,35 @@ private: Modifiers mod; } + class TtyEventQ : EventQ + { + override void put(Event ev) + { + super.put(ev); + ti.wakeUp(); + } + + // Note that this operator (~=) intentionally + // calls the parents put directly to prevent spurious wakeups + // when adding events that have already come from the tty. + // It is significant that this method (indeed the entire class) + // is private, so it should not be accessible by external consumers. + void opOpAssign(string op : "~")(Event rhs) + { + super.put(rhs); + } + + // Permit appending a list of events read from the parser directly, but + // without waking up the reader. + void opOpAssign(string op : "~")(Event[] rhs) + { + foreach (ev; rhs) + { + super.put(ev); + } + } + } + CellBuffer cells; bool clear_; // if a screen clear is requested Coord pos_; // location where we will update next @@ -566,9 +598,9 @@ private: bool started; bool legacy; // legacy terminals don't have support for OSC, APC, DSC, etc. Vt vt; - Event[] events; Parser parser; string title; + TtyEventQ evq; void puts(string s) { diff --git a/source/dcell/wintty.d b/source/dcell/wintty.d index 353de29..84fba8f 100644 --- a/source/dcell/wintty.d +++ b/source/dcell/wintty.d @@ -58,6 +58,10 @@ nothrow: class WinTty : Tty { + /** + * Default constructor. + * This expects the terminal to be connected to STD_INPUT_HANDLE and STD_OUTPUT_HANDLE. + */ this() { input = GetStdHandle(STD_INPUT_HANDLE); @@ -202,6 +206,11 @@ class WinTty : Tty return result; } + void wakeUp() + { + SetEvent(eventH); + } + private: HANDLE output; HANDLE input;