Phase 0: adaptive colors and terminal color environment detection - #18
Open
TimoKramer wants to merge 4 commits into
Open
Phase 0: adaptive colors and terminal color environment detection#18TimoKramer wants to merge 4 commits into
TimoKramer wants to merge 4 commits into
Conversation
Wires up light/dark-adaptive styling (J1) with integer-color coercion (U1)
as its opening step, so adaptive colors aren't built on a constructor that
silently drops ints.
U1 — colors given as integers, keywords or hex strings were silently
dropped, because apply-color-fg/bg dispatched on (:type color), which is
nil for anything but a color map. This made a documented feature, the
library's own default styling (help, list, text-input) and most sample
code render as plain text. coerce-color now accepts integers as ANSI 256,
keywords as ANSI 16 names and strings as hex, and throws an ex-info naming
the offending value otherwise. Coercion lives in apply-color-fg/bg rather
than in each constructor, so style, with-fg, with-bg and styled-str all
pick it up at once.
J1 — terminal/dark-background? queries JLine's getDefaultBackgroundColor
(OSC 11) and computes luminance, defaulting to dark when the terminal
doesn't answer. Adaptive colors are {:type :adaptive :light … :dark …},
built with style/adaptive and resolved at render time. run detects the
profile and background once at startup, binds both dynamic vars around the
event loop, and sends an :environment message so apps can branch
themselves. The previously dormant detect-color-profile and
downgrade-color are now live in the render path.
Fixing downgrade-color was a prerequisite for that last part: its :ansi
branch did (mod code 16) on a 256-cube index, which is colorimetrically
meaningless — orange (rgb 255 128 0) came out cyan. Harmless while nothing
called it, but wiring it in would have turned every RGB color on a
TERM=xterm terminal into an effectively random one of 16. Replaced with a
nearest-neighbour match against the existing ansi-hex table, plus a new
ansi256->rgb covering the cube and grayscale ramp.
The background probe is bounded by JLine's org.jline.terminal.probe.timeout
(200ms default), so a terminal that ignores the query adds that much to
startup once.
Claude-Session: https://claude.ai/code/session_01CQeoheU7AxEmE5ZdATzg9q
Covers the API added alongside J1 and U1: style/adaptive and how it resolves against the detected background, the shorthand color forms (integer, keyword, hex string) that U1 made work, a table of the four color profiles and what each downgrades to, and the :environment message for apps that want to branch on the terminal themselves. Claude-Session: https://claude.ai/code/session_01CQeoheU7AxEmE5ZdATzg9q
Marks J1 and U1 done and records two premises the original review got wrong. First, charm never emitted raw true-color escapes for ANSI-only terminals to be spared from — AttributedString.toAnsi() without a terminal argument already collapses 24-bit color to the 256 palette, so making true color actually reach the terminal is separate Phase 4 work that will touch existing test assertions. Second, downgrade-color was not merely dormant but wrong, and wiring it in would have regressed TERM=xterm terminals had the nearest-color mapping not been fixed alongside. Also notes that the new dynamic vars are thread-local, which constrains the Phase 3/4 loop rewrite. Claude-Session: https://claude.ai/code/session_01CQeoheU7AxEmE5ZdATzg9q
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.
Implements Phase 0 of
PLAN.md— background color detection (J1), withinteger-color coercion (U1) as its opening step so adaptive colors aren't built
on a constructor that silently drops ints.
Includes the
plancommit that addsPLAN.md, which wasn't yet onmain.U1 — colors were silently dropped
apply-color-fg/apply-color-bgdispatched on(:type color), which isnilfor anything that isn't already a color map. So
(style/render (style/style :fg 240) "hi")returned
"hi"with no escapes at all — a documented feature, the library's owndefault styling in
help,listandtext-input, and most of the sample codeacross docs and examples all rendered as plain text.
coerce-colornow accepts integers as ANSI 256, keywords as ANSI 16 names andstrings as hex, and throws an
ex-infonaming the offending value otherwise.Coercion lives in
apply-color-fg/apply-color-bgrather than in eachconstructor, so
style,with-fg,with-bgandstyled-strall pick it up atonce instead of each entry point needing its own normalization.
J1 — adaptive colors and background detection
charm.terminal/dark-background?queries JLine'sgetDefaultBackgroundColor(OSC 11) and computes luminance, defaulting to dark when the terminal doesn't
answer.
{:type :adaptive :light … :dark …}, built withstyle/adaptiveand resolved at render time.rundetects the profile and background once at startup, binds*color-profile*/*dark-background?*around the event loop, and sends an:environmentmessage so apps can branch themselves.detect-color-profileanddowngrade-colorare nowlive in the render path.
The background probe is bounded by JLine's
org.jline.terminal.probe.timeout(200ms default), so a terminal that ignores the query adds that much to startup
once.
A bug found while wiring that in
downgrade-colorwas not merely dormant, it was wrong. Its:ansibranch did(mod code 16)on a 256-cube index, which is colorimetrically meaningless —orange
(rgb 255 128 0)came out cyan. Harmless while nothing called thefunction, but wiring it in would have turned every RGB color on a
TERM=xtermterminal into an effectively random one of 16, which is worse than the previous
pass-everything-through behavior.
Replaced with a nearest-neighbour match against the existing
ansi-hextable,plus a new
ansi256->rgbcovering the cube and the grayscale ramp. Orange nowlands on bright yellow, with a regression test pinning it.
A premise in the plan that turned out to be wrong
J1 was written on the assumption that ANSI-only terminals currently receive raw
true-color escapes. They don't, and neither does anything else:
AttributedString.toAnsi()called without a terminal argument already collapses24-bit color to the 256 palette, so charm has never emitted a
38;2;r;g;bsequence.
(rgb 255 0 0)renders asESC[38;5;196meven under the:true-colorprofile, and the existing tests intest/charm/style/color_test.cljassert exactly that.
Making true color actually reach the terminal needs the
toAnsioverload thattakes a terminal, and changes those assertions — so it's left for the Phase 4
render work rather than smuggled in here. Recorded in
PLAN.md.Testing
162 tests / 994 assertions, up from 152 / 940.
cljfmtclean;clj-kondoat 0errors with only the 10 pre-existing warnings.
Verified end to end that
:fg 240and the other shorthands emit escapes, thatadaptive colors flip with the background, and that each profile downgrades as
intended (
:ansiorange → bright yellow,:ascii→ no escapes).Note for later
*color-profile*and*dark-background?*are thread-local. The currentsingle-threaded render path is fine, but a
viewrendering parts on otherthreads wouldn't see them — a constraint on the Phase 3/4 loop rewrite, noted in
the plan.
https://claude.ai/code/session_01CQeoheU7AxEmE5ZdATzg9q