sugarcraft / candy-palette
PHP port of charmbracelet/colorprofile — magical terminal color profile detection and color degradation (TrueColor → ANSI256 → ANSI → ASCII).
Requires
- php: ^8.3
- react/child-process: ^0.6
- react/promise: ^3.3
- sugarcraft/candy-core: dev-master
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 15:32:11 UTC
README
CandyPalette
PHP port of charmbracelet/colorprofile — magical terminal color profile detection and color degradation.
Features
- Detect terminal color profile from environment variables and TTY info
- Profile enum:
TrueColor(24-bit) →ANSI256(255-color) →ANSI(16-color) →Ascii(no color) →NoTTY - Color conversion: downsample RGBA colors to any target profile
- ProfileWriter: wrap a stream and automatically degrade color codes to match the terminal
- ANSI stripping:
NoTTYstrips all ANSI sequences from output - Environment-aware: reads
TERM,COLORTERM,FORCE_COLOR,NO_COLOR,TERM_PROGRAM - Probe class: static env-detection layer with precedence-ordered rules + infocmp Phase 2 upgrade
- ColorProfile enum: SSOT env-detection enum (NoTTY/Ascii/Ansi/Ansi256/TrueColor) for libs that need raw profile values without constructing a Palette instance
- Perceptual color distance: CIELAB conversion + ΔE₇₆ / ΔE₉₄ / ΔE*₀₀ (CIEDE2000) metrics with an opt-in nearest-palette matcher (
NearestColor), palette Lab values memoised for hot loops
Install
composer require sugarcraft/candy-palette
Quick Start
use SugarCraft\Palette\Color; use SugarCraft\Palette\Palette; use SugarCraft\Palette\ProfileWriter; // Detect the terminal's color profile $palette = new Palette(); $profile = $palette->profile(); echo "Your terminal supports: {$profile->label()}\n"; // Downsample a TrueColor color to the detected profile $color = new Color(0x6b, 0x50, 0xff); // #6b50ff $converted = $palette->convert($color); echo "Converted: {$converted->toHex()}\n"; // Wrap a stream for automatic color degradation on write $writer = ProfileWriter::wrap(STDOUT); $writer->write("\x1b[38;2;107;80;255mFancy text\x1b[0m\n");
Profiles
| Profile | Colors | Description |
|---|---|---|
| TrueColor | 16.7M | Full 24-bit RGB (24-bit ANSI) |
| ANSI256 | 256 | 216 cube + 24 grey + 16 standard |
| ANSI | 16 | Standard terminal colors |
| Ascii | 2 | Black & white |
| NoTTY | 0 | No color (ANSI stripped) |
Color Degradation
use SugarCraft\Palette\Color; use SugarCraft\Palette\Palette; use SugarCraft\Palette\Profile; $color = new Color(100, 50, 255, 255); // Downsample to an explicit profile (static one-off shortcut) $ansi256 = Palette::toProfile($color, Profile::ANSI256); // nearest cube/grey index $ansi = Palette::toProfile($color, Profile::ANSI); // nearest of the 16 slots echo $ansi256->toAnsi256Foreground(); // "\x1b[38;5;…m" echo $ansi->toAnsi16Foreground(); // "\x1b[…m" — 4-bit SGR // Or convert against the detected terminal $auto = (new Palette())->convert($color);
Perceptual Color Distance (opt-in)
Default nearest-color matching stays Euclidean (RGB) for byte-for-byte back-compat. For perceptual matching — where blues, greens and near-greys rank correctly — opt in:
use SugarCraft\Palette\Color; use SugarCraft\Palette\ColorDistance; use SugarCraft\Palette\ColorMath; use SugarCraft\Palette\DeltaE; use SugarCraft\Palette\NearestColor; // sRGB -> linear -> XYZ (D65) -> CIELAB $lab = ColorMath::toLab(64, 96, 128); // ['l' => …, 'a' => …, 'b' => …] $lab2 = (new Color(200, 30, 90))->toLab(); // same chain, from a Color // ΔE metrics (CIE 15:2004; CIEDE2000 per Sharma/Wu/Dalal 2005) $d76 = DeltaE::cie76($lab, $lab2); $d94 = DeltaE::cie94($lab, $lab2); // graphic-arts kL = 1 $d00 = DeltaE::cie2000($lab, $lab2); // Nearest ANSI-256 palette index under a chosen strategy (default: EUCLIDEAN) $matcher = new NearestColor(ColorDistance::Cie2000); $index = $matcher->ansi256(new Color(30, 120, 30)); // Arbitrary palette (list or map), returns the key of the winner $brand = ['logo' => new Color(10, 20, 30), 'accent' => new Color(240, 250, 255)]; $key = $matcher->closest(new Color(12, 34, 56), $brand);
The 256-entry and 16-entry palette Lab tables are memoised statically, so a warm
CIEDE2000 search costs ~1 ms on a stock laptop; recomputing the 256 sRGB→Lab
conversions per search would add ≈40 % on top (see NearestColorTest).
The matcher's 256-entry table is the canonical xterm palette: slots 0-15 are the
ANSI-16 set, 16-231 the 6×6×6 cube at channel levels 0/95/135/175/215/255, and
232-255 the 8+10n grey ramp — derived from Color::fromAnsi256Index() and
cross-pinned to candy-core's decoder (Ansi256TableParityTest). Earlier
versions shipped a private even-51-step cube; that table never matched a real
terminal and is gone.
Probe — Static Environment Detection
The Probe class provides precedence-ordered environment probing for terminal color capability and reduced-motion preference. Use it directly when you need raw detection values without constructing a Palette instance.
use SugarCraft\Palette\Probe; use SugarCraft\Palette\ColorProfile; // Detect the negotiated color profile $profile = Probe::colorProfile(); // ColorProfile::TrueColor|Ansi256|Ansi|Ascii|NoTTY echo $profile->label(); // "TrueColor" // Check for explicit disable/enable flags if (Probe::isNoColor()) { // NO_COLOR env var is set — disable all color output } if (Probe::isForceColor()) { // CLICOLOR_FORCE=1 — force full color regardless of terminal } // Reduced-motion preference (REDUCE_MOTION or PREFERS_REDUCED_MOTION) if (Probe::reducedMotion()) { // Skip animations, spinners, and other motion }
Detection precedence (mirrors charmbracelet/colorprofile):
CLICOLOR_FORCE=1→TrueColor(overrides everything)NO_COLOR(any value) →NoTTYCLICOLOR=0→NoTTYTERM=dumb→NoTTYCOLORTERM=24bit|truecolor|yes→TrueColorWT_SESSION(set) →TrueColor(Windows Terminal)GOOGLE_CLOUD_SHELL=true→TrueColorTMUX/STY+screen*/tmux*base term →Ansi256TERM=xterm-kitty|xterm-ghostty|*-256color→Ansi256TERM=xterm*|screen*|tmux*→Ansi- Default →
Ansi, then Phase 2 infocmp upgrade →TrueColorifTc/RGBcapability found
ColorProfile Enum
ColorProfile is the SSOT enum for environment-driven color capability. It is used by Probe and consumed by libs that need the raw profile value (candy-log, candy-mosaic, candy-freeze, candy-vt).
use SugarCraft\Palette\ColorProfile; use SugarCraft\Palette\Probe; $profile = Probe::colorProfile(); // Human-readable label echo $profile->label(); // "TrueColor"
| Case | Value | Label |
|---|---|---|
NoTTY |
'notty' |
No TTY |
Ascii |
'ascii' |
ASCII |
Ansi |
'ansi' |
ANSI |
Ansi256 |
'ansi256' |
ANSI 256 |
TrueColor |
'truecolor' |
TrueColor |
Architecture
SugarCraft\Palette\
├── Color — RGBA color value object with conversion methods (Color::namedColors() lists standard names)
├── Palette — instance-based detection + degradation + ProfileWriter
├── Profile — legacy detection enum (richest→simplest order)
├── ColorProfile — new SSOT detection enum (simplest→richest order, Probe-driven)
├── Probe — static env-probe layer (colorProfile/isNoColor/isForceColor/reducedMotion)
├── StandardColors — ANSI/ANSI256 standard palette
├── ProfileWriter — stream wrapper for automatic color degradation
└── Lang — i18n strings