环境: Ubuntu 26.04、GNOME、Ptyxis 终端,等宽字体 Ubuntu Sans Mono,提示符用 Powerlevel10k(
nerdfont-v3模式)。
终端里 Powerlevel10k 的那些图标——git 分支、powerline 箭头、home 小房子——全显示成方块(tofu / 豆腐块)或空白。常见建议是"装个 Nerd Font 当终端字体",但完整版 Nerd Font 把字母也一起换了,字形偏笨,我并不想丢掉现在这套好看的 Ubuntu Sans Mono。
目标很明确:正文继续用原字体,只把缺的图标补上。本文记录怎么用一个「只含图标、零字母」的回退字体干净地做到,以及中间一个非踩不可的坑。
根因:p10k 在发图标码位,但没字体能画
Powerlevel10k 设成了 POWERLEVEL9K_MODE=nerdfont-v3,它会输出 Nerd Font 的图标码位(大多落在 Unicode 私有区 PUA,如 U+E725 git 分支)。但系统里压根没装任何 Nerd / Symbols 字体:
$ fc-list | grep -iE 'nerd|symbols'
NotoSansSymbols2-Regular.ttf: Noto Sans Symbols2 ...
NotoSansSymbols-Regular.ttf: Noto Sans Symbols ...
# 只有 Noto 的通用符号字体,不含 powerline / devicons 那些图标码位
发出去的码位没有任何字体覆盖,渲染层只能画成方块。
思路:别换主字体,只挂一个「图标专用」回退字体
Nerd Fonts 官方除了"给某字体打补丁"的完整版,还发布一个 NerdFontsSymbolsOnly —— 里面只有图标字形,不含任何拉丁字母和汉字。把它装上,再用 fontconfig 挂成等宽字体的回退即可:
- 正常字母 → 主字体 Ubuntu Sans Mono(回退字体里没有 ‘a’ 的字形,自动跳过)
- 图标码位 → 回退到 Symbols Nerd Font
- 汉字 → 仍走 CJK 字体,互不干扰
装字体(只取单字宽的 Mono 版,终端对齐更稳):
mkdir -p ~/.local/share/fonts
cd /tmp
TAG=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \
| grep -oP '"tag_name":\s*"\K[^"]+')
curl -sL -o SymbolsOnly.zip \
"https://github.com/ryanoasis/nerd-fonts/releases/download/${TAG}/NerdFontsSymbolsOnly.zip"
unzip -o SymbolsNerdFontMono-Regular.ttf -d ~/.local/share/fonts/ SymbolsOnly.zip
fc-cache -f
$ fc-list | grep -i "symbols nerd"
SymbolsNerdFontMono-Regular.ttf: Symbols Nerd Font Mono:style=Regular
第一版回退规则:append —— 然后翻车
直觉是把回退字体"追加"到等宽字体后面。建 ~/.config/fontconfig/conf.d/76-nerd-symbols-fallback.conf:
<match target="pattern">
<test name="family"><string>monospace</string></test>
<edit name="family" mode="append" binding="weak">
<string>Symbols Nerd Font Mono</string>
</edit>
</match>
刷新缓存后逐个码位验证,大部分图标都对了,但有一个例外:
$ for cp in E0B0 F015 E725 F09B; do
printf "U+%s -> %s\n" "$cp" "$(fc-match "monospace:charset=$cp" | sed 's/.*: //')"
done
U+E0B0 -> "Symbols Nerd Font Mono" # powerline 箭头 ✓
U+F015 -> "Symbols Nerd Font Mono" # home ✓
U+E725 -> "AR PL UMing HK" # git 分支 ✗ 被 CJK 字体抢走了!
U+F09B -> "Symbols Nerd Font Mono" # github ✓
U+E725(git 分支图标)落到了 uming.ttc(AR PL UMing HK)——一个中文字体。
真正的坑:CJK 字体会"霸占"私有区码位
Nerd Font 的图标码位在 Unicode 私有区(PUA)。而很多 CJK 字体(uming 是典型)也会在 PUA 塞自己的字形。于是同一个 U+E725,两个字体都声称"我有"。append(追加到末尾)让 Symbols 字体排在 CJK 之后,CJK 就赢了——你会看到一个莫名其妙的汉字而不是分支图标。
这和之前那篇中文显示成日文字形是同一类问题:字体在 fallback 链里的排序决定了谁来画这个码位。
修复:prepend + strong,让图标字体排到最前
把 append 改成 prepend(放最前)、weak 改成 strong:
<fontconfig>
<match target="pattern">
<test name="family"><string>monospace</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>Symbols Nerd Font Mono</string>
</edit>
</match>
<!-- 终端可能直接请求具体字族名,而非 monospace 别名,补一条 -->
<match target="pattern">
<test name="family"><string>Ubuntu Sans Mono</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>Symbols Nerd Font Mono</string>
</edit>
</match>
</fontconfig>
为什么 prepend 到最前也安全? 因为 Symbols Nerd Font Mono 只有图标、没有字母和汉字。遇到字母 ‘a’ 它没有字形,fontconfig 自动跳到下一个字体(Ubuntu Sans Mono);遇到汉字它也没有,跳到 CJK 字体;只有图标码位它才真正接管——从而盖过 uming。
验证:取证而不是凭感觉
刷新缓存,用终端实际请求的字族名 Ubuntu Sans Mono 来核对(注意别只测裸 monospace 别名,那解析出来的首字体未必是终端真正用的):
$ fc-cache -f
# 图标码位 → 全部命中 Symbols Nerd Font
$ for cp in E0B0 F015 E725 F09B F126 EA60; do
printf "U+%s -> %s\n" "$cp" \
"$(fc-match "Ubuntu Sans Mono:charset=$cp" | sed 's/.*: //')"
done
U+E725 -> "Symbols Nerd Font Mono" # git 分支,修复 ✓
# 正常字符不被符号字体抢走
$ fc-match 'Ubuntu Sans Mono:charset=61' # 字母 a
"Ubuntu Sans Mono" "Regular" # ✓ 原字体保留
$ fc-match 'Ubuntu Sans Mono:charset=4f60' # 汉字 你
"Noto Sans Mono CJK SC" "Regular" # ✓ 中文不受影响
fontconfig 的改动只对新启动的进程生效——开一个新的终端窗口,Powerlevel10k 的图标就正常了。p10k 本身一行都不用改(它本来就在发正确的码位,只是之前没字体能画)。
小结
- 图标显示成方块,本质是"码位发出去了但没字体覆盖",装字体即可,不必换主字体。
- 用 NerdFontsSymbolsOnly(只含图标)做回退,既补上图标又完整保留原等宽字体。
- 关键坑:CJK 字体会霸占 PUA 码位,回退规则必须
prepend+binding="strong",而不是 append。安全的前提是这个回退字体本身不含字母/汉字。 - 一切以
fc-match <实际字族名>:charset=<码位>取证,别靠肉眼猜。