👻

WezTermのworkspace管理の活用法

に公開

WezTermのworkspaceとは

簡単にいうと、tmuxのセッション、ウィンドウとかなり近い考え方になります。
今回はこのworkspaceとタブを活用して、tmuxよりもっと手軽に増えがちなタブの活用を解説していきます。
wezterm_workspaceのイメージ

WezTermの設定ファイル構成

下記のように、それぞれのluaファイルで定義をして、wezterm.luaで集約している構成にしています。
今回は、そのうちのworkspaceの定義について紹介です。

.config/wezterm
├── appearance.lua #見た目の設定
├── const
│   └── workspaces.lua #ワークスペース名の定義
├── font.lua #フォント周りの設定
├── keybindings.lua #キーバインドの設定
├── wezterm.lua #それぞれの設定ファイルがここに集約
└── workspace.lua #ワークスペースの定義

設定ファイル

こちらのリンクにdot_fileを公開しています。

wezterm.lua(エントリーポイント)

まず、作成したモジュールを読み込むために、wezterm.lua に以下の記述が必要です。

wezterm.lua
local wezterm = require("wezterm")
local config = wezterm.config_builder()
local workspace = require("workspace")

-- workspace設定の読み込み
workspace.setup(config)

return config

workspace.lua(メインロジック)

以下が今回の核となる workspace.lua のコードです。

workspace.luaの全コード
workspace.lua
local wezterm = require("wezterm")
local workspaces = require("const.workspaces")

local M = {}

	function M.setup(config)
		local predefined_colors = {
			[workspaces.NAME_1] = "#f1d560",
			[workspaces.NAME_2] = "#60d5f1",
			[workspaces.NAME_3] = "#8cf160",
			[workspaces.NAME_4] = "#d160f1",
			[workspaces.NAME_5] = "#ff6b35",
			[workspaces.NAME_6] = "#ff1744",
			[workspaces.NAME_7] = "#00e5ff",
			[workspaces.NAME_8] = "#76ff03",
			[workspaces.NAME_9] = "#e91e63",
		}

		local function generate_workspace_color(workspace)
			if predefined_colors[workspace] then
				return predefined_colors[workspace]
			end

			local hash = 0
		local hash = 0
		for i = 1, #workspace do
			hash = (hash * 31 + string.byte(workspace, i)) % 360
		end
		local hue = hash / 60
		local x = 1 - math.abs(hue % 2 - 1)
		local r, g, b
		if hue < 1 then
			r, g, b = 1, x, 0
		elseif hue < 2 then
			r, g, b = x, 1, 0
		elseif hue < 3 then
			r, g, b = 0, 1, x
		elseif hue < 4 then
			r, g, b = 0, x, 1
		elseif hue < 5 then
			r, g, b = x, 0, 1
		else
			r, g, b = 1, 0, x
		end
		-- RGB値を16進数カラーコードに変換
		return string.format(
			"#%02x%02x%02x",
			math.floor(128 + r * 127),
			math.floor(128 + g * 127),
			math.floor(128 + b * 127)
		)
	end

	-- タブごとの色を生成する関数(最大9個まで)
	local function get_tab_color(tab_index)
		local tab_colors = {
			"#f1d560", -- 1: 黄色
			"#60d5f1", -- 2: 水色
			"#8cf160", -- 3: 緑色
			"#d160f1", -- 4: 紫色
			"#ff6b35", -- 5: オレンジ
			"#ff1744", -- 6: 赤色
			"#00e5ff", -- 7: シアン
			"#76ff03", -- 8: ライム
			"#e91e63", -- 9: ピンク
		}
		-- タブインデックスは0から始まるので+1して、9個でループ
		return tab_colors[(tab_index % 9) + 1]
	end

	-- タブバーの色を作成する関数
	local function create_tab_bar_colors(active_color)
		return {
			background = "#0a0a0a", -- タブバーの背景色
			active_tab = {
				bg_color = active_color, -- アクティブタブの背景色(ワークスペース色を使用)
				fg_color = "#0a0a0a", -- アクティブタブの文字色
				intensity = "Bold", -- 文字を太字に
			},
			inactive_tab = {
				bg_color = "#1f1f1f", -- 非アクティブタブの背景色
				fg_color = "#a0a0a0", -- 非アクティブタブの文字色
			},
			inactive_tab_hover = {
				bg_color = "#2f2f2f", -- ホバー時の背景色
				fg_color = "#c0c0c0", -- ホバー時の文字色
				italic = false,
			},
			new_tab = {
				bg_color = "#1a1a1a", -- 新規タブボタンの背景色
				fg_color = "#808080", -- 新規タブボタンの文字色
			},
			new_tab_hover = {
				bg_color = "#2a2a2a", -- 新規タブボタンホバー時の背景色
				fg_color = "#a0a0a0", -- 新規タブボタンホバー時の文字色
				italic = false,
			},
		}
	end

	-- タブタイトルのフォーマット設定
	wezterm.on("format-tab-title", function(tab, tabs, panes, config, hover, max_width)
		local title = ""
		-- フォアグラウンドプロセスの名前を取得
		if tab.active_pane.foreground_process_name then
			title = tab.active_pane.foreground_process_name:match("([^/\\]+)$") or ""
		end

		-- プロセス名が空の場合、カレントディレクトリを使用
		if title == "" and tab.active_pane.current_working_dir then
			local cwd = tab.active_pane.current_working_dir
			if cwd then
				cwd = cwd:gsub("file://", ""):gsub("%%20", " ")
				title = cwd:match("([^/]+)/?$") or ""
				-- ホームディレクトリの場合は~に置換
				if cwd == wezterm.home_dir then
					title = "~"
				elseif cwd:find(wezterm.home_dir, 1, true) == 1 then
					title = "~" .. cwd:sub(#wezterm.home_dir + 1)
				end
			end
		end

		-- タイトルが空の場合のデフォルト値
		if title == "" then
			title = "shell"
		end

		-- タブ番号(1から開始)
		local tab_index = tab.tab_index + 1
		-- アクティブタブは太い区切り線、非アクティブは細い区切り線
		local separator = tab.is_active and "│" or "┊"

		-- タブごとの色を取得
		local tab_color = get_tab_color(tab.tab_index)
		local bg_color = tab.is_active and tab_color or "#1f1f1f"
		local fg_color = tab.is_active and "#0a0a0a" or "#a0a0a0"

		return {
			{ Background = { Color = bg_color } },
			{ Foreground = { Color = fg_color } },
			{ Text = "" .. separator .. " " .. tab_index .. ": " .. title .. " " },
		}
	end)

	-- ステータスバーの更新処理
	wezterm.on("update-status", function(window, pane)
		local workspace = window:active_workspace()
		local overrides = window:get_config_overrides() or {}
		local mux = wezterm.mux

		-- カラー設定を保持
		overrides.colors = config.colors

		-- 全ワークスペースを収集
		local workspace_tabs = {}
		local all_workspaces = {}
		for _, w in ipairs(mux.get_workspace_names()) do
			if w ~= "default" then
				table.insert(all_workspaces, w)
			end
		end

		-- ワークスペースタブの作成
		for _, ws_key in ipairs(all_workspaces) do
			local ws_name = ws_key
			local active_color = generate_workspace_color(ws_key)

			-- アクティブなワークスペースはハイライト表示
			if ws_key == workspace then
				table.insert(workspace_tabs, { Background = { Color = active_color } })
				table.insert(workspace_tabs, { Foreground = { Color = "#141414" } })
				table.insert(workspace_tabs, { Text = " " .. ws_name .. " " })
			else
				-- 非アクティブなワークスペースは暗い色で表示
				table.insert(workspace_tabs, { Background = { Color = "#2a2a2a" } })
				table.insert(workspace_tabs, { Foreground = { Color = "#808080" } })
				table.insert(workspace_tabs, { Text = " " .. ws_name .. " " })
			end

			-- ワークスペース間のスペース
			table.insert(workspace_tabs, { Background = { Color = "#1a1a1a" } })
			table.insert(workspace_tabs, { Text = "" })
		end

		-- 左側のステータスバーにワークスペースタブを表示
		window:set_left_status(wezterm.format(workspace_tabs))

		-- 右側のステータスバーにヘルプテキストを表示
		-- window:set_right_status(wezterm.format({
		-- 	{ Background = { Color = "#1a1a1a" } },
		-- 	{ Foreground = { Color = "#606060" } },
		-- 	{ Text = " Switch workspace: Cmd+S " },
		-- }))

		-- アクティブなワークスペースの色でタブバーをカスタマイズ
		local active_color = generate_workspace_color(workspace)
		overrides.colors.tab_bar = create_tab_bar_colors(active_color)

		-- 設定を適用
		window:set_config_overrides(overrides)
	end)
end

return M

コードの解説

このコードには大きく2つの工夫を入れています。

  1. ワークスペースごとの自動色分け
    generate_workspace_color 関数で、ワークスペース名の文字列からハッシュ値を計算し、それを元にユニークな色を生成しています。
    これにより、新しいワークスペースを作ったときも設定ファイルをいじらずに勝手に色が付くため、「今どのワークスペースにいるか」が色で直感的に分かります。

  2. ステータスバーのタブ風表示
    wezterm.on("update-status", ...) を使い、左下のステータスバー領域に現在のワークスペース一覧をタブのように表示させています。
    標準のタブバーは「ワークスペース内のタブ」を表示するために使い、ステータスバーは「ワークスペース自体の切り替え」に使うという、2階層のタブ構造を実現しています。

コード自体はほとんどAIが書いてくれましたが、結構要望通りの動きになっています。

適用にあたっての準備

私のweztermを設定するにあたり、constでワークスペース名を決めておくと結構便利かと思います。

local workspace_name = {
	NAME_1 = "session1",
	NAME_2 = "session2",
	NAME_3 = "session3",
	NAME_4 = "session4",
	NAME_5 = "session5",
	NAME_6 = "session6",
	NAME_7 = "session7",
	NAME_8 = "session8",
	NAME_9 = "session9",
}

return workspace_name

ここを例えば、MainやProjectなどにすると視認性がよくなります。

workspaceの運用方法

個人的な使い方です。

ワークスペースはキーバインドの設定で、cmd + 1~9の数値で移動できるようにしています。
そして、タブはcmd + tで作成していきます。

こうすることで、ワークスペースの中に大量のタブを増やしても、タブ地獄にならないようにしています。

具体的には、NAME_1にはMainというワークスペースを用意して、そこには4つの画面分割したペインを用意して、nvimやopencode, claudecodeといったAIツールを動かしたりしています。

次に、NAME_2にはProjectsとして、開発用のワークスペースを定義しています。ここについては、NAME_3, NAME_4を使って各プロジェクト単位で分割しています。

最後に、AWSなどサーバー作業をする際に、専用のワークスペースが欲しいため、NAME_4かNAME_5あたりに、AWSというワークスペースを作成して、そこでサーバー作業を行います。
サーバー作業においては、事故するのが怖いのと、サーバー側のタイムアウトがあるにせよ繋ぎっぱなしになっていた場合に誤って操作することが怖いので、別のワークスペースとして管理しています。

tmuxとの違い

tmuxは非常に強力なツールで、特にリモートサーバーでの作業や、セッションの永続化(デタッチ・アタッチ)において右に出るものはありません。しかし、ローカルでの開発環境として見た場合、WezTermのworkspace機能には以下のようなメリットがあります。

  1. 設定がLuaで完結する
    tmuxを使う場合、.tmux.confsh,tmuxinator などで独自のスクリプトや設定ファイルを管理する必要がありますが、WezTermなら全てLuaで統一できます。
  2. 操作が直感的
    tmux特有の「プレフィックスキー(Ctrl+bなど)」を押してから操作するという作法よりも、Cmd + 数字Cmd + T といった、モダンなGUIアプリケーションに近いショートカットで操作できるため、脳のコンテキストスイッチが少なくて済みます。

もちろん、「ウィンドウを閉じてもセッションが維持される」 という点ではtmux(+ tmux-resurrect等)の方が強力です。WezTermでもステートの保存は可能ですが、デフォルトではウィンドウを閉じれば消えてしまいます。
「ローカルマシンの電源を切るまで開きっぱなしにする」という運用であれば、WezTermのworkspace機能で十分快適に過ごせるはずです。

おまけ:キーバインドの設定例

記事の中で触れた「cmd + 1~9 で移動」を実現するための設定例も紹介しておきます。
keybindings.lua などに記述して利用してください。

local wezterm = require("wezterm")
local act = wezterm.action
local workspaces = require("const.workspaces")

local keys = {
    -- ワークスペースの切り替え (Cmd + 1 ~ 9)
    { key = "1", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_1 }) },
    { key = "2", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_2 }) },
    { key = "3", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_3 }) },
    { key = "4", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_4 }) },
    { key = "5", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_5 }) },
    { key = "6", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_6 }) },
    { key = "7", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_7 }) },
    { key = "8", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_8 }) },
    { key = "9", mods = "CMD", action = act.SwitchToWorkspace({ name = workspaces.NAME_9 }) },

    -- ワークスペースランチャーを表示 (Cmd + s)
    -- 定義していないワークスペースに移動したい場合などに便利
    { key = "s", mods = "CMD", action = act.ShowLauncherArgs({ flags = "FUZZY|WORKSPACES" }) },
}

return keys

まとめ

WezTermのworkspace機能を活用することで、tmuxを導入せずとも強力なセッション管理が可能になります。
プロジェクトごとにワークスペースを分け、さらにその中で用途ごとにタブを分けることで、迷子の端末を減らして快適なターミナルライフを送りましょう!

Discussion