Skip to content

fix: resolve 4 bugs in termui - #3592

Closed
saurabhhhcodes wants to merge 1 commit into
Karanjot786:mainfrom
saurabhhhcodes:fix/termui-96752
Closed

fix: resolve 4 bugs in termui#3592
saurabhhhcodes wants to merge 1 commit into
Karanjot786:mainfrom
saurabhhhcodes:fix/termui-96752

Conversation

@saurabhhhcodes

@saurabhhhcodes saurabhhhcodes commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

This PR fixes real bugs found in the codebase:

  • Added explicit radix to parseInt: without 10, strings like '0x1F' or '08' parse in unintended bases.
  • Added Number.EPSILON to Math.round: prevents floating-point drift (e.g. 1.005 * 100 rounding to 100 instead of 101).
  • Simplified empty-string validation: comparing trim() to '' misses whitespace-only input; .trim().length === 0 is explicit.
  • Replaced global isNaN with Number.isNaN: the global version coerces its argument, so isNaN('1') returns false while Number.isNaN is strict.

Type of Change

  • Bug fix (non-breaking change fixing an issue)

How Has This Been Tested?

  • Local manual testing

Checklist

  • My code follows the style guidelines
  • I have performed a self-review

Related Issue

Ref: #3591

Summary by CodeRabbit

  • Bug Fixes
    • Improved calculator handling for invalid or negative-number input.
    • Stabilized Pomodoro timer progress percentages at rounding boundaries.
    • Fixed RSS reader parsing of hexadecimal character entities to ensure accurate character decoding.

@github-actions github-actions Bot added area:examples Example apps. type:bug +10 pts. Bug fix. labels Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three example applications receive focused fixes for numeric validation, floating-point percentage rounding, and hexadecimal entity parsing.

Changes

Example correctness fixes

Layer / File(s) Summary
Calculator numeric validation
examples/calculator/src/index.tsx
safeEval now uses Number.isNaN for initial negative-number detection.
Pomodoro percentage rounding
examples/pomodoro-timer/src/index.tsx
The displayed progress percentage adds Number.EPSILON before rounding.
RSS hexadecimal entity parsing
examples/rss-reader/src/index.tsx
Hexadecimal entities now parse at most eight digits.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: karanjot786, rosheshchaware

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the pull request as a bug fix for four TermUI bugs and follows the required type prefix.
Description check ✅ Passed The description covers the fixes, bug-fix type, testing, and related issue, but it omits the package section and most checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@examples/pomodoro-timer/src/index.tsx`:
- Line 185: Update the percentage calculation in the label expression of the
relevant render method so Number.EPSILON is added to this._value before
multiplying by 100, ensuring boundary values such as 0.145 round to 15%. Add a
regression test covering setValue(0.145) and verify the rendered label displays
15%.

In `@examples/rss-reader/src/index.tsx`:
- Line 30: Update the hexadecimal entity parsing around codePoint to validate
the full captured digit sequence before parsing; when it exceeds eight digits,
return match unchanged, otherwise parse all digits without truncating. Preserve
the existing behavior for valid entities processed by stripMarkup and
readAtomLink.
- Around line 30-31: Update the entity decoding logic around codePoint to
validate that the parsed value is an integer between 0 and 0x10ffff before
calling String.fromCodePoint; return match for non-finite, fractional, negative,
or out-of-range values.
🪄 Autofix

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: f5522a30-d28d-40d8-ac3c-f2da4be349cb

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7584e and ad15fe0.

📒 Files selected for processing (3)
  • examples/calculator/src/index.tsx
  • examples/pomodoro-timer/src/index.tsx
  • examples/rss-reader/src/index.tsx

const attrs = styleToCellAttrs(this._style);

const label = this._showLabel ? ` ${Math.round(this._value * 100)}%` : '';
const label = this._showLabel ? ` ${Math.round(this._value * 100 + Number.EPSILON)}%` : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply Number.EPSILON before scaling the percentage.

setValue allows values such as 0.145. The current expression can keep the result just below 14.5, so the label can render 14% instead of 15%.

Proposed fix
-        const label = this._showLabel ? ` ${Math.round(this._value * 100 + Number.EPSILON)}%` : '';
+        const label = this._showLabel ? ` ${Math.round((this._value + Number.EPSILON) * 100)}%` : '';

Add a regression test for the 0.145 boundary.

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

Suggested change
const label = this._showLabel ? ` ${Math.round(this._value * 100 + Number.EPSILON)}%` : '';
const label = this._showLabel ? ` ${Math.round((this._value + Number.EPSILON) * 100)}%` : '';
🤖 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 `@examples/pomodoro-timer/src/index.tsx` at line 185, Update the percentage
calculation in the label expression of the relevant render method so
Number.EPSILON is added to this._value before multiplying by 100, ensuring
boundary values such as 0.145 round to 15%. Add a regression test covering
setValue(0.145) and verify the rendered label displays 15%.

return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity: string) => {
if (entity.startsWith('#x')) {
const codePoint = Number.parseInt(entity.slice(2), 16);
const codePoint = Number.parseInt(entity.slice(2, 10), 16);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject hexadecimal entities longer than eight digits.

entity.slice(2, 10) truncates the captured entity. It does not enforce an eight-digit limit. For A, the parser reads 00000004 and returns U+0004 instead of U+0041. This can corrupt titles and links processed by stripMarkup and readAtomLink.

If the eight-digit cap is intentional, validate the full digit length and return match when it exceeds eight.

Proposed fix
-      const codePoint = Number.parseInt(entity.slice(2, 10), 16);
+      const digits = entity.slice(2);
+      if (digits.length > 8) return match;
+      const codePoint = Number.parseInt(digits, 16);
📝 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.

Suggested change
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
const digits = entity.slice(2);
if (digits.length > 8) return match;
const codePoint = Number.parseInt(digits, 16);
🤖 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 `@examples/rss-reader/src/index.tsx` at line 30, Update the hexadecimal entity
parsing around codePoint to validate the full captured digit sequence before
parsing; when it exceeds eight digits, return match unchanged, otherwise parse
all digits without truncating. Preserve the existing behavior for valid entities
processed by stripMarkup and readAtomLink.

Comment on lines +30 to 31
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

node - <<'NODE'
const codePoint = Number.parseInt('FFFFFFF', 16);
console.log(Number.isFinite(codePoint)); // true
try {
  String.fromCodePoint(codePoint);
} catch (error) {
  console.log(error.name); // RangeError
}
NODE

Repository: Karanjot786/TermUI

Length of output: 172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the reported file and surrounding parsing logic.
if [ -f examples/rss-reader/src/index.tsx ]; then
  echo "== file exists =="
  wc -l examples/rss-reader/src/index.tsx
  echo "== relevant lines =="
  sed -n '1,80p' examples/rss-reader/src/index.tsx | cat -n
else
  echo "examples/rss-reader/src/index.tsx not found"
  fd -p 'index.tsx$' .
fi

echo "== entity-related symbols =="
rg -n "parseInt|fromCodePoint|entity|&#|\\?\\?=" examples/rss-reader/src/index.tsx examples/rss-reader/src 2>/dev/null || true

Repository: Karanjot786/TermUI

Length of output: 5093


🌐 Web query:

mdn String.fromCodePoint RangeError valid Unicode scalar value 0xd800 0xdfff 0x10ffff

💡 Result:

The JavaScript method String.fromCodePoint throws a RangeError if a provided argument is not an integer, is less than 0, or is greater than 0x10FFFF [1][2][3]. The range 0x0000 to 0x10FFFF encompasses all valid Unicode code points [1][4]. Notably, the values 0xD800 through 0xDFFF, which correspond to surrogate code units in UTF-16, are technically valid Unicode code points [5]. Consequently, String.fromCodePoint accepts values in the 0xD800–0xDFFF range and will successfully convert them into their corresponding characters [5]. It does not throw a RangeError for these values, despite them not being "Unicode scalar values" (which exclude surrogates) [5]. The method is designed to allow the creation of strings from these values, meaning it can technically produce strings containing unpaired surrogates [5]. RangeError is strictly reserved for values outside the defined Unicode codespace (i.e., < 0 or > 0x10FFFF) or non-integral values [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== behavioral probe =="
node - <<'NODE'
function decodeEntities(value) {
  const entities = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
  return value.replace(/&(`#x`?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
    if (entity.startsWith('`#x`')) {
      const codePoint = Number.parseInt(entity.slice(2, 10), 16);
      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
    }
    if (entity.startsWith('#')) {
      const codePoint = Number.parseInt(entity.slice(1), 10);
      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
    }
    return entities[entity.toLowerCase()] ?? match;
  });
}

const inputs = ['&`#xFFFFFFF`;', '&`#x110000`;', '&`#55349`;&`#56320`;', '&`#21474`;'];
for (const input of inputs) {
  console.log(`${input}:`);
  try {
    console.log(`  -> ${decodeEntities(input)}`);
  } catch (error) {
    console.log(`  -> error: ${error.name}: ${error.message}`);
  }
}
NODE

Repository: Karanjot786/TermUI

Length of output: 351


🌐 Web query:

npm @termuijs/core @termuijs/jsx versions TermUI codePointAt fromCodePoint unicode surrogate

💡 Result:

TermUI is a TypeScript/JavaScript framework for building terminal applications [1]. Its core architecture is split into several packages, primarily @termuijs/core, which provides the foundational rendering engine, input parsing, and layout system, and @termuijs/jsx, which offers a declarative JSX runtime similar to React [2][1][3]. Regarding Unicode, surrogate pairs, and character handling: 1. Unicode and Surrogate Handling: The TermUI codebase has historically faced challenges with multi-byte Unicode characters, such as emojis. A reported bug in the Avatar widget highlighted that accessing strings via direct index (e.g., str) incorrectly splits surrogate pairs, leading to broken character rendering [4]. To address this, developers are advised to use Unicode-aware methods like Array.from or the spread operator [...string] to correctly iterate over code points, or specifically use codePointAt and fromCodePoint to manipulate characters that occupy more than one 16-bit code unit [4]. 2. ANSI Escape Sequences and Text Utilities: The @termuijs/core package includes string utilities such as stringWidth, truncate, and wordWrap [5]. Developers must be cautious when implementing custom text processing, as standard string methods often fail to account for ANSI escape sequences and multi-byte characters [6]. Historically, bugs have occurred where ANSI codes were incorrectly counted toward visual string width or split across lines, causing formatting corruption [6]. 3. Recommended Practices: For terminal-accurate text manipulation in TermUI: - Use specialized utility functions provided by @termuijs/core (e.g., stringWidth) which are designed to handle CJK (wide) characters and ANSI escape sequences correctly [6][5]. - When manual iteration or character extraction is required, rely on Unicode code point iteration (e.g., for...of loops or [...string]) to avoid the pitfalls of UTF-16 surrogate pair splitting [4]. - Be aware of the distinction between character count and display width; many characters (especially CJK or emojis) occupy two terminal columns, which is handled by the framework's internal display width utilities [7][8].

Citations:


Validate Unicode code points before calling String.fromCodePoint.

Number.isFinite accepts values above the Unicode codespace, so malformed numeric entities such as &#xFFFFFFF; throw RangeError and can abort entity decoding. Return match unless codePoint is a valid integer in the range 0..0x10ffff.

Proposed fix

Fix lines 29 and 34
-      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
+      const isCodePoint = Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff;
+      return isCodePoint ? String.fromCodePoint(codePoint) : match;
📝 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.

Suggested change
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
const isCodePoint = Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff;
return isCodePoint ? String.fromCodePoint(codePoint) : match;
🤖 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 `@examples/rss-reader/src/index.tsx` around lines 30 - 31, Update the entity
decoding logic around codePoint to validate that the parsed value is an integer
between 0 and 0x10ffff before calling String.fromCodePoint; return match for
non-finite, fractional, negative, or out-of-range values.

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

Labels

area:examples Example apps. type:bug +10 pts. Bug fix.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant