Skip to content

CRT: Add direct color scheme support for TERM=*-direct - #2072

Open
rahawii wants to merge 5 commits into
htop-dev:mainfrom
rahawii:fix-direct-color
Open

CRT: Add direct color scheme support for TERM=*-direct#2072
rahawii wants to merge 5 commits into
htop-dev:mainfrom
rahawii:fix-direct-color

Conversation

@rahawii

@rahawii rahawii commented Aug 16, 2026

Copy link
Copy Markdown

Description

This pull request adds true 24-bit direct color (RGB) support for modern terminals to htop.

  • The Problem: Previously, terminals with the -direct suffix could suffer from inconsistent styling, blocky background artifacts, or require forced fallback behavior to legacy 16-color palettes.
  • The Solution: We implemented a robust TrueColor scheme (COLORSCHEME_DIRECT) alongside safe detection and fallback mechanisms to fully utilize modern terminal capabilities without breaking older environments.

Key Implementation Details

  1. TrueColor Integration: Transitioned from the extended 256-color palette to exact 24-bit RGB hex values using the ncurses extended API (init_extended_pair). This ensures crisp, vibrant colors (e.g., warm orange, neon cyan) with correct transparent backgrounds.

  2. Dynamic Capability Detection: Instead of strictly matching the TERM suffix, the implementation uses tigetflag("RGB") to accurately query the terminal's actual direct-color capabilities.

  3. Robust Fallbacks & Safety Guards:

    • Added a runtime guard: if the COLORSCHEME_DIRECT is loaded from a user's htoprc configuration file, but they launch htop in a non-RGB terminal (e.g., standard xterm or vt100), the application safely downgrades to COLORSCHEME_DEFAULT to prevent rendering crashes or blank screens.
    • Wrapped the extended API calls in preprocessor directives (#if defined(NCURSES_EXT_FUNCS) && defined(NCURSES_EXT_COLORS)) to ensure htop continues to compile flawlessly on older systems with legacy ncurses libraries.
  4. UI Integration: Added the new Direct scheme to ColorsPanel.c, allowing users to naturally select it from the Setup (F2) menu.

Visual Comparison

  • Before (Default htop): When running with the standard configuration, the interface relies on legacy 16-color mappings. Here is how it looked previously using TERM=xterm-direct htop:
Screenshot 2026-08-17 at 08 28 32
  • After (With Our Direct Color Custom Scheme): After applying our custom modifications and running our compiled binary with TERM=xterm-direct ./htop, the interface uses our enhanced palette layout with correct styling and transparency:
Screenshot 2026-08-17 at 08 27 39

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c24673c9-6ab9-4946-9be0-a16530cfea6d

📥 Commits

Reviewing files that changed from the base of the PR and between bd0eab5 and 8dc3a08.

📒 Files selected for processing (1)
  • CRT.c

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Adds direct-color support to CRT. The implementation detects terminal RGB support, exposes CRT_hasDirectColors, and adds COLORSCHEME_DIRECT. The direct scheme defines RGB color pairs for interface, process, resource, graph, dynamic, and related elements. CRT_setColors falls back when direct colors are unavailable and initializes direct RGB pairs when supported.

Poem

Direct colors fill the screen,
RGB values stay clean.
CRT checks the terminal state,
Then selects the matching palette.
Each display element gains a hue.

Merge Risk: 🟡 Moderate · up to 8dc3a

The new Direct color scheme can leave the setup panel pointing at an invalid scheme entry, causing incorrect behavior or a crash. Merge should wait until the scheme lists are kept consistent or this risk is explicitly accepted.


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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a7ac5a30-8bc8-4218-997c-e1d74e6336d3

📥 Commits

Reviewing files that changed from the base of the PR and between 6f33ddd and fe104eb.

📒 Files selected for processing (2)
  • CRT.c
  • CRT.h

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.

Comment thread CRT.c Outdated
Comment thread CRT.c Outdated
Comment on lines +1350 to +1352
if (term_type_len >= suffix_len && strcmp(termType + term_type_len - suffix_len, suffix) == 0) {
CRT_hasDirectColors = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Please follow the project’s conditional style in both direct-color checks: use String_eq() instead of raw strcmp(...) == 0, and omit braces when the conditional body contains only the single assignment. Apply the brace change here and at the corresponding check near lines 1488–1490.

📍 Affects 1 file
  • CRT.c#L1350-L1352 (this comment)
  • CRT.c#L1488-L1490

Source: Coding guidelines

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CRT.c (1)

961-1077: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add the COLORSCHEME_DIRECT entry to ColorsPanel.

ColorSchemeNames[] has no entry for COLORSCHEME_DIRECT. Therefore, ColorsPanel_new() fails its size assertion, and a non-assert build can call Panel_get(super, 8) without a matching item. Add the entry and restrict it to terminals with direct-color support, or keep the internal active scheme separate from the panel index.

Source: MCP tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35192a0d-6cab-40d5-be93-434c983791e5

📥 Commits

Reviewing files that changed from the base of the PR and between fe104eb and d305fa3.

📒 Files selected for processing (1)
  • CRT.c

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.

Comment thread CRT.c
Comment thread CRT.c

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CRT.c (1)

1474-1499: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate direct-color detection on extended-color support.

When NCURSES_EXT_COLORS is unavailable, tigetflag("RGB") can still select COLORSCHEME_DIRECT, while the fallback initializes indexed pairs. When only NCURSES_EXT_FUNCS is enabled, init_extended_pair() may also lack a library implementation. Require both macros for direct-color detection and pair initialization; otherwise select the indexed scheme.

Source: MCP tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3afc6cb4-9514-4f2c-824f-bd6566489f1b

📥 Commits

Reviewing files that changed from the base of the PR and between d305fa3 and db38b3e.

📒 Files selected for processing (2)
  • CRT.c
  • ColorsPanel.c

Included review availability: Your plan includes up to 8 reviews per rolling hour; 6 remain after this review.

Comment thread CRT.c Outdated
@fasterit

Copy link
Copy Markdown
Member

Previous PR on the same topic: #1926

@rahawii

rahawii commented Aug 17, 2026

Copy link
Copy Markdown
Author

Thanks for pointing that out! I reviewed #1926. It looks like that PR closed, so I hope this new approach can help finally get this feature merged. In this implementation, I focused heavily on safety: it uses tigetflag("RGB") for dynamic detection, includes runtime fallbacks to COLORSCHEME_DEFAULT for non-RGB terminals, and adds compile-time guards for older ncurses versions to prevent build failure.

@ravi-arnan

Copy link
Copy Markdown
Contributor

Read and tested this on Linux (Alder Lake-P laptop, ncurses 6.4, Zorin/Ubuntu). Not a maintainer,
so this is a comment rather than an approval.

Test harness, since it affects what you can trust below. This box has no ncurses-term, so
xterm-direct did not exist. I compiled a local entry into ~/.terminfo with the same
capabilities the ncurses one carries:

xterm-direct|xterm with direct-color indexing (local test entry),
	RGB, colors#0x1000000, pairs#0x10000,
	setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
	setab=... (same shape, 48), use=xterm-256color,

tigetflag("RGB") returns 1 and colors is 16777216 under it, which is what your check keys on.
Both trees built clean with ./configure --enable-unicode && make: bd0eab56 and 6f33ddd5
(current main) for comparison. Each run is HTOPRC=<rc> TERM=<term> script -qec "stty rows 40 cols 160; timeout 5 ./htop -d 5", then counting the truecolor SGR sequences in the raw capture.

The one thing I would fix before this lands: Default becomes unreachable

CRT_setColors promotes the user's choice:

if (colorScheme == COLORSCHEME_DEFAULT && CRT_hasDirectColors)
   colorScheme = COLORSCHEME_DIRECT;

so on any terminal whose terminfo has RGB, a user who has Default selected silently gets the new
scheme. With color_scheme=0 in the rc file:

bd0eab56, TERM=xterm-direct     973x 38:2::0:255:255   486x 50:205:50   110x 255:140:0   76x 255:20:147
6f33ddd5, TERM=xterm-direct     308x 38:2::0:0:8  (and nothing else)
bd0eab56, TERM=xterm-256color   no truecolor sequences at all

Those four triples are exactly DIRECT_COLOR_*, so the rc file says Default and the screen is
Direct. It is also not visible in the UI as an override: ColorsPanel.c:108 marks the checkbox
from CRT_colorScheme, which is the promoted value, so Setup > Colors shows Direct checked while
htoprc still says color_scheme=0. Selecting Default there writes 0, gets promoted again, and
the check jumps back to Direct. As far as I can tell there is no way to ask for the Default scheme
on a direct-color terminal once this is in, short of -C, which gives monochrome rather than Default.

The promotion is also the only asymmetric part. Nord under the same terminal is untouched
(325 sequences, all 38:2::0:0:8, i.e. the same as main), so every other scheme keeps working and
only Default is taken over. Dropping those two lines makes this a plain opt-in scheme and costs the
PR nothing.

The reverse direction is fine and worth keeping: color_scheme=8 on TERM=xterm-256color
produced zero truecolor sequences, so the graceful fallback works.

The #else fallback in the pair setup is unreachable

if (colorScheme == COLORSCHEME_DIRECT) {
#if defined(NCURSES_EXT_FUNCS) && defined(NCURSES_EXT_COLORS)
   init_extended_pair(...);
#else
   init_pair(DIRECT_PAIR_ORANGE, COLOR_YELLOW, -1);   /* dead */

In a build without those macros, CRT_hasDirectColors is hardcoded false above, so the first check
rewrites COLORSCHEME_DIRECT to COLORSCHEME_DEFAULT and the promotion cannot restore it. The
#else branch can never run. Either drop it, or gate the scheme out of ColorSchemeNames in that
build so the menu does not offer an entry that silently renders as Default.

Something that argues for this PR, in case it is useful

On TERM=xterm-direct, current main emits 38:2::0:0:8 for color index 8: in direct-color mode
ncurses takes the index as an RGB value, so htop's gray is drawn as RGB(0,0,8), which is black on
black. That is a real legibility bug on those terminals today and it is independent of any new
scheme. If the maintainers want direct color support rather than one more palette, fixing index 8
for the schemes that already exist may be the more valuable half of this work, and it is the part a
user cannot work around by picking a different scheme.

Checked and fine, so nobody needs to re-check

  • Pair numbers 100 to 103 do not collide: ColorIndex(i,j) maxes at (7-0)*8+7 = 63, and both
    special pairs (ColorIndexGrayBlack, ColorIndexWhiteDefault) are inside that range.
  • init_extended_pair(..., -1) is legal here: CRT_init calls use_default_colors() at
    CRT.c:1404, before CRT_setColors at 1406.
  • The new scheme is complete. All 116 ColorElements have an entry, no duplicates, so nothing
    falls through to 0.
  • COLORSCHEME_DIRECT is appended before LAST_COLORSCHEME, so existing color_scheme= values in
    users' rc files keep meaning what they meant.
  • No documentation lists the schemes by name, so there is nothing stale to update alongside.

Nits

  • CRT.c:1481 is indented with 4 spaces inside a 3-space file.
  • The blank line added after const char* termType = getenv("TERM"); in CRT_init is unrelated to
    the feature.

- Make COLORSCHEME_DIRECT strictly opt-in by removing auto-promotion from Default.
- Remove unreachable '#else' fallback block, as the downgrade guard already handles non-RGB terminals.
- Fix accidental assignment ('=') to comparison ('==') in the downgrade guard.
- Fix indentation to 3 spaces and remove braces for the single-statement block per coding guidelines.
- Remove stray blank line in CRT_init.
@rahawii

rahawii commented Aug 18, 2026

Copy link
Copy Markdown
Author

Thank you for the incredibly detailed testing and feedback! I really appreciate the time you took to compile a local terminfo and verify the truecolor sequences. This was helpful.

You brought up good points, and I have pushed a commit to address them:

  1. Default becomes unreachable: I removed the auto change forcing the user to use direct instead of default.
    COLORSCHEME_DIRECT strictly opt-in.
  2. Unreachable #else fallback: Since the downgrade guard (if (colorScheme == COLORSCHEME_DIRECT && !CRT_hasDirectColors)) safely rewrites the scheme to Default earlier in the flow, the fallback init_pair block is indeed dead code. I've removed the #else branch entirely, while keeping the #if defined(...) macros around init_extended_pair to ensure older builds don't fail at compile time.
  3. Nits: Fixed the 4-space indentation and removed the stray blank line.

Let me know if everything looks good to you now.

@ravi-arnan

Copy link
Copy Markdown
Contributor

Re-tested 8dc3a084 on the same box and harness as before (Alder Lake-P laptop, ncurses 6.4, local xterm-direct terminfo entry with RGB and colors#0x1000000). All three points are addressed, and I could not find anything new. Still a comment rather than an approval, I am not a maintainer.

The promotion is gone, and Default is back to being Default

Each run is HTOPRC=<rc> TERM=<term> script -qec "stty rows 40 cols 160; timeout 4 ./htop -d 5", counting truecolor SGR sequences in the raw capture. The four DIRECT_COLOR_* triples are 0:255:255, 50:205:50, 255:140:0 and 255:20:147.

build color_scheme TERM truecolor sequences which
bd0eab56 (previous head) 0 xterm-direct 1083 all four DIRECT_COLOR_*
8dc3a084 (this head) 0 xterm-direct 256 only 0:0:8
6f33ddd5 (main) 0 xterm-direct 216 only 0:0:8
8dc3a084 8 xterm-direct 920 all four DIRECT_COLOR_*
8dc3a084 8 xterm-256color 0 none

The counts differ run to run because the process list does; the set of triples is the part that matters. With color_scheme=0 on a direct-color terminal this head now emits exactly what main emits and nothing else, so Default is indistinguishable from main again, and the scheme is reachable only by asking for it. The fallback in the other direction still works.

Dropping the #else is safe

I checked the thing that would have made it unsafe: DIRECT_PAIR_ORANGE/CYAN/GREEN/PINK are referenced only inside the direct colour table (CRT.c:962 onwards), which is installed only when colorScheme == COLORSCHEME_DIRECT. In a build without NCURSES_EXT_FUNCS/NCURSES_EXT_COLORS, CRT_hasDirectColors is hardcoded false and the guard above rewrites the scheme to Default, so nothing can reach a pair that is now never initialised.

Checked and fine, so nobody needs to re-check

  • ColorsPanel.c:108 still marks the checkbox from CRT_colorScheme, but that is no longer a mismatch: with the promotion removed, CRT_colorScheme equals the value in htoprc on any terminal that can do direct colour. On one that cannot, the panel shows the scheme actually in effect, which seems right.
  • Opening Setup > Colors on a non-direct terminal does not silently downgrade a saved color_scheme=8. settings->colorScheme is written at ColorsPanel.c:71 only on an explicit Enter, space or click, not when the panel is built.
  • Both asserts still hold with Direct appended: ColorSchemeNames is 9 names plus NULL, and LAST_COLORSCHEME + 1 is 10.
  • Builds clean, ./configure --enable-unicode && make, zero warnings.
  • The indentation and the stray blank line are both fixed.

Still open, and cleanly separate from this PR now

On TERM=xterm-direct, colour index 8 is emitted as 38:2::0:0:8, so htop's gray is drawn as RGB(0,0,8), black on black. That is true on main today and it is still true here with Default selected, which is exactly as it should be now that this PR leaves Default alone. It affects every existing scheme on those terminals and a user cannot dodge it by picking a different one, so it may be worth its own issue rather than riding along with a new palette.

@rahawii

rahawii commented Aug 19, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed testing and validation.

It's good to confirm that the fallback logic, preprocessor guards, and UI panel are behaving as expected across environments.

Regarding the RGB(0,0,8) issue on TERM=xterm-direct: I agree with your assessment. Since this is a pre-existing behavior on main that affects all standard schemes, it is better suited for a separate tracking issue rather than being coupled with this PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants