GNU Readline:如何清空输入行?

7
我使用GNU Readline以 "select" 方式,通过注册回调函数来实现,如下所示:
rl_callback_handler_install("", on_readline_input);

然后将rl_callback_read_char作为回调函数连接到我的select()循环中,用于处理STDIN_FILENO。这是相当标准的东西,也能正常工作。

现在,我的程序会异步地向屏幕上打印消息,有时还会与用户的输入交错显示。一个“干净”的会话看起来像这样:

user input
SERVER OUTPUT
SERVER OUTPUT
user input
SERVER OUTPUT

但是如果用户在服务器响应到达时正在输入一行文字,会怎样呢?那么情况就会变得很糟糕:

user input
SERVER OUTPUT
user inSERVER OUTPUT
put
SERVER OUTPUT

我通过在用户输入了任何内容(可以通过检查rl_line_buffer)之前打印一个换行符,然后在打印服务器输出后执行rl_forced_update_display()来解决了这个问题。现在它看起来像这样:

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

这已经比之前好很多,但还不完美。问题出现在用户输入了整行文字但还没有按回车键时,界面看起来是这样的:

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

这种情况很糟糕,因为用户看起来好像输入了三个命令(对于三个输入而言,有可能会得到三个响应,就像实际发生的两个输入得到三个响应一样)。

一个不太优雅但可行的方法是这样做:

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

我认为可以通过打印回退('\b')字符来改善这个问题,而不是打印" - INCOMPLETE",但在我的终端上(Ubuntu Hardy的gnome-terminal),这似乎没有任何作用。例如:printf("ABC\b");只会打印出ABC,原因未知。
那么我该如何擦除不完整的输入行呢?可以通过某种方式打印回退字符(我可以计算需要打印的数量——即strlen(rl_line_buffer)),或者使用一些Readline工具,但我还不了解。
6个回答

6

经过一番尝试,我终于找到了这个方法。我希望其他人也能从中受益。这个方法甚至不需要使用select()函数,但我相信你可以理解其原理。

#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();
    }

这应该是被接受的解决方案。它可能有些古怪,但它应该正确处理多行输入。 - Přemysl J.

3

有空格?尝试为要“删除”的每个字符打印"\b \b"而不是单个'\b'


编辑

它是如何工作的
假设您已将“Hello,world!”写入显示设备,并且您想用“Jim。”替换“world!”。

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 */

可移植性
我不确定,但标准特别描述了 '\b' 和 '\r' 的行为,就像在回答您的问题中所描述的那样。

第5.2.2节字符显示语义

> 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的想法是正确的。 - Carl Norum
这在我的终端中可以工作。但是由于我对它为什么能够工作以及它的可(不)移植性没有更好的理解,因此我选择使用 '\r' (正如另一个答案中建议的那样)。 - John Zwinck
使用'\r'代码一段时间后,我改变了主意...我更喜欢这种方式,因为它不需要在结尾处用空格进行过打印(我更喜欢在服务器输出之前进行过打印,所以这个解决方案最适合我,更不用说它是对我最初问题的最直接回答了)。 - John Zwinck

1

你可以使用\r跳到服务器输出的行首,然后使用字段宽度说明符将输出右对齐填充到该行的其余部分。这实际上会覆盖用户已经输入的任何内容。

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

在执行该操作之前,您可能需要使用fflush(stdout)来确保缓冲区处于一致状态。


这很聪明,我实现并使用了一段时间。最终,我发现我更喜欢pmg发布的"\b \b"解决方案。但是,不错! - John Zwinck

0

我尝试使用ncurses窗口分离服务器输出和用户输入。服务器输出是通过线程模拟的。程序一直运行,直到您输入以“q”开头的行。

#include <unistd.h> 
#include <curses.h> 
#include <pthread.h> 

WINDOW *top, *bottom;

int win_update( WINDOW *win, void *data ){
  wprintw(win,"%s", (char*)data ); wrefresh(win);
  return 0;
}

void *top_thread( void *data ){
  char buff[1024];
  int i=0;
  while(1){
    snprintf(buff, 1024, "SERVER OUTPUT: %i\n", i++ );
    use_window( top, win_update, (void*)buff );
    sleep(1);
  }
  return NULL;
}

int main(){
  initscr();
  int maxy, maxx;
  getmaxyx( stdscr, maxy, maxx );

  top = newwin(maxy-1,maxx,0,0);
  wsetscrreg(top,0,maxy-1); idlok(top,1); scrollok(top,1);
  pthread_t top_tid;
  pthread_create(⊤_tid, NULL, top_thread, NULL);

  bottom = newwin(1,maxx,maxy-1,0);
  char buff[1024], input[maxx];
  do{
    werase(bottom); wmove(bottom,0,0);
    wprintw(bottom,"input> " ); wrefresh(bottom);
    wgetnstr(bottom,input,sizeof(input));
    snprintf(buff, 1024, "user input: '%s'\n", input );
    use_window( top, win_update, (void*)buff );
  }while( input[0] != 'q' );

  endwin();
}

不幸的是,curses和readline不能混合使用。请参见https://dev59.com/7nRB5IYBdhLWcg3wLk1M(也由我提问)。 - John Zwinck

0

这些函数中有哪些对你有帮助呢?

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

另外,你能控制服务器输出吗?比如说,你可以在应用程序运行在curses模式下时,将窗口分为两个子窗口。在底部的一个子窗口中只保留一两行用户输入,而在上面的第二个子窗口中则显示剩余的所有输出(包括服务器输出和已接受的用户输入)。


如果你建议我将我的readline应用程序也变成curses应用程序,那似乎是不可能的:https://dev59.com/7nRB5IYBdhLWcg3wLk1M - John Zwinck
我尝试了 rl_reset_line_state()rl_clear_message(),但都没有帮助。等我有时间的时候会尝试更多的 readline 函数,但我觉得我已经尝试了很多看起来很有趣的函数了。 - John Zwinck
@John Zwinck:我还没有充分测试readline库,不知道它们是否有用。如果readline不能与curses一起使用(这并不奇怪),那么有两种可能性:(1)忽略建议,或者(2)修改应用程序以使用curses而不是readline。第二个选项肯定更费力。 - Jonathan Leffler

0

这个似乎也可以工作:

rl_clear_visible_line();
printf(...);
rl_reset_line_state();
rl_redisplay();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接