カルキチブログ

Rust製のクロスプラットフォームターミナルweztermについて

最近長らく愛用していたiTerm2 + tmuxを使うのを辞めて、weztermというRust製のターミナルに乗り換えました。

最初はお試しで使ってみて合わなかったら、戻そうと考えていたのですが、想像以上に良かったので、思い切って乗り換えました。

今回はweztermの特徴と、設定周りについて解説した記事を執筆してみることにしました。

当記事の内容は、11/12(土)【もくもく会 @ 新宿】Developers Guildで発表しました。
興味がある方は発表資料もぜひ見ていただければと思います!

weztermとは?

metaで働いているエンジニアであるwezさんが開発したRust製のターミナルです。

ターミナルを作ってしまうエンジニアがいるって、冷静に考えると、とんでもないことですよね...

主な特徴としては以下の点が挙げられます。

  • クロスプラットフォームなターミナルなので、macOS、Windows, 
Linux等環境を問わず使用できる
  • GPUレンダリングによって高速な描画処理を実現している
  • 設定ファイルをLuaというプログラミング言語で記述できるので、
カスタマイズ性が非常に高い

環境問わず使えて、GPUレンダリングできるターミナルだとAlacrittyとかがありますが、Luaというプログラミング言語を使って設定を記述できる点がAlacrittyとは異なります。

Alacrittyはtomlで設定を記述します。

weztermの最大の特徴として、設定にLuaというプログラミング言語を使用できる点が挙げられます。

ループや条件分岐はもちろん、外部のコマンドをLuaから実行したりすることもの可能なので、カスタマイズ性能はかなり高いと思います。

GPUレンダリング(アプリケーションの描画処理を高速化するための機能)に関してですが、描画速度はiTerm2よりは速くなった気はします。

イメージとしては、iTerm2の時よりもサクサク動くようになった感じです!

ただ、パフォーマンスの計測をちゃんと行なったわけではないので、どれくらい速度が上がったのかはよくわかりません。

インストール方法

macの場合は、以下のコマンドを実行してください。

brew install --cask wez/wezterm/wezterm-nightly

自分の環境では原因不明なのですが、nightly版を使用しないと、バッテリーの残量を表示する処理が正常に動作しませんでした。

最初はバグかなと思って、weztermのリポジトリのイシューに起票しようとしたら、nightly版を使用しているかチェックする項目があったので、その情報を元にnightly版をインストールし直したら解決できました。

イシューのテンプレもちゃんと整備されていて、すごいなと思いました。

windows, linuxユーザーの方は公式を確認してインストールを行ってください。

自分の設定と設定の解説

weztermのいい部分でもあり悪い部分でもあるのですが、ある程度使える環境を整備するためにはLuaでゴリゴリ設定を書いていく必要があります。

今回はweztermに興味はあるけど、設定を自分で調べて書くのは面倒だという方向けに自分の設定ファイルを共有することにしました。

設定が面倒な人は、以下の設定をコピペすれば自分と同じ設定がすぐに使用できます。
自分の使用している設定をそのままコピペしたので、コメントアウトとかもあえて残しています。

※フォントの設定のみ、Ricty Diminishedというフォントをダウンロードして使用しているので、そこだけは自分の環境に合わせたフォントを使用するように変更してください。

-- NOTE: 関数を保護されたモードで呼び出すpcallを使って、weztermを呼び出す。
-- pcallの最初の戻り値がfalseの時は、funcの実行中にエラーが発生している。
--
-- http://www.rtpro.yamaha.co.jp/RT/docs/lua/tutorial/library.html#pcall
local status, wezterm = pcall(require, 'wezterm')
if (not status) then return end

-- os.dateによって返却された数値から曜日を判定し、漢字に変換する
-- (曜日, 1–7, 日曜日が 1)
local function day_of_week_ja (w_num)
  if w_num == 1 then
    return '日'
  elseif w_num == 2 then
    return '月'
  elseif w_num == 3 then
    return '火'
  elseif w_num == 4 then
    return '水'
  elseif w_num == 5 then
    return '木'
  elseif w_num == 6 then
    return '金'
  elseif w_num == 7 then
    return '土'
  end
end

-- 年月日と時間、バッテリーの残量をステータスバーに表示する
-- ウィンドウが最初に表示されてから1秒後に開始され、1秒に1回トリガーされるイベント
wezterm.on('update-status', function(window, pane)
  -- 日付のtableを作成する方法じゃないと曜日の取得がうまくいかなかった
  -- NOTE: https://www.lua.org/pil/22.1.html
  local wday = os.date('*t').wday
  -- 指定子の後に半角スペースをつけないと正常に表示されなかった
  local wday_ja = string.format('(%s )', day_of_week_ja(wday))
  local date = wezterm.strftime('📆  %Y-%m-%d ' .. wday_ja .. ' ⏰  %H:%M:%S');

  local bat = ''

  for _, b in ipairs(wezterm.battery_info()) do
    local battery_state_of_charge = b.state_of_charge * 100;
    local battery_icon = ''

    if battery_state_of_charge >= 80 then
      battery_icon = '🌕  '
    elseif battery_state_of_charge >= 70 then
      battery_icon = '🌖  '
    elseif battery_state_of_charge >= 60 then
      battery_icon = '🌖  '
    elseif battery_state_of_charge >= 50 then
      battery_icon = '🌗  '
    elseif battery_state_of_charge >= 40 then
      battery_icon = '🌗  '
    elseif battery_state_of_charge >= 30 then
      battery_icon = '🌘  '
    elseif battery_state_of_charge >= 20 then
      battery_icon = '🌘  '
    else
      battery_icon = '🌑  '
    end

    bat = string.format('%s%.0f%% ', battery_icon, battery_state_of_charge)
  end

  window:set_right_status(wezterm.format {
    { Text = date .. '  ' .. bat },
  })
end)

-- タブの表示をカスタマイズ
wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width)
  local tab_index = tab.tab_index + 1

  -- Copymode時のみ、"Copymode..."というテキストを表示
  if tab.is_active and string.match(tab.active_pane.title, 'Copy mode:') ~= nil then
    return string.format(' %d %s ', tab_index, 'Copy mode...')
  end

  return string.format(' %d ', tab_index)
end)

local base_colors = {
  dark = '#172331',
  yellow = '#ffe64d'
}

local colors = {
  cursor_bg = base_colors['yellow'],
  split = '#6fc3df',
  -- the foreground color of selected text
  selection_fg = base_colors['dark'],
  -- the background color of selected text
  selection_bg = base_colors['yellow'],
  tab_bar = {
    background = base_colors['dark'],
    -- The active tab is the one that has focus in the window
    active_tab = {
      bg_color = 'aliceblue',
      fg_color = base_colors['dark'],
    },
    -- plus button hidden
    new_tab = {
      bg_color = base_colors['dark'],
      fg_color = base_colors['dark'],
    },
  },
}

-- キーバインドの設定、macOSの場合は以下のようになる
--
-- CTRL →  CMD
-- ALT → OPTION

-- leader keyを CTRL + qにマッピング
local leader = { key = 'q', mods = 'CTRL', timeout_milliseconds = 1000 };
local act = wezterm.action;
local keys = {
  -- CMD + cでタブを新規作成
  { key = 'c', mods = 'LEADER', action = act({ SpawnTab = 'CurrentPaneDomain' })},
  -- CMD + xでタブを閉じる
  { key = 'x', mods = 'LEADER', action = act({ CloseCurrentTab = { confirm = true } })},
  -- CTRL + q + numberでタブの切り替え
  { key = '1', mods = 'LEADER', action = act({ ActivateTab = 0 })},
  { key = '2', mods = 'LEADER', action = act({ ActivateTab = 1 })},
  { key = '3', mods = 'LEADER', action = act({ ActivateTab = 2 })},
  { key = '4', mods = 'LEADER', action = act({ ActivateTab = 3 })},
  { key = '5', mods = 'LEADER', action = act({ ActivateTab = 4 })},
  { key = '6', mods = 'LEADER', action = act({ ActivateTab = 5 })},
  { key = '7', mods = 'LEADER', action = act({ ActivateTab = 6 })},
  { key = '8', mods = 'LEADER', action = act({ ActivateTab = 7 })},
  { key = '9', mods = 'LEADER', action = act({ ActivateTab = 8 })},
  -- PANEを水平方向に開く
  { key = '-', mods = 'LEADER', action = act({ SplitVertical = { domain = 'CurrentPaneDomain' } }) },
  -- PANEを縦方向に開く
  { key = '|', mods = 'LEADER', action = act({ SplitHorizontal = { domain = 'CurrentPaneDomain' } }) },
  -- hjklでPANEを移動する
  { key = 'h', mods = 'LEADER', action = act({ ActivatePaneDirection = 'Left' }) },
  { key = 'l', mods = 'LEADER', action = act({ ActivatePaneDirection = 'Right' }) },
  { key = 'k', mods = 'LEADER', action = act({ ActivatePaneDirection = 'Up' }) },
  { key = 'j', mods = 'LEADER', action = act({ ActivatePaneDirection = 'Down' }) },
  -- PANEを閉じる
  { key = 'x', mods = 'ALT', action = act({ CloseCurrentPane = { confirm = true } }) },
  -- ALT + hjklでペインの幅を調整する
  { key = 'h', mods = 'ALT', action = act({ AdjustPaneSize = { 'Left', 5 } }) },
  { key = 'l', mods = 'ALT', action = act({ AdjustPaneSize = { 'Right', 5 } }) },
  { key = 'k', mods = 'ALT', action = act({ AdjustPaneSize = { 'Up', 5 } }) },
  { key = 'j', mods = 'ALT', action = act({ AdjustPaneSize = { 'Down', 5 } }) },
  -- QuickSelect・・・画面に表示されている文字をクイックにコピペできる機能
  { key = 'Enter', mods = 'SHIFT', action = 'QuickSelect' },
}

-- デフォルトディレクトリを/Documents/に変更
-- NOTE: ~でホームディレクトリを指定する方法だとうまくいかなかった
local default_cwd = os.getenv('HOME')..'/Documents/'

return {
  color_scheme = 'nightfox',
  default_cwd = default_cwd,
  colors = colors,
  leader = leader,
  keys = keys,
  font = wezterm.font('Ricty Diminished', { weight = 'Bold' }),
  font_size = 16.5,
  line_height = 1.25,
  use_fancy_tab_bar = false,
  -- アクティブではないペインの彩度を変更しない
  inactive_pane_hsb = {
    saturation = 1,
    brightness = 1,
  },
}

上記の設定を適用すると、スクショのような感じになります。

ポイントになりそうな設定をピックアップして解説していこうと思います。

イベントについて

weztermでは、wezterm.onというアクションを利用することで、特定のタイミングや動作が行われたときに任意の処理を実行することが可能です。

自分の設定ファイルでは、以下の2つのイベントを使用しています。

  • update-right-status・・・一定の間隔で処理を実行される(デフォルトだと1秒に1回処理が実行される)
  • format-tab-title・・・タブ タイトルのテキストを再計算する必要(タブタイトルを変更する必要があるときとも解釈できる)がある場合に実行される

自分は使っていないのですが、weztermではカスタムイベント(あるキーを押下したときに特定の処理を行うとか)も定義できるようです。

曜日と時間の表示について

年月日と曜日、時間の表示方法について書いていきます。

自分の環境では、年月日と曜日、時間、macのバッテリーの残量(次の見出しで解説します)をステータスバーに表示するようにしています。

年月日と曜日、時間をステータスバーに表示する処理の流れとしては、weztermが提供するwezterm.strftimeという関数を使用して、取得した現在の年月日と時刻をフォーマットした状態で出力して、update-right-statusで1秒に1回更新することで、現在の年月日と曜日、時間を表示するといった感じです。

取得したデータは、window:set_right_statusという関数に渡されて、最終的にステータスバーの右側に表示されます。

年月日と時間表示の大枠の処理は公式ドキュメントのサンプルをそのままコピってきて実装しました。

大枠の処理はwindow:set_right_statusのドキュメントをベースにしましたが、曜日を英語ではなく漢字で表示している部分だけ少し手を加えています。

os.dateというLuaが提供する関数で現在の曜日を1〜7の数値で取得できることが分かったので、取得した数値を元に曜日を漢字に変換する処理をかましています。

local function day_of_week_ja (w_num)
  if w_num == 1 then
    return '日'
  elseif w_num == 2 then
    return '月'
  elseif w_num == 3 then
    return '火'
  elseif w_num == 4 then
    return '水'
  elseif w_num == 5 then
    return '木'
  elseif w_num == 6 then
    return '金'
  elseif w_num == 7 then
    return '土'
  end
end

Luaには、switch・case文がなさそうだったので、if文で書いてます。

バッテリーの残量の表示と残量によってアイコンを変える処理について

バッテリーの残量の表示は、wezterm.battery_infoという関数を使用しています。

ドキュメントざっと読んでみた感じですが、この関数がバッテリーの状態に関する情報をオブジェクトの配列で持っていて、forでループしてバッテリーに関する状態を参照することで、バッテリーの残量を表示しているっぽいです。

バッテリーの残量は、state_of_chargeというフィールドで取得できます。
バッテリーの残量の表示も基本の処理は公式ドキュメントからそのままコピって使っていますが、バッテリーの残量によってアイコンが変わるようにしている部分のみ手を加えています。

local bat = ''

for _, b in ipairs(wezterm.battery_info()) do
  local battery_state_of_charge = b.state_of_charge * 100;
  local battery_icon = ''

  if battery_state_of_charge >= 80 then
    battery_icon = '🌕  '
  elseif battery_state_of_charge >= 70 then
    battery_icon = '🌖  '
  elseif battery_state_of_charge >= 60 then
    battery_icon = '🌖  '
  elseif battery_state_of_charge >= 50 then
    battery_icon = '🌗  '
  elseif battery_state_of_charge >= 40 then
    battery_icon = '🌗  '
  elseif battery_state_of_charge >= 30 then
    battery_icon = '🌘  '
  elseif battery_state_of_charge >= 20 then
    battery_icon = '🌘  '
  else
    battery_icon = '🌑  '
  end

  bat = string.format('%s%.0f%% ', battery_icon, battery_state_of_charge)
end

tmuxだと、プラグインを入れないと実装できなかった機能が何も手を加えなくても実現できるのはありがたいと思いました。

タブの表示について

タブに表示する情報のカスタマイズは、format-tab-titleというイベントを使用しており、基本的にはタブのインデックスのみ表示するようにしています。

デフォルト状態だと、現在のディレクトリがタブに表示されるのですが、自分の個人的な好みで基本はインデックスのみ表示するようにして、コピーモードを使用中の時「Copy mode...」というテキストを表示するようにしています。

-- タブの表示をカスタマイズ
wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width)
  local tab_index = tab.tab_index + 1

  -- Copymode時のみ、"Copymode..."というテキストを表示
  if tab.is_active and string.match(tab.active_pane.title, 'Copy mode:') ~= nil then
    return string.format(' %d %s ', tab_index, 'Copy mode...')
  end

  return string.format(' %d ', tab_index)
end)

コピーモードは、マウスやトラックパッドを使用せずキーボード操作のみでテキストのコピーを可能にする機能です。
→tmuxを使っていた時もお世話になっていました。

コピーモードが使えると生産性が爆上がりするので、使えるようになるとすごく便利です!

キーマッピングについて

キーマッピングに関しては以下の設定を行っています。
QuickSelectに関しては、「QuickSelect機能について」という見出しで解説しています。

※macを使用している方は、ALTをoptionキーに置き換えて考える必要があります!

  • タブの新規作成・削除・タブの切り替え
  • ペインの新規作成・削除・移動・幅の調整
  • QuickSelectの実行

タブとペインとは画面上の以下の項目のことを指します。

タブの中にペインがあるようなイメージです。
ペインに関しては新規作成や削除に加えて、ペイン自体の移動や幅の調整もキーボード操作で、できるようにしています。

キーバインドに関することだと、Leader Keyという概念があるのが地味に便利です。
Leader Key(自分はq + commandに設定)を指定しておくと、Leader Key + 好きなキーみたいな感じでキーマッピングができるので、キーバインドが被ることを防ぐことができます。

この機能のおかげで、tmuxのキーバインドをほぼそのままweztermに移植することができました。

その他の設定

その他で以下の設定を行っています。

  • テーマの設定(neovimのテーマと同じものを使用)
  • デフォルトのディレクトリを/Documentsに変更
  • テーマの色の微調整
  • 使用するフォントや文字の大きさ、行間の調整
  • タブのUIを変更
  • アクティブではないペインの彩度がデフォルトだと暗くなってしまうので、暗くならないようにする

テーマの設定に関して少しだけ補足すると、weztermはテーマがめちゃ多いです。

有名どころのテーマはだいたいカバーしている印象なので、Vim・Neovimを使っている方はエディターと同じテーマを使用できる点も良きです。

weztermに変えてよかったこと

weztermに変えてよかったことはいっぱいあります。

  • タブを新規作成したときに、作成元のディレクトリを起点にタブを作成してくれる
  • QuickSelect機能がマジで便利
  • デフォルトで使用可能なテーマがいっぱいある
  • 画像が表示できる
  • tmuxのレイヤーを意識しなくて良くなった
  • 公式ドキュメントがめっちゃ丁寧でわかりやすい

上記の中で主要な機能であるQuickSelect機能と画像を表示できる機能についてちょっと書いていきたいと思います。

QuickSelect機能について

Quick Select機能とは、よくコピーされるパターンの文字列を強調表示して、表示されたテキストに関しては、短い文字列(1〜2文字)をタイプするだけで、クリップボードにコピーできる機能のことです。

赤枠で囲った文字列の場合は、「nn」とタイプするだけで該当文字列をクリップボードにコピーすることができます。

wezterm側でよくコピーするパターンであると認識された文字列でしか使用できないので、使用に制限があるものの、テキストを選択することなく、コピーができるのは非常に大きなメリットだと思います!

自分が把握している限りですが、以下のケースに該当する文字列をコピーするときに使用できそうです。

  • Gitのコミットのハッシュ値
  • DockerのコンテナIDやイメージのID
  • 時刻

画像を表示できる機能について

画像を表示できるのものweztermの大きな特徴です。

weztermは、Sixelと呼ばれる画像を表示するためのフォーマットにも対応しているので、ターミナル上で画像を表示することが可能です。

今まで画像を表示したい時は一度ターミナルから離れて、FinderかVSCodeを使って画像を開いていたのですが、この機能のおかげでターミナルで画像を確認する動作を完結することができるようになりました。

まとめ

  • クロスプラットフォームなターミナルなので、macOS、Windows、
Linux等環境を問わず使用できる
  • GPUレンダリングによって高速な描画処理を実現している
  • 設定ファイルをLuaというプログラミング言語で記述できるので、
カスタマイズ性が非常に高い
  • ターミナル上で画像も表示できる

設定は大変ですが、ちゃんと設定して使えるようになると開発がとても捗ると思いました!

おまけ

この記事の冒頭で少し触れましたがRust製でGPUレンダリングが可能なターミナルだとAlacrittyがあります。

weztermに移行する際、近しい仕様や機能を持つAlacrittyも移行先の候補として実際に使ってみました。

使ってみた感じですが、設定ファイルの記述方法以外だとカスタマイズの自由度の点も結構違うなと感じました。

Alacrittyの方はあくまでターミナルとして使用することに特化しているため、年月日や時刻を表示したりするためにはtmuxを併用する必要があります。

Alacrittyのみで、年月日や時刻の表示ができないかも調べましたが、なさそうでした。
画像の表示などもAlacrittyの方はできません。

weztermにはないAlacrittyの強みとしては、あくまでターミナルとしての使用に特化しているので、設定はweztermよりもシンプルで済みます。

Luaで設定を書くことができるという部分がそのまま、デメリットにもなってしまっています。

設定を書くのに手間がかかる、プログラムで設定を書く特性上バグなどが生まれる可能性もある。(記法ミスやタイポなど明らかなミスはweztermが警告で出してくれる機能はあります。)

上記の理由から、あくまで高機能なターミナルが使用したい場合はAlacrittyが、自由度を求めるならweztermがいいのかなと思いました。