Skip to content

Support ncurses extended colors API#130

Merged
shugo merged 4 commits intomasterfrom
feature/extended-colors
Mar 6, 2026
Merged

Support ncurses extended colors API#130
shugo merged 4 commits intomasterfrom
feature/extended-colors

Conversation

@shugo
Copy link
Member

@shugo shugo commented Mar 6, 2026

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:

  • Curses.support_extended_colors? — runtime check for extended support
  • Curses.reset_color_pairs — reset all pairs to undefined (ncurses 6.1+)

shugo and others added 2 commits March 6, 2026 11:42
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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_color and extended content getters when available.
  • Add Curses.support_extended_colors? and Curses.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();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
reset_color_pairs();
int ret = reset_color_pairs();
if (ret == ERR) {
rb_sys_fail("reset_color_pairs");
}

Copilot uses AI. Check for mistakes.
Comment on lines +1413 to +1419
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);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1455 to +1461
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);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
addstr "This Terminal supports #{colors} colors, #{color_pairs} pairs"
addstr extended ? " (extended).\n" : ".\n"

(extended ? [512, color_pairs].min : colors).times { |i|
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
(extended ? [512, color_pairs].min : colors).times { |i|
(extended ? [512, color_pairs].min : colors).times { |i|
next if i == 0

Copilot uses AI. Check for mistakes.
Comment on lines +2795 to +2798
#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;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
#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, &current_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

Copilot uses AI. Check for mistakes.
#ifdef HAVE_RESET_COLOR_PAIRS
static VALUE
curses_reset_color_pairs(VALUE obj)
{
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
{
{
curses_stdscr();

Copilot uses AI. Check for mistakes.
shugo and others added 2 commits March 6, 2026 12:25
- 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>
@shugo shugo merged commit ffcc80f into master Mar 6, 2026
8 checks passed
@shugo shugo deleted the feature/extended-colors branch March 6, 2026 04:14
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.

2 participants