-
Notifications
You must be signed in to change notification settings - Fork 3
feat!: Add support for underline styling (fixes #28) #52
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
WalkthroughAdds a new DCell styles demo and expands text-styling support: reorganizes Attr bits and adds underline variants and invalid sentinel, adds underline color to Style, changes Screen.style() to return a ref, and updates TtyScreen to emit VT100 sequences and handle underline variants and color. Changes
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: 4
🧹 Nitpick comments (2)
source/dcell/ttyscreen.d (2)
592-606: Consider conditionally emitting underline color sequences.The code emits underline color sequences regardless of whether any underline style is active (i.e., whether
style.attr & Attr.underlineis set). While this may be harmless on most terminals, it could be more efficient to only emit these sequences when an underline is actually being rendered.Consider adding a guard condition:
+ if (style.attr & Attr.underline) + { if (style.ul == Color.reset) { puts(vt.underFg); } else if (style.ul.isRGB && vt.numColors > 256) { auto rgb = decompose(style.ul); puts(format!(vt.underRGB)(rgb[0], rgb[1], rgb[2])); } else { auto ul = toPalette(style.ul, vt.numColors); puts(format!(vt.underColor)(ul)); } + }
675-676: Question: Why concatenate sequences instead of emitting separately?The code concatenates
vt.underline ~ vt.doubleUnderbefore emitting. While this works, it differs from the pattern used for other attributes (lines 658-669) which callputs()individually. Is there a specific reason for concatenating these sequences, or would it be more consistent to call:puts(vt.underline); puts(vt.doubleUnder);This is a minor style question; both approaches should work equivalently.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
demos/styles/dub.json(1 hunks)demos/styles/source/styles.d(1 hunks)source/dcell/attr.d(1 hunks)source/dcell/screen.d(1 hunks)source/dcell/style.d(1 hunks)source/dcell/ttyscreen.d(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-08T19:17:33.678Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:448-494
Timestamp: 2025-12-08T19:17:33.678Z
Learning: In source/dcell/ttyscreen.d, Color.reset is defined as a value above the range of valid colors. In the sendColors() function, after emitting the reset sequence (vt.resetFgBg), Color.reset values naturally skip all subsequent color-setting logic (isRGB checks and palette range checks < 8, < 256) because they are out of range, which is the correct behavior by design.
Applied to files:
source/dcell/ttyscreen.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 (9)
demos/styles/dub.json (1)
1-12: LGTM! Demo build configuration is well-structured.The build manifest correctly defines the executable target with appropriate metadata and a relative path dependency to the dcell library.
source/dcell/ttyscreen.d (2)
505-508: LGTM! Correctly implements the updated interface.The change to return
ref Stylematches the interface definition inscreen.dand correctly returns a reference to the internalcells.style.
97-103: SGR extended sequences are properly handled with terminal capability detection.The extended SGR sequences (colon-separated subparameters for underline styles and colors) are standardized and supported by modern terminals (xterm, Kitty, VTE, iTerm2, hterm). The code already handles compatibility through terminal detection at lines 169–206, which identifies legacy terminals and limits features based on
numColorsand terminal type. RGB underline colors are only applied whenvt.numColors > 256(line 597), and older terminals matching patterns likevt*are marked as legacy, preventing unsupported sequences from being sent.demos/styles/source/styles.d (3)
106-109: Excellent demonstration of underline color feature!This clearly showcases the new
ul(underline color) field by setting it to blue while using the plain underline attribute. This is a great example for users to understand the new feature.
111-114: Great use of custom RGB color for underline.Using
fromHex()to specify a custom lavender color (#c58af9) with curly underline effectively demonstrates the flexibility of the new underline color system.
168-171: Good practice: Using scope(exit) for cleanup.The use of
scope(exit)ensures the screen is properly cleaned up even if an exception occurs, which is a best practice in D.source/dcell/attr.d (2)
22-24: Breaking change: Attribute bit positions shifted.The bit positions for
dim,italic, andstrikethroughhave been shifted to make room for extended underline styles. This is a breaking change marked asfeat!:in the commit, and it is intentional. The codebase contains no serialized or persistedAttrvalues, so compatibility is not a concern. All usage is runtime in-memory attribute handling through proper enum values and bitwise operations.
26-34: [rewritten review comment]
[classification tag]source/dcell/screen.d (1)
225-225: API design: style() returns mutable reference, matching opIndex pattern.The getter returns
ref Styleinstead of a copy, enabling direct mutation patterns likescreen.style.fg = Color.red. This is consistent withopIndex, which also returnsref Cell, creating a uniform API where both methods avoid copying.This is a breaking change for code expecting an independent copy of the style. Callers that assigned the result to an automatic variable will now get a reference to the screen's internal state rather than a copy.
0c979de to
3eea5ed
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/ttyscreen.d (1)
677-700: Critical: Fix the typo in the case label.Line 682 uses
attr.doubleUnderline(lowercase variable) instead ofAttr.doubleUnderline(enum type). This will cause a compilation error.Apply this diff to fix the typo:
case Attr.plainUnderline: puts(vt.underline); break; - case attr.doubleUnderline: + case Attr.doubleUnderline: puts(vt.underline); puts(vt.doubleUnder); break;
🧹 Nitpick comments (1)
source/dcell/attr.d (1)
26-34: Clarify the example in the comment.The comment uses
aas a placeholder variable, which could be clearer. Consider specifyingattras the variable name to match the typical usage pattern.Apply this diff to improve clarity:
- /// Underlines are a bit field, because they can be styled. Use a ^= underlineMask; a |= plainUnderline. + /// Underlines are a bit field, because they can be styled. Use: attr ^= Attr.underlineMask; attr |= Attr.plainUnderline. /// If you only use simple underlines you can just set underline as a bool.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
demos/styles/dub.json(1 hunks)demos/styles/source/styles.d(1 hunks)source/dcell/attr.d(1 hunks)source/dcell/screen.d(1 hunks)source/dcell/style.d(1 hunks)source/dcell/ttyscreen.d(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- demos/styles/dub.json
- demos/styles/source/styles.d
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-08T19:17:33.678Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:448-494
Timestamp: 2025-12-08T19:17:33.678Z
Learning: In source/dcell/ttyscreen.d, Color.reset is defined as a value above the range of valid colors. In the sendColors() function, after emitting the reset sequence (vt.resetFgBg), Color.reset values naturally skip all subsequent color-setting logic (isRGB checks and palette range checks < 8, < 256) because they are out of range, which is the correct behavior by design.
Applied to files:
source/dcell/ttyscreen.dsource/dcell/style.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 (8)
source/dcell/style.d (1)
25-25: LGTM! The underline color field integrates correctly with the rendering logic.The
ulfield defaults toColor.invalidand is properly guarded insendColors()at line 597 of ttyscreen.d with the checkif (style.ul.isValid && (style.attr & Attr.underlineMask)). This ensures underline color is only processed when explicitly set, similar to how foreground/background colors are handled.source/dcell/screen.d (1)
225-225: Public API change: style() now returns a mutable reference.Returning
ref Styleinstead ofconst Styleenables callers to mutate the style in-place. This is a breaking change for any code that relied on the immutable return type. Ensure this aligns with the intended API design.source/dcell/attr.d (2)
22-24: LGTM! Bit position reorganization to accommodate underline variants.The bit shifts for
dim,italic, andstrikethroughfree up space for the expanded underline bit-field. This is a breaking change for code that directly manipulates Attr bit values, but the enum constants remain valid.
36-37: LGTM! The invalid sentinel provides clear initialization semantics.Using bit 15 for
invalidand aliasinginitto it ensures uninitialized Attr values are distinguishable from valid attributes.source/dcell/ttyscreen.d (4)
97-103: LGTM! VT100 escape sequences for underline variants and color.The sequences follow the standard VT100/OSC format for underline styling and color control. The underline color sequences use SGR 58 (underline color) and 59 (reset to foreground).
508-511: LGTM! Consistent with the Screen interface change.Returning
ref Styleenables in-place style mutations and aligns with the interface update in source/dcell/screen.d.
597-613: LGTM! Underline color handling integrates correctly.The guard
if (style.ul.isValid && (style.attr & Attr.underlineMask))ensures underline color is only emitted when both conditions are met:
- The underline color is explicitly set (not
Color.invalid)- An underline attribute is present
This correctly handles the default initialization of
ultoColor.invalid.
707-720: LGTM! Correct save/restore pattern for style during clear.The
clearScreen()function properly saves the current style before applyingbaseStylefor the clear operation, then restores it afterward. This ensures the clear uses the style that was active whenclear()was called (saved at line 319), while preserving the current write position's style.
3eea5ed to
5b3db7a
Compare
This also changes the API for the style member to make it a bit more natural to use, and adds a demo program.
5b3db7a to
5b7d285
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 (2)
source/dcell/ttyscreen.d (1)
677-700: Critical: Typo on line 682 not actually fixed.Despite the past review comment being marked as "Addressed in commit 3eea5ed", line 682 still has the typo
case attr.doubleUnderline:(lowercaseattr) instead ofcase Attr.doubleUnderline:(uppercaseAttr). This will cause a compilation error.Apply this diff to fix the typo:
case Attr.plainUnderline: puts(vt.underline); break; - case attr.doubleUnderline: + case Attr.doubleUnderline: puts(vt.underline ~ vt.doubleUnder); puts(vt.doubleUnder); break;demos/styles/source/styles.d (1)
137-158: Prefer returning from main over calling exit().Lines 144-148 call
exit(0)directly, which bypasses D's scope guards and may leave the terminal in an inconsistent state. Thescope(exit)block at lines 166-169 won't execute whenexit()is called.Consider refactoring to return from the event loop instead:
Option 1: Throw an exception to unwind the stack
void handleEvent(Screen ts, Event ev) { - 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); + throw new Exception("exit"); } break;And catch it in main:
displayStyles(ts); - for (;;) + try { - Event ev = ts.waitEvent(); - handleEvent(ts, ev); + for (;;) + { + Event ev = ts.waitEvent(); + handleEvent(ts, ev); + } + } + catch (Exception e) + { + // normal exit } }Option 2: Return a boolean to signal exit
-void handleEvent(Screen ts, Event ev) +bool handleEvent(Screen ts, Event ev) // returns true to continue, false to 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); + return false; } break;And update main:
for (;;) { Event ev = ts.waitEvent(); - handleEvent(ts, ev); + if (!handleEvent(ts, ev)) + break; }
🧹 Nitpick comments (2)
demos/styles/source/styles.d (2)
18-23: Consider using size_t for safer arithmetic.Line 21 casts
str.length(size_t) to int, which could theoretically overflow for very long strings. While unlikely in practice for terminal display, using size_t throughout would be more type-safe.Apply this diff for improved type safety:
-void centerStr(Screen s, int y, Style style, string str) +void centerStr(Screen s, size_t y, Style style, string str) { s.style = style; - s.position = Coord((s.size.x - cast(int)(str.length)) / 2, y); + s.position = Coord(cast(int)((s.size.x - str.length) / 2), cast(int) y); s.write(str); }
160-179: Remove duplicate import.Line 162 imports
std.stdioagain, which is already imported at line 13.Apply this diff:
void main() { - import std.stdio; - auto ts = newScreen();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
demos/styles/dub.json(1 hunks)demos/styles/source/styles.d(1 hunks)source/dcell/attr.d(1 hunks)source/dcell/screen.d(1 hunks)source/dcell/style.d(1 hunks)source/dcell/ttyscreen.d(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- demos/styles/dub.json
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-08T19:17:33.678Z
Learnt from: gdamore
Repo: gdamore/dcell PR: 27
File: source/dcell/ttyscreen.d:448-494
Timestamp: 2025-12-08T19:17:33.678Z
Learning: In source/dcell/ttyscreen.d, Color.reset is defined as a value above the range of valid colors. In the sendColors() function, after emitting the reset sequence (vt.resetFgBg), Color.reset values naturally skip all subsequent color-setting logic (isRGB checks and palette range checks < 8, < 256) because they are out of range, which is the correct behavior by design.
Applied to files:
source/dcell/ttyscreen.dsource/dcell/style.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 (9)
source/dcell/style.d (1)
25-25: LGTM!The new
ulfield for underline color is well-documented and properly integrated. The default initialization toColor.initis handled correctly by the validity check insendColors()at line 597 of ttyscreen.d.source/dcell/screen.d (1)
225-225: LGTM!Changing the style getter to return a mutable reference enables more natural and efficient style manipulation patterns, as demonstrated in the styles demo. This is a well-justified breaking change.
demos/styles/source/styles.d (1)
25-135: LGTM!The style demonstrations are comprehensive and correctly showcase the new underline variants and colored underline features. The proper cleanup of style fields (ul, url) between demonstrations is well-handled.
source/dcell/attr.d (1)
22-38: LGTM!The underline bit-field design is well-structured and documented. The repositioning of attributes and the encoding of underline variants (using bit 6 for underline presence and bits 7-9 for style) provides a clean, extensible approach that supports up to 8 underline styles while preserving space for future attributes.
source/dcell/ttyscreen.d (5)
508-511: LGTM!The ref-returning style property correctly implements the Screen interface update and enables efficient in-place style modifications.
703-721: LGTM!The clearScreen logic correctly applies the baseStyle (saved when clear() was called) for the clear operation while preserving the current style. The save/restore mechanism ensures proper style management across screen clears.
597-613: LGTM!The underline color handling correctly checks validity and underline presence before emitting sequences. The logic properly handles Color.reset, RGB colors, and palette colors consistently with foreground/background handling. The
isValidcheck correctly filters out invalid colors via UFCS, and Color.reset is handled correctly as it's out of range and skips subsequent processing.
317-320: The baseStyle save/restore logic is correct.The mechanism properly preserves the style from when
clear()was called (line 319:baseStyle = style) and applies it duringclearScreen()(line 710:style = baseStyle). ThesavedStylepattern (lines 709, 719) correctly isolates the clear operation from affecting the current style state. Ifresize()setsclear_before any explicitclear()call,baseStyledefaults toStyle.initwithColor.blackvalues, whichsendColors()handles correctly as a valid palette color.
97-103: LGTM!The VT100 underline sequences at lines 97-103 follow the ISO 8613-6 colon-separated SGR format correctly. The implementation uses a sound fallback strategy: base underline (SGR 4) is sent first, followed by style modifiers (SGR 4:2 through 4:5), allowing older terminals that don't support extended underline styles to still display underlines while modern terminals render the styled variants (double, curly, dotted, dashed). Terminal support for these sequences is strong across modern emulators (Kitty, WezTerm, Windows Terminal, VTE-based terminals, iTerm2, Alacritty), with graceful degradation on older terminals.
This also changes the API for the style member to make it a bit more natural to use, and adds a demo program.
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.