Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@gdamore
Copy link
Owner

@gdamore gdamore commented Dec 14, 2025

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

    • Multiple underline styles added (plain, double, curly, dotted, dashed) and independent underline color support (including RGB).
    • New interactive demo demonstrating styling, live resize handling, and exit controls.
  • Bug Fixes

    • Styles can now be modified and applied reliably at runtime, improving live rendering and clear/restore behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 14, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
New styles demo
demos/styles/dub.json, demos/styles/source/styles.d
Adds a new D demo project and styles.d demo that renders various text styles, handles resize/exit events, and demonstrates underline variants and colors.
Attribute enum expansion
source/dcell/attr.d
Reorders attribute bit assignments (underline/dim/italic/strikethrough), adds underline styling bit-field constants (plainUnderline, doubleUnderline, curlyUnderline, dottedUnderline, dashedUnderline, underlineMask) and introduces invalid (alias init).
Style struct enhancement
source/dcell/style.d
Adds a new public Color ul field to Style for underline color.
Screen API update
source/dcell/screen.d
Changes @property getter style() from returning a const Style to returning ref Style (mutable reference).
Terminal rendering implementation
source/dcell/ttyscreen.d
Adds VT100/OSC sequences for underline variants and underline color (doubleUnder, curlyUnder, dottedUnder, dashedUnder, underColor, underRGB, underFg); updates sendColors() and sendAttrs() to emit underline/color sequences and map new Attr variants; adds savedStyle/baseStyle handling around clears.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to: Attr bit reassignment and mask correctness; composition/usage of underline bit-field constants and mask; implications of ref return for Screen.style() (aliasing and safety); VT sequence emission logic for underline colors (palette vs RGB) in ttyscreen.d.

Possibly related PRs

Poem

🐰 I nibble code in twilight's light,
underlines dance, some thin, some bright.
double, curly, dotted, dashed in play,
colored trails that hop and sway.
— the little Style Rabbit 🎨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat!: Add support for underline styling (fixes #28)' accurately captures the main change—adding underline styling support with breaking API changes—and is clear, concise, and specific.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch gdamore/underline-styles

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.underline is 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.doubleUnder before emitting. While this works, it differs from the pattern used for other attributes (lines 658-669) which call puts() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5edab4b and 0c979de.

📒 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 Style matches the interface definition in screen.d and correctly returns a reference to the internal cells.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 numColors and terminal type. RGB underline colors are only applied when vt.numColors > 256 (line 597), and older terminals matching patterns like vt* 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, and strikethrough have been shifted to make room for extended underline styles. This is a breaking change marked as feat!: in the commit, and it is intentional. The codebase contains no serialized or persisted Attr values, 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 Style instead of a copy, enabling direct mutation patterns like screen.style.fg = Color.red. This is consistent with opIndex, which also returns ref 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.

@gdamore gdamore force-pushed the gdamore/underline-styles branch from 0c979de to 3eea5ed Compare December 14, 2025 06:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 of Attr.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 a as a placeholder variable, which could be clearer. Consider specifying attr as 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0c979de and 3eea5ed.

📒 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.d
  • source/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 ul field defaults to Color.invalid and is properly guarded in sendColors() at line 597 of ttyscreen.d with the check if (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 Style instead of const Style enables 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, and strikethrough free 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 invalid and aliasing init to 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 Style enables 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:

  1. The underline color is explicitly set (not Color.invalid)
  2. An underline attribute is present

This correctly handles the default initialization of ul to Color.invalid.


707-720: LGTM! Correct save/restore pattern for style during clear.

The clearScreen() function properly saves the current style before applying baseStyle for the clear operation, then restores it afterward. This ensures the clear uses the style that was active when clear() was called (saved at line 319), while preserving the current write position's style.

@gdamore gdamore force-pushed the gdamore/underline-styles branch from 3eea5ed to 5b3db7a Compare December 14, 2025 06:59
This also changes the API for the style member to make it a bit more
natural to use, and adds a demo program.
@gdamore gdamore force-pushed the gdamore/underline-styles branch from 5b3db7a to 5b7d285 Compare December 14, 2025 07:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: (lowercase attr) instead of case Attr.doubleUnderline: (uppercase Attr). 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. The scope(exit) block at lines 166-169 won't execute when exit() 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.stdio again, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3eea5ed and 5b7d285.

📒 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.d
  • source/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 ul field for underline color is well-documented and properly integrated. The default initialization to Color.init is handled correctly by the validity check in sendColors() 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 isValid check 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 during clearScreen() (line 710: style = baseStyle). The savedStyle pattern (lines 709, 719) correctly isolates the clear operation from affecting the current style state. If resize() sets clear_ before any explicit clear() call, baseStyle defaults to Style.init with Color.black values, which sendColors() 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.

@gdamore gdamore merged commit 6031c94 into main Dec 14, 2025
5 checks passed
@gdamore gdamore deleted the gdamore/underline-styles branch December 14, 2025 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants