Search code examples
cterminalcolorsncursesguix

How the background color pair should affect the color of subsequent characters in ncurses?


I was able to reproduce my issue with this small example code:

#include <ncurses.h>

int main() {
        initscr();
        start_color();
        init_pair ( 1, COLOR_BLUE, COLOR_BLACK );
        init_pair ( 2, COLOR_YELLOW, COLOR_BLACK );
        bkgd ( (chtype) COLOR_PAIR(1) );
        attrset(COLOR_PAIR(2));
        printw("NO WAR!");
        refresh();
        getch();
        endwin();
        return 0;
}

Once compiled with gcc test.c -lncurses, where test.c is that code, ./a.out gives blue text if compiled in guix and, if compiled outside guix, it gives text in the curses equivalent of yellow instead.


I am looking into the code of a game written in C and using ncurses. The code initiates color pairs, then sets the background to a color pair. My issue is that when I make the game from source code, the further behavior is different depending on whether (1) I build the code as usual in Debian or Fedora or whether (2) I build the code with guix. Typically, further setting of color of characters works as expected and I get the background overwritten with colored characters in places where the characters appear on screen. But if I build it with guix, all further characters are drawn in the same color pair which the background was assigned to. (I can change that color pair and it affects everything.)

If I remove the part which sets the background color pair, the programs works as expected in both cases with the default color pair being seemingly white-on-black and further characters colored according to the code.

I commented over the line below, removing which helps.

    /* set up colors */
    (void) start_color();
    if ( has_colors() && ( COLOR_PAIRS > 7 ) ) {
        state.options |= OPTION_HAS_COLOR;
        (void) init_pair ( 1, COLOR_GREEN, COLOR_BLACK );
        (void) init_pair ( 2, COLOR_RED , COLOR_BLACK );
        (void) init_pair ( 3, COLOR_YELLOW, COLOR_BLACK );
        (void) init_pair ( 4, COLOR_BLUE, COLOR_BLACK );
        (void) init_pair ( 5, COLOR_MAGENTA, COLOR_BLACK );
        (void) init_pair ( 6, COLOR_CYAN, COLOR_BLACK );
        (void) init_pair ( 7, COLOR_WHITE, COLOR_BLACK );
        
        /* Removing the following line helps */
        (void) bkgd ( (chtype) COLOR_PAIR(WHITE) );

        state.items[ROBOT].color = WHITE;
        state.items[KITTEN].color = randcolor();
        for ( i = BOGUS; i < state.num_items; i++ ) {
            state.items[i].color = randcolor();
        }
    } else {
        state.options &= ~ OPTION_HAS_COLOR;
    }
}

/*@-globstate@*/
static void draw ( const screen_object *o ) {
    attr_t new;

    /*@-nullpass@*/
    assert ( curscr != NULL);
    if ( ( state.options & OPTION_HAS_COLOR ) != 0 ) {
        new = COLOR_PAIR(o->color);
        if ( o->bold ) { new |= A_BOLD; }
        if ( o->reverse ) { new |= A_REVERSE; }
        (void) attrset ( new );
    }
    (void) addch ( o->character );
    /*@+nullpass@*/
}

The complete C source: https://github.com/robotfindskitten/robotfindskitten/blob/main/src/robotfindskitten.c

I tried building with guix against ncurses versions 6.2.20200212 and 6.2.20210619. The versions in Debian which I tried making against were 6.2+20201114-2 and 6.3-2. The version on Fedora is 6.2.20210508.

The difference does not seem to be caused by environment variables, since I can reproduce the result within the same terminal on Debian having locally compiled, repository, and guix versions of the same package. Also, I can change the background color pair by modifying the source code, so the terminal is properly identified as supporting color.

According to ncurses manual pages (man bkgd):

  • The library first compares the character, and if it matches the current character part of the background, it replaces that with the new background character.
  • The library then checks if the cell uses color, i.e., its color pair value is nonzero. If not, it simply replaces the attributes and color pair in the cell with those from the new background character.
  • If the cell uses color, and that matches the color in the current background, the library removes attributes which may have come from the current background and adds attributes from the new background. It finishes by setting the cell to use the color from the new background.
  • If the cell uses color, and that does not match the color in the current background, the library updates only the non-color attributes, first removing those which may have come from the current background, and then adding attributes from the new background.

My understanding is that, for some reason, in guix situation, the new characters are considered to be in the background and are not colored as expected but inherit the background color pair, while, in usual situation, they would be colored as expected.

What may affect the setting of characters' color? Is it a bug in a library or a variation in implementation? Removing bkgd part works, but I am not sure whether I should report it as a bug to the software authors or against ncurses library at Guix or just forget about it.


Solution

  • After calling bkgd, subsequent calls to waddch use the background color unless the waddch call (or wattrset) has specified a color. That's done in the render_char function of ncurses:

    static NCURSES_INLINE NCURSES_CH_T
    render_char(WINDOW *win, NCURSES_CH_T ch)
    /* compute a rendition of the given char correct for the current context */
    {
        attr_t a = WINDOW_ATTRS(win);
        int pair = GetPair(ch);
    
        if (ISBLANK(ch)
        && AttrOf(ch) == A_NORMAL
        && pair == 0) {
        /* color/pair in attrs has precedence over bkgrnd */
        ch = win->_nc_bkgd;
        SetAttr(ch, a | AttrOf(win->_nc_bkgd));
        if ((pair = GET_WINDOW_PAIR(win)) == 0)
            pair = GetPair(win->_nc_bkgd);
        SetPair(ch, pair);
        } else {
        /* color in attrs has precedence over bkgrnd */
        a |= AttrOf(win->_nc_bkgd) & COLOR_MASK(a);
        /* color in ch has precedence */
        if (pair == 0) {
            if ((pair = GET_WINDOW_PAIR(win)) == 0)
            pair = GetPair(win->_nc_bkgd);
        }
        AddAttr(ch, (a & COLOR_MASK(AttrOf(ch))));
        SetPair(ch, pair);
        }
    
        TR(TRACE_VIRTPUT,
           ("render_char bkg %s (%d), attrs %s (%d) -> ch %s (%d)",
        _tracech_t2(1, CHREF(win->_nc_bkgd)),
        GetPair(win->_nc_bkgd),
        _traceattr(WINDOW_ATTRS(win)),
        GET_WINDOW_PAIR(win),
        _tracech_t2(3, CHREF(ch)),
        GetPair(ch)));
    
        return (ch);
    }
    

    However, there was a bug in ncurses 6.2 as discussed on the mailing list in March 2020 reported with guix 2.0. There was a simple workaround for callers (which may affect some users). The version of guix is not stated in the question.