featured.svg

在上一篇〈Berkeley Mono 與 Ioskeley Mono〉中,我們深入比較了 U.S. Graphics Company 的商業字體 Berkeley Mono (TX-02) 與開源社群基於 Iosevka 打造的 Ioskeley Mono,並透過線上互動實驗室對照了兩者在點狀 0、單層 g、平弧括號 () 以及 Normal / SemiCondensed 字寬上的細節表現。

然而,當我把 Ioskeley Mono 換成日常主力字體、在編輯器與 terminal 裡寫了一整天程式碼後,心裡突然冒出一個小小的念頭:如果我想讓數字 7 的斜桿正中央多加一道水平橫槓(crossbar),就像歐洲手寫體或老式工程圖紙上的 7 那樣,有沒有辦法自己動手改?

如果今天面對的是一般用 FontForge 或 Glyphs 手工繪製的傳統字體,這個念頭大概會在五分鐘內被打消——因為一個完整的現代程式字體家族包含 10 種字重(Thin 100 到 Black 900)、2 種字寬(Normal 與 SemiCondensed)以及正體與斜體,光是一個家族就有 40 個 .ttf 字型檔。手動去改 40 個檔案的貝茲曲線,不僅筆畫粗細與斜率難以對齊,還會破壞原有的 TrueType hinting 與連字表。

但 Ioskeley Mono 最迷人的地方就在這裡:它不是一套靜態的向量圖檔,而是一座由程式碼驅動的參數化造字工廠。 昨晚我在 herdr 的 ioskeley-mono workspace 裡,直接從零複製儲存庫、修改一行設定、一路追蹤到底層的 Patel (.ptl) 幾何語言,並寫腳本用 8 個平行 worker 打上完整的 Nerd Font 圖示補丁。這篇文章就來完整紀錄這趟從原始碼編譯與客製化字體的實戰過程,以及途中踩到的兩個精彩陷阱。

為什麼從原始碼編譯字體比想像中更有趣?

打開 ahatem/IoskeleyMono 的儲存庫會發現,它本身並不儲存成千上萬個字元的曲線座標,核心只有一份不到 400 行的 private-build-plans.toml 藍圖檔,搭配上游的 Iosevka 造字引擎(固定於 v34.4.0)。

當我們從原始碼建置時,整個流程其實是一條橫跨編譯器、計算幾何與二進位封裝的八階段管線:

ioskeley-build-pipeline.svg

藍圖結構:四個繼承自同一核心的字體家族

在 private-build-plans.toml 中,作者透過 Iosevka 的繼承機制(inherits = "buildPlans.IoskeleyMono")定義了四個編譯目標(build plans):

  1. 標準版 IoskeleyMono:設定 spacing = "normal"、serifs = "sans" 與 noCvSs = true,包含完整的程式碼連字規則(calt)、10 種字重、2 種字寬(Normal = 600、SemiCondensed = 540)與 11.8° 斜體。
  2. 終端機版 IoskeleyMonoTerm:繼承自標準版,唯一關鍵差異是設定 spacing = "term"。這會強制將所有箭頭、幾何方塊與 box-drawing 符號嚴格限制在單一 terminal 字元格(character cell)寬度內,避免在 Ghostty、Kitty 或 Alacritty 中破格。
  3. 無連字版 IoskeleyMonoNL:設定 noLigation = true,供無法單獨關閉連字的編輯器(如舊版 Xcode)使用。
  4. 網頁子集版 IoskeleyMonoWeb:透過 [buildPlans.IoskeleyMonoWeb.subset.include] 僅保留拉丁字母、標點、箭頭、數學運算子與框線符號的 Unicode 區間,將原本完整版每個約 480–550 KB 的 .woff2 透過字元集子集化(subsetting)大幅縮減至約 94 KB。

此外,所有家族都共享一組精心調校的度量參數(private-build-plans.toml 第 201 至 223 行):

[buildPlans.IoskeleyMono.metricOverride]
xHeight = 520               # 小寫字母高度
cap = 690                   # 大寫字母與數字高度 (CAP)
ascender = 740              # 上伸部高度
sb = 85                     # 左右邊距 (Sidebearing)

accentWidth = 182           # 變音符號寬度與間距參數
accentClearance = 76
accentHeight = 162
accentStackOffset = 208

leading = 1250              # 預設行距
parenSize = 860             # 括號垂直跨度
dotSize = 'blend(weight, [100, 110], [400, 125], [900, 150])'
periodSize = 140

essRatio = 1.03             # 微調 S / s 與問號中央轉折處的水平加寬比例
essRatioUpper = 1.03
essRatioLower = 1.03
essRatioQuestion = 1.03
archDepth = 152
smallArchDepth = 157

注意看 dotSize 那行:blend(weight, [100, 110], [400, 125], [900, 150]) 是一個隨字重動態插值的函數,確保在極細體(Thin 100)時標點與 i 的方點不會太小而消失,而在極粗體(Black 900)時又不會過度膨脹糊成一團。

本地端建置環境準備

要在 Linux 本地端從頭編譯 Ioskeley Mono,只需要準備 Node.js 20+(上游 package.json 最低需求為 >=16.0.0,官方 CI 採用 Node.js 20)與 ttfautohint(用於產生標準解析度螢幕所需的 TrueType hinting 指令):

# 1. 複製 Ioskeley Mono 儲存庫
git clone [email protected]:ahatem/IoskeleyMono.git ioskeley-mono
cd ioskeley-mono

# 2. 複製上游 Iosevka 引擎(鎖定 v34.4.0 確保變體名稱與度量完全一致)
git clone --depth 1 --branch v34.4.0 https://github.com/be5invis/Iosevka.git iosevka-src

# 3. 確認系統已安裝 ttfautohint 與 fontforge(後續打 Nerd Font 補丁需要)
sudo pacman -S --needed ttfautohint fontforge python-fonttools

為什麼一定要鎖定 Iosevka v34.4.0?因為上游 Iosevka 的 main 分支演進速度極快,有時會重新命名部分字形變體或調整內部幾何常數;固定在 v34.4.0 可以確保我們編譯出來的結果與官方發行版完全一致。

一行設定的魔法:為數字 7 加上水平橫槓

現在回到我們最初的目標:把數字 7 改成中間帶有水平橫槓的版本。

打開 private-build-plans.toml 的第 15 行,在 [buildPlans.IoskeleyMono.variants.design] 區塊中可以看到上游預設使用的是無襯線直桿 7:

[buildPlans.IoskeleyMono.variants.design]
one = "no-base"
two = "straight-neck-serifless"
three = "flat-top-serifless"
four = "semi-open-serifless"
five = "oblique-flat-serifless"
six = "open-contour"
seven = "straight-serifless"             # <-- 上游預設:無橫槓的俐落直桿 7
eight = "two-circles"
nine = "open-contour"
zero = "dotted"

在 Iosevka 的命名系統中,數字 7 的變體名稱是由三個正交維度組合而成的:[主幹形狀 body]-[左上角襯線 serif]-[中段橫槓 overlay]:

變體名稱 (seven = ...) 主幹形狀 (body) 左上角垂直短襯線 (serif) 中段水平橫槓 (crossbar) 視覺風格建議
straight-serifless(預設) 直線斜桿 無 無 極簡現代,貼近 TX-02 原生設定
straight-serifless-crossbar 直線斜桿 無 有 最推薦:保留幾何直桿,同時強化 7 與 1/T 的辨識度
straight-serifed-crossbar 直線斜桿 有 有 帶有濃厚打字機與古典印刷感
bend-serifless-crossbar 微彎弧形斜桿 無 有 偏柔和手寫風格,較不適合方正的 TX-02 家族

因為 IoskeleyMonoTerm、IoskeleyMonoNL 與 IoskeleyMonoWeb 全部都透過 inherits = "buildPlans.IoskeleyMono" 繼承字形設定,我們只需要改動 private-build-plans.toml 的第 15 行:

seven = "straight-serifless-crossbar"

接著把修改後的設定檔複製進 iosevka-src/,啟動 Verda 建置系統:

cp private-build-plans.toml iosevka-src/
cd iosevka-src
npm install

# 同時編譯標準版 (IDE 用) 與 Term 版 (終端機用)
npm run build -- contents::IoskeleyMono contents::IoskeleyMonoTerm

Verda 是 Iosevka 作者 Belleve Invis 專門開發的增量平行建置引擎(定義於 iosevka-src/verdafile.mjs)。當我們修改 private-build-plans.toml 並針對單一家族執行建置時,Verda 會自動解析相依圖、調動多核心 CPU 平行更新 614 個建置目標(終端機印出 Finished with 614 targets updated.),短短幾分鐘內就在 iosevka-src/dist/ 下產出全部 40 種樣式的 TTF/(Hinted)、TTF-Unhinted/、WOFF2/ 與 WOFF2-Unhinted/。

深入造字引擎:Patel (.ptl) 如何將一行字串編譯成 TrueType 輪廓?

改一行 TOML 就能讓 40 個字型檔同步長出比例完美的水平橫槓,這背後到底發生了什麼事?我在 ioskeley-mono workspace 中一路追進了 iosevka-src/packages/font-glyphs/src/number/7.ptl,發現 Iosevka 內部的幾何編譯管線設計得極為精妙。

讓我們結合實際從編譯後的 IoskeleyMono-Regular.ttf 抽取出來的 TrueType 頂點座標,一步步拆解 "straight-serifless-crossbar" 是如何變成字型檔裡的二進位曲線(下圖與後文的頂點追蹤圖皆直接擷取自編譯後 TTF 的真實 glyf 輪廓座標):

seven-anatomy.svg

Patel 語法編譯與 SuffixCfg.weave 變體編織

Iosevka 所有的字形都不是手畫的,而是用一種名為 Patel(.ptl)的領域特定語言(Lisp 方言,採用縮排語法)撰寫的程式。由於 Node.js 無法直接執行 .ptl,建置系統第一步會呼叫 patel-c 編譯器,將 7.ptl 轉譯為標準的 JavaScript ES Module(lib/number/7.mjs):

patel-c --strict --esm packages/font-glyphs/src/number/7.ptl -o packages/font-glyphs/lib/number/7.mjs

打開 packages/font-glyphs/src/number/7.ptl 的第 65 至 93 行,可以看到 SuffixCfg.weave 如何動態編織出所有合法的 7 變體組合:

local SevenConfig : SuffixCfg.weave
    object # body
        straight              StraightBody
        bend                  BendBody
        curly                 CurlyBody
    object # serif
        serifless             nothing
        serifed               StartSerif
    function [body] : object # crossbar
        ""                    nothing
        crossbar : match body
            [Just "straight"] Crossbar
            [Just "bend"]     BendCrossbar
            [Just "curly"]    CurlyCrossbar

foreach { suffix { Body Serif Crossbar } } [Object.entries SevenConfig] : do
    create-glyph "seven.lnum.\(suffix)" : glyph-proc
        include : MarkSet.capital
        if Body : include : Body CAP
        if Serif : include : Serif CAP
        if Crossbar : include : Crossbar CAP

    create-glyph "seven.onum.\(suffix)" : glyph-proc
        include : OnumMarks.p
        include : refer-glyph "seven.lnum.\(suffix)"
        include : ShiftDown

select-variant 'seven.lnum' [CodeLnum '7'] (follow -- 'seven')
select-variant 'seven.onum' [CodeOnum '7'] (follow -- 'seven')

當我們在 TOML 中指定 seven = "straight-serifless-crossbar" 時:

  • Body 對映到 StraightBody 幾何程序;
  • Serif 對映到 nothing(不畫左上角垂直襯線);
  • Crossbar 根據 body == "straight" 自動匹配到 Crossbar 程序;
  • 最後 select-variant 將產生的 seven.lnum.straight-serifless-crossbar 綁定到等高數字(Lining Figures)的 Unicode 碼位 U+0037 ('7')(同時也透過 seven.onum 衍生出供 OpenType onum 特徵使用的老式高低數字版本)。

參數化座標求值:StraightBody 與 Crossbar

當 Node.js 在編譯某個特定字重與字寬(例如 Regular 400,字寬 600、大寫高度 CAP = 690、筆畫粗細 Stroke = 73、左右邊距 SB = 85、右邊界 RightSB = 600 - 85 = 515)時,它會把這些度量常數代入 7.ptl 第 13 至 20 行與第 44 至 49 行的幾何公式中:

define SevenXLeft : mix SB RightSB 0.28
define [StraightBody top] : union [HBar.t SB RightSB top] : intersection
    Rect (top - Stroke) 0 SB RightSB
    dispiro
        widths.rhs
        corner RightSB (top - Stroke)
        corner [mix RightSB (SevenXLeft + [HSwToV : 0.75 * Stroke]) 2] [mix (top - Stroke) 0 2]

# ... (第 21–43 行定義 BendBody、CurlyBody 與 StartSerif)

define [Crossbar top] : glyph-proc
    local m : mix SevenXLeft (RightSB - [HSwToV HalfStroke]) 0.5
    include : HBar.m
        m - [HSwToV QuarterStroke] - LongJut * 0.8
        m + [HSwToV QuarterStroke] + LongJut * 0.7
        mix 0 (top - QuarterStroke) 0.5

這幾行 Patel 程式碼藏著字體設計師滿滿的視覺補償細節:

  1. 頂部橫樑 (HBar.t SB RightSB top):在 X = 85..515、Y = 617..690 畫出一道頂部切齊 CAP (690)、厚度為 Stroke (73) 的水平橫樑。
  2. 斜向主幹 (dispiro + Rect 交集裁切): 如果直接畫一條斜線段,斜線的上下兩端切口會垂直於斜線本身(呈現斜角突出)。因此 Iosevka 故意用 mix ... 2 把 dispiro 斜桿往外延伸兩倍長,再用一個 Rect (top - Stroke) 0 SB RightSB(即 Y = 0..617 的矩形遮罩)做布林交集(intersection)!這樣斜桿頂端就會完美貼合在 Y = 617(頂部橫樑下緣),底端則在 Y = 0 基線上切出俐落的水平平頭(X = 180..267)。
  3. 水平橫槓 (Crossbar) 的不對稱視覺平衡:
    • 垂直位置:不是單純取 CAP / 2 = 345,而是 mix 0 (top - QuarterStroke) 0.5 = 336(扣除頂部橫樑視覺重量後的幾何中心,落在 Y = 299..373)。
    • 水平位置 (m):計算斜桿在半高處的水平中心點 m。
    • 左右突出的視覺微調:注意橫槓往左延伸了 0.8 * LongJut(到 X = 158),往右卻只延伸 0.7 * LongJut(到 X = 501)!為什麼左邊比右邊多出 0.1 * LongJut?因為 7 的斜桿是從右上往左下傾斜,左上方的字懷空間比右下方開闊;左端稍微多探出一點,在人眼看來才會覺得橫槓「正好位在斜桿正中央」。

Spiro 螺旋曲線展開與 2D CSG 布林聯集 (typo-geom)

在傳統字體設計中,曲線是由三次貝茲曲線(cubic Bézier)的控制點手拉出來的;而在 Iosevka 中,曲線是由 Raph Levien(同時也是 Google Fonts Inconsolata 與 Zed GPUI 部分文字渲染的先驅)提出的 Spiro 螺線演算法(Euler spiral)計算而成,確保曲率達到 G2 甚至 G4 連續。

經過 spiro-pen-expand.mjs 將一維骨架向兩側展開成二維筆畫多邊形後,重頭戲交給了 typo-geom 幾何引擎:

  • 當我們把頂部橫樑、斜向主幹與水平橫槓透過 union 組合在一起時,三個零件在十字交會處會有內部的重疊邊線。
  • 如果直接把三個重疊的矩形寫進字型檔,在開啟半透明渲染、外框描邊或低解析度光柵化時,交界處會出現破洞(winding rule 衝突)或墨水過深。
  • typo-geom 會執行嚴謹的 2D 建構實體幾何布林聯集運算(CSG Union),自動找出所有交點(例如斜桿與橫槓相交的 (300, 299)、(329, 373)、(417, 373)、(388, 299) 四個內角),徹底剔除內部重疊線段,融合成單一封閉的 15 頂點外輪廓!

我們直接用 Python 的 fontTools 把編譯出來的 IoskeleyMono-Regular.ttf 拆開,印出 seven 字形的 SVG 路徑指令,就能親眼驗證這個結果:

M180 0 300 299H158V373H329L427 617H85V690H515V617L417 373H501V299H388L267 0Z
seven-csg-vertices.svg

順著上圖右半邊標出的 [1] 到 [15] 號頂點走一圈,可以看到 typo-geom 如何把原本三個重疊的幾何零件融合成一條順時針方向(TrueType 外輪廓規範)的一筆畫封閉路徑:從基線左下角 [1] (180, 0) 出發,沿斜桿左緣爬升到橫槓下緣交角 [2] (300, 299),往左折出橫槓左翼 [3]➔[4]➔[5],繼續沿斜桿爬升到頂樑下緣交角 [6] (427, 617),繞過頂部橫樑 [7]➔[8]➔[9]➔[10],再沿斜桿右緣一路折出橫槓右翼 [11]➔[12]➔[13]➔[14] 回到基線右下角 [15] (267, 0) 閉合。整顆字元沒有任何多餘的內部節點,而且當 Verda 編譯 Thin (100)、Bold (700)、Black (900) 或 SemiCondensed (90%) 時,這 15 個頂點都會根據各自的 Stroke 與 Width 參數自動重新計算。

二次貝茲曲線擬合 (Quadify) 與 OpenType 封裝 (ot-builder)

最後兩個底層步驟則是跨格式的幾何轉換:

  1. 三次轉二次貝茲曲線 (Quadification): Iosevka 內部的幾何運算全部採用三次樣條(degree-3 cubic curves),但 TrueType(.ttf)的 glyf 表規範只支援二次貝茲曲線(degree-2 quadratic Bézier,每段曲線只有一個 off-curve 控制點)。packages/geometry/src/quadify.mjs 會呼叫 TypoGeom.Quadify.auto(arc, 1, 8),將所有曲線自動細分並擬合為誤差小於 1 個字體設計單位(1/1000 em)的二次貝茲曲線。
  2. 二進位表格序列化 (ot-builder) 與 ttfautohint: 最後由 ot-builder 將輪廓點座標寫入 glyf 與 loca 表、將 Unicode 映射寫入 cmap 表、將連字與 zero 特徵寫入 GSUB 表,產出 TTF-Unhinted/,再交由 ttfautohint 注入對齊螢幕像素網格的 bytecode 指令產出 TTF/。

為自訂字體打上 Nerd Font 補丁:平行加速與兩個實戰陷阱

在 Linux 桌面環境(尤其是搭配 eza、lazygit、neovim、starship 與 herdr 的 terminal 工作流)中,只有純文字的 .ttf 是不夠的——我們還需要把 FontAwesome、Devicons、Octicons、Material Design Icons 與 Powerline 符號打包進去,做成 Nerd Font 版本。

平行化批次修補腳本 (xargs -P 8)

官方的 Nerd Fonts Patcher 是一個由 FontForge Python 介面驅動的腳本。如果用單執行緒 for 迴圈逐一修補 40 個字型檔,每個檔案約需 15 至 20 秒,跑完一個家族就得花上十幾分鐘。

實務上,我們可以直接下載 FontPatcher.zip(下方範例固定於 v3.5.1 確保可重現性),並利用 xargs -n 2 -P 8 啟動 8 個平行 worker 同時修補:

#!/usr/bin/env bash
set -e

# 1. 下載並解壓縮官方 Nerd Fonts Patcher (v3.5.1)
if [ ! -d "nerd-fonts-patcher" ]; then
    curl -fLO https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/FontPatcher.zip
    unzip -q FontPatcher.zip -d nerd-fonts-patcher
fi

DIST_TERM="iosevka-src/dist/IoskeleyMonoTerm/TTF"
PATCHER="nerd-fonts-patcher/font-patcher"
mkdir -p patched-fonts-term/Normal patched-fonts-term/SemiCondensed

patch_font() {
    local font="$1"
    local outdir="$2"
    fontforge -script "$PATCHER" "$font" \
        --complete --careful --mono --outputdir "$outdir" >/dev/null 2>&1
    echo "Finished $(basename "$font")"
}
export -f patch_font
export PATCHER

# 2. 建立任務佇列(區分 SemiCondensed 與 Normal)
for font in "$DIST_TERM"/IoskeleyMonoTerm-SemiCondensed*.ttf; do
    echo "$font" "patched-fonts-term/SemiCondensed"
done > /tmp/fonts-term-to-patch.txt

for font in "$DIST_TERM"/IoskeleyMonoTerm-*.ttf; do
    [[ "$font" == *Condensed* ]] && continue
    echo "$font" "patched-fonts-term/Normal"
done >> /tmp/fonts-term-to-patch.txt

# 3. 使用 8 個平行 worker 批次修補 40 個字型檔
cat /tmp/fonts-term-to-patch.txt | xargs -n 2 -P 8 bash -c 'patch_font "$@"' _

這裡傳給 font-patcher 的三個參數至關重要:

  • --complete:注入所有開發者圖示集(FontAwesome、Devicons、Octicons、Codicons、Powerline 等)。
  • --careful:若圖示碼位與 Ioskeley Mono 原生已有的字元重疊,保留原生字元而不覆蓋。
  • --mono:將所有注入的圖示嚴格縮放至單一等寬字元格(1-cell),產出家族名稱帶有 Nerd Font Mono 後綴的字型,確保在 terminal 裡圖示不會溢位蓋住後面的空白或文字。

FontForge 清除 post.isFixedPitch 等寬旗標的副作用

當 font-patcher 透過 FontForge 重新匯出 .ttf 字型檔時,有一個歷史悠久的經典陷阱:FontForge 有時會將 TrueType post 表中的 isFixedPitch 欄位重設為 0。

一旦 post.isFixedPitch 不等於 1,部分 terminal 模擬器或作業系統的字體選擇器就會誤以為這是一套「比例字體(proportional font)」,進而拒絕在終端機字體清單中顯示它,或是導致字元間距計算異常。

因此在 font-patcher 跑完之後,務必補上一段幾行就能跑完的 Python fontTools 修復腳本,把所有 .ttf 的 isFixedPitch 強制改回 1:

from pathlib import Path
from fontTools.ttLib import TTFont

fixed = 0
for f in sorted(Path("patched-fonts-term").rglob("*.ttf")):
    font = TTFont(f)
    if "post" in font and font["post"].isFixedPitch != 1:
        font["post"].isFixedPitch = 1
        font.save(f)
        fixed += 1

print(f"Restored isFixedPitch on {fixed} fonts")

為何重刷 fc-cache 後終端機裡的 7 依然沒有橫槓?

這是我昨晚在 ioskeley-mono workspace 裡遇到最有趣的一個實戰插曲。

當我改完 seven = "straight-serifless-crossbar"、編譯完 contents::IoskeleyMono、跑完 40 個檔案的 Nerd Font 修補、複製進 ~/.local/share/fonts/,並執行 fc-cache -fv 重新載入後——我滿懷期待地在 terminal 裡打出 1234567890,結果畫面上的 7 依然是光禿禿的一根直桿,完全沒有水平橫槓!

第一時間我還以為是 terminal 沒重讀字體快取(在 Kitty 可以按 Ctrl + Shift + F5 熱重載,Ghostty 可按 Ctrl + Shift + , 重載設定,Alacritty 則會自動監聽設定檔變更,VS Code 可執行 Developer: Reload Window)。但就算重開整個 terminal 視窗,7 還是沒變。

一查之下才揪出真正的兇手:

  1. IDE 與 Terminal 使用不同的編譯目標 (IoskeleyMono vs. IoskeleyMonoTerm): 檢查我的 ~/.config/ghostty/config、~/.config/kitty/kitty.conf、~/.config/alacritty/alacritty.toml、~/.config/foot/foot.ini 與 ~/.config/fontconfig/fonts.conf 後發現,為了讓 terminal 的格線完美對齊,我所有的終端機設定檔指定的都是 IoskeleyMonoTerm Nerd Font Mono(也就是 Term 家族),而不是標準版的 IoskeleyMono Nerd Font Mono。
  2. ~/.local/share/fonts/ 裡殘留了舊版的 Term 字型檔: 因為我第一輪只編譯並安裝了標準版 contents::IoskeleyMono,而 ~/.local/share/fonts/IoskeleyMono/ 資料夾底下還躺著先前安裝的舊版 IoskeleyMonoTermNerdFontMono-*.ttf!於是當 Fontconfig 解析 IoskeleyMonoTerm Nerd Font Mono 時,毫無懸念地繼續指向那批還沒加上橫槓的舊檔案。

找出原因後,解法就非常俐落了:

  1. 補跑 npm run build -- contents::IoskeleyMonoTerm 編譯帶橫槓 7 的 Term 家族。
  2. 執行 patch-term-fonts.sh 將 40 個 IoskeleyMonoTerm 字型檔打上 Nerd Font 補丁並修復 isFixedPitch = 1。
  3. 清理 ~/.local/share/fonts/ 下舊的散落檔案,統一安裝至 ~/.local/share/fonts/IoskeleyMono/ 與 ~/.local/share/fonts/IoskeleyMonoNerdFont/。
  4. 重刷快取並用 fc-match 驗證最終解析路徑:
# 重新建立 Fontconfig 快取
fc-cache -fv

# 驗證 Fontconfig 實際匹配到的字型檔路徑
fc-match -v "IoskeleyMonoTerm Nerd Font Mono" | grep -E "file:|family:"

當看到輸出明確指向剛出爐的 ~/.local/share/fonts/IoskeleyMonoNerdFont/IoskeleyMonoTermNerdFontMono-Regular.ttf,再重新載入終端機,整個畫面裡的 7 瞬間長出了那道俐落的水平橫槓!

結語:把字體當作可編譯的基礎設施

從單純「下載別人打包好的 .zip 字型檔」,跨出一步到「自己擁有字體的建置管線」,帶來的掌控感非常奇妙。

透過 Iosevka 的 Patel 參數化引擎與 private-build-plans.toml,字體不再是一顆無法修改的黑盒子:不管你是想幫數字 7 加上水平橫槓、把預設的點狀 0 換成斜線 0、微調行距 leading,還是針對自己的螢幕 DPI 調整字重曲線,都只需要改幾行設定檔,就能在幾分鐘內編譯出一套橫跨 10 種字重、2 種字寬、涵蓋 IDE 與 terminal Nerd Font 的專屬字體家族。如果你也在使用 Ioskeley Mono,強烈推薦把儲存庫 clone 下來親手編譯一次,打造真正百分百貼合自己視覺習慣的開發者字體!