ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

kitty 终端文本缩放协议(Text Sizing Protocol)全解析:OSC 66 多单元字符渲染与字符宽度算法

kitty 终端文本缩放协议(Text Sizing Protocol)全解析:OSC 66 多单元字符渲染与字符宽度算法 kitty 终端文本缩放协议Text Sizing Protocol全解析OSC 66 多单元字符渲染与字符宽度算法【免费下载链接】kittyIf you live in the terminal, kitty is made for you! Cross-platform, fast, feature-rich, GPU based.项目地址: https://gitcode.com/GitHub_Trending/ki/kittykitty 在 0.40.0 版本引入的文本缩放协议Text Sizing Protocol打破了传统终端等宽网格、单一字号的桎梏允许客户端程序在同一屏幕内以不同字号渲染文本如标题、上标、下标并从根本上解决了终端生态中长期存在的字符占几格cell width协调难题。本文以 docs/text-sizing-protocol.rst 为骨架结合 kitty 仓库中 kitty/parse-multicell-command.h、kitty/screen.c、kitty/line.c 等源码实现完整讲解该协议的转义序列、元数据参数、分数字缩放、多单元字符的换行覆盖规则、能力探测方法以及字符分割算法读完即可在自己的 TUI 应用中原样复刻头条标题、上下标等排版效果。协议背景为什么终端需要多字号传统终端本质上是等尺寸字符组成的网格因此屏幕上所有文本的字号天然相同唯一的例外是少数字符被允许占用两个单元格以容纳东亚方块字如汉字与 Emoji。所谓单一文本尺寸指的就是屏幕上所有文本的字体大小一致。文本缩放协议改变了这一点允许文本以大于或小于基础字号的多种尺寸显示解决了长期存在的字符应该占几格cell width难以稳健判定的问题应用可以在屏幕上交错排布不同尺寸的文本实现标题、上标、下标等排版效果。该协议完全向后兼容不支持或未使用该协议的终端与应用依旧正常工作。正因如此它在字体尺寸上并未完全放开仍须基于终端字符网格的底层特性工作。快速上手三条 printf 体验多字号文本协议用法极其简单三个示例即可上手printf \e]_text_size_code;s2;Double sized text\a\n\n printf \e]_text_size_code;s3;Triple sized text\a\n\n\n printf \e]_text_size_code;n1:d2;Half sized text\a\n注意最后一条半尺寸文本字符高度减半但每个字符仍占一个单元格因此上下会有空隙。通过w键可以修复printf \e]_text_size_code;n1:d2:w1;Ha\a\e]66;n1:d2:w1;lf\a\nw1机制允许程序告诉终端文本应占用的宽度。它不仅能修复小号文本的对齐问题也解决了终端生态中客户端程序不知道终端会把某些文本渲染成几格这一长期 bug。转义序列规范OSC 66 的完整语法协议只使用一条转义序列由客户端程序发送给终端模拟器指示其按指定尺寸渲染指定文本格式为OSC _text_size_code ; metadata ; text terminatorOSC即字节ESC ]0x1b 0x5dmetadata是以冒号分隔的keyvalue键值对列表最后的text是纯文本必须以safe_utf8编码且不得超过 4096 字节更长的字符串必须拆分为多条转义序列发送定义中的空格仅为可读性而设实际应忽略terminator为BEL0x07或ESC ST0x1b 0x5c之一。元数据键完整对照表KeyValueDefault描述s1 到 7 的整数1整体缩放系数文本将被渲染在一个s*w乘s的单元格块中w0 到 7 的整数0文本应渲染所占的单元格宽度为零时终端像处理普通文本一样自行计算宽度并按缩放后的单元拆分n0 到 15 的整数0分数字缩放的分子numeratord0 到 15 的整数0分数字缩放的分母denominator非零时必须 nv0 到 2 的整数0分数字缩放n d时的垂直对齐0顶部、1底部、2居中h0 到 2 的整数0分数字缩放n d时的水平对齐0左、1右、2居中工作原理多单元multicell渲染协议的核心思想是允许客户端告诉终端把文本渲染到多个单元格中终端再依据指定的空间调整实际使用的字体大小。渲染空间由sscale、wwidth、nnumerator、ddenominator四个键共同控制其中s与w最为关键。文本会被渲染在s*w乘s的单元格块中。w0默认值是特例终端像无协议时那样把文本拆分成单元但此时每个单元变成一个s乘s的单元格块。例如文本abc、s2时普通拆分是三个单元│a│b│c│而s2时则被拆成│a░│b░│c░│ │░░│░░│░░│终端在渲染这些字符时将字号乘以s于是得到两倍基础字号的效果。当w非零时它指定后续文本在缩放单元中的宽度且该转义序列内的全部文本必须渲染在s*w个单元中。当s与w同时存在时整个转义序列的文本渲染在一个(s*w, s)单元的网格内即多单元块宽s*w格、高s格。文本放不下怎么办如果文本放不进指定空间终端可以自由处置截断文本或在渲染时缩小字号。因此客户端应明智使用w键不要试图在过少的单元格里塞过多文本。发送带非零w的长文本时正确做法是把文本拆成能放进w格的若干块每块发一条转义序列。例如字符串cool-的转义序列省略头尾应为w1;c w1;o w1;o w1;l w1;- w2:注意最后一只猫 Emoji使用w2。实际应用中客户端可以假定终端对所有 ASCII 码点的宽度判断都正确从而对 ASCII 部分使用高效的w0形式于是上面可简化为cool- w2:非零w应主要用于非 ASCII 字符以及下述的分数字缩放场景。缩放与基础字号的关系缩放指定的文本尺寸是相对基础字号而言的因此基础字号改变缩放尺寸也随之改变。例如终端基础字号为11pt时s2大约渲染为22pt大约是因为并非所有字体都线性缩放终端可能需微调字号以保证恰好放下若用户把基础字号改成12pt则缩放字号变为约24pt以此类推。仓库实现佐证从源码可以印证上述流程转义序列在 kitty/vt-parser.c 中被分发到parse_multicell_code定义于 kitty/parse-multicell-command.h随后调用 kitty/screen.c 中的screen_handle_multicell_command并根据w是否为零分别进入handle_fixed_width_multicell_command与handle_variable_width_multicell_commandkitty/screen.c。Python 侧对应的发送接口为 kitty/client.py 的multicell_command()它会校验未知键并抛错。多单元字符在行缓冲中的保存、续写与 ANSI 前缀重写逻辑则集中在 kitty/line.c如write_multicell_ansi_prefix、start_multicell_if_needed。分数字缩放上标、下标与半行留白仅靠主缩放参数s只能得到 7 档字号。幸运的是协议还支持分数字缩放它叠加在主缩放s之上可实现诸如正常字号文本但上下各留半行空白s2:n1:d2:v2上标n1:d2下标n1:d2:v1更多……分数字缩放不影响文本所占的单元格数量它只调整这些单元格内渲染的字号。分数用整数分子n与分母d指定。借助v键可把缩放后的渲染区域垂直对齐到顶部、底部或中间同理h键控制水平对齐——左、右、居中。注意这里的对齐不是文本对齐而是指分数字缩放后的渲染区域如何在s*w乘s的完整渲染区域内摆放因此对齐仅在n d时生效。使用分数字缩放时通常希望每格容纳超过一个字符。此时需要w键来指定渲染文本所占的单元格数。例如上标通常把字符串拆成两两一组每组发送OSC _text_size_code ; n1:d2:w1 ; ab terminator ... 对每对字符重复修复终端生态的字符宽度问题终端用单元格网格中的文本来构建用户界面。对于构建复杂 TUI 的软件来说客户端程序与终端必须就某字符串渲染为几格达成一致一旦两者分歧整个 UI 就可能崩坏造成灾难性后果。这本质上是一个协调问题客户端与终端必须共享同一份字符属性数据库、同一套基于该库计算字符串宽度的算法。现实中并不存在这样的共享数据库——最接近的是 Unicode 标准但 Unicode 几乎每年出新版本不同版本会改变某些字符的宽度而且要用它算对字符串宽度必须做字素分割grapheme segmentation这是一个复杂算法。指望所有终端与终端程序都拥有最新字符库且实现零 bug 并不现实。协议给出的解法由一方独裁宽度文本缩放协议通过消除协调问题来稳健解决让唯一一方负责判定字符串宽度。客户端负责用自己顺手的方式、基于手头任意版本的 Unicode 数据库做字素分割然后把分割好的字符串连同合适的w值发送给终端使终端把文本渲染在客户端期望的精确格数内。值得注意终端可以只实现本规范的宽度部分而忽略缩放部分。该转义序列只带w键同样有效用作指定每段文本占多少格此时s默认为 1。客户端应用探测终端支持能力的方法见下文能力探测。换行与覆盖行为规则超屏丢弃如果多单元块s*w乘s格在任一维度上大于屏幕尺寸终端必须丢弃该字符。特别地把终端窗口缩小到放不下多单元字符时该字符会丢失。换行wrapping绘制多单元字符时若启用了自动换行DECAWM 置位且字符宽度s*w放不进当前行光标移到下一行行首再绘制若禁用换行且宽度放不下光标会回退到足以容纳s*w格的最近位置再绘制并遵循下述覆盖规则。覆盖规则绘制普通文本或协议文本时若将覆盖已有多单元字符按下述优先级从高到低执行若文本是组合字符combining character则并入现有多单元字符若文本将覆盖多单元字符的左上角单元格整个多单元字符必须被擦除若文本将覆盖多单元字符最顶行的任意单元格整个多单元字符必须以空格替换此规则用于与宽字符的既有覆盖行为保持向后兼容若文本将覆盖第一行之后各行的单元格则光标应先越过该多单元字符在该行的单元格然后才写入文本。注意该行为与 DECAWM 取值无关这是为了简化实现。最后一条的跳过行为可能很复杂需要终端跳过大量单元格但它是让多行多单元字符在换行场景下正常工作所必需的。探测终端是否支持本协议使用CPRCursor Position Report游标位置报告转义序列即可探测。具体步骤发送CR回车、一个CPR然后发送\e]_text_size_code;w2; \a在两个单元格里画一个空格再发一个CPR发送\e]_text_size_code;s2; \a在 2×2 单元格块里画一个空格再发一个CPR等待终端对三次 CPR 查询的三次响应。判定逻辑三次响应中游标位置相同→ 终端完全不支持本协议第二次响应显示游标移动了 2 格 → 支持宽度部分w第三次响应显示游标又移动了 2 格 → 支持缩放部分s。与其他终端控件的交互本协议不改变终端基于字符网格的本质。大多数终端控件假设一格一字符因此规范明确规定了这些控件与多单元字符的交互方式。游标移动游标移动不受多单元字符影响所有游标移动命令仍按单格步进与终端一贯行为一致。也就是说游标可以被放到某个多单元字符内部的任意单格上。用本协议创建多单元字符时游标在同一行内向右移动s*w格。当实际游标位置落在多单元块内任意单元格上时终端应当显示一个覆盖整个多单元块的大游标块状游标覆盖字符的全部单元格条形游标出现在字符第一列的所有单元格上以此类推。编辑控件插入字符CSI ICH在(x, y)插入n个字符后y行x及之后所有字符右移。任何与y行从x起相交的多行字符必须被擦除被x与xn-1格分割的单行多单元字符也必须被擦除。删除字符CSI PDCH删除n个字符后y行x及之后字符左移。与多行/单行多单元字符的擦除规则同插入字符。擦除字符CSI XECH清空从x起的n格。任何与这n格相交的多单元字符必须被擦除。擦除显示CSI JED任何与屏幕被擦除区域相交的多单元字符必须被擦除。使用模式22时屏幕内容含所有多单元字符先复制进历史缓冲区。行内擦除CSI KEL)与擦除字符类似任何与行内被擦单元格相交的多单元字符被擦除。插入行CSI LIL)在y处插入n行时任何在y行处被分割的多行字符必须被擦除当多行字符的第二行或后续行位于y行时发生分割。插入会使屏幕底部移除n行任何在屏幕底部被分割的多行字符即除最后一行外的某一行落在插入后屏幕最后一行上必须被擦除。删除行CSI MDL)删除y处n行时任何与删除行相交的多单元字符必须被擦除。文本分割为单元格的算法kitty 附带一个测试终端合规性的工具安装 kitty 后在任何终端里运行kitten __width_test__即可测试。它使用 Unicode 联盟发布的GraphemeBreakTest.txt测试数据。该算法正处于公开讨论中若有严重问题可能会有小幅调整若未来 Unicode 标准影响该算法也会随之更新。当前算法基于Unicode 版本 16。算法基础是 Unicode 标准的字素分割算法Grapheme segmentation algorithm但仅靠它不足以完整规定终端文本处理。完整算法如下终端必须先用 UTF-8 把接收的字节解码为 Unicode 标量值即排除代理项后的码点。遇到任何 UTF-8 畸形子序列时必须把该畸形子序列的每个最大子部分替换为UFFFD替换字符。对每个解码出的码点先检查是否为 ASCII 控制码并适当处理。ASCII 控制码是小于U0032的码点以及U0127 DEL。U0000 NUL必须被丢弃。再检查码点是否无效无效则丢弃并结束处理。无效码点包括 Unicode 类别为Cc或Cs的码点以及 66 个额外码点[0xfdd0, 0xfdef]、[0xfffe, 0x10ffff-1, 0x10000]和[0xffff, 0x10ffff, 0x10000]。检查当前游标位置之前是否存在前一格要么游标在x 0前一格在同行的x-1要么前一格是上一行的最后一格前提是前后两行之间没有换行符。计算该码点在单元格中的宽度依据 Unicode 标准中的码点属性可为 0、1 或 2。若没有前一格且码点宽度为零丢弃该码点并结束处理。若有前一格用字素分割算法 UAX29-C1-1 判断前一格与当前码点之间是否存在字素边界。若无边界把当前码点加入前一格并结束处理变体选择符的处理见下文。若有边界但当前码点宽度为零把它加入前一格并结束处理。否则把码点加入当前格游标依码点宽度向右移动 1 或 2 格。码点宽度的计算规则码点按优先级从高到低分为以下类别记法[start, stop, step]表示从start到stop、步长step的整数序列省略 step 时默认为 1区域指示符Regional indicators从0x1F1E6开始的 26 个码点宽度均为 2。双宽Doublewidth解析 Unicode 标准的EastAsianWidth.txt。标记为W或F的码点宽度为 2以下范围内的码点宽度为 2除非在EastAsianWidth.txt中被标记为A[0x3400, 0x4DBF]、[0x4E00, 0x9FFF]、[0xF900, 0xFAFF]、[0x20000, 0x2FFFD]、[0x30000, 0x3FFFD]。宽 EmojiWide emoji解析 Unicode 标准的emoji-sequences.txt。所有Basic_Emoji宽度为 2除非其在文件中后跟FE0F所有RGI_Emoji_Modifier_Sequence与RGI_Emoji_Tag_Sequence的引导码点宽度为 2RGI_Emoji_Flag_Sequence中的所有码点宽度为 2。标记Marks所有零宽码点。包括 Unicode 类别首字母为M或S的码点、类别为Cf的码点以及上面宽 Emoji规则中RGI_Emoji_Modifier_Sequence的所有修饰符码点。其余所有码点宽度为1 格。Unicode 变体选择符的特殊处理有两个码点UFE0E与UFE0F能通过切换Emoji_Presentation与Text_Presentation来改变前一码点的宽度把码点加入前一格时必须特殊处理UFE0EVariation Selector 15当前一格宽度为 2且前一格最后一个码点是宽 Emoji规则中的某个Basic_Emoji且未后跟FE0F时前一格的宽度减小为 1。UFE0FVariation Selector 16当前一格宽度为 1且前一格最后一个码点是宽 Emoji规则中的某个Basic_Emoji且后跟FE0F时前一格的宽度增大为 2。UFE0E的规则对终端尤其棘手它意味着字符串宽度在不知道渲染屏幕宽度时无法确定。因为当前行只剩一格而收到宽 Emoji 时它会换行到下一行若随后收到UFE0EEmoji 变为一格宽但它不会移回上一行。为避免此问题规范建议应用检测到UFE0E存在时使用文本缩放协议的宽度部分w来控制渲染。或者应用可以把文本分割成字素有了字素列表及其宽度后用简单函数安全输出。下面是规范给出的 Python 参考实现class Grapheme: text: str width: int def output_one_line(iterator_over_graphemes): Output graphemes so that they affect exactly wcswidth cells only (works for 2 graphemes) graphemes tuple(iterator_over_graphemes) if not graphemes: return yield graphemes[0].text for i in range(1, len(graphemes)): g graphemes[i] if g is graphemes[-1]: prev_g graphemes[i-1] yield f\x1b[{prev_g.width}D # move cursor back yield g.text yield f\x1b[{g.width}D # move cursor back yield f\x1b[{prev_g.width} # insert cells yield prev_g.text yield f\x1b[{g.width}C # move cursor forward之后应用可用wcswidth()把长文本切分成不超过屏幕宽度的行再用上述函数稳健地把每行写入终端。仓库内的合规性测试工具规范中提到的kitten __width_test__在仓库中确实存在其实现位于 tools/cli/wcswidth_kitten.go该子命令会向终端逐条发送测试字符串含CPR游标位置报告\x1b[6n查询比对整串宽度与逐字素宽度两组期望游标位置来判定合规性测试数据加载自 kitty 自带的GraphemeBreakTestJSON 形式存放于 kitty_tests/GraphemeBreakTest.json并在 tools/cmd/tool/main.go 中注册。你可以直接运行kitten __width_test__验证当前终端对本文所述字符分割算法的支持程度——它是理解并验证整个协议的一手工具。【免费下载链接】kittyIf you live in the terminal, kitty is made for you! Cross-platform, fast, feature-rich, GPU based.项目地址: https://gitcode.com/GitHub_Trending/ki/kitty创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表