Skip to content

feat: hex color strings for directive color fields - #146

Open
natemoo-re wants to merge 6 commits into
mainfrom
feat/hex-colors
Open

natemoo-re wants to merge 6 commits into
mainfrom
feat/hex-colors

Conversation

@natemoo-re

@natemoo-re natemoo-re commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Stacked on #139.

Implements support for CSS-styled hex color strings alongside rgba(). There's no C or wire format change, hex values are expanded to their rgba() equivalent at pack() time.

open("root", { bg: "#f80" })
text("hi", { color: "#ff8800aa" })

Supported hex grammar is

Form Meaning Alpha
#RGB digit d expands as d×17 (#f80 → #ff8800) 255
#RGBA 3-digit RGB + 1-digit alpha, expanded the same way (#f80a → #ff8800aa) from 4th digit
#RRGGBB standard RGB 255
#RRGGBBAA standard RGBA 2-digit alpha

… capability

Full-screen frames wrap in BSU/ESU when RuntimeCapabilities.syncOutput is
true; the flag rides reduce() per frame so a capability flip takes effect
on the next rendered frame with no invalidation signal. Zero-diff frames
emit zero bytes even when wrapped, and line mode is never wrapped.

Renderer-spec 7.9 discharges the deferral recorded in 7.8.
Color fields (open bg, border color/bg, border side color/bg, text
color/bg) accept a hex color string in addition to the packed ARGB
number. Hex grammar: '#' + exactly 3/4/6/8 hex digits, case-insensitive;
3/4-digit forms double each digit; missing alpha defaults to 255.
Conversion happens at the transfer-encoding boundary — hex strings never
appear on the wire; packed values are bit-identical to the equivalent
rgba() result. Invalid hex is invalid directive input: validate()
reports it and the packer MUST throw a TypeError before producing bytes.
Static types stay number | string; directive constructors check string
literals against the grammar at compile time (the TS equivalent of
#${string{3,4,6,8}}), reserving room for full CSS Color 4 syntax later.
Color fields (open bg, border color/bg, border side color/bg, text
color/bg) accept '#RGB', '#RGBA', '#RRGGBB', and '#RRGGBBAA' alongside
packed ARGB numbers. colorValue() expands strings at the transfer-
encoding boundary (RRGGBBAA digit order -> AARRGGBB word layout, 3/4-
digit forms double each digit, missing alpha = 255); the wire format
and the C renderer are untouched. Directive constructors type-check
string literals against the grammar via per-character recursion — a
flat template union is unrepresentable (TS2590) at 6/8 digits — while
computed strings pass through and are checked normatively by validate()
and the packer. Invalid hex throws TypeError before any bytes are
produced.
@bombshell-cooper

bombshell-cooper Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Changeset suggestion

The current changeset no longer matches this PR. This review contains a corrected replacement.

Why: Changeset should start with an approved active present-tense verb. Changeset package scope does not match the affected packages confidently. Changeset contains unsupported consumer-facing claims.

View the proposed changeset
---
'@bomb.sh/tty': minor
---

Adds CSS color string support to all directive color fields.

`open({ bg })`, `border.color`, `border.bg`, per-side `color`/`bg`, and `text({ color, bg })` now accept CSS color strings alongside packed ARGB numbers from `rgba()`. Accepted forms: hex (`#f80`, `#ff8800aa`), named colors (`rebeccapurple`, `transparent`), and `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, and `oklch()`. Strings resolve to sRGB at the transfer-encoding boundary; the wire format, snapshots, and renderer are unchanged. Out-of-gamut colors clip per channel.

Invalid strings are caught at every layer: string literals are rejected at compile time by `open()` and `text()`, `validate()` rejects them as invalid field values, and `pack()` throws a `TypeError` before producing any bytes.

#### Migration

The `Color` type is new — color fields that previously accepted only `number` now accept `number | string`. Passing a plain `number` continues to work without any change.

```diff
- open("box", { bg: rgba(255, 136, 0) })
+ open("box", { bg: "#ff8800" })   // string form, or keep the rgba() call
  open("box", { bg: rgba(255, 136, 0) }) // unchanged — numbers still work
```

If you pass color values typed as `string` at runtime, `validate()` and `pack()` will reject strings that do not match the CSS color grammar. Ensure any dynamically constructed color strings are valid before passing them to a directive.

Review this changeset manually

If this draft is correct, react with 🚀 and Cooper will commit it to this branch.

Comment thread .changeset/hex-color-strings.md Outdated
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Size Increased — +3.0 KB

132.4 KB unpacked

@pkg-pr-new

pkg-pr-new Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@bomb.sh/tty@146

commit: 2055853

@natemoo-re
natemoo-re marked this pull request as ready for review October 12, 2026 01:07
Base automatically changed from feat/sync-output to main October 19, 2026 11:58
# Conflicts:
#	.changeset/sync-output-capability.md
#	src/clayterm.c
#	src/clayterm.h
#	term-native.ts
#	term.ts
#	test/term.test.ts
#	test/width.test.ts
Widens the §8.5 string form from hex-only to CSS Color 4: hex, named
colors and transparent, rgb()/rgba() and hsl()/hsla() (modern and
legacy), hwb(), lab(), lch(), oklab(), oklch(). Strings resolve to sRGB
at the transfer-encoding boundary; channels clamp to [0, 255] and round,
alpha clamps to [0, 1] and scales to 255. Out-of-gamut colors clip per
channel with no perceptual gamut mapping. Hex results stay bit-identical
to the previous grammar, and the wire format is unchanged.

color(), color-mix(), relative color syntax, and device-cmyk() are
invalid. System colors and currentcolor are reserved: system colors will
map to the themable ANSI 16-color palette, which packed sRGB cannot
express, so both are specified separately.

Static checking extends to named-color literals and function-name
prefixes; function arguments are checked at runtime only.
Color strings now resolve through hueful's parse() + convert() instead
of the hand-rolled hex expander. A new internal color.ts owns
resolveColor(), shared by the packer (colorValue in ops.ts) and
validate(), which swaps its hex regex for a TypeBox "css-color" format
backed by the same resolver, so the two can't drift.

hueful returns unclamped sRGB for wide-gamut spaces, so each channel and
alpha clamps to [0, 1] before scaling to a byte; this is the §8.5
per-channel clip. hueful also parses device-cmyk(), which §8.5 excludes,
so the cmyk space is rejected explicitly. System colors and currentcolor
aren't in hueful's name table and fall out as invalid with no extra code.

Literal checking keeps the per-character hex recursion and adds two
branches: literals containing "(" must lowercase to a supported
function prefix, and anything else must lowercase to a key of hueful's
names export (or "transparent"). hueful becomes a runtime dependency of
the npm package; dnt maps the npm: import into package.json.
@@ -0,0 +1,14 @@
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changeset needs revision.

Changeset should start with an approved active present-tense verb. Changeset package scope does not match the affected packages confidently. Changeset contains unsupported consumer-facing claims.

View the proposed replacement
---
'@bomb.sh/tty': minor
---

Adds CSS color string support to all directive color fields.

`open({ bg })`, `border.color`, `border.bg`, per-side `color`/`bg`, and `text({ color, bg })` now accept CSS color strings alongside packed ARGB numbers from `rgba()`. Accepted forms: hex (`#f80`, `#ff8800aa`), named colors (`rebeccapurple`, `transparent`), and `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, and `oklch()`. Strings resolve to sRGB at the transfer-encoding boundary; the wire format, snapshots, and renderer are unchanged. Out-of-gamut colors clip per channel.

Invalid strings are caught at every layer: string literals are rejected at compile time by `open()` and `text()`, `validate()` rejects them as invalid field values, and `pack()` throws a `TypeError` before producing any bytes.

#### Migration

The `Color` type is new — color fields that previously accepted only `number` now accept `number | string`. Passing a plain `number` continues to work without any change.

```diff
- open("box", { bg: rgba(255, 136, 0) })
+ open("box", { bg: "#ff8800" })   // string form, or keep the rgba() call
  open("box", { bg: rgba(255, 136, 0) }) // unchanged — numbers still work
```

If you pass color values typed as `string` at runtime, `validate()` and `pack()` will reject strings that do not match the CSS color grammar. Ensure any dynamically constructed color strings are valid before passing them to a directive.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant