Conversation
Use init_extended_pair/init_extended_color/extended_color_content/ extended_pair_content when available (ncurses 6+), removing the 256 color pair limitation. Window#color_set is upgraded to use wattr_set internally when available, allowing pair numbers > 255. New methods: - Curses.support_extended_colors? — runtime check for extended support - Curses.reset_color_pairs — reset all pairs to undefined (ncurses 6.1+) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Curses.support_extended_colors? returns true, display up to 512 color pairs (capped at color_pairs) using stdscr.color_set instead of attrset(color_pair()), since COLOR_PAIR() cannot encode pair numbers > 255. Pairs 256-511 use a dark grey background to distinguish them from the first 256. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Ruby curses extension to take advantage of ncurses’ extended colors API (ncurses 6+) when available, lifting the historical 256 color-pair limitation and updating Window#color_set behavior to better support large pair numbers.
Changes:
- Prefer
init_extended_pair/init_extended_colorand extended content getters when available. - Add
Curses.support_extended_colors?andCurses.reset_color_pairs. - Update sample + README to reflect/support extended color pairs usage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
sample/colors.rb |
Demonstrates extended color-pair usage and avoids color_pair() for pairs > 255. |
ext/curses/extconf.rb |
Detects extended-color-related ncurses functions/macros at build time. |
ext/curses/curses.c |
Implements extended-color-aware wrappers and adds new Ruby-facing APIs. |
README.md |
Updates limitation docs to reflect extended color support on ncurses 6+. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static VALUE | ||
| curses_reset_color_pairs(VALUE obj) | ||
| { | ||
| reset_color_pairs(); |
There was a problem hiding this comment.
reset_color_pairs() returns a status code (OK/ERR). The wrapper currently ignores the return value and always returns nil, making failures silent. Consider returning true/false (consistent with other wrappers) or raising on ERR.
| reset_color_pairs(); | |
| int ret = reset_color_pairs(); | |
| if (ret == ERR) { | |
| rb_sys_fail("reset_color_pairs"); | |
| } |
ext/curses/curses.c
Outdated
| extended_color_content(NUM2INT(color), &r, &g, &b); | ||
| return rb_ary_new3(3, INT2FIX(r), INT2FIX(g), INT2FIX(b)); | ||
| } | ||
| #else | ||
| { | ||
| short r, g, b; | ||
| color_content(NUM2INT(color), &r, &g, &b); |
There was a problem hiding this comment.
extended_color_content/color_content return OK/ERR, but the code ignores the return value and then returns potentially uninitialized r/g/b when the call fails. Handle the return code and raise/return nil on ERR to avoid undefined behavior.
| extended_color_content(NUM2INT(color), &r, &g, &b); | |
| return rb_ary_new3(3, INT2FIX(r), INT2FIX(g), INT2FIX(b)); | |
| } | |
| #else | |
| { | |
| short r, g, b; | |
| color_content(NUM2INT(color), &r, &g, &b); | |
| int rc = extended_color_content(NUM2INT(color), &r, &g, &b); | |
| if (rc == ERR) { | |
| return Qnil; | |
| } | |
| return rb_ary_new3(3, INT2FIX(r), INT2FIX(g), INT2FIX(b)); | |
| } | |
| #else | |
| { | |
| short r, g, b; | |
| int rc = color_content(NUM2INT(color), &r, &g, &b); | |
| if (rc == ERR) { | |
| return Qnil; | |
| } |
ext/curses/curses.c
Outdated
| extended_pair_content(NUM2INT(pair), &f, &b); | ||
| return rb_ary_new3(2, INT2FIX(f), INT2FIX(b)); | ||
| } | ||
| #else | ||
| { | ||
| short f, b; | ||
| pair_content(NUM2INT(pair), &f, &b); |
There was a problem hiding this comment.
extended_pair_content/pair_content return OK/ERR, but the code ignores the return value and then returns potentially uninitialized f/b when the call fails. Handle the return code and raise/return nil on ERR to avoid undefined behavior.
| extended_pair_content(NUM2INT(pair), &f, &b); | |
| return rb_ary_new3(2, INT2FIX(f), INT2FIX(b)); | |
| } | |
| #else | |
| { | |
| short f, b; | |
| pair_content(NUM2INT(pair), &f, &b); | |
| int rc = extended_pair_content(NUM2INT(pair), &f, &b); | |
| if (rc == ERR) return Qnil; | |
| return rb_ary_new3(2, INT2FIX(f), INT2FIX(b)); | |
| } | |
| #else | |
| { | |
| short f, b; | |
| int rc = pair_content(NUM2INT(pair), &f, &b); | |
| if (rc == ERR) return Qnil; |
sample/colors.rb
Outdated
| addstr extended ? " (extended).\n" : ".\n" | ||
|
|
||
| (extended ? [512, color_pairs].min : colors).times { |i| | ||
| Curses.init_pair(i, i%256, i>=256 ? 8 : 0) |
There was a problem hiding this comment.
In the extended branch, init_pair(i, i%256, ...) can pass invalid color numbers when colors < 256 (e.g., a 16-color terminal built against an extended-capable ncurses). Consider using i % colors (and choosing a background within range) so the sample works on low-color terminals too.
| Curses.init_pair(i, i%256, i>=256 ? 8 : 0) | |
| fg_color = i % colors | |
| bg_color = (i / colors) % colors | |
| Curses.init_pair(i, fg_color, bg_color) |
| addstr "This Terminal supports #{colors} colors, #{color_pairs} pairs" | ||
| addstr extended ? " (extended).\n" : ".\n" | ||
|
|
||
| (extended ? [512, color_pairs].min : colors).times { |i| |
There was a problem hiding this comment.
The loop initializes color pair 0 (Curses.init_pair(0, ...)). In ncurses, pair 0 is the default pair and typically cannot be redefined (init_pair(0, ...) returns ERR). Consider starting the loop at 1 (or skipping 0) to avoid silent errors in this sample.
| (extended ? [512, color_pairs].min : colors).times { |i| | |
| (extended ? [512, color_pairs].min : colors).times { |i| | |
| next if i == 0 |
ext/curses/curses.c
Outdated
| #ifdef HAVE_WATTR_SET | ||
| return (wattr_set(winp->window, 0, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | ||
| #else | ||
| return (wcolor_set(winp->window, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; |
There was a problem hiding this comment.
Window#color_set now uses wattr_set(..., 0, pair, ...) when available. Passing attrs = 0 will clear any existing window attributes (bold/underline/etc.), which changes behavior compared to wcolor_set (and to the method’s intent of only changing the color pair). Preserve the current attributes when switching pairs (e.g., fetch current attrs via wattr_get if available, or prefer wcolor_set when it exists and only fall back to wattr_set when needed).
| #ifdef HAVE_WATTR_SET | |
| return (wattr_set(winp->window, 0, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | |
| #else | |
| return (wcolor_set(winp->window, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | |
| #ifdef HAVE_WCOLOR_SET | |
| /* Prefer wcolor_set when available, as it only changes the color pair. */ | |
| return (wcolor_set(winp->window, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | |
| #elif defined(HAVE_WATTR_SET) | |
| # ifdef HAVE_WATTR_GET | |
| /* | |
| * When wcolor_set is unavailable but wattr_set/wattr_get are, | |
| * preserve existing attributes and only change the color pair. | |
| */ | |
| attr_t attrs; | |
| short current_pair; | |
| if (wattr_get(winp->window, &attrs, ¤t_pair, NULL) == ERR) { | |
| return Qfalse; | |
| } | |
| return (wattr_set(winp->window, attrs, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | |
| # else | |
| /* | |
| * Fallback: wcolor_set and wattr_get are unavailable; use wattr_set | |
| * with attrs = 0 (may clear attributes, but there is no way to | |
| * query and preserve them portably here). | |
| */ | |
| return (wattr_set(winp->window, 0, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse; | |
| # endif |
| #ifdef HAVE_RESET_COLOR_PAIRS | ||
| static VALUE | ||
| curses_reset_color_pairs(VALUE obj) | ||
| { |
There was a problem hiding this comment.
Curses.reset_color_pairs should follow the pattern of other color APIs by calling curses_stdscr() before invoking ncurses functions. As written, calling it before init_screen can behave inconsistently vs. the rest of the API.
| { | |
| { | |
| curses_stdscr(); |
- curses_color_content, curses_pair_content: check return value of extended_color_content/color_content/extended_pair_content/pair_content and return nil on ERR instead of returning uninitialized values - curses_reset_color_pairs: add curses_stdscr() call for consistency with other color APIs - window_color_set: use wattr_get to preserve existing window attributes when calling wattr_set; use NCURSES_PAIRS_T for the pair argument to match the ncurses header type in both standard and extended modes; fall back to wattr_set without attr preservation if wattr_get is unavailable, and to wcolor_set if wattr_set is also unavailable - extconf.rb: add have_func check for wattr_get - sample/colors.rb: skip pair 0 (cannot be redefined); use i%colors instead of i%256 to handle terminals with fewer than 256 colors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NCURSES_PAIRS_T is ncurses-specific and not defined in PDCurses. Fall back to short when it is not available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Support ncurses extended colors API
Use init_extended_pair/init_extended_color/extended_color_content/
extended_pair_content when available (ncurses 6+), removing the 256
color pair limitation. Window#color_set is upgraded to use wattr_set
internally when available, allowing pair numbers > 255.
New methods: