【macOS】
ターミナルのフォントを変えたら★がずれた
(Unicode の曖昧幅文字)

関連記事

1. 【macOS】ターミナルのフォントを変えたら★がずれた(Unicode の曖昧幅文字)

  • ターミナルは文字を「セル」という固定格子に配置するため、フォントの字形幅とセル幅がずれると表示が崩れる
  • α のような記号は Unicode の East Asian Width(東アジア幅)で Ambiguous(曖昧)に分類され、環境によって1カラムにも2カラムにもなりうる
  • macOS Terminal.app の「Unicode 東アジア A(曖昧)の文字幅を W(広)にする」設定が、フォント選びより先に確認すべき設定だった

1.1. 等幅フォントに変えても ★ が揃わない

macOS のターミナルで使うフォントを HackGen Console NF に変えたとき、 といった記号が詰まって見えたり、次の文字と重なったりすることに気づきました1

1.1. 等幅フォントに変えても ★ が揃わない

最初はフォントのせいだと思いました。
しかし調べていくうちに、その問題はずっとあったことがわかってきました。
フォントを変えたことで、初めて目に見えるようになっただけでした。

原因の一つは、Windows のコマンドプロンプトを長く使ってきた経験が、「 は全角幅で表示される」という前提を作っていたことです。
macOS のターミナルはその前提と少し違う動きをしていました。

2. ターミナルはセル単位で文字を並べる

ブラウザやテキストエディタでは、文字はそれぞれの字形の幅に合わせてプロポーショナルに配置されます。
i は狭く、W は広く、グリフごとの幅に応じてカーソルが進みます。

ターミナルは違います。
「セル」という固定格子に文字を配置します2
全角文字なら2セル、半角文字なら1セルと、セル数だけカーソルが進みます。
フォントが実際にどれくらいの幅で字形を描くかは、セル数の計算とは切り離されています。

ここがずれの発生源です。
端末側が「この文字は1セル」と決めてカーソルを1セル分だけ進めても、フォントが2カラムっぽい字形を描いていれば、次の文字と重なって見えます。
逆に、端末側が2セル進めたのにフォントが1カラム分しか描かなければ、余白が空いてしまいます。

2.1. Shift_JIS 時代は全角・半角の二分法だった

Windows のコマンドプロンプトを長く使っていると、「 は全角文字、つまり2カラム幅」という感覚が自然と身につきます。
Shift_JIS の構造がそうなっているからです。

Shift_JIS では、ASCII の範囲にある文字は1バイト、日本語文字(かな・漢字・全角記号)は2バイトで表現されます3
端末もシェルも、1バイト文字は1カラム、2バイト文字は2カラムとして処理すれば一貫して動きます。
α のような記号も、Shift_JIS の文字集合では日本語記号として2バイト側に収まっています。
そのため、日本語の Windows 環境では は自然に2カラム幅として扱われてきました。

コマンドプロンプトは今でも Shift_JIS を前提にしています。
そのため、UTF-8 のファイルを cat で表示すると文字化けします4
これはエンコーディングの不一致であって、文字幅とは別の問題ですが、「Windows のターミナルは Shift_JIS の世界で動いている」という状態を示しています。

2.1. Shift_JIS 時代は全角・半角の二分法だった

Windows のメモ帳は UTF-8 を自動認識するので、同じファイルを開いてもそのまま読めます。

2.1. Shift_JIS 時代は全角・半角の二分法だった

この差は、アプリがどのエンコーディングを前提にしているかの違いです。
メモ帳はエンコーディングを確認してから表示する。
コマンドプロンプトは Shift_JIS を前提に動く。
それだけのことです。

3. Unicode には幅が曖昧な文字がある

Shift_JIS の世界では、 は日本語記号として2バイト、つまり2カラムで扱われてきました。
Unicode ではその単純さが崩れます。

3. Unicode には幅が曖昧な文字がある

Unicode には「East Asian Width(東アジア幅)」という文字の幅分類があります5
日本語や中国語など東アジア圏の文字を表示するとき、端末やアプリがこの分類を参照して、文字を何セルに配置するかを決めます。

分類は複数あります。
W(Wide: 全角相当、2セル)、N(Narrow: 半角相当、1セル)のほかに、A(Ambiguous: 曖昧)というカテゴリがあります6
Ambiguous に分類される文字は、環境によって1セルにも2セルにもなりえます。

★ ☆ ○ ● □ ■ △ ▲ → ← ↑ ↓ ⇒ ⇔
α β γ δ ε ζ η θ ι κ
± × ÷ ≠ ≤ ≥ ∞ ∴ ∵

これらが Ambiguous の代表例です。
日本語環境では α は全角文字として並んでいることが多いですが、欧文圏では同じ文字コードの α が1セル幅の記号や数式文字として使われます。

Unicode は「同じ意味の文字はなるべく同じコードにまとめる」という方針を採ったため、日本語用の全角 と欧文用の が同じコードになりました。
その結果、文字コードは共通でも、表示幅だけが環境依存になったのです。

Shift_JIS 時代は日本語ローカルの閉じた世界の中で全角・半角を決め打ちできていました。
Unicode になると、欧文圏のプログラムやフォントと同じコードを共有するため、「この文字は何カラムか」という判断が端末・アプリ・フォントのそれぞれに委ねられるようになりました。

3.1. 問題はフォントではなく端末の設定だった

HackGen で問題が出たなら Menlo に戻せばいいと思っていました。
しかし同じチェックシートで確認すると、Menlo でも設定次第で同じように崩れることがわかりました。

3.1. 問題はフォントではなく端末の設定だった
3.1. 問題はフォントではなく端末の設定だった

フォントの種類に関係なく、ある設定の ON/OFF で表示が変わります。
そのスイッチが Terminal.app のプロファイル設定の「詳細」タブにある「Unicode 東アジア A(曖昧)の文字幅を W(広)にする」です7

この設定をオンにすると、Ambiguous に分類される文字を2セル幅として扱います。
オフなら1セル幅になります。
フォントを変えたことで気づいたのですが、問題の本質はフォントではなく、端末側のこの設定でした。

3.2. Terminal.app と Emacs の設定をそろえる

Terminal.app で曖昧幅文字を2セルとして扱うなら、その上で動く Emacs などのアプリ側でも同じ幅で扱う必要があります。
端末が「★は2セル」と思って2マス進んでも、Emacs が「★は1セル」と判断して1マスしか確保しなければ、カーソル位置がずれます。

Emacs 内での現在の扱いは、次を評価して確認できます。

(char-width ?★)
(string-width "★○□αβγ→")Code language: Lisp (lisp)

結果が 2 なら Emacs は ★ を2カラムとして扱っています。
1 なら1カラムです。

Emacs 30 以降では cjk-ambiguous-chars-are-wide というオプションが追加され、明示的に制御できるようになりました8

(setq cjk-ambiguous-chars-are-wide t)Code language: Lisp (lisp)

これを t にすると、Ambiguous 幅文字を2カラムとして扱います。
Terminal.app 側と組み合わせると、両方を「2セル扱い」にそろえられます。

ターミナル上の Emacs(emacs -nw)では、Emacs がフォントを直接参照できないため、端末側の設定との一致がより重要になります。
GUI 版の Emacs はフォントのメトリクスを直接参照できるので、この問題はやや扱いやすいです。

iTerm2 や WezTerm など他の端末にも同様の設定があります9
tmux や Vim など他の TUI アプリも、曖昧幅文字の扱いをそれぞれ持っているため、ターミナル側の設定だけ変えても全部が解決するとは限りません10
「端末・エディタ・シェルの各層で設定を一致させる」という意識が必要です。

4. ギリシャ文字は全角、という前提を更新する

「ギリシャ文字は全角」という感覚は、Shift_JIS 時代に日本語フォントの文字集合の中で α β γ が全角枠に入っていたことから来ています。
日本語のワープロやコマンドプロンプトでは自然な前提でした。

4. ギリシャ文字は全角、という前提を更新する

欧文環境では、α β γ は普通のラテン文字と同じように1カラム幅で使われる文字です。
数式、コメント、コード内でも普通に1文字として現れます。
Unicode の文字コードとしても、日本語専用の「全角ギリシャ文字」として存在しているのではなく、世界共通の文字として存在しています11

これは「欧文が正しくて日本語が間違っている」という話ではありません。
日本語の等幅表示では2カラム幅が自然で、欧文では1カラム幅が自然という、文化的な慣習の違いです。
Unicode はどちらにも合わせられるよう Ambiguous と分類し、環境側に決定を委ねました。

その結果として、等幅環境でこれらの文字を使うときは、「この文字を何カラムとして扱うか」を端末・エディタ・フォントの間でそろえることが必要になりました。
ASCII だけを使っていた時代や、Shift_JIS でローカルに閉じていた時代には気にしなくてよかった問題が、日本語と欧文・記号が同じ画面に混在する現代の開発環境では表面に出てきます。

フォントを変えたら ★ がずれた。
その原因は端末の設定にあって、フォントの善し悪しではありませんでした。
そして背景にあったのは、日本語と欧文が Unicode という共通基盤でつながったことによる、文字幅の概念のすり合わせです。

  1. 白源(はくげん/HackGen)は、英文フォント Hack と源柔ゴシック(源ノ角ゴシック派生)を合成したプログラミングフォントです。NF は Nerd Fonts 合成版を意味し、Font Awesome などのアイコングリフが追加されています。 – プログラミングフォント 白源 (はくげん/HackGen) – GitHub
  2. 端末が「文字を何セル幅として管理するか」という問題の全体像(端末・アプリ・フォント・ロケールそれぞれの役割)について詳しく解説されています。 – 端末の文字幅問題の傾向と対策 – IIJ Engineers Blog
  3. Unicode の仕様書 UAX #11 でも、東アジアのレガシーエンコーディングにおける「1バイト文字=半角(hankaku)、2バイト文字=全角(zenkaku)」という慣習と Unicode との関係が説明されています。 – UAX #11: East Asian Width – Unicode
  4. UTF-8 と Shift_JIS の関係、および UNIX 系環境におけるテキストのエンコーディングについては、当ブログの関連記事を参照してください。 – UTF-8がUTF-16よりプログラムとの親和性がよい理由 – Chiilabo Note
  5. East Asian Width の正式な仕様は Unicode Annex UAX #11 で定義されています。W(Wide)、N(Narrow)、Na(Narrow/ASCII互換)、H(Half-width互換)、F(Fullwidth互換)、A(Ambiguous)の6分類があります。 – UAX #11: East Asian Width – Unicode
  6. 曖昧幅文字を「1セルか2セルか」をめぐる端末・ロケール・アプリごとの違いについて、実例をもとに詳しく解説されています。 – Unicodeの曖昧幅の文字は2セルか1セルか? – 標準愚痴出力
  7. この設定は Apple のサポートページでも確認できます。オンにすると East Asian Ambiguous 文字を Wide(広)として扱い、オフにすると Narrow(狭)として扱います。 – Change Profiles Advanced settings in Terminal on Mac – Apple Support
  8. Emacs 30.1 の変更点についての詳細な解説記事。cjk-ambiguous-chars-are-wide は「text-mode frames(端末表示)で重要」と説明されており、string-width などの計算結果にも影響します。 – What’s New in Emacs 30.1? – Mastering Emacs
  9. WezTerm では treat_east_asian_ambiguous_width_as_wide = true にすると曖昧幅文字を2セルとして扱います。設定変更がTUIアプリ側との整合性に影響する場合がある点も注記されています。iTerm2 では Profiles → Text の「Ambiguous characters are double-width」がこれに相当します。 – treat_east_asian_ambiguous_width_as_wide – WezTerm
  10. tmux では文字幅の計算に wcwidth(3) を参照していますが、バージョンによって en_US.UTF-8 固定となっており、日本語ロケールでの曖昧幅対応には追加のパッチが必要とされてきました。パッチ適用済みバイナリや AppImage も配布されています。 – tmux-eaw-fix – GitHub
  11. UAX #11 では「Unicode の規格において半角・全角の区別は設けていないが、レガシーシステムとのデータ交換においてはこの区別を理解する必要がある」と説明されています。 – UAX #11: East Asian Width – Unicode