🐰

WezTermのworkspace管理をいい感じにする

に公開

#はじめに
こんにちは, @kazuです.
weztermでのworkspaceをいい感じに管理する方法について, 自分の中で満足できる設定を作れたので共有です.

背景

前提

前提として, 自分の作業スタイルを記載しておきます.

  • 大体の開発作業はWezTerm上で行っている.
  • なんとなくの作業内容ごとに, 適切にworkspaceを切り替えながら作業したい.
  • WezTermのwindowは常にひとつだけにしたい.
  • workspace中のlayoutなどは特に気にしない.

実現したいこと

いい感じにworkspaceを管理するために, 以下が実現したいことです.

  • 任意のcwdから, そこを起点とするworkspaceが楽に作れること.
    • そこを起点とするworkspaceがすでに存在する場合は, 新規作成せずに切り替えのみを行うこと.
  • ショートカットでworkspace切り替え画面が呼び出せること.
  • workspace切り替え画面では, 以下のworkspaceが選択できること.
    • すでに存在するworkspace
    • 事前定義したworkspace

成果物

先に成果物をお見せします.

一番上のcurrent:として表示されているものは, 現在アクティブな(フォーカスしている)workspaceです.
二番のdefault:として表示されているものは, home directoryを起点とするworkspaceです.
それ以降の, 背景色がない部分(ここでは1のみ)は, 現在存在するworkspceです(currentは除外ずみ.).
背景色が青い部分は, 事前定義してある かつ まだ存在していないworkspaceです.

これらの選択肢をfuzzyに選択し, 任意のworkspaceに切り替えることができます.

設定内容

workspaceを作成する設定

まず, workspaceを作成する部分のコードをお見せします.

M.create_or_switch_new = function(win, pane)
  -- 現在の作業ディレクトリを取得
  local cwd = pane:get_current_working_dir()
  local workspace_name = cwd.file_path

  win:perform_action(wezterm.action.SwitchToWorkspace { name = workspace_name,
    spawn = {
      cwd = workspace_name
    }
  }, pane)
end

ここでやってることは,

  • 現在のpaneでcwdを取得
  • そのcwdを名前としてworkspaceを切り替える.

だけです.

SwitchToWorkspaceが以下のような挙動をするため, 設定上ではそのworkspaceがすでに存在するか否かを気にする必要はありません.

  • nameで指定されたworkspaceがすでに存在すれば, そのworkspaceに切り替える.
  • 存在しない場合, spawnで指定した設定で新規workspaceを作成し,そのworkspaceに切り替える.

ref:
https://wezterm.org/config/lua/window/perform_action.html
https://wezterm.org/config/lua/pane/get_current_working_dir.html
https://wezterm.org/config/lua/keyassignment/SwitchToWorkspace.html
https://wezterm.org/config/lua/SpawnCommand.html

workspaceを選択する設定

続いて, workspaceを選択する部分のコードをお見せします.

メインの部分はこちら.

M.switch_to_workspace = function(win, pane)
  local current_workspace = wezterm.mux.get_active_workspace()
  -- workspace のリストを作成
  local workspaces        = {}

  if current_workspace ~= "" then
    table.insert(workspaces, {
      id    = current_workspace,
      label = wezterm.format {
        { Background = { Color = "#80d4ff" } },
        { Foreground = { Color = '#383a42' } },
        { Text = "current: " },
        { Text = current_workspace }
      },
    })
  end

  if current_workspace ~= 'default' then
    table.insert(workspaces, {
      id = "default",
      label = wezterm.format {
        { Background = { Color = "#ffff99" } },
        { Foreground = { Color = '#383a42' } },
        { Text = "default: " },
        { Text = wezterm.home_dir }
      },
    })
  end

  local count = 1;
  local workspace_names = wezterm.mux.get_workspace_names()
  for _, name in pairs(workspace_names) do
    if name == current_workspace or name == "default" then
      goto continue
    end
    table.insert(workspaces, {
      id = name,
      label = string.format("%7d: %s", count, name),
    })
    count = count + 1
    ::continue::
  end

  -- 別途定義してあるworkspaceを読み込む.後述
  local local_workspaces = load_local_workspaces()

  for _, workspace in pairs(local_workspaces) do
    if not utils.table_contains(workspace_names, workspace) then
      table.insert(workspaces, {
        id = workspace,
        label = wezterm.format {
          { Background = { Color = "#79a6d2" } },
          { Foreground = { Color = '#383a42' } },
          { Text = string.format("%7d: %s", count, workspace) }
        },
      })
      count = count + 1
    end
  end

  -- 選択メニューを起動
  win:perform_action(wezterm.action.InputSelector {
    action = wezterm.action_callback(function(_, _, id, label)
      if not id and not label then
        wezterm.log_info "Workspace selection canceled"
      else
        win:perform_action(wezterm.action.SwitchToWorkspace { name = id,
          spawn = { cwd = id }
        }, pane)
      end
    end),
    title = "Select workspace",
    choices = workspaces,
    fuzzy = true,
  }, pane)
end

事前定義のworkspaceを読み込む設定はこちら.

local function load_local_workspaces()
  if utils.file_exists("local_workspaces") then
    return require("local_workspaces")
  end
  return {}
end

utilsはこちら.

M.file_exists = function(name)
  local full_path = package.searchpath(name, package.path)
  if not full_path then
    return false
  end
  local f = io.open(full_path, 'r')
  if f then
    io.close(f)
    return true
  else
    return false
  end
end

M.table_contains = function(table, element)
  for _, value in pairs(table) do
    if value == element then
      return true
    end
  end
  return false
end

事前定義してあるworkspaceの設定例がこちら

return {
  "/home/kazu914/dotfiles",
  "/home/kazu914/Downloads",
  "/home/kazu914/Desktop",
}

普通にworkspaceの起点にしたいpathを羅列しているだけです.

PCごとに異なるworkspaceを事前定義したかったので, あえてlocal_workspacesとして分離してますが, そのような要望がない場合は普通にベタ書きで良いと思います.

全体としてやっていることは,

  • wezterm.mux.get_workspace_names()で既存のworkspace一覧を取得
    • currentdefalutを特別扱いしつつ, 後述するInputSelectorに渡せる形式に変換する
  • load_local_workspaces事前定義したworkspace一覧を取得
    • 既存のworkspace一覧と重複しないように気をつけながら同様にInputSelectorに渡せる形式に変換する
  • 作成したworkspace一覧を, InputSelectorに渡し, 選ばれたものに切り替える.
    • labelには色々装飾ができるので, 特別な選択肢を目立つようにしてます.

ref:
https://wezterm.org/config/lua/wezterm.mux/get_workspace_names.html
https://wezterm.org/config/lua/keyassignment/InputSelector.html

keybindings設定

あとはお好みのkeybindingとして設定するだけです.

  {
    key = "n",
    mods = "LEADER",
    action = wezterm.action_callback(workspace_management.create_or_switch_new),
  },
  {
    mods = 'LEADER',
    key = 's',
    action = wezterm.action_callback(workspace_management.switch_to_workspace),
  },

ref:
https://wezterm.org/config/keys.html

参考記事

日本語情報だと, 以下の記事が大変参考になりました.
https://zenn.dev/sankantsu/articles/e713d52825dbbb

My dotfiles Repository

自分のdofilesのrepositoryは以下です.参考までにどうぞ.
https://github.com/kazu914/dotfiles/tree/master/wezterm

あとがき

バシバシworkspaceを切り替えながら作業するのは大変楽しいです.

Discussion