Unicode では各コードに対応する文字がターミナルで、どれだけの幅をとるかという補助情報を扱う「East Asian Width」というプロパティを定義しています。
| 値 | 意味 | 表示幅 |
|---|---|---|
| F (Fullwidth) | 全角文字 | 2 桁 |
| H (Halfwidth) | 半角文字 | 1 桁 |
| W (Wide) | 通常全角扱い | 2 桁 |
| Na (Narrow) | 通常半角扱い | 1 桁 |
| A (Ambiguous) | 曖昧 | 環境依存 |
| N (Neutral) | 幅の概念がない | 通常 0 |
Ambiguous とされる文字の扱いが環境依存となっていて、これを確定しないと TUI アプリケーションでは画面のレイアウトが狂ってしまいます。
Ambiguous Width が 1桁か、2桁かの判断するには、いろんな方法があります。
(1)端末・コードページ・ロケールの種類を見る
Go言語の文字幅算出のデファクトスタンダートライブラリの mattn/go-runewidth は次のようなロジックを採用しています (ちょっと端折っています)
- 環境変数
WT_SESSIONなどが定義されている
→Windows Terminal
→Ambiguous Width は1桁 - Windows 環境でコードページが 932, 51932, 936, 949, 950
UNIX 系環境で、locale がja,ko,zhで始まっている
→Ambiguous Width は2桁 - Ambiguous Width は 1桁
(2)実測する
Ambiguous Width な文字を実際に出力してカーソル移動量から実装します。
- 復帰文字(
\r) + Ambiguous Width な文字(∇等) を表示 ESC[6nというシーケンスを出力する- 端末から
ESC[行位置;桁位置Rという形式でカーソル位置が入力される
( Enter 入力がないため、端末を RAW モードにする必要がある ) - 桁位置が3 なら2桁、2なら1桁となる
( 桁位置は 1 スタートなので -1 している )
しかしながら、稀に日本語も表示できないような非GUI系のターミナルなど ESC[6n というシーケンスを認識しない環境もあります。この場合、ユーザから見ると、ずっとキー入力待ちで動作が止ってるように見えます。
これの回避策はないのですが、次善の策として、\r∇\rESC[6n を表示する前に
"\r Calibrating terminal... (press any key to skip)\r"
と表示しておいて、ユーザが気付いたら手動でスキップしてもらうようにするのがよいでしょう。この場合なにひとつ情報が得られませんが、そもそも ESC[6n を認識しない端末は Unicode 自体表示できない場合が多いので、もういらんでしょう。英数字だけしか表示しない環境だとみなすのがよいかも
(3)環境変数で判別する
最終手段としてはユーザに環境変数やオプションなどで明示してもらうのもありです。
mattn/go-runewidth では RUNEWIDTH_EASTASIAN という環境変数を見ています。
これが
RUNEWIDTH_EASTASIAN |
Ambiguous Width |
|---|---|
未定義 or "" |
指定なし |
"1" |
2 |
"1"以外で1文字以上 |
1 |
さて、最適解は?
Go言語で開発している場合は mattn/go-runewidth の判断をそのまま採用すればよいでしょう。さらに一歩進めて、未知のターミナルでの文字幅も適切に対応したいという場合は
RUNEWIDTH_EASTASIANが定義されていなければ、(2) を実行して、桁数を算出する- さもなければ、go-runewidth の判断をそのまま採用する
がよいかもしれません( 拙作の hymkor/csvi で採用している方法です )