Search code examples
ccommand-line-interfacereadlinelibreadline

How to highlight text using readline?


I'm trying to make a CLI that highlights text using readline. However, whenever I replace the line with a string containing ANSI codes so it can be colored, instead of the line showing up in color, the ANSI codes are displayed. How can I make the text show up colored instead?

I am using the alternate interface to replace the line with highlighted text every time a character is typed.

Here is a minimal example using that just replaces everything typed with the text RED in red (using the ANSI code \e[0;31m)

#include <stdio.h>
#include <readline/readline.h>

void linehandler(char *line) { printf("line: %s\n", line); }

int main() {
  rl_callback_handler_install("> ", linehandler);
  while (1) {
    rl_callback_read_char();
    rl_replace_line("\e[0;31mRED\e[0m", 0);
    rl_redisplay();
  }
}

The output I get:

> ^[[0;31mRED^[[0m
line: RED

Note that on the second line, the text RED is colored red, as expected. The first line is colorless, however.

I tried wrapping the ANSI codes in \001 and \002 (I made the call to replace_line use "\001\e[0;31m\002RED\001\e[0m\002"). That didn't help, the ANSI codes still showed up, as well as ^A and ^B.

How can I make the text show up with colors rather than the ANSI codes being printed? Should I be trying a completely different approach?


Solution

  • The thing is what you are editing here is no an output line, sent to stdout, so to the printer, with some escape code to instruct the printer with meta information (bold, etc.). What you are editing is the input line, from the keyboard.

    And sure, nowadays (and for the last 50 years at least) there is no actual printer involved, and the device that emulate it is the same that emulate the input (a terminal ; and even a terminal emulator, that is a "console window" which is a printer emulator emulator). But well, those ansi codes are for output not for input.

    Sure, it is readline. So in reality it is just emulating the said behaviour and printing the rl_buffer_line as it would print other output. But even if it is so, that emulation is correctly done, and escape chars are not interpreted. Plus, it doesn't necessarily print everything.

    It is more reverse engineering that something I read in the doc, so I don't know how much you can count on this. But apprently, rl_replace_line rewrite only part of the line that has changed (probably because readline is quite old, and date back from the times of the physical terminals, printing things at 9600 bps. So, at this speed, or lack of it, you don't want the whole line to be rewritten each time you change a single byte of it)

    See this (ugly, and unsafe with my tmp[100], but it is just an experiment) code

    #include <stdio.h>
    #include <string.h>
    
    #include <readline/readline.h>
    #include <readline/history.h>
    
    void linehandler(char *line)
    {
      printf("line: %s\n", line);
    }
    
    int main()
    {
      rl_callback_handler_install("> ", linehandler);
      while (1)
      {
        rl_callback_read_char();
        char tmp[100]={0};
        if(strstr(rl_line_buffer, "red")){
            printf("\033[31m");
            fflush(stdout);
            strcpy(tmp, rl_line_buffer);
            for(int i=5; rl_line_buffer[i] && i<10; i++) tmp[i] = toupper(rl_line_buffer[i]);
            rl_replace_line(tmp, 0);
        }
    
        rl_redisplay();
      }
    }
    

    So what it does is, it scans the current input line for the three letters red. And if it finds it, it change the color to red for now on (note that this is done just by sending ANSI control via printf. So readline is not aware of that at all. So only reason why it doesn't mess the input, is because I only print those controls, so they don't take any place at all on the screen).

    Plus, once it finds this red it also put chars 5 to 10 to uppercase.

    If you type one two three red four, you'll see that input string is indeed changed to one tWO THree red four. With WO TH and four in red. So, only what was changed after the terminal switched to red.

    Proof that one t and ree red was not even reprinted since before that color switch. rl_replace_line printed just WO TH. And four was printed when I typed it.

    You can even see that, if you go back with arrows keys to, change, say the n of one by a X, the said X (but nothing else) will also be in red.

    Point is, the input you see on screen is not really printed as a string, but char by char.

    Now, I don't know what you are trying to do exactly. So hard to give an advise for another approach. Do you want to highlight only part of the input? Or to highlight the whole line under certain condition?

    But my little experiment gives you some hint on possible solutions. Which is sending yourself control char to the terminal (via printf, or puts or write, not readline) so control in which color readline print the future things it prints.

    And, if you don't mind relying on an undocumented behaviour (well, maybe it is documented. It just that I know about it by reverse engineering. And, after all, printing ESC[33m codes is also UB: you don't know for sure that the printing device understand this), you can even trick readline into printing only a word in red

    See following code:

    #include <stdio.h>
    #include <string.h>
    
    #include <readline/readline.h>
    #include <readline/history.h>
    
    void linehandler(char *line)
    {
      printf("line: %s\n", line);
    }
    
    int main()
    {
      rl_callback_handler_install("> ", linehandler);
      while (1)
      {
        rl_callback_read_char();
        char tmp[100]={0};
        strcpy(tmp, rl_line_buffer);
        for(int i=0; tmp[i] && i<97; i++){
            if(!strncmp(tmp+i, "red", 3)){
                memcpy(tmp+i, "XXX", 3);
                rl_replace_line(tmp, 0);
                rl_redisplay();
                printf("\033[31m"); fflush(stdout);
                memcpy(tmp+i, "red", 3);
                rl_replace_line(tmp, 0);
                rl_redisplay();
                printf("\033[m"); fflush(stdout);
            }
        }
    
        rl_redisplay();
      }
    }
    

    It prints in red the words "red" in the input line.

    Note that it doesn't impact the line itself (the buffer). So if you want the line to actually contain the ANSI codes, and/or to be printed in red when validated, you have to deal with that in linehandler.