Skip to content

fix(core): detect style-only line changes in diff renderer#709

Merged
Karanjot786 merged 2 commits into
Karanjot786:mainfrom
Siddh2024:main
Jun 5, 2026
Merged

fix(core): detect style-only line changes in diff renderer#709
Karanjot786 merged 2 commits into
Karanjot786:mainfrom
Siddh2024:main

Conversation

@Siddh2024
Copy link
Copy Markdown
Contributor

@Siddh2024 Siddh2024 commented Jun 4, 2026

Description

The differential renderer's line-level comparison only checked character text (\getLine(r) === getPreviousLine(r)), completely ignoring style attributes. When a widget updated only colors, bold, italic, or other style attributes without changing text content, the renderer skipped the line — making style changes invisible.

Root Cause

In \Renderer._flush()\ at line 130:
\\ s
// Before: only text comparison
if (this._screen.getLine(r) === this._screen.getPreviousLine(r)) continue;
\\

\getLine()\ returns only the character text (.char) joined together. Style attributes like \ g, \�g, \�old, \italic, \underline, \dim, \strikethrough, \inverse, and \link\ are completely ignored.

The Fix

  1. **\Screen.ts**: Added \getStyleLine()\ which computes a fast fingerprint hash of all style attributes for each cell in the row, and \getPreviousStyleLine()\ to retrieve the saved fingerprint. \saveLines()\ now stores both text and style fingerprints.

  2. **\Renderer.ts**: The diff check now requires BOTH text AND style to be unchanged before skipping a line:
    \\ s
    // After: text AND style comparison
    if (this._screen.getLine(r) === this._screen.getPreviousLine(r)
    && this._screen.getStyleLine(r) === this._screen.getPreviousStyleLine(r)) continue;
    \\

Closes #706

Summary by CodeRabbit

  • Bug Fixes

    • Improved terminal rendering accuracy by properly detecting style-only changes (colors, text formatting). Previously, visual updates were inconsistently applied when content styling changed without text modifications.
  • Performance

    • Enhanced differential rendering efficiency to more accurately determine which screen areas require redrawing.

@Siddh2024 Siddh2024 requested a review from Karanjot786 as a code owner June 4, 2026 13:34
@github-actions github-actions Bot added type:bug +10 pts. Bug fix. area:core @termuijs/core and removed type:bug +10 pts. Bug fix. labels Jun 4, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 052e22d6-dc28-4d1d-9d70-47cd4bc6696e

📥 Commits

Reviewing files that changed from the base of the PR and between ef0934d and 04e5704.

📒 Files selected for processing (2)
  • packages/core/src/terminal/Renderer.ts
  • packages/core/src/terminal/Screen.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/terminal/Renderer.ts
  • packages/core/src/terminal/Screen.ts

📝 Walkthrough

Walkthrough

Terminal rendering now detects style-only changes. Screen computes per-row style fingerprints from cell attributes and snapshots them; Renderer's differential renderer skips emitting rows only when both text content and style match previous frames.

Changes

Style-aware differential rendering

Layer / File(s) Summary
Style fingerprinting infrastructure in Screen
packages/core/src/terminal/Screen.ts
Screen adds _previousStyleLines buffer, computes style fingerprints via getStyleLine(row) from cell foreground/background types and text attributes, exposes getPreviousStyleLine(row) accessor, and snapshots both text and style lines in saveLines().
Apply style-aware comparison in differential rendering
packages/core/src/terminal/Renderer.ts
Renderer's diff fast-path now skips row emission only when both the current line text and style fingerprint match their previous-buffer counterparts, enabling detection of style-only changes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #681: Both changes manage Screen's diffing caches; this PR adds _previousStyleLines fingerprinting alongside existing _previousLines, so the same cache-reset mechanism applies to the new style buffer.

Poem

🐰 Style whispers now in diff's keen ear,
Where colors shift but text stays clear,
The renderer sees what once was missed—
A painted change won't be dismissed! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(core): detect style-only line changes in diff renderer' accurately describes the main change: enabling the differential renderer to detect and handle style-only modifications.
Description check ✅ Passed The description provides clear explanation of the bug, root cause, and the fix with code examples. However, it's missing the 'Related Issue' link section with the issue number that should be formatted as 'Closes #706'.
Linked Issues check ✅ Passed The PR successfully addresses issue #706 by implementing style-aware diffing in the terminal renderer. It adds getStyleLine() for style fingerprinting and updates the diff comparison to check both text and style content before skipping rows.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the style-only change detection issue. Modifications to Screen.ts and Renderer.ts are focused and necessary to implement the required style fingerprinting and diff comparison logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions github-actions Bot added the needs-star PR author has not starred the repo. label Jun 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Hi @Siddh2024 👋

Star this repo before your PR merges.

Why? GSSoC 2026 contributors who star get priority review and points credit. After you star, push any commit (or re-run this check). The needs-star label lifts automatically.

Thanks for your contribution to TermUI.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Thanks for your first PR to TermUI, @Siddh2024.

Before your PR merges:

  1. Star the repo. Required. The star-check job blocks your merge otherwise.
  2. ✅ All checks green: build, test, typecheck.
  3. 🏷 PR title follows type: short description. Example: fix: handle empty list.
  4. 🔗 Link your closing issue in the description.

GSSoC 2026 points come from labels after merge:

  • gssoc:approved. +50 base points.
  • level:beginner / intermediate / advanced / critical. +20 / +35 / +55 / +80.
  • quality:clean / exceptional. x 1.2 / x 1.5.
  • type:*. Stackable bonus.

Your reviewer responds within 48 hours. Ping @Karanjot786 on Discord for urgent help.

🚀 Welcome to the cohort.

@github-actions github-actions Bot added the type:bug +10 pts. Bug fix. label Jun 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Hi @Siddh2024 👋

Star this repo before your PR merges.

Why? GSSoC 2026 contributors who star get priority review and points credit. After you star, push any commit (or re-run this check). The needs-star label lifts automatically.

Thanks for your contribution to TermUI.

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/terminal/Screen.ts`:
- Around line 154-164: The style fingerprint currently only uses the first
character of fg.type/bg.type which collapses distinct colors; update the seed
computation in Screen (around the seed/hash logic using cell, fg, bg, bits) to
incorporate the full color payload rather than charCodeAt(0) of the type:
include both the color type and the actual color value (e.g., combine fg.type
and its payload/value/string representation and same for bg) when deriving seed
so different named/hex/rgb values produce different seeds before updating hash.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bbf61fd-a54f-4b66-af46-658fb83ce7f1

📥 Commits

Reviewing files that changed from the base of the PR and between c36cc8c and 648743f.

📒 Files selected for processing (2)
  • packages/core/src/terminal/Renderer.ts
  • packages/core/src/terminal/Screen.ts

Comment on lines +154 to +164
const fg = cell.fg.type;
const bg = cell.bg.type;
const bits =
(cell.bold ? 1 : 0) |
(cell.italic ? 2 : 0) |
(cell.underline ? 4 : 0) |
(cell.dim ? 8 : 0) |
(cell.strikethrough ? 16 : 0) |
(cell.inverse ? 32 : 0);
const seed = fg.charCodeAt(0) * 65536 + bg.charCodeAt(0) * 4096 + bits;
hash = ((hash << 7) - hash + seed) | 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Style fingerprint currently misses real color changes within the same color type.

On Line 154–Line 164, the fingerprint seed only uses fg.type/bg.type (and only their first character), not the actual color payload. This means many style-only color edits still hash equal (e.g., named:rednamed:blue, and even none vs named both start with 'n'), so rows can be incorrectly skipped.

Suggested fix (allocation-light, includes full color payload)
@@
     getStyleLine(row: number): string {
         if (row < 0 || row >= this._rows) return '';
         let hash = 0;
+        const colorSeed = (color: Color): number => {
+            switch (color.type) {
+                case 'none':
+                    return 1;
+                case 'named': {
+                    let h = 2;
+                    for (let i = 0; i < color.name.length; i++) h = ((h << 5) - h + color.name.charCodeAt(i)) | 0;
+                    return h;
+                }
+                case 'ansi256':
+                    return 3 + color.code;
+                case 'rgb':
+                    return 4 + ((color.r << 16) | (color.g << 8) | color.b);
+                case 'hex': {
+                    let h = 5;
+                    for (let i = 0; i < color.hex.length; i++) h = ((h << 5) - h + color.hex.charCodeAt(i)) | 0;
+                    return h;
+                }
+            }
+        };
         for (const cell of this.back[row]) {
             if (cell.width === 0) continue;
-            const fg = cell.fg.type;
-            const bg = cell.bg.type;
             const bits =
                 (cell.bold ? 1 : 0) |
                 (cell.italic ? 2 : 0) |
                 (cell.underline ? 4 : 0) |
                 (cell.dim ? 8 : 0) |
                 (cell.strikethrough ? 16 : 0) |
                 (cell.inverse ? 32 : 0);
-            const seed = fg.charCodeAt(0) * 65536 + bg.charCodeAt(0) * 4096 + bits;
+            const seed = colorSeed(cell.fg) * 65536 + colorSeed(cell.bg) * 4096 + bits;
             hash = ((hash << 7) - hash + seed) | 0;
             if (cell.link) {
                 for (let i = 0; i < cell.link.length; i++)
                     hash = ((hash << 5) - hash + cell.link.charCodeAt(i)) | 0;
             }
         }
         return String(hash);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/terminal/Screen.ts` around lines 154 - 164, The style
fingerprint currently only uses the first character of fg.type/bg.type which
collapses distinct colors; update the seed computation in Screen (around the
seed/hash logic using cell, fg, bg, bits) to incorporate the full color payload
rather than charCodeAt(0) of the type: include both the color type and the
actual color value (e.g., combine fg.type and its payload/value/string
representation and same for bg) when deriving seed so different named/hex/rgb
values produce different seeds before updating hash.

@github-actions github-actions Bot removed the needs-star PR author has not starred the repo. label Jun 4, 2026
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
packages/create-termui-app/src/index.ts (1)

16-29: ⚡ Quick win

Consider additional validation for cross-platform robustness.

The regex and explicit "." / ".." checks effectively prevent path traversal. However, a few edge cases could improve robustness:

  1. Windows reserved names: Names like "CON", "PRN", "AUX", "NUL", "COM1"-"COM9", "LPT1"-"LPT9" will cause mkdirSync to fail on Windows with a cryptic error.
  2. Trailing dots: The regex allows names ending with a dot (e.g., "myapp."), which can cause issues on Windows where trailing dots in filenames are sometimes ignored.
  3. Scoped package message: The error message mentions "start with @ for scoped packages," but the regex doesn't allow /, so proper scoped package names like "@org/package" are rejected. Consider clarifying the message (e.g., "or start with @ for scoped names") or documenting that / is intentionally disallowed.
🛡️ Suggested enhancement for Windows reserved names
+const WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
+
 function validateProjectName(name: string, source: string): void {
     if (!VALID_PROJECT_NAME_RE.test(name)) {
         throw new Error(
             `Invalid project name "${name}" (from ${source}). Use only letters, digits, hyphens, underscores, dots, or start with @ for scoped packages.`
         );
     }
     if (name === "." || name === "..") {
         throw new Error(
             `Invalid project name "${name}". "." and ".." are not allowed as project names.`
         );
     }
+    if (WINDOWS_RESERVED_NAMES.test(name)) {
+        throw new Error(
+            `Invalid project name "${name}". "${name.toUpperCase()}" is a reserved name on Windows.`
+        );
+    }
+    if (name.endsWith(".")) {
+        throw new Error(
+            `Invalid project name "${name}". Project names cannot end with a dot.`
+        );
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create-termui-app/src/index.ts` around lines 16 - 29, The
validateProjectName function currently uses VALID_PROJECT_NAME_RE and explicit
"."/".." checks but misses Windows-reserved device names, trailing dots, and
misleads about scoped names; update validateProjectName to (1) reject Windows
reserved names case-insensitively (e.g., CON, PRN, AUX, NUL, COM1–COM9,
LPT1–LPT9) by comparing name (and name without trailing dots) against that set,
(2) reject names that end with a dot or contain trailing spaces/dots (trim and
check for '.' suffix) to avoid Windows filesystem quirks, and (3) either allow
scoped-style names (contain a single '/' after an '@') or change the error text
to say "start with @ for scoped names" so the message matches
VALID_PROJECT_NAME_RE; reference VALID_PROJECT_NAME_RE and the
validateProjectName function when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/create-termui-app/src/index.ts`:
- Around line 16-29: The validateProjectName function currently uses
VALID_PROJECT_NAME_RE and explicit "."/".." checks but misses Windows-reserved
device names, trailing dots, and misleads about scoped names; update
validateProjectName to (1) reject Windows reserved names case-insensitively
(e.g., CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9) by comparing name (and name
without trailing dots) against that set, (2) reject names that end with a dot or
contain trailing spaces/dots (trim and check for '.' suffix) to avoid Windows
filesystem quirks, and (3) either allow scoped-style names (contain a single '/'
after an '@') or change the error text to say "start with @ for scoped names" so
the message matches VALID_PROJECT_NAME_RE; reference VALID_PROJECT_NAME_RE and
the validateProjectName function when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2b71488-a463-4f09-8584-f71f81fcb7dd

📥 Commits

Reviewing files that changed from the base of the PR and between 648743f and ef0934d.

📒 Files selected for processing (1)
  • packages/create-termui-app/src/index.ts

@Karanjot786 Karanjot786 added gssoc:approved Approved PR. Earns +50 base points. quality:clean x 1.2 multiplier. Clean implementation. level:intermediate +35 pts. Moderate task. type:feature +10 pts. New feature. labels Jun 4, 2026
@Karanjot786 Karanjot786 merged commit 0f5e969 into Karanjot786:main Jun 5, 2026
7 checks passed
@Karanjot786
Copy link
Copy Markdown
Owner

Thank you so much for your contribution, @Siddh2024!

Your PR #709 fixed the diff renderer to detect style-only line changes. Terminals now repaint correctly when only colors or styles change without content changes.

Keep contributing. We appreciate your work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core @termuijs/core gssoc:approved Approved PR. Earns +50 base points. level:intermediate +35 pts. Moderate task. quality:clean x 1.2 multiplier. Clean implementation. type:bug +10 pts. Bug fix. type:feature +10 pts. New feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Path travsersal issue

2 participants