ottijp blog

ターミナル(tmux, Vim)でズレないフォントを作る

  • 2026-04-10

背景

Unicodeには,East Asian Width(以下EAWと略)という概念があって,表示幅がNarrowかWideかが定義されていない(Ambiguousな)文字が存在します.Narrowというのは所謂「半角」のことで「half width」とか「1セル」などとも呼ばれます.Wideというのは所謂「全角」のことで「full width」とか「2セル」などとも呼ばれます.たとえば「→」(U+2192), 「①」(U+2460), 「λ」(U+03BB)などがEAWの文字です.なお,この記事でEAWの文字と書いているのはEAWのAmbigiousカテゴリの文字のことを指します.

cf. UAX #11: East Asian Width

EAWの文字はフォントによってNarrowかWideかが異なるため,CLI(ターミナル, tmux, Vim)で表示がズレてしまう問題をよく引き起こします.

mac terminal eaw

上図はmac標準ターミナルで「Migu 1M」フォントを使ってEAWの文字を含む文字列を表示した例です.フォントはWideにも関わらず表示領域がNarrowになっており,次の文字と重なってしまいます.

ターミナルやエディタによってはEAWの文字に対する設定が存在し,例えばiTerm2では「Ambiguous characters are double-width」,Vimでは「ambiwidth」という設定で,EAWの文字をWideとして強制表示することができます.(Vimの場合はさらに「setcellwidths」関数で特定の文字のセル数を設定することもできます.)しかし,EAWの文字がNarrowで表示されることを前提にしているアプリケーションもあり,そのアプリケーションではWideに強制することで逆に表示が崩れてしまいます.

mac iterm2 eaw border

上図はiTerm2で「Ambiguous characters are double-width」をONにして「Migu 1M」フォントを使ってEAW文字を含む文字列を表示した例です.単純な表示は問題なさそうですが,EAWの文字である罫線を出力するアプリケーション(sqlite)が,罫線がNarrowで表示されることを前提としているのでズレてしまいます.(ただし,罫線に関しては後述するBox Drawingにより実際には問題が発生しない可能性が高いです.)

アプリケーションによってはこの問題を解決するオプションを用意してくれていて,例えばfzfであれば,--no-unicodeというオプションを使うことで,EAWの文字である罫線に代えてASCII文字を使った罫線を出力してくれます.ただし,現在・将来に使うアプリケーションのすべてがこのようなオプションを提供してくれてるとは限らないでしょう.また,私の環境では上記のように強制的にWideにした場合,文字数(幅)のカウントがおかしくなるのか,カーソルの位置がズレたり,文字を編集した際に消したはずの文字が残像として残ってしまうこともありました.

このEAWの文字の表示に関する問題は多くの方が経験しているようで,検索するとたくさんの事例がヒットします.

特にターミナル,tmux(などのマルチプレクサ),Vim(などのエディタ)が多重に動いてる環境では,すべてのアプリケーションが一貫してEAWの文字をハンドリングできなければ表示やカーソルがズレてしまうので,色々と設定を変えたりアプリケーションにパッチを当てたりして試行錯誤している例をよく見かけました.

方針

この問題を解決するにあたり,ターミナルもマルチプレクサもエディタも,今後ずっと同じものを使い続けるかわからないですしアプリケーションによってサポートの有無が異なる中で,EAWの文字に翻弄されてアレコレやるのは辛いと感じました.そこで,EAWの文字がNarrowであることを前提とするアプリケーションがメジャーである(東アジアの事情を考慮してくれるアプリケーションはグローバルでは少ない)と考え,EAWの文字をNarrowに固定する方針としました.つまり,普段使用しているフォントをベースに,EAWの文字のグリフをNarrowに変換する方法を考えました.

この考えの起点になったのは,以下のブログとフォントです.

環境

  • OS: mac 15.7.1 (Sequoia)
  • ターミナル: iTerm2 3.6.6
  • ターミナルマルチプレクサ: tmux 3.6a (HomeBrewでインストール)
  • エディタ: vim 9.2 (HomeBrewでインストール)
  • フォント編集ツール: FontForge 20251009 (HomeBrewでインストール)
  • ベースとなるフォント: Migu 1M

フォントの変換

ターミナルで使っているフォントをFontForgeを使って編集し,EAWの文字のグリフをNarrow化します.私の場合は「Migu 1M」を使っているのでそれをベースとしました.フォントはmonospaceであることを前提とします.

以下のような変換を行います.

  • 対象はフォントに含まれているEAWの文字とします.
  • EAWの文字のグリフは(左右マージンも考慮した上で)フォントのem/2の範囲内に収まるように縮小します.
  • 罫線の文字は変換しません.
    • iTerm2を含め多くのターミナルにはBox Drawingという機能があり,罫線はフォントのグリフが利用されないためです.
    • X方向だけ縮小してもグリフの太さが変わってしまうし,普通はBox DrawingをONにする(iTerm2の場合はデフォルトON)ので,下手にいじらないほうがよいと考えました.
    • iTerm2の場合は,Advancedメニューにある「Use your typefaces’ box-drawing characters instead of iTerm2’s custom drawing code.」というオプションがそれに当たります.

ここでは,フォントファミリの名前を「Migu 1M Narrow」としました.

index.py
import fontforge
import unicodedata

# read base font
font = fontforge.open("migu-1m-regular.ttf")

x_margin = 0.05 # 5%
target_width = font.em // 2  # Narrow
target_width_with_margin = target_width * (1 - x_margin)
left_edge_with_margin = target_width * x_margin // 2

for code in range(0x110000):
    ch = chr(code)
    if (
        code in font \
        and unicodedata.east_asian_width(ch) == 'A' # only EAW characters
        and not (0x2500 <= code <= 0x257F) # exclude border characters
    ):
        g = font[code]
        if font.em <= g.width: # only wide characters
            xmin, ymin, xmax, ymax = g.boundingBox()
            b_width, b_height = xmax - xmin, ymax - ymin
            scale = min(target_width_with_margin / b_width, 1)

            print(f'U+{code:04x}', chr(code), (xmin, ymin, xmax, ymax), (b_width, b_height), f'{scale:0.2f}')

            # scale glyph to target_width_with_margin
            g.transform((scale, 0, 0, scale, 0, 0))

            # adjust x-offset
            if scale < 1:
                # move xmin to left edge
                xmin_scaled = g.boundingBox()[0]
                offset_x = left_edge_with_margin - xmin_scaled
            else:
                # match x-center with original x-center position ratio
                center_x = xmin + b_width / 2
                offset_x = (center_x / font.em * target_width_with_margin + left_edge_with_margin) - center_x
            g.transform((1, 0, 0, 1, offset_x, 0))

            # set new glyph width
            g.width = target_width

# write converted font
font.fontname = "migu-1m-narrow-regular"
font.familyname = "Migu 1M Narrow"
font.fullname = "Migu 1M Narrow Regular"
font.generate("migu-1m-narrow-regular.ttf")

FontForgeをimportできるようにするため,コマンドは以下のように実行します.

fontforge -script index.py

比較

ベースの「Migu 1M」と変換した「Migu 1M Narrow」をターミナル上で比較すると,以下のようにEAWの文字がNarrowになっていることが確認できます.ズレがわかるように文字の前後に_を入れています.

Migu 1M 20px Migu 1M 40px Migu 1M Narrow 20px Migu 1M Narrow 40px
terminal wide 20 terminal wide 40 terminal 20 terminal 40

はじめの例を「Migu 1M Narrow」で表示した例が以下です.

mac terminal eaw fixed

変換後のフォントはグリフが縮小されるので,フォントサイズが小さいと潰れてしまって見にくくなる文字もありますが,ズレることはなくなりました.ベースのフォントによって見え方は異なるでしょうし,上記のpythonスクリプトは単純なルールで変換しているだけなので,調整されたフォントと比べると不都合が出る可能性はあると思います.ただし,ズレるよりは256倍良いので,とりあえずはこのように一律なルールでスケーリングしたフォントで様子を見ようと思います.


ottijp
都内でアプリケーションエンジニアをしています
© 2026, ottijp