tags:

views:

435

answers:

5

I use GNU Readline in the "select" fashion, by registering a callback function like so:

rl_callback_handler_install("", on_readline_input);

And then hooking up rl_callback_read_char as the callback for my select() loop for STDIN_FILENO. That's all pretty standard stuff, and works fine.

Now, my program asynchronously prints messages to the screen, sometimes interleaved with input from the user. A "clean" session would look like this:

user input
SERVER OUTPUT
SERVER OUTPUT
user input
SERVER OUTPUT

But what if the user is midway through a line when the server response arrives? Then it gets ugly:

user input
SERVER OUTPUT
user inSERVER OUTPUT
put
SERVER OUTPUT

I fixed this simply by printing a newline before the server output if the user had typed anything (this is easy to tell by checking rl_line_buffer), and then doing rl_forced_update_display() after printing the server output. Now it looks like this:

user input
SERVER OUTPUT
user in
SERVER OUTPUT
user input
SERVER OUTPUT

This is better, but still not perfect. The problem comes when the user typed an entire line but didn't yet press Enter--then it looks like this:

user input
SERVER OUTPUT
user input
SERVER OUTPUT
user input
SERVER OUTPUT

This is bad because it appears to the user that they typed three commands (three responses for three inputs is just as possible as three responses for two inputs, which is what actually happened).

A nasty hack (which works) is to do this:

user input
SERVER OUTPUT
user input - INCOMPLETE
SERVER OUTPUT
user input
SERVER OUTPUT

I figured I could improve this by printing backspace ('\b') characters instead of " - INCOMPLETE", but that doesn't seem to do anything at all on my terminal (gnome-terminal on Ubuntu Hardy). printf("ABC\b"); just prints ABC, for whatever reason.

So how can I erase the incomplete input line? Either by printing backspaces somehow (I can figure out how many to print--it's strlen(rl_line_buffer)), or by using some Readline facility I don't yet know about?

+3  A: 

With spaces? Try to print "\b \b" for each character you want to "delete" rather than a single '\b'.


Edit

How it works
Suppose you have written "Hello, world!" to the display device and you want to replace "world!" with "Jim."

Hello, world!
             ^ /* active position */ /* now write "\b \b" */
               /* '\b' moves the active position back;
               // ' ' writes a space (erases the '!')
               // and another '\b' to go back again */
Hello, world
            ^ /* active position */ /* now write "\b \b" again */
Hello, worl
           ^ /* active position */ /* now write "\b \b" 4 times ... */
Hello, 
       ^ /* active position */ /* now write "Jim." */
Hello, Jim.
           ^ /* active position */

Portability
I'm not sure, but the Standard specifically describes the behaviour of '\b' and '\r' as has been described in answers to your question.

Section 5.2.2 Character display semantics

> 1   The active position is that location on a display device where the next character output by
>     the fputc function would appear. The intent of writing a printing character (as defined
>     by the isprint function) to a display device is to display a graphic representation of
>     that character at the active position and then advance the active position to the next
>     position on the current line. The direction of writing is locale-specific. If the active
>     position is at the final position of a line (if there is one), the behavior of the display devic e
>     is unspecified.
>  
> 2   Alphabetic escape sequences representing nongraphic characters in the execution
>     character set are intended to produce actions on display devices as follows:
>     \a (alert) Produces an audible or visible alert without changing the active position.
>     \b (backspace) Moves the active position to the previous position on the current line. If
>        the active position is at the initial position of a line, the behavior of the display
>        device is unspecified.
>     \f ( form feed) Moves the active position to the initial position at the start of the next
>        logical page.
>     \n (new line) Moves the active position to the initial position of the next line.
>     \r (carriage return) Moves the active position to the initial position of the current line.
>     \t (horizontal tab) Moves the active position to the next horizontal tabulation position
>        on the current line. If the active position is at or past the last defined horizontal
>        tabulation position, the behavior of the display device is unspecified.
>     \v (vertical tab) Moves the active position to the initial position of the next vertical
>         tabulation position. If the active position is at or past the last defined vertical
>         tabulation position, the behavior of the display device is unspecified.
>  
> 3   Each of these escape sequences shall produce a unique implementation-defined value
>     which can be stored in a single char object. The external representations in a text file
>     need not be identical to the internal representations, and are outside the scope of this
>     International Standard.
pmg
Some terminals/terminal emulators have different behaviour for the backspace character. pmg has the right idea.
Carl Norum
This does work in my terminal. But without a better understanding of why it works and how (un)portable it is, I've chosen to use '\r' instead (as suggested in another answer).
John Zwinck
Changed my mind after using the '\r' code for a while...I do like this better, because it doesn't require overprinting with spaces at the end (I prefer to do the overprinting before the server output, so this solution suits me best, not to mention that it's the most direct answer to my original question).
John Zwinck
A: 
sambowry
Unfortunately, curses and readline don't mix. See http://stackoverflow.com/questions/691652/using-gnu-readline-how-can-i-add-ncurses-in-the-same-program (also asked by me).
John Zwinck
+1  A: 

One thing you can do is to use \r to jump to the beginning of the line for the server output. Then you can use field width specifiers to right pad the output to the rest of the line. This will, in effect, overwrite whatever the user had already entered.

fprintf (stdout, "\r%-20s\n", "SERVER OUTPUT");

You may want to fflush(stdout) to ensure that the buffers are in a consistent state before you do that.

ezpz
This is clever and I implemented it and used it for a while. In the end I found I preferred the "\b \b" solution posted by pmg. But, good one!
John Zwinck
A: 

Do any of these functions help?

  • rl_reset_line_state()
  • rl_clear_message()
  • rl_delete_text()
  • rl_kill_text()

Also, can you mediate the server output - have the server output controlled so that it only appears when and where you want it to, rather than just sprawling over what the user is typing? For example, if your application is running in curses mode, could you have a split window with a line or two at the bottom in one sub-window reserved for user input and the rest of the output (server output and accepted user input) in a second sub-window above it?

Jonathan Leffler
If you are suggesting that I make my readline application also be a curses application, it seems to be impossible: http://stackoverflow.com/questions/691652/using-gnu-readline-how-can-i-add-ncurses-in-the-same-program
John Zwinck
I tried `rl_reset_line_state()` and `rl_clear_message()`...neither of these is helpful. I'll try some more readline functions when I can, but I think I already went through quite a lot of interesting-looking ones.
John Zwinck
@John Zwinck: I've not pushed the readline library hard enough to know whether any of those were useful. And if readline won't work with curses (not a big surprise), then there are two possibilities: (1) ignore the suggestion, or (2) revise the application to use curses instead of readline. That (option 2) is definitely more work.
Jonathan Leffler
A: 

After quite a lot of hacking I was able to get this mechanism. I hope other people will find it useful. It does not even use select(), but I hope you will get the point.

#include <readline/readline.h>
    #include <readline/history.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>

    const char const* prompt = "PROMPT> ";

    void printlog(int c) {
        char* saved_line;
        int saved_point;
        saved_point = rl_point;
        saved_line = rl_copy_text(0, rl_end);
        rl_set_prompt("");
        rl_replace_line("", 0);
        rl_redisplay();
        printf("Message: %d\n", c);
        rl_set_prompt(prompt);
        rl_replace_line(saved_line, 0);
        rl_point = saved_point;
        rl_redisplay();
        free(saved_line);
    }


    void handle_line(char* ch) {
        printf("%s\n", ch);
        add_history(ch);
    }

    int main() {
        int c = 1;

        printf("Start.\n");
        rl_callback_handler_install(prompt, handle_line);

        while (1) {
            if (((++c) % 5) == 0) {
                printlog(c);
            }

            usleep(10);
            rl_callback_read_char();
        }
        rl_callback_handler_remove();
    }
dpc.ucore.info
See http://github.com/dpc/xmppconsole/blob/master/src/io.c for working example.
dpc.ucore.info