-
Notifications
You must be signed in to change notification settings - Fork 3
Redesign events API to use ranges and to facilitate integration into extern event loops #53
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
This new API allows applications to use the events as both an input and an output range (FIFO). It also is designed to support use with external poll loops (once we have exposed the file for the tty.)
This should allow applications to integrate dcell into external polling loops. These applications should use the events and waitForEvent APIs to adjust their poll intervals as needed. A cautionary note is given for macOS, because macOS bizarrely does not support poll or kqueue with tty devices. (The only way to work on such systems is to use a separate thread and the self-pipe trick.)
WalkthroughThis PR replaces single-event blocking with a queued, batched event model across the terminal layer and demos: adds an EventQ, exposes Screen.events(), introduces Screen.waitForEvent(...), adds Tty.wakeUp and wake plumbing in posix/win backends, rewrites TtyScreen to use an internal EventQ, and updates demos to iterate all pending events per wake. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as Demo App
participant Screen as Screen / TtyScreen
participant Tty as Tty backend
participant EvQ as EventQ
App->>Screen: waitForEvent(timeout, ref resched)
Screen->>Tty: poll/read with timeout
Tty-->>Screen: push events into evq (wake)
Screen-->>App: true (events) / false (timeout)
alt events available
App->>Screen: events()
Screen-->>App: reference to EvQ
loop for each pending event
App->>EvQ: front / popFront
EvQ-->>App: Event
App->>App: handle/dispatch Event
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
demos/mouse/source/mouse.d (1)
123-123: Uninitializedlbmay cause incorrect color when drawing box on first button release.
dchar lb;is default-initialized todchar.init(0xFFFF). If aButtons.noneevent occurs before any button press event setslb, the expressioncast(Color)(lb - '0')at line 240 will produce an unexpected color value.Consider initializing
lbto a safe default:- dchar lb; + dchar lb = '0';
🧹 Nitpick comments (2)
source/dcell/event.d (1)
74-104: LGTM: EventQ correctly implements FIFO queue semantics.The EventQ class properly implements the input range protocol with
front(),empty(), andpopFront(). The use of array concatenation input()is acceptable for typical event queue usage patterns where event bursts are small.For high-throughput scenarios, consider using
std.array.Appenderto reduce allocations:import std.array : Appender; private: Appender!(Event[]) events;However, this optimization is likely unnecessary for typical UI event handling.
demos/mouse/source/mouse.d (1)
282-282: Remove commented-out code.This commented line appears to be leftover from refactoring and should be removed to keep the code clean.
- // mousePos = ev.mouse.pos;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
demos/colors/source/colors.d(5 hunks)demos/hello/source/hello.d(2 hunks)demos/mouse/source/mouse.d(3 hunks)demos/styles/source/styles.d(1 hunks)source/dcell/event.d(2 hunks)source/dcell/screen.d(1 hunks)source/dcell/termio.d(4 hunks)source/dcell/tty.d(1 hunks)source/dcell/ttyscreen.d(4 hunks)source/dcell/wintty.d(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-08T19:10:45.993Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.993Z
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/wintty.dsource/dcell/termio.dsource/dcell/ttyscreen.d
🔇 Additional comments (14)
source/dcell/tty.d (1)
90-94: LGTM: Clean interface extension for wake-up capability.The new
wakeUp()method enables responsive interruption of blocked read operations, which is essential for integrating with external event loops and supporting the new batched event processing model introduced in this PR.source/dcell/wintty.d (1)
209-212: LGTM: Correct Windows event signaling.The implementation properly signals the event handle to wake up readers blocked in
read()(which waits oneventHat line 133). This is consistent with thestop()method's approach.demos/hello/source/hello.d (1)
82-86: LGTM: Event loop updated to batched processing pattern.The refactored event loop correctly uses
waitForEvent()followed by iterating over all pending events viats.events(), enabling more responsive event handling compared to the previous single-event approach.demos/styles/source/styles.d (1)
173-177: LGTM: Consistent adoption of batched event processing.The event loop correctly implements the new pattern introduced across all demos in this PR, ensuring all pending events are processed in each iteration.
source/dcell/termio.d (2)
64-81: LGTM: Well-documented File-based constructor.The new constructor properly validates the file is open and initializes the TTY from an existing File descriptor. The documentation clearly explains the use case and platform-specific limitations (macOS poll/kqueue).
320-328: LGTM: Correct wake-up implementation using pipe signaling.The implementation writes a byte to the wake-up pipe (
sigWfd), which will unblock any reader waiting onsigRfdin theread()methods (lines 213, 268). The nothrow specification is appropriate since failures can be safely ignored.demos/colors/source/colors.d (3)
41-51: LGTM: Improved encapsulation with Screen parameter.The refactored constructor properly initializes the Screen field, enabling better encapsulation and supporting the new event-driven architecture.
159-207: LGTM: Well-structured event loop with labeled break.The new
run()method effectively encapsulates the event loop logic. Key improvements:
- Uses labeled break (
break loop) to exit cleanly from nested event processing- Processes all pending events via
foreach(ev; s.events())- Provides smooth animation timing with
waitForEvent(msecs(50))- Properly handles scope exit to ensure
stop()is called
209-222: LGTM: Cleaner main() with improved separation of concerns.The refactored
main()delegates event loop responsibilities to theColorBoxes.run()method, resulting in cleaner code organization and better testability.demos/mouse/source/mouse.d (2)
160-296: Event processing loop looks well-structured.The refactored event loop correctly iterates over all pending events and handles each type appropriately. The switch statement covers key, mouse, paste, focus, and resize events with proper state management.
145-145: The review comment is incorrect. A zero-argument overload ofwaitForEvent()exists insource/dcell/screen.d:116as a final method with a default parameter:bool waitForEvent(Duration timeout = Duration.max). This overload calls the two-parameter variant internally, making the calls.waitForEvent()at line 145 valid and correct. Multiple demos in the codebase (styles.d, hello.d, mouse.d) successfully use this zero-argument form.Likely an incorrect or invalid review comment.
source/dcell/ttyscreen.d (3)
449-502: Timeout and residual parsing logic looks correct.The
waitForEventimplementation properly handles:
- Timeout expiration with
MonoTimecalculations- At least one read attempt before returning on timeout (
readOnceflag)- Shorter wake-up intervals (5ms) when partial escape sequences are pending
- Returning
reschedhint to caller for rescheduling incomplete sequences
504-507: LGTM!The
events()accessor correctly returns the internal event queue with appropriatenothrow @safe @nogcattributes.
160-160: Event queue initialization order is correct.The
evqis initialized aftertiis set (line 157), ensuringti.wakeUp()calls inTtyEventQ.put()won't encounter a null reference.
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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
demos/mouse/source/mouse.d(4 hunks)source/dcell/event.d(2 hunks)source/dcell/screen.d(1 hunks)source/dcell/ttyscreen.d(4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/screen.d:96-126
Timestamp: 2025-12-14T19:56:40.073Z
Learning: In dcell, the EventQ class is intentionally designed as both an input range and an output range of Event. The output range functionality allows applications to inject synthetic events into the event stream.
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/ttyscreen.d:557-577
Timestamp: 2025-12-14T19:58:59.815Z
Learning: In source/dcell/ttyscreen.d, the TtyEventQ class intentionally has different behavior between put() and opOpAssign methods. The put() method calls ti.wakeUp() for external callers injecting synthetic events, while opOpAssign (used via ~= operator) bypasses wakeUp() as a performance optimization when appending events internally during tty event processing where the thread is already active.
📚 Learning: 2025-12-14T19:58:59.815Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/ttyscreen.d:557-577
Timestamp: 2025-12-14T19:58:59.815Z
Learning: In source/dcell/ttyscreen.d, the TtyEventQ class intentionally has different behavior between put() and opOpAssign methods. The put() method calls ti.wakeUp() for external callers injecting synthetic events, while opOpAssign (used via ~= operator) bypasses wakeUp() as a performance optimization when appending events internally during tty event processing where the thread is already active.
Applied to files:
source/dcell/screen.dsource/dcell/ttyscreen.dsource/dcell/event.d
📚 Learning: 2025-12-14T19:56:40.073Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/screen.d:96-126
Timestamp: 2025-12-14T19:56:40.073Z
Learning: In dcell, the EventQ class is intentionally designed as both an input range and an output range of Event. The output range functionality allows applications to inject synthetic events into the event stream.
Applied to files:
source/dcell/screen.dsource/dcell/ttyscreen.dsource/dcell/event.d
📚 Learning: 2025-12-08T19:10:45.993Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.993Z
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.d
🔇 Additional comments (10)
source/dcell/screen.d (2)
96-120: LGTM! Clean API design with primary + convenience overload pattern.The two-overload design provides both flexibility (with
reschedfor integration into external event loops) and simplicity (for basic polling). The convenience overload correctly delegates to the primary implementation using a localreschedvariable.
122-126: LGTM! EventQ accessor is well-documented.The documentation correctly describes EventQ as both an input and output range, which allows applications to inject synthetic events as needed.
Based on learnings, the dual range functionality is intentional.
source/dcell/ttyscreen.d (4)
160-160: LGTM! EventQ initialization is correct.The TtyEventQ is properly initialized in the constructor.
449-502: LGTM! waitForEvent logic correctly handles timeouts and partial sequences.The implementation properly:
- Guards against overflow when setting expiration time
- Ensures at least one read attempt before timing out (via
readOnceflag)- Handles incomplete escape sequences with shorter intervals for residual data
- Sets appropriate
reschedvalues based on whether residual data remains
504-507: LGTM! EventQ accessor is straightforward.Returns the internal event queue for consumption.
603-603: LGTM! EventQ field properly declared.demos/mouse/source/mouse.d (3)
13-13: LGTM! Import updated for range operations.The change from
std.stdiotostd.rangealigns with the new event iteration model.
123-123: LGTM! Proper initialization prevents undefined behavior.Initializing
lbto 0 ensures it has a defined value before first use.
145-295: LGTM! Demo properly migrated to batched event processing model.The changes correctly:
- Use
waitForEvent()to wait for at least one event- Iterate over all pending events via
foreach(ev; s.events())- Handle all event types comprehensively (resize, paste, key, mouse, focus)
This demonstrates the new event API design effectively.
source/dcell/event.d (1)
74-104: LGTM! EventQ implementation is clean and correct.The class properly implements both input and output range interfaces:
put(Event ev)provides output range functionality for injecting eventsfront(),empty(),popFront()provide input range functionality for consuming events- Internal
Event[]array is correctly managed:popFront()calls the array's popFront which slices away consumed eventsThe range method attributes are appropriate:
- Input range methods use
nothrow pure @nogc @safesince they only manipulate array slicesput()correctly omits@nogcsince~=can allocateBased on learnings, the dual input/output range design is intentional and allows applications to inject synthetic events into the event stream.
748b20d to
5200bc5
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/event.d (1)
82-85: Consider adding@safeattribute toput()method.The
put()method uses array concatenation (~=) which is memory-safe. Adding@safewould make it consistent with the other methods and theevents()return type declared as@safeinscreen.d.Apply this diff:
- void put(Event ev) + void put(Event ev) @safe { events ~= ev; }demos/mouse/source/mouse.d (1)
145-160: Consider checking the return value ofwaitForEvent().The
waitForEvent()call on line 145 returns a boolean indicating whether events are available, but the return value is not checked. While the subsequentforeachwill simply skip if no events are present, explicitly checking the return value could make the flow clearer and potentially avoid unnecessary work.Consider this pattern:
emitStr(s, pos, white, format(pasteFmt, pStr.length, ps)); s.show(); - s.waitForEvent(); + if (!s.waitForEvent()) + continue; // timeout with no events Style st;Alternatively, if processing the draw state updates is intentional even when no events arrive, the current code is fine. In that case, you might want to add a comment explaining the intent.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
demos/mouse/source/mouse.d(4 hunks)source/dcell/event.d(2 hunks)source/dcell/screen.d(1 hunks)source/dcell/ttyscreen.d(4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/screen.d:96-126
Timestamp: 2025-12-14T19:56:40.073Z
Learning: In dcell, the EventQ class is intentionally designed as both an input range and an output range of Event. The output range functionality allows applications to inject synthetic events into the event stream.
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/ttyscreen.d:557-577
Timestamp: 2025-12-14T19:58:59.815Z
Learning: In source/dcell/ttyscreen.d, the TtyEventQ class intentionally has different behavior between put() and opOpAssign methods. The put() method calls ti.wakeUp() for external callers injecting synthetic events, while opOpAssign (used via ~= operator) bypasses wakeUp() as a performance optimization when appending events internally during tty event processing where the thread is already active.
📚 Learning: 2025-12-14T19:56:40.073Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/screen.d:96-126
Timestamp: 2025-12-14T19:56:40.073Z
Learning: In dcell, the EventQ class is intentionally designed as both an input range and an output range of Event. The output range functionality allows applications to inject synthetic events into the event stream.
Applied to files:
source/dcell/event.dsource/dcell/ttyscreen.dsource/dcell/screen.d
📚 Learning: 2025-12-14T19:58:59.815Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 53
File: source/dcell/ttyscreen.d:557-577
Timestamp: 2025-12-14T19:58:59.815Z
Learning: In source/dcell/ttyscreen.d, the TtyEventQ class intentionally has different behavior between put() and opOpAssign methods. The put() method calls ti.wakeUp() for external callers injecting synthetic events, while opOpAssign (used via ~= operator) bypasses wakeUp() as a performance optimization when appending events internally during tty event processing where the thread is already active.
Applied to files:
source/dcell/event.dsource/dcell/ttyscreen.dsource/dcell/screen.d
📚 Learning: 2025-12-08T19:10:45.993Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:763-766
Timestamp: 2025-12-08T19:10:45.993Z
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.d
🔇 Additional comments (11)
source/dcell/screen.d (2)
96-120: LGTM!The dual-overload design is excellent. The primary
waitForEventwithreschedprovides fine-grained control for integrating into external event loops, while the convenience wrapper simplifies typical polling scenarios.
122-126: LGTM!The
events()API correctly exposes the event queue as both an input and output range. Based on learnings, the output range capability is intentional for injecting synthetic events.demos/mouse/source/mouse.d (4)
13-13: LGTM!Moving
std.stdioto a local import withinmain()(line 93) and addingstd.rangeat module level is appropriate for the new event iteration pattern.
123-123: LGTM!Initializing
lbto0prevents undefined behavior. This is a good fix.
176-183: LGTM!The double-ESC exit pattern is a good UX design, preventing accidental exits. The implementation correctly increments the counter and exits only on the second press.
184-196: LGTM!The key handling for Graph events is well-designed:
C/cclears the screenCtrl-Ltriggers a redraw following UNIX conventions (line 189)The modifier check
ev.key.mod == Modifiers.ctrlensures only plain Ctrl-L triggers the sync.source/dcell/ttyscreen.d (4)
557-584: LGTM!The
TtyEventQdesign is excellent. Based on learnings, the distinction betweenput()(which callswakeUp()for external synthetic events) andopOpAssign(which bypasseswakeUp()for internal parser events) is intentional and provides a performance optimization. The comments clearly explain this design decision.
449-502: LGTM!The
waitForEventimplementation is well-designed:
- Overflow protection: Line 452 guards against
Duration.maxoverflow- Parser event flushing: Line 461 ensures parser events are consumed
- Resize detection: Lines 462-468 inject resize events when needed
- Smart timeout handling: Lines 480-488 ensure at least one read attempt even after timeout expires
- Partial escape handling: Lines 495-498 reduce the interval to 5ms when a partial escape sequence is detected, and line 484 sets
reschedto 25ms to request caller retryThe intervals (5ms for waiting on partial escapes, 25ms for rescheduling) are reasonable values for interactive responsiveness.
504-507: LGTM!The
events()accessor correctly returns the internal event queue with appropriate attributes (nothrow @safe @nogc).
160-160: LGTM!The
evqinitialization is correctly placed afterti.start()(line 158). SinceTtyEventQ.put()callsti.wakeUp()(line 562), thetifield must be set before constructing the event queue.source/dcell/event.d (1)
97-100: ThepopFront()attributes are correct and properly validated.The
popFront()method correctly delegates tostd.range.popFront()with the attributesnothrow pure @nogc @safe. According to the D standard library,popFront()for dynamic arrays has the signaturepure nothrow @nogc @safe void popFront(T)(ref scope inout(T)[] a), making the declared attributes accurate. Array slicing (advancing the slice viaa = a[1 .. $]) is indeed O(1) and does not allocate, confirming the@nogcattribute is valid.
| // NB: this does is the unmasked value! | ||
| // It also does not support chording mouse buttons | ||
| switch (ev.mouse.btn) |
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.
Clarify or fix comment about mouse button handling.
The comment on line 232-233 states "NB: this does is the unmasked value!" but the switch on line 234 uses ev.mouse.btn which is the original unmasked value (before the button variable was masked on line 225). However, the comment's wording "this does is" appears to be a typo and the intent is unclear.
Consider clarifying the comment:
- // NB: this does is the unmasked value!
- // It also does not support chording mouse buttons
+ // NB: this switch uses the unmasked btn value (includes wheel events)!
+ // It does not support chord detection (multiple buttons simultaneously)
switch (ev.mouse.btn)This makes it clear that the switch intentionally uses the full ev.mouse.btn value (not the masked button variable), which allows individual case branches to match exact button combinations.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // NB: this does is the unmasked value! | |
| // It also does not support chording mouse buttons | |
| switch (ev.mouse.btn) | |
| // NB: this switch uses the unmasked btn value (includes wheel events)! | |
| // It does not support chord detection (multiple buttons simultaneously) | |
| switch (ev.mouse.btn) |
🤖 Prompt for AI Agents
In demos/mouse/source/mouse.d around lines 232-234, the inline comment contains
a typo ("this does is") and is unclear about why the switch uses ev.mouse.btn
instead of the masked button variable; update the comment to correct the typo
and explicitly state that the switch intentionally uses the full unmasked
ev.mouse.btn value (not the masked button from line 225) so case branches can
match exact button combinations and support chording or combined button values.
Summary by CodeRabbit
New Features
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.