Chapter 12

DLC2: 落ち穂ひろい

zetamatta
zetamatta
2021.03.07に更新

「これを解決したらUnicode沼を抜け出せる」ー そう思っていた時期が自分にもありました。

(1)ReadConsoleInput:KeyDown のない KeyUp コード

DLC1 にて 「ALTキーが押された」だけでなく「ALTキーが押されている間にテンキーが1回以上押された」 というのを KeyUp 時のキーコードをキー入力として採用するための条件としていたわけですが、その後、テンキーが一度も押されていないのに KeyDown時に Unicode が送られてこない文字入力がありました。
① とか ② などの丸付きの数字を IME 入力した時です。

$ go-console-test.exe
Hit ESCAPE key to stop.
KeyDown:0 UnicodeChar:13(0xD) VirtualKeyCode:13(0xD) VirtualScanCode:28(0x1C) ControlKeyState:0(0b0)
KeyDown:0 UnicodeChar:0(0x0) VirtualKeyCode:244(0xF4) VirtualScanCode:41(0x29) ControlKeyState:0(0b0)
KeyDown:1 UnicodeChar:0(0x0) VirtualKeyCode:18(0x12) VirtualScanCode:56(0x38) ControlKeyState:2(0b10) LEFT_ALT_PRESSED
KeyDown:0 UnicodeChar:9312(0x2460) VirtualKeyCode:18(0x12) VirtualScanCode:56(0x38) ControlKeyState:0(0b0)
データ 説明
1 KeyUp:UnicodeChar:13(0xD) go-console-test.exe を実行する際の Enterキー入力
2 KeyUp:VK_OEM_ENLW(0x4F) IMEをONにするために「半角/全角」クリック
3 KeyDown:VK_MENU(0x12) 左ALTキー押下扱い
4 KeyUp:UnicodeChar:9312(0x2460) 「①」

こうなると解釈を再び見直しする必要がありそうです。そして、検討した結果

  1. KeyDown のコードがないとはいえ、KeyUp で Unicode が来るということは、その Unicode に対するキーが押されていたという状態扱いには違いない
  2. ならば KeyDown コードの検出がなければ、KeyUp の Unicode をそのまま、キー入力として受け入れた方が無難
  3. ただし、そうするとアプリケーションが起動する直前から押下されていた Enter などが誤認識されてしまう。そこで U+0000~U+001F までの制御コードの KeyUp はキー入力とみなさない

としました。WindowsTerminal 1.6 時点ではこれで一応 Ok のようです。とはいえ、これもまたいつか見直しがあるでしょうねぇ(ということで、未だ go-tty 本家にプルリクを送っていません…)

(2)囲み数字

おそるべき絵文字 で言及されていた、#️⃣=「ただの半角文字(#)」+「異体字セレクタ15」+「囲み文字□」というコンボです。

  • WindowsTerminal では、そのままだと #□ と表示される(これはおそらくどうしようもなさそう)
  • ただし、異体字セレクタ15の存在によって、文字のセル数は1→ 2 になっている
  • go-readline-ny v0.2.5 では、「#<20E3>」と表示される。これはセル数ゼロだから、表示不能文字と判断してしまったらしい

とりあえず期待どおり表示できないものは表示できないと諦めて、 #□ と表示されるならば、せめてその表示を維持した形で編集だけできるようにするのがベストです。検討した結果

  • 囲み文字□は異体字セレクタと同じ扱いにする
    • 異体字セレクタの判定条件: unicode.Is(unicode.Variation_Selector,ch)unicode.Is(unicode.Variation_Selector, ch) || unicode.Is(unicode.Me, ch) とする
  • 新異体字セレクタのクラスは、入れ子が可能にする
    • type VariationSequence stringtype VariationSequence [2]Moji とする

よう go-readline-ny を改修しました。

(3)ひたいに手をあてる女性の異体字15 🤦‍♀️

Windows10からWindowsキー+ピリオドで、絵文字入力ウインドウが出せるようになりました。適当に打っていると「🤦 ♀<FE0F>」などと表示されます。なんだこりゃ?

どうやら、次のようなシーケンスだったようです。

  1. U+1F926:ひたいに手を当てる人(しかもサロゲートペア)
  2. U+200D:ZeroWidthJoin、つまり合字
  3. U+2640:女性のサイン
  4. U+FE0F:異体字セレクタ15

要は合字+異体字ですね。もうなんでも来てくれ!(悲鳴)。これについても対応は(2)の囲み数字と基本は同じです。要は異体字セレクタの対象として合字もオッケーにすればよいのです(細かい話はもう省略)

(4)国旗文字

REGIONAL INDICATOR (U+1F1E6..U+1F1FF)という A~Z に対応する文字がありまして、それを2つならべると国旗扱いにするというものだそうです。たとえば「🇯」と「🇵」を並べると「🇯🇵」で日本国旗になるというものだそうです。が、どうも実際に表示される環境は限られてるようです。PCのChromeでも現時点ではダメでした。

さて、WindowsTerminal ではどうかというと、国旗表示はダメですが 🇯 🇵 とは表示できます。ただし、文字幅がおかしい。Unicode では「🇯」一つだけで neutral でコンソール1セル分と規定されているのに 2セル分カーソルが移動している(Eastern Ambiguous Width ではないのに!)

仕方がないので、これらだけ2セル分の幅を取る文字として特別扱いにしました。

(5)波打つ白旗

ブラウザ上ではそっくりなんですが、🏳(U+1F3F3) と🏳️(U+1F3F3 U+FE0F) の二種類があります。そして、WindowsTerminal では

とサイズが違う白旗として表示が変わるようです。ここまでなら異体字シーケンスとして普通に処理すればよさそうなものですが、WindowsTerminal の不具合で、U+1F3F3 表示後のカーソル移動量が API 呼び出しタイミングによってセル幅1になったり2になったりします

例によって常に2になるようにエスケープシーケンスで表示を調整すればよさそうなものですが、問題がもうひとつありました。U+1F3F3 U+FE0F のセットでもセル幅が 2 になる

普通、異体字セレクタをくっつけるとセル幅は +1 されるのですが、セル幅が増えない異体字が新登場です。参りました…

仕方がないので、また特別扱いです。白旗(U+1F3F3)、この文字だけ文字幅は常に2とし、続く異体字セレクタ側でも U+1F3F3 の後ならばセル幅を増やさないものとしました。どんだけ例外が増えるねん

さらに合字処理についても白旗(U+1F3F3)の後の合字ではセル幅を増やさないようにしました。結果、白旗(🏳)+虹(🌈)の合字である虹色の旗:🏳‍🌈 (U+1F3F3 200D 1F308)もいうシーケンスもいけるようになりました。

まとめ

さて、ここで問題です。最終的に go-readline-ny の1文字を表現する型はどれだけになったでしょうか?

  1. _RawCodePoint … Unicode をそのまま出力してよい文字。文字幅は Unicode 標準準拠
  2. _EscCodePoint … 表示不能文字。<1234> のように代替表示する
  3. _CtrlCodePoint … U+0~U+1F までの制御文字。^H みたいに代替表示する
  4. _RegionalIndicator … 国旗向け文字。Unicode の規定では neutral な文字なので、幅は1セルにすべきだが、WindowsTerminal では2セル幅になる
  5. _WavingWhiteFlagCodePoint … 白旗🏳(U+1F3F3)専用。単独で使う時は2セル幅で、合字・異体字に使われた時も例外的に全体は2セル幅のままになる
  6. _ZeroWidthJoinSequence … 合字。2つのコードポイントを ZeroWidthJoin(U+200D)で連結する。_WavingWhiteFlagCodePointの時を除けば、一般に幅が〈2つのコードポイントの幅の合計〉+1セル分になる
  7. _VariationSequence … 異体字シーケンス。任意の文字+異体字セレクタからなる。_WavingWhiteFlagCodePoint の時を除けば、〈前側の文字の幅〉+1セル分の幅になる(が、WindowsTerminal では不具合で +1 セル幅が出ないケースがあるのでエスケープシーケンスで調整する)

1~5 については rune を別名に type 定義したもの、6と7については [2]MojiMoji はこれらの型すべてを表す interface 定義) を type 定義したものになります。

もう賢明なる読者諸氏はお気づきでしょう、これは沼でした。落ち穂をひろっているうちに、我々はうっかり Unicode という沼にどっぷりハマってしまったのです