Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Type the following command, and see `[rdoc]` of curses:

## Limitations

* curses.gem doesn't support more than 256 color pairs. See https://reversed.top/2019-02-05/more-than-256-curses-color-pairs/ for details.
* On ncurses 6+ compiled with extended color support, more than 256 color pairs are supported transparently via the extended colors API (`init_extended_pair`, etc.). Use `Curses.support_extended_colors?` to check at runtime.

## Developers

Expand Down
113 changes: 98 additions & 15 deletions ext/curses/curses.c
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,11 @@ curses_init_pair(VALUE obj, VALUE pair, VALUE f, VALUE b)
{
/* may have to raise exception on ERR */
curses_stdscr();
#ifdef HAVE_INIT_EXTENDED_PAIR
return (init_extended_pair(NUM2INT(pair), NUM2INT(f), NUM2INT(b)) == OK) ? Qtrue : Qfalse;
#else
return (init_pair(NUM2INT(pair),NUM2INT(f),NUM2INT(b)) == OK) ? Qtrue : Qfalse;
#endif
}

/*
Expand All @@ -1345,8 +1349,13 @@ curses_init_color(VALUE obj, VALUE color, VALUE r, VALUE g, VALUE b)
{
/* may have to raise exception on ERR */
curses_stdscr();
#ifdef HAVE_INIT_EXTENDED_COLOR
return (init_extended_color(NUM2INT(color), NUM2INT(r),
NUM2INT(g), NUM2INT(b)) == OK) ? Qtrue : Qfalse;
#else
return (init_color(NUM2INT(color),NUM2INT(r),
NUM2INT(g),NUM2INT(b)) == OK) ? Qtrue : Qfalse;
#endif
}

/*
Expand Down Expand Up @@ -1397,11 +1406,22 @@ curses_colors(VALUE obj)
static VALUE
curses_color_content(VALUE obj, VALUE color)
{
short r,g,b;

curses_stdscr();
color_content(NUM2INT(color),&r,&g,&b);
return rb_ary_new3(3,INT2FIX(r),INT2FIX(g),INT2FIX(b));
#ifdef HAVE_EXTENDED_COLOR_CONTENT
{
int r, g, b;
if (extended_color_content(NUM2INT(color), &r, &g, &b) == ERR)
return Qnil;
return rb_ary_new3(3, INT2FIX(r), INT2FIX(g), INT2FIX(b));
}
#else
{
short r, g, b;
if (color_content(NUM2INT(color), &r, &g, &b) == ERR)
return Qnil;
return rb_ary_new3(3, INT2FIX(r), INT2FIX(g), INT2FIX(b));
}
#endif
}


Expand Down Expand Up @@ -1430,11 +1450,22 @@ curses_color_pairs(VALUE obj)
static VALUE
curses_pair_content(VALUE obj, VALUE pair)
{
short f,b;

curses_stdscr();
pair_content(NUM2INT(pair),&f,&b);
return rb_ary_new3(2,INT2FIX(f),INT2FIX(b));
#ifdef HAVE_EXTENDED_PAIR_CONTENT
{
int f, b;
if (extended_pair_content(NUM2INT(pair), &f, &b) == ERR)
return Qnil;
return rb_ary_new3(2, INT2FIX(f), INT2FIX(b));
}
#else
{
short f, b;
if (pair_content(NUM2INT(pair), &f, &b) == ERR)
return Qnil;
return rb_ary_new3(2, INT2FIX(f), INT2FIX(b));
}
#endif
}

/*
Expand Down Expand Up @@ -1465,6 +1496,41 @@ curses_pair_number(VALUE obj, VALUE attrs)
curses_stdscr();
return INT2FIX(PAIR_NUMBER(NUM2CHTYPE(attrs)));
}

/*
* Document-method: Curses.support_extended_colors?
*
* Returns +true+ if the ncurses library was compiled with extended color
* support (i.e., init_extended_pair, init_extended_color, etc. are available),
* +false+ otherwise.
*/
static VALUE
curses_support_extended_colors(VALUE obj)
{
#if defined(HAVE_INIT_EXTENDED_PAIR) && defined(HAVE_INIT_EXTENDED_COLOR) && \
defined(HAVE_EXTENDED_COLOR_CONTENT) && defined(HAVE_EXTENDED_PAIR_CONTENT)
return Qtrue;
#else
return Qfalse;
#endif
}

/*
* Document-method: Curses.reset_color_pairs
*
* Resets all color pairs to undefined. Requires ncurses 6.1+.
*/
#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.
curses_stdscr();
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.
return Qnil;
}
#else
#define curses_reset_color_pairs rb_f_notimplement
#endif
#endif /* USE_COLOR */

#ifdef USE_MOUSE
Expand Down Expand Up @@ -2717,7 +2783,7 @@ window_setscrreg(VALUE obj, VALUE top, VALUE bottom)
#endif
}

#if defined(USE_COLOR) && defined(HAVE_WCOLOR_SET)
#if defined(USE_COLOR) && (defined(HAVE_WCOLOR_SET) || defined(HAVE_WATTR_SET))
/*
* Document-method: Curses::Window.color_set
* call-seq: color_set(col)
Expand All @@ -2729,13 +2795,28 @@ static VALUE
window_color_set(VALUE obj, VALUE col)
{
struct windata *winp;
int res;

GetWINDOW(obj, winp);
res = wcolor_set(winp->window, NUM2INT(col), NULL);
return (res == OK) ? Qtrue : Qfalse;
#if defined(HAVE_WATTR_SET) && defined(HAVE_WATTR_GET)
/* Use wattr_set to support pair numbers > 255; preserve existing attrs. */
{
attr_t attrs;
#ifdef NCURSES_PAIRS_T
NCURSES_PAIRS_T current_pair;
#else
short current_pair;
#endif
if (wattr_get(winp->window, &attrs, &current_pair, NULL) == ERR)
return Qfalse;
return (wattr_set(winp->window, attrs, NUM2INT(col), NULL) == OK) ? Qtrue : Qfalse;
}
#elif defined(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;
#endif
}
#endif /* defined(USE_COLOR) && defined(HAVE_WCOLOR_SET) */
#endif /* defined(USE_COLOR) && (defined(HAVE_WCOLOR_SET) || defined(HAVE_WATTR_SET)) */

/*
* Document-method: Curses::Window.scroll
Expand Down Expand Up @@ -5071,6 +5152,8 @@ Init_curses(void)
rb_define_module_function(mCurses, "pair_content", curses_pair_content, 1);
rb_define_module_function(mCurses, "color_pair", curses_color_pair, 1);
rb_define_module_function(mCurses, "pair_number", curses_pair_number, 1);
rb_define_module_function(mCurses, "support_extended_colors?", curses_support_extended_colors, 0);
rb_define_module_function(mCurses, "reset_color_pairs", curses_reset_color_pairs, 0);
#endif /* USE_COLOR */
#ifdef USE_MOUSE
rb_define_module_function(mCurses, "getmouse", curses_getmouse, 0);
Expand Down Expand Up @@ -5203,9 +5286,9 @@ Init_curses(void)
rb_define_method(cWindow, "move", window_move, 2);
rb_define_method(cWindow, "move_relative", window_move_relative, 2);
rb_define_method(cWindow, "setpos", window_setpos, 2);
#if defined(USE_COLOR) && defined(HAVE_WCOLOR_SET)
#if defined(USE_COLOR) && (defined(HAVE_WCOLOR_SET) || defined(HAVE_WATTR_SET))
rb_define_method(cWindow, "color_set", window_color_set, 1);
#endif /* USE_COLOR && HAVE_WCOLOR_SET */
#endif /* USE_COLOR && (HAVE_WCOLOR_SET || HAVE_WATTR_SET) */
rb_define_method(cWindow, "cury", window_cury, 0);
rb_define_method(cWindow, "curx", window_curx, 0);
rb_define_method(cWindow, "maxy", window_maxy, 0);
Expand Down
4 changes: 3 additions & 1 deletion ext/curses/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def exec_command(cmd)
def_prog_mode reset_prog_mode timeout wtimeout nodelay
init_color wcolor_set use_default_colors assume_default_colors
newpad unget_wch get_wch wget_wch PDC_get_key_modifiers
chgat wchgat newterm)
chgat wchgat newterm
init_extended_color init_extended_pair extended_color_content
extended_pair_content reset_color_pairs wattr_set wattr_get)
have_func(f) || (have_macro(f, curses) && $defs.push(format("-DHAVE_%s", f.upcase)))
end
convertible_int('chtype', [["#undef MOUSE_MOVED\n"]]+curses) or abort
Expand Down
21 changes: 16 additions & 5 deletions sample/colors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# The TERM environment variable should be set to xterm-256color etc. to
# use 256 colors. Curses.colors returns the color numbers of the terminal.
# With ncurses 6+ extended color support, color_pairs may exceed 256.

begin
init_screen
Expand All @@ -14,14 +15,24 @@
else
start_color

addstr "This Terminal supports #{colors} colors.\n"

Curses.colors.times { |i|
Curses.init_pair(i, i, 0)
attrset(color_pair(i))
extended = Curses.support_extended_colors?
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.
next if i == 0
Curses.init_pair(i, i % colors, (i / colors) % colors)
if extended
# color_pair() encodes into chtype and can't handle pairs > 255;
# use color_set on stdscr instead, which calls wattr_set internally.
stdscr.color_set(i)
else
attrset(color_pair(i))
end
addstr("#{i.to_s.rjust(3)} ")
addstr("\n") if i == 15 || (i > 16 && (i - 15) % 36 == 0)
}
stdscr.color_set(0)
end

getch
Expand Down