在没有 ncurses 的 C/C++ 中编写“真正的”交互式终端程序,如 vim、htop、...

Posted

技术标签:

【中文标题】在没有 ncurses 的 C/C++ 中编写“真正的”交互式终端程序,如 vim、htop、...【英文标题】:Writing a "real" interactive terminal program like vim, htop, ... in C/C++ without ncurses 【发布时间】:2012-01-18 13:35:01 【问题描述】:

不,我不想使用 ncurses,因为我想了解 终端工作,并享受我自己编程的乐趣。 :) 它没有 必须是可移植的,它只能在基于 linux xterm 的终端仿真器上工作。

我想做的是编写一个交互式终端应用程序,例如 htop 和 vim。我的意思不是输出看起来像框或设置颜色的字符,这是微不足道的;也使内容适合窗口大小。我需要的是

    如何获得 鼠标交互,例如单击字符并滚动鼠标滚轮(当鼠标位于特定字符时)以实现滚动 [编辑:在终端中当然是模拟器],以及

    如何完全保存和恢复父进程的输出并将我的打印与其输出分开,所以在离开我的应用程序后,我在 shell 中输入的命令应该在那里,就像在运行 htop 并再次退出时一样:此应用程序不再可见任何内容。

我真的不想使用 ncurses。但是当然,如​​果你知道ncurses的哪个部分负责这些任务,欢迎告诉我在源代码中哪里可以找到,我会好好研究的。

【问题讨论】:

"欢迎您告诉我在源代码的哪里可以找到它" 我这么说是因为我确信我对 ncurses 源代码的了解不如整天使用它的人。 :) 大多数人使用它的 API 但不会改变它的实现,所以你遇到这样的人的机会很小。只需阅读源代码,例如,我在一分钟内找到了文件“lib_mvcur.c”(包括下载源代码),其中包含“移动物理光标和滚动的例程”。检查文件 cmets,文档看起来不错 大多数终端至少模拟vt220,因此您可以开始实现控制。 (尽管很少有程序足够疯狂,包括 vim,它们宁愿使用 ncurses 或至少使用 termcap) 首先您需要知道如何将终端设置为原始模式,其次您至少需要 termcap (libtermcap) 或滚动您自己的 tput 和抽象层。如果没有鼠标,我估计这对于具有 C 和 unix 经验的人来说需要几个月的工作。在 ESC 上超时的 tgetc 对解析器来说是讨厌的。当然不适合胆小的人...... 【参考方案1】:

我有点困惑。你说的是“终端应用程序”, 像 vim;终端应用程序不会获得鼠标事件,也不会 响应鼠标。

如果您谈论的是真正的终端应用程序,它们运行在 xterm,需要注意的重要一点是许多可移植性 问题涉及终端,而不是操作系统。终端受控 通过发送不同的转义序列。哪些取决于终端;然而,ANSI 转义码现在相当普遍,请参阅http://en.wikipedia.org/wiki/ANSI_escape_code。例如xterm一般都理解这些。

您可能必须在开始和结束时输出额外的序列才能进入和离开“全屏”模式;这是xterm 所必需的。

最后,您必须在输入/输出级别执行一些特殊操作,以确保您的输出驱动程序不会添加任何字符(例如将简单的 LF 转换为 CRLF),并确保输入不会回显, 是透明的,并立即返回。在 Linux 下,这是使用ioctl 完成的。 (同样,完成后不要忘记恢复它。)

【讨论】:

终端应用中鼠标事件有GPM,不是吗? 这个答案的开头完全是假的。有一个协议可以通知终端您想要鼠标单击事件,还有一个协议可以将它们从终端发送到应用程序,所有这些都作为转义序列。 Linux 控制台不支持这一点,而是使用可怕的 GPM 方法,但xterm 和其他人确实支持它。 对于鼠标事件,我的意思是在终端模拟器中。我已经禁用了用户输入和输入缓冲区的回显,所以我使用标准输入作为一种“关键事件”流而不是文本行。我在哪里可以找到我需要在“全屏”模式的开始/结束时放置哪些额外的序列?没有ANSI转义码吗? @R..:我在哪里可以找到这个协议?这正是我正在寻找的。当然,在真正的 linux 终端中我不需要鼠标控制,因为在 vim 和 htop 中没有这样的东西(这就是我与 vim 和 htop 进行比较的原因) @leemes 这是您需要发送的 ANSI 转义。我不确定是什么,但它应该在termcaps 文件中。 (ncurses/termcaps的重点是控制序列是从配置文件中读取的,并且根据终端的类型而有所不同。)【参考方案2】:

为了操作终端,您必须使用控制序列。不幸的是,这些代码取决于您使用的特定终端。这就是为什么terminfo(以前的termcap)首先存在的原因。

您没有说是否要使用 terminfo。所以:

如果您将使用 terminfo,它将为您的终端支持的每个操作提供正确的控制序列。 如果您不使用 terminfo... 好吧,您必须手动编写要支持的每种终端类型中的每个操作。

因为你想要这个用于学习目的,我将在第二个详细说明。

您可以从环境变量$TERM 中发现您正在使用的终端类型。在 linux 中,最常用的是 xterm 用于终端仿真器(XTerm、gnome-terminal、konsole),linux 用于虚拟终端(X 未运行时)。

您可以使用命令tput 轻松发现控制序列。但是当tput在控制台上打印它们时,它们会立即应用,所以如果你想真正看到它们,请使用:

$ TERM=xterm tput clear | hd
00000000  1b 5b 48 1b 5b 32 4a                              |.[H.[2J|

$ TERM=linux tput clear | hd
00000000  1b 5b 48 1b 5b 4a                                 |.[H.[J|

也就是说,要在 xterm 中清除屏幕,您必须在 xterm 中输出 ESC [ H ESC [ 2J 而在 linux 终端中输出 ESC [ H ESC [ J

关于您询问的特定命令,您应该仔细阅读man 5 terminfo。那里有很多信息。

【讨论】:

给猫剥皮的方法有很多种。 还有很多方法可以清除屏幕。特别是 IIRC,ESC[2J 清除整个屏幕,而 ESC[J 从光标清除到屏幕末尾。但是由于ESC[H 将光标移动到HOME,所以它们应该是等效的。毫不奇怪,xtermlinux 终端往往非常相似。 @leemes:那是因为您使用的终端似乎支持两者。 只有真正晦涩的东西在现实世界的终端之间实际上差异很大,它们都大致符合 ANSI/ECMA 标准。如果你只处理 ^H 和 ^?作为退格键并遵循标准,curses/termcap/terminfo 已经过时了...... @R.. 我会说相反,只有最常用的东西是常见的。例如,按 F2 (tput kf2) 将在 xterm 中生成 ESC OQ,但在 linux 中生成 ESC[[B。但是按右箭头 (tput cuf1) 将在两者上都执行 ESC [C【参考方案3】:

虽然这个问题有点老了,但我想我应该分享一个简短的例子来说明如何在不使用 ncurses 的情况下做到这一点,这并不难,但我相信它不会那么便携。

此代码将标准输入设置为原始模式,切换到备用缓冲区屏幕(在启动终端之前保存终端的状态),启用鼠标跟踪并在用户单击某处时打印按钮和坐标。使用 Ctrl+C 退出后,程序将恢复终端配置。

#include <stdio.h>
#include <unistd.h>
#include <termios.h>

int main (void)

    unsigned char buff [6];
    unsigned int x, y, btn;
    struct termios original, raw;

    // Save original serial communication configuration for stdin
    tcgetattr (STDIN_FILENO, &original);

    // Put stdin in raw mode so keys get through directly without
    // requiring pressing enter.
    cfmakeraw (&raw);
    tcsetattr (STDIN_FILENO, TCSANOW, &raw);

    // Switch to the alternate buffer screen
    write (STDOUT_FILENO, "\e[?47h", 6);

    // Enable mouse tracking
    write (STDOUT_FILENO, "\e[?9h", 5);
    while (1) 
        read (STDIN_FILENO, &buff, 1);
        if (buff[0] == 3) 
            // User pressd Ctr+C
            break;
         else if (buff[0] == '\x1B') 
            // We assume all escape sequences received 
            // are mouse coordinates
            read (STDIN_FILENO, &buff, 5);
            btn = buff[2] - 32;
            x = buff[3] - 32;
            y = buff[4] - 32;
            printf ("button:%u\n\rx:%u\n\ry:%u\n\n\r", btn, x, y);
        
    

    // Revert the terminal back to its original state
    write (STDOUT_FILENO, "\e[?9l", 5);
    write (STDOUT_FILENO, "\e[?47l", 6);
    tcsetattr (STDIN_FILENO, TCSANOW, &original);
    return 0;

注意:这不适用于列数超过 255 列的终端。

我发现的转义序列的最佳参考是 this 和 this 之一。

【讨论】:

对于任何想知道为什么 buff[0] == 3 表示按下 Ctrl+C 的人,这是因为在 ASCII 中 0x03 是 ETX 符号,代表“文本结束”。

以上是关于在没有 ncurses 的 C/C++ 中编写“真正的”交互式终端程序,如 vim、htop、...的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Linux 的 ncurses 中显示特殊字符?

ncurses + SLD2 和 SDL2_Mixer:尝试播放 mp3 时没有声音

在 ncurses 中使用数字键盘键

为啥这个文本没有被 ncurses 着色?

在没有其他库的情况下用纯 c/c++ 编写 BMP 图像

Linux ncurses编写 FlapyBird 第一步