カラクリスタ

ただの「人」の個人ブログ。人です

mlterm + tmux + Neovim で East Asian Ambiguous Width 問題をなんとかする on NixOS

日本語の仮想端末環境では文字の曖昧幅(Ambiguous width)が原因でPowerlineの表示が崩れたり、 Nerdfontsによるリッチなアイコン文字が上手く表示されなかったり、という事がよく起きます。

この問題を回避するためには、

  • 仮想ターミナルソフトウェアが扱う文字幅
  • ターミナルマルチプレクサ(tmuxなど)が扱う文字幅
  • Neovimなどのエディタが扱う文字幅

を統一すると言う大変な作業が必要なわけですが、今回はNixOS環境下でその辺りを揃える作業をしたので、 そこのところを解説したいと思います。

利用しているソフトウェアと環境

この記事では下記のOS環境を前提にしています:

またこの記事で取り扱った端末関連のソフトウェアは次の三つです:

  • mlterm - v3.9.3(vanilla)
  • tmux - 3.5a(patched)
  • Neovim - v0.10.3(vanilla)

基本的な設定の流れ

今回の作業では、下記のリポジトリとドキュメントを参考に曖昧幅な文字の扱いを統一しました:

hamano/locale-eaw: East Asian Ambiguous Width問題のための修正ロケール

修正ロケール(UTF-8-EAW-CONSOLE)の利用方法

具体的には UTF-8-EAW-CONSOLE をNixOSで扱えるようにした上で文字幅を統一していく、という方針です。

実際の作業

1. NixOSに UTF-8-EAW-CONSOLE のcharmapsをインストールする

最初の難所がこれです。

NixOS以外の一般的なLinux Distributionにおいて、この作業は UTF-8-EAW-CONSOLE 用のcharmapsファイルを/usr/share/charmapsに設置した上でsudo locale-genすれば簡単に完了します。

しかしNixOSではそもそもlocale-gencharmapsがシステムファイルとしては存在しません。

じゃあどうするの?と言うと、実際にはこうしました:

1.1. カスタマイズされたcharmapsを含む glibcLocales パッケージを作る

まずNixOSにおいてlocale関連のファイルはglibcLocalesというパッケージを基点として構築されているので、 これを加工したパッケージ定義を作ります。具体的にはこんな感じ:

{
  glibcLocales,
  fetchurl,
  allLocales ? true,
  locales ? [ "en_US.UTF-8/UTF-8" ],
}:

let
  UTF-8-EAW-CONSOLE = fetchurl {
    url = "https://github.com/hamano/locale-eaw/raw/7623d45f9b87d9b44dca6ef8c46fc97b22690f50/dist/UTF-8-EAW-CONSOLE";
    sha256 = "1jm54m7abyc0fvvavr2qilrbiilcp8fz5k9d4fp6pdqpq87k9irv";
  };
in

(glibcLocales.override {
  inherit allLocales;
  inherit locales;
}).overrideAttrs
  (old: {
    preBuild =
      ''
        cp ${UTF-8-EAW-CONSOLE} $(find .. -type d -name 'glibc-2*' | head -n1)/localedata/charmaps/UTF-8-EAW-CONSOLE
        echo 'ja_JP.UTF-8/UTF-8-EAW-CONSOLE \' >> ../glibc-2*/localedata/SUPPORTED
        echo 'en_US.UTF-8/UTF-8-EAW-CONSOLE \' >> ../glibc-2*/localedata/SUPPORTED
      ''
      + old.preBuild;
  })

ポイントは、

  1. glibc-2*/localedata/charmaps/ディレクトリ下にcharmapsファイルを追加
  2. glibc-2*/localedata/SUPPORTEDUTF-8-EAW-CONSOLEを扱うロケールを追加
  3. 上記の処理をpreBuildフェーズの前段階に追加する

と言った辺りです。

glibcLocalesパッケージは、作成時にpreBuildフェーズで諸々の事前処理を行なっているため、 追加されたcharmapsが処理されるためにはpreBuildフェーズが始まるより前にファイルを追加する必要があります。

1.2. カスタムされたglibcLocalesi18n.glibcLocalesに設定する

これは設定を見た方が早いので設定を掲載します:

{ pkgs, ... }:
{
  i18n = rec {
    defaultLocale = "en_US.UTF-8";
    supportedLocales = [
      "en_US.UTF-8/UTF-8-EAW-CONSOLE"
      "ja_JP.UTF-8/UTF-8-EAW-CONSOLE"
    ];
    # ここが今回作ったカスタムファイルを含む `glibcLocales`パッケージ
    glibcLocales = pkgs.glibc-locales-eaw.override {
      allLocales = false;
      locales = supportedLocales;
    };
  };
}

この設定では、

  • i18n.supportedLocalesUTF-8-EAW-CONSOLE を使うロケールを指定する
  • i18n.glibcLocales に先程作ったカスタムパッケージを指定する

と言うことを行なっていますが、これによって UTF-8-EAW-CONSOLE を使ったロケールがシステムで使われるようになります。

※ なお i18n.defaultLocale = "en_US.UTF-8" については私の環境固有の設定です。そのため本筋とはあまり関係ありません。

2. mltermの unicode_full_width_areas で全角幅文字を設定する

これについては先述した参考リポジトリに該当ファイルが含まれているので、 それを参考にmltermの設定へ unicode_full_width_areas を組み込みます。

locale-eaw/dist/eaw-fullwidth.mlterm at master · hamano/locale-eaw

3. tmuxにパッチを当てる

tmuxについてはutf8.cに強制的に全角幅で表示されるコードポイントがハードコードされているので、 その一覧をヘッダファイルに移し、utf8.cからは該当するヘッダファイルを読み込ませる、という手法を採りました。

具体的なパッチなどは次のURLからアクセスできます:

utf8.cへのパッチファイル

utf8.cへ組み込むヘッダファイル

最後にNixOSのoverlaysで上記を組み込んだtmuxパッケージをインストールすればtmuxへのパッチは完了です:

final: prev:
{
  tmux = prev.tmux.overrideAttrs (_: rec {
    preConfigure = ''
      cp ${./patches/utf8_force_wide.h} utf8_force_wide.h
    '';

    patches = [ ./patches/tmux3.5a-utf8.patch ];
  });
}

ちなみにヘッダファイルへ書き込むコードポイントについては、下記のPerlスクリプトで生成しています。Perlはやっぱり便利。

#!/usr/bin/env perl

use strict;
use warnings;

my @codepoints = split q{,},
"U+2030-2031,U+203B-203B,U+2121-2121,U+213B-213B,U+214F-214F,U+2160-2182,U+2190-21FF,U+2318-2318,U+2325-2325,U+2460-24FF,U+25A0-25D7,U+25D9-25E5,U+25E7-2653,U+2668-2668,U+2670-2712,U+2744-2744,U+2747-2747,U+2763-2763,U+2776-2793,U+27F5-27FF,U+2B33-2B33,U+3248-324F,U+E000-EDFF,U+EE0C-F8FF,U+1F000-1F02B,U+1F030-1F093,U+1F0A0-1F0F5,U+1F100-1FAF8,U+F0000-10FFFD";

for my $range (@codepoints) {
    $range =~ s{U\+}{};
    my ( $start, $end ) = split qr{-}, $range;
    $start = hex $start;
    $end   = hex $end;

    for my $code ( $start .. $end ) {
        print sprintf( "\t0x%X,\n", $code );
    }
}

4. Neovimの setcellwidths で全角幅文字を設定する

Neovimについては、Neovimのdotfilesへ、

local M = {
  { 0xa1, 0xa1, 1 },
  ...
}

return M

というような文字幅の一覧が含まれるファイルを作成し、

vim.fn.setcellwidths(require("wcwidth"))

というような流れでNeovimのdotfilesに組み込みました。

なお文字幅が含まれるwcwidth.luaについては、参考リポジトリに含まれる、

locale-eaw/dist/eaw-console.vim at master · hamano/locale-eaw

を適宜加工して用意しました。

※ Neovimのdotfilesについてはこの記事を書いている時点で大幅に改修中であるため、今のところGitHubなどでは公開されていません。

以上

と言う流れでこれらの設定が上手く出来上がるとmlterm + tmux + Neovimで曖昧幅の文字に起因する表示の崩れが無くなります。

しかしこれらの設定はtmuxとNeovimはともかくとして、mltermの設定はmlterm固有の機能に強く依存する設定であるため、 他のターミナルエミュレーターで流用できるか?と言われるとかなり微妙な気がします。

仮に他のターミナルエミュレーターへ今回の設定を移植する場合、 該当するターミナルエミュレーターが文字幅を設定している箇所へ何らかのパッチを当てる、 という手法になると思われるので、場合によっては上手く調整できない可能性もあります。

そのためそういった設定が上手く行かない or そもそも設定出来ないと言った場合には、素直に諦めるか、mltermへ乗り換えましょう。

mltermはm17nへの対応が非常に良いターミナルエミュレーターなので、多言語を扱いたい時には非常に便利ですよ!


と言う事でこちらからは以上です。今回の記事が何かの役に立てば。