=> 上一篇:為什麼你學不會 Emacs?
前言
在古代……計算機文明的古代——大致從上個世紀 80 年代算起,無論是操作系統,還是文字處理軟件,對漢字的支持,都是極為困難的。無數前輩,不論中西,前仆後繼,在大多數軟件裏,漢字的問題已得以妥善解決,以致今日的你我,在軟件裏輸入漢字,幾乎像呼吸與喝水一樣自然。
Emacs 還殘存着一些古老的記憶,以致於當你嘗試為 Emacs 配置你喜歡的字體時,依然能觸摸到些許蒼涼的歷史。此外,你應該有些心理準備,為 Emacs 配置字體,本質上是在編寫一個 Elisp 程序。
等寬字體
適合用於文本編輯器的字體,通常是等寬(Mono)字體。所謂等寬,是指每個西文字符的寬度相同。有些場景裏也稱等寬字體為打字機字體,原因也是打字機產生的字符是等寬的。
為什麼強調是西文字符呢?因為漢字的寬度本來就是相同的。西文的等寬字體,主要是用於排版或顯示程序代碼。如下圖所示,同樣一段代碼,左側為非等寬字體排版,右側為等寬字體排版,程序員羣體傾向於右側,而文本編輯器的主要應用場景通常與編程密切相關,故而適合使用等寬字體。
我喜歡的西文等寬字體是 Monaco。至於漢字字體,黑體較楷體、宋體更適合文本編輯器,我喜歡微軟雅黑(Mircrosoft YaHei)字體。不過,這兩個字體,前者是蘋果公司開發的,後者是微軟公司開發的,而我一直以來使用的操作系統是 Linux,於道德上,不應從 macOS 和 Windows 裏獲取它們,放在 Linux 裏用。
倘若你是君子,在 Ubuntu 系統裏能夠找到 Ubuntu Mono 字體,它與 Monaco 相似。至於漢字的黑體,在十幾年前若不使用國內開源社區的一款眾創作品文泉驛黑體,只能不講武德,從 Windows 系統裏偸取了。現在 Ubuntu 裏有 Google 主導開發的一系列 Noto 字體,其中 Noto Sans CJK SC,亦稱簡體思源黑體,足以顯示或排版簡體漢字。除了思源黑體外,Google 也開發了思源宋體。此外,文泉驛黑體現在依然可用。
不知此刻,你是否還像以往那樣,覺得在軟件裏輸入漢字,幾乎像呼吸與喝水一樣自然的事。雖然我直至現在,依然在 Linux 裏使用 Monaco 和微軟雅黑,一直不曾有過任何良心的譴責,不過我還是應當告訴你,該如何為 Emacs 配置合乎道德的字體,且以容易獲得的 Ubuntu Mono 和 Noto Sans CJK SC 為例。
Elisp 程序
用 Elisp 編寫可由 Emacs 運行的程序並不困難。例如,倘若你在 init.el 文件裏添加以下代碼:
(insert "Hello world!")
之後每次當你開啓 Emacs 時,都能看到緩衝區的開頭是「Hello world!」,此即 Elisp 的 Hello world 程序。insert 是 Elisp 函數,它可以將字符串寫入 Emacs 緩衝區。
練習:在 Emacs 裏編寫上述代碼,然後將光標移到該行代碼的末尾,執行 C-x C-e,觀察發生了什麼。
Elisp 程序可由一個或一組表達式構成,每個表達式都是一對小括號包圍的文本。Emacs 運行 Elisp 程序的過程便是對錶達式求值。C-x C-e 可臨時驅使 Emacs 對光標之前的表達式求值。對於上述的代碼中的表達式,其求值結果為空,Elisp 用 nil 表示空值。用 C-x C-e 對錶達式求值時,求值結果會顯示於微緩衝區裏。由於 insert 函數是將字符串插入到光標所在的位置,故而上述練習在緩衝區裏的結果是
(insert "Hello world!")Hello world!
現在,將上述代碼從 init.el 裏刪除。
設置主字體
為 Emacs 配置字體原本並不難,難的是,在古代知道一個字體的名字異常困難。例如下面的代碼,是 Ubuntu Mono 字體在 Emacs 裏的名字,希望它不會讓你做噩夢。
-DAMA-Ubuntu Mono-regular-normal-normal-*-12-*-*-*-m-0-iso10646-1
現在,我們可以不必面對這些像噩夢的字體名了,它們是古老的 X11 時代的怪物。在現代的 Emacs 裏,可以用 font-spec 函數自動生成這些噩夢。例如,
(font-spec :name "Ubuntu Mono" :size 12)
即使你完全不懂 Elisp 編程,單從字面上也能看出,上述的代碼指定了字體 Ubuntu Mono,字號為 12pt。
font-spec 中使用的字體名,與現代的桌面操作系統中的字體名是一致的。例如,假設你在微軟的 Word 軟件裏能找到 Ubuntu Mono 字體,則該字體名便可用於 font-spec,從而讓 Emacs 在操作系統中找到這款字體。在 Linux 桌面環境裏,可以使用 fc-list 命令查看系統裏的所有字體。例如,查看 Ubuntu Mono,只需
$ fc-list | grep "Ubuntu Mono"
/usr/share/fonts/truetype/ubuntu/UbuntuMono[wght].ttf: Ubuntu Mono:style=Regular
/usr/share/fonts/truetype/ubuntu/UbuntuMono-Italic[wght].ttf: Ubuntu Mono:style=Medium Italic
... ... ... ... ... ...
UbuntuMono[wght].ttf 便是 Ubuntu Mono 的正體(Regular)字體。西文字體,往往不是單一字體。每種字體,通常又分為正體、斜體、粗體、粗斜體等,故而通常以字族(Font Family)指代西文字體。上述的 font-spec 函數的應用,將 :name 換成 :family 便可為 Emacs 生成一個字族的信息。
font-spec 函數構造的信息,傳遞於 set-frame-font 函數,便可完成 Emacs 字體的配置工作,即
(set-frame-font (font-spec :family "Ubuntu Mono" :size 12))
上述代碼是嵌套形式的表達式。Emacs 對該表達式求值時,會先對內部的 font-spec 函數求值,然後將求值結果作為 set-frame-font 的參數,再對 set-frame-font 求值。最終的求值結果是,將當前窗口的字體修改為 Ubuntu Mono。將上述代碼複製到 init.el 裏,之後每次啓動 Emacs 時,Emacs 緩衝區所用的字體便是 Ubuntu Mono 了。
倘若不信,可在 Emacs 裏當場寫下以下代碼:
(insert (frame-parameter nil 'font))
將光標移動上述代碼的尾部,然後執行 C-x C-e 便可得到當前框架所用字體的噩夢之名。frame-parameter 函數可獲得指定框架(Frame)所用字體的名字,若其第 1 個參數為空值,即 nil,則獲取當前框架所用字體的名字。
需要注意,上文用的術語是「框架」,而非「窗口」。在 Emacs 的圖形界面中,框架是最外層的結構。一個框架,可以包含 1 個或多個窗口。還記得嗎?Emacs 可通過 C-x 2 或 C-x 3 將當前窗口分割為多個窗口。更嚴謹説法應該是,Emacs 可通過 C-x 2 或 C-x 3 將當前框架分割為多個窗口。
字體替補
Ubuntu Mono 是西文字體,只包含字母、數字和少量符號,並不包含任何漢字。現在的 Emacs 足夠智能,遇到框架字體缺乏的字符時,會從系統中自動搜索一個包含該字符的字體作為替補(Fallback)字體。倘若你不想親手設定替補字體,那麼本文閲讀至此便可結束了。
我們可以在 init.el 中設定我們所期望的替補字體,而非 Emacs 自作主張胡亂找到的某個字體。設定替補字體的函數是 set-fontset-font,該函數的用法如下:
(set-fontset-font 主字體 字符集腳本 替補字體)
亦即 set-fontset-font 可將主字體中的部分字符更改為替補字體。所謂的字符集腳本,要理解它,前提是需要理解 Unicode 編碼範圍。不過,在此我可以基於我的理解大致概括一下。
Unicode 為這個世界上幾乎所有的文字賦予了編碼。你可以將 Unicode 理解為一個編碼空間,其中任何一個編碼都表示着某個文字。在這個空間裏,整個漢字集合被規劃為多個子集,散佈在這個空間裏。每個漢字子集,可以通過對應的腳本選取。常用的漢字子集,在 Emacs 裏,可通過 'han 這個腳本選取。故而,若用 Noto Sans CJK SC 作為常用漢字子集的替補字體,只需在 init.el 中作以下設定:
(set-fontset-font (frame-parameter nil 'font) 'han (font-spec :name "Noto Sans CJK SC" :size 12))
上述代碼看似複雜,但實際上所有內容,你應該都有所理解了。frame-parameter 函數可獲取當前框架的主字體。'han 是 Unicode 腳本,用於指定常用漢字子集。font-spec 用於構造 Emacs 能理解的字體名,就是那種像噩夢一樣的字體名。倘若將上述代碼的形式修改為
(set-fontset-font
(frame-parameter nil 'font)
'han
(font-spec :name "Noto Sans CJK SC" :size 12))
也許會更容易理解,這一切無非是將一些函數的求值結果作為參數傳遞於某個函數而已。
用 'han 從 Unicode 空間裏選出常用漢字子集還不夠,一些中文標點符號並不在這個子集內,它們需要用 'cjk-misc 這個腳本獲取。故而,在 init.el 裏,需要再添加以下代碼:
(set-fontset-font
(frame-parameter nil 'font)
'cjk-misc
(font-spec :name "Noto Sans CJK SC" :size 12))
實際上,'han 和 'cjk-misc 還不足以指定全部的中文字符。台灣地區還有一些注音符號,也是中文字符,如「ㄅ、ㄆ、ㆠ、ㆺ」等。為了統一大業,需要用 'bopomofo 腳本從 Unicode 空間選出它們,也用替補字體予以支持,故而在 init.el 中需添加
(set-fontset-font
(frame-parameter nil 'font)
'bopomofo
(font-spec :name "Noto Sans CJK SC" :size 12))
這些重複的代碼也許已經讓你有所厭煩了,甚至開始擔心還需要再引入更多的 Unicode 腳本……在 Elisp 裏,可以用列表簡化這些代碼。
列表
Elisp 語言是 Lisp 語言的一種方言,亦即它本質上是 Lisp 語言。Lisp 這個名字,實際上是 List Programming 的簡寫,即列表編程。顧名思義,Lisp 是很擅長處理列表的一種編程語言,事實的確如此。
以下表達式構造了一個數字列表:
'(1 2 3 4 5)
單引號是必須的。倘若去掉它,Emacs 在對錶達式求值時,會錯以為 1 是個函數。單引號可以讓 Emacs 明白,括號表達式表示的是列表,而非函數。不過,列表表達式也可以寫為函數的形式。例如下面這個表達式,與上述表達式等價。
(list 1 2 3 4 5)
事實上,帶引號的列表構造表達式,只是以下表達式
(quote (1 2 3 4 5))
的簡寫。你可以用 C-x C-e 對上述表達式求值,在微緩衝區裏觀察求值結果,應該是同一個結果,即 (1 2 3 4 5)。
dolist 函數可以遍歷列表中每個元素,並對其作出處理。例如
(dolist (i '(1 2 3 4 5))
(princ (format "%d " i)))
執行上述表達式,可在微緩衝區輸出 1 2 3 4 5 nil。princ 函數可將某個 Elisp 對象的內容輸出到微緩衝區。format 函數能夠以格式化的方式構造字符串對象,上述代碼是將數字對象轉化為字符串對象。倘若你懂一些 C 語言,可將上述代碼理解為
int a[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("%d ", a[i]);
}
需要注意的是,微緩衝區輸出內容中,末尾的 nil 並非 princ 的輸出,而是 Emacs 對 dolist 函數的求值結果。請記住,Elisp 的每個表達式必須有一個結果,沒有結果的結果便是 nil。
在充分理解列表的構造以及遍歷過程的基礎上,可以將多個 Unicode 腳本寫為列表的形式,然後在遍歷這個列表的過程中完成替補字體的設定:
(dolist (script '(han cjk-misc bopomofo))
(set-fontset-font
(frame-parameter nil 'font)
script
(font-spec :name "Noto Sans CJK SC" :size 12)))
條件表達式
Emacs 有兩種工作模式,一種是圖形界面模式,一種是終端模式。我們直接以命令 emacs 開啓的 Emacs,便是圖形界面模式,其終端模式對應的命令是
$ emacs -nw
或
$ emacs --no-window-system
由於終端通常有自己的字體配置方式,無需在 Emacs 中配置字體,故而上文所述的字體配置程序在 Emacs 的終端模式下無效。
Emacs 提供了一個變量 window-system,用於表徵當前模式是否為圖形界面模式。若 window-system 的值為空值,即 nil,便表明 Emacs 正處於終端模式。我們可以用這個變量作為條件,有選擇的實現 Emacs 字體配置。
Elisp 語言可以用 when 構造條件表達式。例如
(when window-system
(set-frame-font (font-spec :family "Ubuntu Mono" :size 12))
(dolist (script '(han cjk-misc bopomofo))
(set-fontset-font
(frame-parameter nil 'font)
script
(font-spec :name "Noto Sans CJK SC" :size 12))))
上述的代碼,使用 when 探測變量 window-system 的值是否為真值,若為真值,則對其後的表達式逐一求值。在 Elisp 語言中,若一個值非 nil,即為真。上述代碼,在 Emacs 處於圖形界面模式時,此時 window-system 的值非 nil,字體配置代碼便會被 Emacs 求值,而 Emacs 處於終端模式時,window-system 的值為 nil,when 表達式的條件無法滿足,故而其後的表達式便會被 Emacs 忽略。
請將上述代碼作為最終的 Emacs 字體配置程序,寫入 init.el 吧,或者將字體換成你喜歡的其他字體,只要你知道它們的名字。
總結
為 Emacs 配置字體並不困難,不過是在 init.el 中寫入區區 7 行代碼。我們覺得這一切很難,原因是,我們希望 Emacs 能像其他軟件那樣,打開一個對話框,從下拉列表裏選出一個字體作為西文字體,再選出一個字體作為中文字體,而 Emacs 卻至今也不肯如此。也許它在等待,在未來能有一款完美的字體,讓中文用户不需要使用這種替補字體機制。
也許你會覺得奇怪,為何捨近求遠,為何不直接將 Noto Sans CJK SC 這樣的中文字體作為 Emacs 主字體,難道中文字體沒有包含西文字符嗎?你的疑問,實際上就是現實的無奈。迄今為止,的確還沒有一款字體,其西文部分和漢字部分都能讓我們滿意。我們面對的現實很長時間以來,一直是西文字體不支持漢字,中文字體支持西文字符,但是西文字形設計遠遜於西文字體。
無論如何,Emacs 字體的問題,暫且一勞永逸地解決了。更重要的是,我們已經掌握了 Elisp 編程的一些基本知識。倘若你覺得 Elisp 編寫程序,似乎是有趣的。這種感受的意義遠大於一種完美的中文字體。
=> 下一篇:Emacs 素顏淡妝