-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Completely refactor termio processing and eliminate threads (fi… #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThreaded event queues and turnstiles were removed; Screen API changed from receiveEvent to waitEvent. TtyScreen now polls TtyImpl.read with a Parser-driven buffer and uses a signal pipe for resize wakeups. Demos and termio updated to use the new waitEvent/start semantics. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Demo / App
participant Screen as TtyScreen.waitEvent
participant TTY as TtyImpl.read
participant Parser as Parser.parse
participant OS as OS (select/poll)
App->>Screen: waitEvent(dur)
Screen->>TTY: read(dur)
TTY->>OS: select/poll(fd, dur)
alt data available
OS-->>TTY: bytes
TTY-->>Parser: parse(bytes)
Parser-->>Screen: produce Event(s)
Screen-->>App: return Event
else timeout or none
OS-->>TTY: timeout/no data
Screen-->>App: return none/timeout
end
Note over OS,TTY: resize signal writes to sigPipe -> wakes select/poll
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (2)
source/dcell/ttyscreen.d (2)
126-221: Constructor/parser init and start() refactor look correct; consider clearing parser/events on reuseCreating
parserin the constructor and makingstart()idempotent while only manipulating terminal state aligns well with the new threadless design. The addedeventsbuffer plusstartedflag give you a clean, single-threaded lifecycle.One small, non-blocking suggestion: if a
TtyScreeninstance is everstop()ped and laterstart()ed again, any residual parser state and queuedeventsfrom the previous run would carry over. If you want to support that reuse pattern robustly, consider clearingeventsand resetting/replacingparserinstart()orstop()to guarantee a fresh input state per session.Also applies to: 228-245, 469-471
19-20: waitEvent() timeout and polling logic are sound; only minor readability tweaks worth consideringThe new
waitEvent(Duration dur = msecs(100))implementation correctly:
- Accumulates events from
parser.events()into a localeventsqueue.- Synthesizes resize events when
ti.resized()is true.- Uses
MonoTimeto enforce a timeout whenduris non‑negative, and a blockingti.read()path whenduris negative.- Uses the
residualflag plus a shortmsecs(1)interval to give the parser a chance to flush incomplete escape sequences (e.g., lone ESC) without busy‑looping.Functionally this looks solid for both finite and “infinite” waits.
If you want, you could make two small readability improvements:
- Only compute/use
expirewhen!dur.isNegative, so it’s obvious theMonoTime.maxsentinel is purely defensive.- Add a brief comment on the expected
TtyImpl.resized()semantics (edge‑triggered vs level‑triggered), since you append a resize event whenever it returns true; that will help future readers reason about potential duplicate resize events.These are cosmetic; no changes are required for correctness.
Also applies to: 392-445
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
demos/colors/source/colors.d(1 hunks)demos/hello/source/hello.d(2 hunks)demos/mouse/source/mouse.d(2 hunks)source/dcell/evqueue.d(0 hunks)source/dcell/parser.d(1 hunks)source/dcell/screen.d(2 hunks)source/dcell/termio.d(9 hunks)source/dcell/ttyscreen.d(6 hunks)source/dcell/turnstile.d(0 hunks)
💤 Files with no reviewable changes (2)
- source/dcell/evqueue.d
- source/dcell/turnstile.d
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-08T19:10:45.974Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.974Z
Learning: In source/dcell/ttyscreen.d, the newTtyScreen() factory function intentionally does not accept a term parameter because the TtyScreen constructor handles environment variable resolution internally with an optional term parameter. Users needing to override the terminal type should call the constructor directly: new TtyScreen(newDevTty(), "custom-term").
Applied to files:
source/dcell/termio.ddemos/hello/source/hello.dsource/dcell/ttyscreen.d
🔇 Additional comments (13)
demos/colors/source/colors.d (1)
139-139: LGTM!The migration from
receiveEventtowaitEventis correct. The 50ms timeout is appropriate for the animation refresh rate in this demo.demos/mouse/source/mouse.d (2)
107-107: LGTM!Correctly migrated from
start(thisTid())to the simplifiedstart()API.
142-142: LGTM!The migration to
waitEvent()(with default 100ms timeout) is appropriate for this interactive demo's event loop.source/dcell/screen.d (2)
96-100: LGTM!The new
waitEventAPI with a default 100ms timeout is a clean replacement for the previousreceiveEventmethods. The documentation correctly notes that this should be called frequently to rescan for changes.
203-215: LGTM!The updated
start()andstop()documentation accurately reflects the simplified lifecycle without threading concerns.source/dcell/termio.d (4)
19-34: LGTM!The version blocks correctly identify Apple platforms that need the
select()-based implementation due topoll()not working properly on/dev/tty.
77-77: LGTM!The updated interface signature with
Duration dur = Duration.zeroprovides a clean API for timed reads while maintaining backward compatibility with the default.
411-426: LGTM on the signal pipe mechanism.The pipe-based signaling for resize notifications is a clean solution for waking up the reader thread-safely from a signal handler.
454-472: LGTM!The pipe initialization in
watchResizecorrectly sets both ends to non-blocking mode, which is essential for safe signal handler interaction.demos/hello/source/hello.d (4)
20-27: emitStr implementation is clear and matches its ASCII-only intentThe byte-wise loop over
str[0]with the explicit “assumes only ASCII” comment aligns with how you use it in this demo and keeps the function straightforward. No issues here.
31-47: displayHelloWorld correctly sets up styles and content for the demoUsing a default background/foreground style, then drawing the two instructional strings and the centered red “X” gives a nice visual demonstration of the API. The function’s control flow is simple and side‑effect free beyond updating the screen and calling
s.show(). Nothing to change from a correctness standpoint.
49-69: handleEvent cleanly restores the terminal before exiting and handles resize appropriatelyStopping the screen before calling
exit(0)ensures the terminal gets restored even thoughexitbypasses D destructors, which is appropriate for a small demo. The resize branch (resize→displayHelloWorld→sync) also looks correct and matches the intended behavior with the refactoredScreenAPI.
72-85: Main loop’s switch to waitEvent() matches the new single-threaded modelUsing:
ts.start(); displayHelloWorld(ts); for (;;) { Event ev = ts.waitEvent(); handleEvent(ts, ev); }fits the new
waitEvent-based API and removes the dependency on threads/channel-style receive. With the default 100ms timeout,EventType.nonewakeups are ignored byhandleEvent, so the loop behaves effectively as a blocking event loop for this demo. This looks good.
#8) This now uses a new api for obtaining events, which allows the application to run in a single event loop without any locks or threading. Once small consequence of this is that if the application wants to work in an MT way with the screen, then it will need to provide its own guards. This design means that this can now be used in designs that may not be conducive to using runtime threads (such as reactor designs found in Weka's mecca library), and it also should make debugging much simpler.
This way if we stop and restart, we get fresh state each time.
899da76 to
f97fc23
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
source/dcell/termio.d (1)
330-375: Critical bugs in poll() implementation.Two critical issues remain from the previous review:
Line 337:
pfd[1].events = 0;should bepfd[1].revents = 0;- currently this overwrites the events mask, preventing signal pipe notifications.Line 351:
poll(pfd.ptr, 1, dly)should bepoll(pfd.ptr, 2, dly)- currently only polling the tty fd, completely ignoring the signal pipe.These bugs prevent the signal pipe from waking up blocked reads on resize events.
Apply this diff:
pfd[1].fd = sigRfd; pfd[1].events = POLLRDNORM; - pfd[1].events = 0; + 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, 1, dly); + long rv = poll(pfd.ptr, 2, dly);
🧹 Nitpick comments (1)
demos/hello/source/hello.d (1)
74-74: Remove redundant import.
std.stdiois already imported at module level (line 13).void main() { - import std.stdio; - auto ts = newScreen();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
demos/colors/source/colors.d(1 hunks)demos/hello/source/hello.d(2 hunks)demos/mouse/source/mouse.d(2 hunks)source/dcell/evqueue.d(0 hunks)source/dcell/parser.d(1 hunks)source/dcell/screen.d(2 hunks)source/dcell/termio.d(11 hunks)source/dcell/ttyscreen.d(5 hunks)source/dcell/turnstile.d(0 hunks)
💤 Files with no reviewable changes (2)
- source/dcell/turnstile.d
- source/dcell/evqueue.d
🚧 Files skipped from review as they are similar to previous changes (2)
- demos/mouse/source/mouse.d
- demos/colors/source/colors.d
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.974Z
Learning: In source/dcell/ttyscreen.d, the newTtyScreen() factory function intentionally does not accept a term parameter because the TtyScreen constructor handles environment variable resolution internally with an optional term parameter. Users needing to override the terminal type should call the constructor directly: new TtyScreen(newDevTty(), "custom-term").
📚 Learning: 2025-12-08T19:10:45.974Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.974Z
Learning: In source/dcell/ttyscreen.d, the newTtyScreen() factory function intentionally does not accept a term parameter because the TtyScreen constructor handles environment variable resolution internally with an optional term parameter. Users needing to override the terminal type should call the constructor directly: new TtyScreen(newDevTty(), "custom-term").
Applied to files:
source/dcell/ttyscreen.ddemos/hello/source/hello.dsource/dcell/termio.d
🔇 Additional comments (18)
demos/hello/source/hello.d (4)
18-27: LGTM!The added comment about ASCII handling is helpful documentation. The implementation is appropriate for a demo.
29-47: LGTM!Formatting improvements with no functional changes.
49-70: LGTM!The event handling logic is straightforward. Calling
ts.stop()beforeexit(0)ensures proper terminal cleanup.
79-85: Nice demonstration of the new thread-free API.The new event loop pattern using
ts.start()andts.waitEvent()is clean and clearly demonstrates the refactored API from the PR objectives.source/dcell/screen.d (2)
96-100: LGTM!The new
waitEventAPI clearly documents the polling behavior and the default 100ms timeout is appropriate for event-loop integration.
203-215: LGTM!Documentation correctly reflects the refactored API that no longer uses threading or std.concurrency.
source/dcell/termio.d (6)
260-319: LGTM!The select()-based implementation correctly includes
sigRfdin the fd set and max calculation, and properly clears the signal pipe. The previous bug has been fixed.
19-34: LGTM!The version blocks correctly route Apple platforms (OSX, iOS, tvOS, VisionOS) to the select-based implementation, which is appropriate given poll() limitations on
/dev/ttyfor these platforms.
77-77: LGTM!The addition of a Duration parameter to the
read()method enables timeout-based I/O, which is essential for the polling-based event loop design.
384-384: LGTM!The comment clarifying edge-triggered semantics is helpful documentation.
460-479: LGTM!The pipe-based signaling mechanism is correctly initialized with non-blocking flags on both ends. The approach of using a pipe to wake blocked readers from the signal handler is a standard and safe pattern.
240-241: LGTM!The change from
DarwintoAppleis correct and consistent with the broader platform coverage defined in the version blocks above.source/dcell/parser.d (1)
320-320: LGTM!The comment grammar has been corrected to "fully parsed" as suggested in the previous review.
source/dcell/ttyscreen.d (5)
19-19: LGTM!The import change from
std.concurrencytostd.datetimealigns with the removal of thread-based event handling.
472-473: LGTM!The addition of internal
eventsbuffer andparserfields supports the new polling-based event handling model.
227-246: LGTM!The refactored
start()method correctly initializes a new Parser and sets up the terminal without spawning threads. Creating a new Parser on restart properly discards stale parsing state.
393-449: Verify overflow safety with large Duration values.The waitEvent implementation is well-structured with adaptive polling and residual tracking for partial escape sequences. However, there's a potential edge case at line 400:
expire = MonoTime.currTime() + dur;If
duris close toDuration.max(but not exactly equal), adding it to the current time could overflow. While line 340 in termio.d checks fordur == dur.max, it may not catch values that are very large but not exactly max.Consider adding an additional safeguard:
if (!dur.isNegative) { - expire = MonoTime.currTime() + dur; + if (dur >= (MonoTime.max - MonoTime.currTime())) + { + expire = MonoTime.max; + } + else + { + expire = MonoTime.currTime() + dur; + } }Alternatively, verify that the existing checks at line 340 in termio.d (
dur == dur.max) are sufficient for all realistic use cases.
409-447: LGTM!The residual tracking and adaptive polling logic is well-designed. Using a 1ms interval when waiting for partial escape sequences ensures timely timeout detection (e.g., lone ESC key) while using longer intervals during normal operation to reduce CPU usage.
06008c8 to
0390be2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
source/dcell/termio.d (2)
73-77: UpdateTtyImpl.readdocstring for the newdurtimeout semanticsThe interface comment for
readstill only talks about blocking and non-blocking modes and doesn’t describe whatdurdoes (zero vs positive vs negative /dur.max) or under which conditions an empty string is returned (timeout vs “no data” vs EINTR).It would help downstream implementers and callers if the doc clarified the contract, e.g., roughly:
dur == Duration.zero: poll and return immediately.dur > 0: block up todur, possibly returning empty on timeout.dur.isNegativeordur == dur.max: block until input or interrupt.
421-432: Initialize the wakeup byte inhandleSigWinChbefore writing tosigWfd
handleSigWinChallocatesubyte[1] buf;but never initializes it before callingunistd.write(sigWfd, buf.ptr, 1);. The actual value is irrelevant for wakeups, but this still writes uninitialized stack contents, which can trip analyzers and is easy to avoid.Initialize the byte before writing:
- ubyte[1] buf; - import unistd = core.sys.posix.unistd; - - // we do not care if this fails - unistd.write(sigWfd, buf.ptr, 1); + ubyte[1] buf; + import unistd = core.sys.posix.unistd; + + // we do not care if this fails + buf[0] = 1; + unistd.write(sigWfd, buf.ptr, 1);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
source/dcell/termio.d(11 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.974Z
Learning: In source/dcell/ttyscreen.d, the newTtyScreen() factory function intentionally does not accept a term parameter because the TtyScreen constructor handles environment variable resolution internally with an optional term parameter. Users needing to override the terminal type should call the constructor directly: new TtyScreen(newDevTty(), "custom-term").
📚 Learning: 2025-12-08T19:10:45.974Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.974Z
Learning: In source/dcell/ttyscreen.d, the newTtyScreen() factory function intentionally does not accept a term parameter because the TtyScreen constructor handles environment variable resolution internally with an optional term parameter. Users needing to override the terminal type should call the constructor directly: new TtyScreen(newDevTty(), "custom-term").
Applied to files:
source/dcell/termio.d
| 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"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
wc -l source/dcell/termio.dRepository: gdamore/dcell
Length of output: 82
🏁 Script executed:
sed -n '250,360p' source/dcell/termio.dRepository: gdamore/dcell
Length of output: 3681
🏁 Script executed:
sed -n '320,390p' source/dcell/termio.d | cat -nRepository: gdamore/dcell
Length of output: 2522
Align Duration semantics between select- and poll-based read implementations
In the version (UseSelect) path, read treats only dur.isNegative as "block forever", but in the poll-based path you treat both dur.isNegative and dur == dur.max as infinite (dly = -1). This creates inconsistent behavior across platforms: read(Duration.max) blocks indefinitely on Apple systems but attempts a conversion on other platforms, risking overflow when Duration.max is converted to microseconds and cast to the timeval fields.
Align the select implementation with the poll implementation by handling Duration.max:
- 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;
- }
+ if (dur.isNegative || dur == dur.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;
+ }🤖 Prompt for AI Agents
In source/dcell/termio.d around lines 260 to 347, the select-based read treats
only dur.isNegative as “block forever” while the poll-based read treats both
dur.isNegative and dur == dur.max as infinite; update the select branch to also
consider dur == dur.max as infinite (i.e., set tvp = null when dur.isNegative ||
dur == dur.max) and only compute usecs/timeval when the duration is finite, so
both implementations have consistent semantics and avoid overflow when
Duration.max is passed.
…xes #8)
This now uses a new api for obtaining events, which allows the application to run in a single event loop without any locks or threading. Once small consequence of this is that if the application wants to work in an MT way with the screen, then it will need to provide its own guards.
This design means that this can now be used in designs that may not be conducive to using runtime threads (such as reactor designs found in Weka's mecca library), and it also should make debugging much simpler.
Summary by CodeRabbit
Refactor
Demos
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.