如何让 ncurses 输出星体平面 unicode 字符

Posted

技术标签:

【中文标题】如何让 ncurses 输出星体平面 unicode 字符【英文标题】:How to get ncurses to output astral plane unicode characters 【发布时间】:2014-06-24 22:26:48 【问题描述】:

我有以下一段非常简单的代码,它应该输出(除其他外)三个 unicode 字符:

/*
 * To build:
 *   gcc -o curses curses.c -lncursesw
 *
 * Expected result: display these chars:
 *   http://www.fileformat.info/info/unicode/char/2603/index.htm  (snowman)
 *   http://www.fileformat.info/info/unicode/char/26c4/index.htm  (snowman without snow)
 *   http://www.fileformat.info/info/unicode/char/1f638/index.htm (grinning cat face with smiling eyes)
 *
 * Looks like ncurses is NOT able to display second and third char
 * (only the first one is OK...)
 */

#include <ncurses.h>
#include <stdio.h>
#include <locale.h>

int
main (int argc, char *argv[])

    WINDOW *stdscr;
    char buffer[] = 
        '<',
        0xE2, 0x98, 0x83,       // U+2603 : snowman: OK
        0xE2, 0x9B, 0x84,       // U+26C4 : snowman without snow: ERROR (space displayed)
        0xF0, 0x9F, 0x98, 0xB8, // U+1F638: grinning cat face: ERROR (space displayed)
        '>',
        '\0' ;

    setlocale (LC_ALL, "");

    stdscr = initscr ();
    mvwprintw (stdscr, 0, 0, buffer);
    getch ();
    endwin ();

    /* output the buffer outside of ncurses */
    printf("%s\n",buffer);
    return 0;

最终的 printf 输出所有字符,如我所料“”(因为我使用了正确配置的语言环境、终端仿真器和适当的字体组合)——但是第一部分,它应该使用 ncurses 函数输出文本不能正常工作。您只能看到第一个字符(雪人),而其他两个只是呈现为空格。 “”。

我读过很多谷歌帖子说我也需要包含

#define _XOPEN_SOURCE_EXTENDED 1

在源代码中 - 但这样做并没有改变我的输出。

那么 - 我在这里做了一些极其愚蠢的事情,还是在使用 unicode 空间的某些部分时 ncurses 被破坏了?

【问题讨论】:

【参考方案1】:

并不是ncurses 坏了。更像是,glibc 坏了。或者您正在使用的libc 的任何实现;我只是假设它是glibc

与简单的控制台输出(即printf)不同,ncurses 需要知道每个字符在打印时的宽度,因为它需要维护自己的屏幕外观模型以及光标所在的位置.并非所有 Unicode 代码点都是 1 个单位宽,即使是比例字体也是如此:许多代码点的宽度为零(例如,组合重音符号),不少代码点是两个单位宽(汉字)[注 1]。

事实证明,有一个标准 C 库函数 wcwidth,它接受 wchar_t 并返回 0、1 或 2(或理论上任何整数,但 afaik 这些是唯一实现的宽度),如果字符是“可打印的”,如果字符无效或控制字符,则为 -1。启用宽字符的ncurses 版本使用wcwidth 来预测打印字符后光标将移动多远。如果wcwidth 返回错误指示,ncurses 将替换为一个空格。

wcwidth 从语言环境的charmapWIDTH 部分读取宽度,但该定义仅提供例外情况;任何没有定义宽度的可打印字符都假定宽度为 1。因此wcwidth also 需要检查该字符是否可打印,这在 LC_CTYPE 语言环境规范中定义。这与驱动 iswprint 库函数的数据相同。

不幸的是,不能保证终端仿真器与 C 库函数共享相同的 Unicode 字符数据视图。而对于实际显示宽度与语言环境配置宽度不同的字符,ncurses 将产生意外行为。

在这种情况下,宽度没有问题(字符都是1个单位宽,所以默认是正确的);问题是这些字符实际上存在于您的控制台字体中并且您想使用它们,但它们不存在于glibc 的字符数据库中,因为该数据库是still based on Unicode 5.0。 (事实上​​,这个 bug 本身应该更新,因为 Unicode 现在是 6.3,而不是 6.1。)

为了帮助您了解这一点,这里有一个小程序,它转储 unicode 代码点的配置 ctype 信息 [注 2]:

#define _XOPEN_SOURCE 600
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wctype.h>
#include <wchar.h>

#define CONC_(x,y) x##y
#define IS(x) (CONC_(isw,x)(c)?#x" ":"")

int main(int argc, char** argv) 
  setlocale(LC_CTYPE,"");
  for (int i = 1; i < argc; ++i) 
    wint_t c = strtoul(argv[i], NULL, 16);
    printf("Code %04X: width %d %s%s%s%s%s%s%s%s%s%s%s%s\n", c, wcwidth(c),
           IS(alpha),IS(lower),IS(upper),IS(digit),IS(xdigit),IS(alnum),
           IS(punct),IS(graph),IS(blank),IS(space),IS(print),IS(cntrl));
  
  return 0;

编译它你可以查看你的角色数据。大概是这样的:

$ gcc -std=c11 -Wall -o wcinfo wcinfo.c
$ ./wcinfo 2603 26c4 1f638
Code 2603: width 1 punct graph print 
Code 26C4: width -1 
Code 1F638: width -1 

那么,该怎么办?您可以等待glibc 数据库更新,但我怀疑这不会很快发生。因此,如果您真的想使用这些字符,则需要修改自己的语言环境定义。

如果你和我有同样的glibc 安装(并且语言环境文件有一段时间没有改变,所以你可能会改变),那么你会在/usr/share/i18n/locales 和实际中找到你的语言环境文件在语言环境文件中,LC_CTYPE 部分将包含指令copy "i18n",这意味着实际的 ctype 配置在文件 /usr/share/i18n/locales/i18n 中。然后,您可以编辑该文件以进行适当的更改。 (当然,在更改文件之前制作一个备份副本。您需要 sudo 编辑器,因为该文件只能由 root 写入。)

首先找到以 graph 开头的行,[注 3],然后向前搜索 U26(我的配置中的第 716 行,fwiw。)您会发现一行包含类似 &lt;U26A0&gt;..&lt;U26C3&gt;; 的条目,这意味着代码点 26A026C3 是图形(可见打印)字符。根据需要扩大该范围。 (我将26C3 更改为26C4 以进行最小测试,但您可能希望包含更多字符。)再往下几行,您将看到第二个平面graph 范围;添加适当的条目。 (再次,作为极简主义者,我添加了一个新行:

   <U0001F638>;/

但您可能希望包含一个范围。 (顺便说一下,结尾的/ 是继续标记。)

接下来,再往下走几行,您会找到print 部分。进行完全相同的更改

然后你可以通过运行重新生成你的语言环境信息:

$ sudo locale-gen

然后你就可以测试了:

$ ./wcinfo 2603 26c4 1f638
Code 2603: width 1 punct graph print 
Code 26C4: width 1 graph print 
Code 1F638: width 1 graph print 

一旦你这样做了,你原来的 ncurses 程序应该会产生预期的输出。

顺便说一句,你可以在 ncurses 中使用宽字符串;您不必手动生成 UTF-8 编码:

int
main (int argc, char *argv[])

    WINDOW *stdscr;
    setlocale (LC_ALL, "");
    const wchar_t* wstr = L"<\u2603\u26c4\U0001F638>";
    stdscr = initscr ();
    mvwaddwstr(stdscr, 0, 0, wstr);
    getch ();
    endwin ();
    return 0;


备注

    有关更多信息,请参阅***halfwidth and fullwidth forms。

    这是一个快速而简单的无错误检查程序,但它足以满足我们在这里的需要。出于生产目的,需要多几行代码:)

    您可能不需要修复graph wctype; print 可能就足够了。我没有检查。我这样做是因为ncurses 有时还需要知道字符是否透明,并且将字符标记为可见似乎更安全,因为它是。

【讨论】:

这只是一个非常全面的答案。非常感谢! 确实令人震惊!好消息:该错误最近已修复,glibc 现在已更新为 Unicode 7.0 :) 关于该死的时间;)尽管如此,我仍然希望我有不止一张赞成票给 rici。这是我收到的关于 *** 的问题的最佳答案。它真的让我大吃一惊。 好问题;出色的答案。 这是我在 SO 上找到的最完整和高质量的答案之一。感谢您的宝贵时间。

以上是关于如何让 ncurses 输出星体平面 unicode 字符的主要内容,如果未能解决你的问题,请参考以下文章

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

在 Windows 中,如何输入 Unicode 基本多语言平面之外的字符?

使用 GNU 阅读线;如何在同一程序中添加 ncurses?

如何在类星体中使用cordova在启动画面中添加.gif?

初始化firebase连接后如何将类星体应用程序挂载到dom?

Bresenham 线算法。文件是不是存在 ncurses 输出?