feat: Merge box-drawing lines into junctions where they meet - #94
Merged
FXschwartz merged 38 commits intoAug 10, 2026
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves terminal UI rendering by introducing box-drawing “line blending” so overlapping borders/dividers merge into the correct Unicode junction glyphs (e.g. ├ ┤ ┬ ┴ ┼) instead of leaving gaps or overwriting each other.
Changes:
- Added arm/weight-based merging for box-drawing characters via
mergeBoxCharacters()/isMergeableBoxCharacter(). - Extended
TerminalCanvas.drawText()withblendBoxLinesto optionally merge box characters while drawing. - Enabled blending behavior for
Divider/VerticalDivider(including half-arm end caps for negative indents) and forBoxBordercorners; added tests + a demo example.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| test/utils/box_line_merging_test.dart | Unit tests for low-level box-character merging behavior (tees/crosses/weights/mergeability). |
| test/rendering/border_blend_test.dart | Rendering tests validating overlay border corners tee into underlying borders and background doesn’t erase underlying border ring cells. |
| test/components/divider_blend_test.dart | Rendering tests ensuring dividers tee into borders with negative indents and form crosses when intersecting. |
| lib/src/utils/box_line_merging.dart | Implements the arm/weight model and merge logic for Unicode box-drawing characters. |
| lib/src/framework/terminal_canvas.dart | Adds blendBoxLines option to drawText() to merge box-drawing characters with existing buffer content. |
| lib/src/components/divider.dart | Makes dividers blend by default (except ascii), adds half-arm caps for negative indents, and updates component docs. |
| lib/src/components/decorated_box.dart | Adjusts background fill to avoid erasing border ring cells; enables blending for border corner cells. |
| lib/nocterm.dart | Exports the new box line merging utilities. |
| example/box_line_blending_demo.dart | Adds an example showcasing divider/border blending and negative-indent behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Collaborator
|
@FXschwartz Really nice feature. I have suggested some changes here FXschwartz#1 that I think should be included before merging.
|
Same-weight pairs and cross-weight pairs with disjoint arms are order independent; shared arms let the newly drawn weight win.
A double divider reaching into a border merges as a cross (╪ ╫ ╬) instead of a tee (╞ ╡, ╥ ╨, ╠ ╣). The light-divider-into-double-border test passes, pinning the gap to double caps.
Unicode has half lines at light and heavy weight only, so a lone double arm has no glyph and the end cell fell back to ═/║, merging as a cross. Ends are now expressed as arms - mergeArmsIntoCharacter plus a blendArms override on drawText - so ╞ ╡ ╥ ╨ ╠ ╣ form.
Covers the arm combination Unicode cannot name, and ends landing on a cell with nothing to merge into.
A zero-extent rule reaching into a border painted only the border cell, leaving a tee pointing at a line that does not exist.
Stamping the half-arm regardless overwrote a title character, and punched holes in a border the arm had no glyph to join.
isMergeableBoxCharacter never had a caller; its tests went with it.
Directory.current is process-wide, not per-isolate, so tests that chdir to their own temp directory raced under isolate-per-test. getProjectDirectory now takes the directory to walk up from.
The path is keyed by process id - fine for one server per app, not for a test file that starts one per test under isolate-per-test, where they overwrote, deleted and read each other's ports.
Dividers already had heavy lines; borders did not, so ┠ ┨ ┰ ┸ had no way to occur.
Adds junctions the light-only version never showed: ├ from a dashed rule, ╥ ╨ from a double divider, ╢ where the two meet, and ┠ ┨ ┯ ┷ against the bold border.
The panel moves into the pane so it appears in both columns, and now carries a background: it hides what it covers while its corners still tee into the border. Title tests move to 80x40, where the panes have room.
Three dividers reach into one fixed-length title, so the columns are exact at any width rather than an 80-column coincidence.
drawJunction takes a position and the arms, nothing else. Leaving the cell alone when there is nothing to join falls out of it rather than being a branch inside drawText.
Junction review
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Borders and dividers previously painted their cells independently, so a divider ending at a border stopped one cell short or overwrote it, the UI showed gaps where ├ ┤ ┬ ┴ ┼ junctions belong.
What this does
Adds arm-based box-character merging (the approach ratatui/tview use): every char in U+2500–257F maps to four arms with weights, drawing one line char onto another ORs the arms and emits the junction glyph.
mergeBoxCharacters()+isMergeableBoxCharacter()inlib/src/utils/box_line_merging.dart, exported fromnocterm.dartTerminalCanvas.drawText(..., blendBoxLines:), the internal mechanism the components paint with, defaults to false so ordinary text never merges (a│inside a log line still overwrites its cell)Divider/VerticalDivider: always merge; negativeindent/endIndentreach into a border row/column and cap with half-arms (╷+─→┬)BoxBorder: corner cells always merge (overlaid panels tee into the border underneath), edges/titles/backgrounds occludeBehavior change
Blending is always on for
Divider,VerticalDivider, andBoxBorder, there is no per-component flag. Existing apps change appearance wherever lines overlap:│inside a log line) renders as a junction.Added Example
dart example/box_line_blending_demo.dart- side-by-sideindent: 0vsindent: -1, crossings, mixed-weight tees, and an overlaid panel.