1

I am using the (excellent) readline (version 6.3, default [non-vi] mode) library from within my own program, running in a Terminal window (on a PC). There is a problem when there is previous output not terminated by newline when readline() is called.

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

void main(void)
{
  // Previous output from some other part of application
  // which *may* have output stuff *not* terminated with a '\n'
  printf("Hello ");
  fflush(stdout);

  char *in = readline("OK> ");
}

So the line looks like:

Hello OK> <caret here>

If you type a small number of characters (up to 5?) and then, say, Ctrl+U (may be others) to delete your input so far it all seems well --- readline() moves the caret back to just after its own prompt. However, try typing, say:

123456 <Ctrl+U>

Now it deletes back into the Hello, leaving just Hell on the line, followed by the caret.

I need one of two possible solutions:

  1. This does look like a bug, now that I have realised it depends on how many characters are typed on the line where it goes wrong. Any fix/workaround?

  2. Alternatively, is there a readline library call I could make which would tell me what position/column the caret is at before I invoke readline()? Then at least I could recognise the fact that that I am at the end of an existing line and output a \n so as to position myself at the start of a new line first.

P.S. Is this an OK place to ask about readline programming under Ubuntu or should I be posting at stackoverflow.com?

JonBrave
  • 649

2 Answers2

0

[Stackoverflow would be a more appropriate place for such a programming question]

This is not a bug but the expected behaviour. readline is not aware of what was written on the terminal earlier and at what position it is writing. Think "basic serial terminal". Moreover other background processes (which your program is not aware of) may also write to the terminal.

So, readline assumes it starts to write at the beginning of the terminal line. When you Ctrl-U (unix-line-discard), readline goes back where it thinks you started to type characters, i.e. just after its prompt. Your prompt "OK> " is four characters long, so readline put the caret at the 5th place and erase the line, leaving just "Hell".

The workaround could be to skip a line before calling readline, or to begin your prompt with a CR character (i.e. \r), which would force the prompt at the beginning of the line, overwriting "Hello" (but a longer text would only be partially overwritten).

[update]

As for why sometimes Ctrl-U erases just the last typed characters, and sometimes it erases (almost) the whole line, it is a readline optimisation.

readline can emit two different character sequences to erase the whole input:

  • either: n × <BS> (backspace) + <control sequence to erase the whole line> (e.g. ANSI <ESC> [ K), where n is the number of characters typed so far.
  • or: <CR> + m × <control sequence to move the cursor to the right> (e.g. ANSI <ESC> [ C) + <control sequence to erase the whole line>, where m is the length of the prompt.

readline choose the shortest, which depends on the number of characters typed wrt the length of your prompt.

xhienne
  • 391
  • I think you are not taking into account the reported behaviour (try out my program). If you type up to and including 5 characters after the prompt readline deletes and goes back by 5 characters; if you type 6 characters it deletes and goes back by 6+6==12 characters. If readline requires an assumption that it will only be called at the start of a terminal line (prior to any prompt) why doesn't it say so somewhere? – JonBrave Dec 17 '16 at 12:20
  • @JBarchan I have added an explanation for this behaviour to my answer. – xhienne Dec 17 '16 at 13:31
  • I think I agree with the gist of your explanation of the 2 ways readline can do the erase. (But your #2 is not enough: there is no erasing there, only cursor repositioning, there must be an additional erase-to-EOL output?) If I could force it to always choose backspacing that would solve: is there a way I can tell it to choose that, or that my terminal cannot do your #2? – JonBrave Dec 17 '16 at 16:21
  • @JBarchan Good catch, I fixed my answer. Sorry, I don't know readline enough to answer your last question. I don't think changing your termcap definition could solve this. Maybe one of the numerous readline's options. – xhienne Dec 17 '16 at 16:40
  • @JBarchan Is there something in my answer you are still unhappy with and that keeps from from choosing it, or at least vote for it? – xhienne Dec 19 '16 at 18:27
  • Sorry, I had gone off to stackoverflow.com for this question. I am up-voting your answer now. But at present I do not wish to mark it as "the solution" since I have not found any way to get readline to work as desired if there is previous output. – JonBrave Dec 20 '16 at 16:20
  • and it looks like with my low reputation here my vote "gets recorded but not displayed" :( ... – JonBrave Dec 20 '16 at 17:02
  • @JBarchan :) Did you try my workaround (\n or \r in the prompt)? It kind of work. You can even prompt with \r<lot of spaces>\r<your prompt> so that any previous characters are erased (and as a welcome side-effect, your prompt will be long enough to disable the undesirable behaviour of Ctrl-U). – xhienne Dec 20 '16 at 17:08
  • That is not my goal/question. I know I can erase what is there and all will work. The whole point is that I do want to be able to see anything that has been output. With \r I will lose it, and with \n I will get a blank line for every usual case where there was nothing. Neither of those is "acceptable"! – JonBrave Dec 21 '16 at 16:01
  • I see that bash suffers the same problem. Try: echo -n abcdefghi<RETURN>. Then type a lot of characters (30? 123456789012345678901234567890 and then Ctrl+U. Your prompt gets overwritten. – JonBrave Dec 21 '16 at 16:05
0

It turns out that readline cannot recognise if it is not starting at column #1, and thereby stop itself from messing up the previous output on the line.

The only way to deal with this is to recognise the starting column ourselves, and move to the start of the next line down if the current position is not column #1. Then it will always starts from the left-most column, without outputting an unnecessary newline when it is already at column #1.

We can do this for the standard "Terminal" because it understands an ANSI escape sequence to query the current row & column of the terminal. The query is sent via characters to stdout and the response is read via characters the terminal inserts into stdin. We must put the terminal into "raw" input mode so that the response characters can be read immediately and will not be echoed.

So here is the code:

rl_prep_terminal(1);       // put the terminal into "raw" mode
fputs("\033[6n", stdout);  // <ESC>[6n is ANSI sequence to query terminal position
int row, col;              // terminal will reply with <ESC>[<row>;<col>R
fscanf(stdin, "\033[%d;%dR", &row, &col);
rl_deprep_terminal();      // restore terminal "cooked" mode
if (col > 1)               // if beyond the first column...
  fputc('\n', stdout);     // output '\n' to move to start of next line

in = readline(prompt);     // now we can invoke readline() with our prompt
JonBrave
  • 649