こんにちは、 Gunosy Tech Lab AdsML チームで広告のロジック改善をしている m-hamashita です。昨年 FlexiSpot E6 と ErgoDox EZ を導入してからひどかった肩こりが改善したのでおすすめです。 FlexiSpot は最近 Black Friday で安くなっていたので、購入した人も少なくないのではないでしょうか。
こちらの記事は Gunosy Advent Calendar 2021 の 8 日目の記事です。昨日の記事は 吉岡(@rikusouda) さんの『2021年にSwiftUIを部分利用しつつ新規のiOSアプリを作った 』でした。
本記事ではターミナルエミュレータを iTerm2 から kitty に移行し、Hammerspoon で Hotkey 周りをいい感じにした話を紹介します。
はじめに
私はしばらく iTerm2 + tmux + fish + Neovim という構成*1で基本的な開発をおこなっていましたが、最近ターミナルエミュレータを iTerm2 から kitty に乗り換えたので、その際の移行作業や工夫した点を書いていこうと思います*2。
以下の構成での動作を確認しています。
- macOS Big Sur 11.6
- kitty 0.23.1
- Hammerspoon 0.9.90
kitty
kitty について
kitty はクロスプラットフォームに対応している GPU ベースのターミナルエミュレータです。 github.com
特徴として動作が軽快で多機能であることが挙げられます。設定は kitty.conf というファイルに記述するだけでよく、 GUI 操作をおこなう必要がないのも個人的に嬉しいところです。
kitty には機能拡張するためのフレームワークが用意されており、それによって作成されたプログラムは kitten と呼ばれています。 デフォルトで用意されている kitten がいくつかありますが、私が特に便利だと思うのは Hints という kitten です。 Hints は画面上から URL やファイル名、単語などを検出して開いたり、貼り付けたりすることができる機能です。これによって、マウス操作やキーボード操作の回数を減らすことができます。
Hints の例をひとつ紹介します。 Mod + e
で URL を検出し、対応する文字を入力することで、その URL をブラウザで開くことができます。ここで Mod
は kitty 特有のキーで、 デフォルトでは control + shift
にマッピングされています。 他にもハッシュ値やファイルパスを取得したり、デフォルトアプリケーションで開いたりすることができます。

インストールは以下のコマンドでおこなうことができ、 macOS では /Applications/kitty.app
が作成されます。
curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin
kitty の設定
設定は通常 ~/.config/kitty/kitty.conf
を見ているので、ここにファイルを作成して記述していきます。
私の kitty.conf は次のような感じになっています。ここでフォントサイズや、背景の不透明度などを設定しています。
include colorscheme/gruvbox_dark.conf font_size 18 background_opacity 0.85 # Mod を command に mapping kitty_mod cmd # for URL settings url_color #0087bd url_style single open_url_with default url_prefixes http https file ftp gemini irc gopher mailto news git detect_urls yes
1 行目では color scheme を include しています。以下のリポジトリから gruvbox_dark という theme を選んで設定しました。 github.com
# colorscheme/gruvbox_dark.conf background #282828 foreground #ebdbb2 cursor #928374 selection_foreground #928374 selection_background #3c3836 # black color0 #282828 color8 #928374 # red color1 #cc241d color9 #fb4934 # green color2 #98971a color10 #b8bb26 # yellow color3 #d79921 color11 #fabd2d # blue color4 #458588 color12 #83a598 # magenta color5 #b16286 color13 #d3869b # cyan color6 #689d6a color14 #8ec07c # light gray color7 #a89984 color15 #928374
また、フルスクリーンで起動したいため ~/.config/kitty/macos-launch-services-cmdline
を作成し、次のように記述します。
--start-as=fullscreen
iTerm2 と比較して不便な部分の補完
私は iTerm2 では Hotkey の設定をしており、 control
2 回押しで表示/非表示を切り替えていました。一方 kitty 単体では Hotkey を設定することはできません。そこで、今回は Hammerspoon というツールを使用して Hotkey の設定をおこなっていきます。
Hammerspoon
Hammerspoon は macOS で Hotkey の設定や、ウィンドウ操作など、 OS の操作をおこなうことができるツールです。
Hammerspoon の設定ファイルは Lua で記述します。 ~/.hammerspoon/init.lua
を作成し、おこないたい処理を記述していきます。
今回 Hotkey の設定で求める要件は次のようになります。
- アクティブなデスクトップで表示する
- ディスプレイの解像度が変わっても、全画面表示する
control
2 回押しで表示/非表示が切り替わる
私の設定ファイルは GitHub に公開しています。 github.com
Hotkey を押した時の基本処理
ここで Hotkey を押した時の基本的な処理の説明をします。 Hotkey が押された時、次のように kitty の状態に応じて動作を分岐させます。
- 起動していない時: 起動する
- ウィンドウが最前面にある時: 非表示にする
- ウィンドウが最前面にない時: 最前面表示する
これらの動作をおこなうものが次のコードになります。
local module = {} -- toggle で kitty を表示/非表示する module.action = function() local appName = "kitty" local app = hs.application.get(appName) if app == nil then hs.application.launchOrFocus(appName) elseif app:isFrontmost() then app:hide() else hs.application.launchOrFocus(appName) end end
アクティブなデスクトップで表示するようにする
kitty が他のデスクトップで起動した時でも、その時アクティブなデスクトップで表示したいです。 しかし上で説明した基本処理のまま実行すると、起動した時のディスプレイへの移動が発生してしまい、ストレスを感じていました。

そこで、_asm.undocumented.spaces というモジュールを使って、常にアクティブなデスクトップで表示するようにします。
次のようなコードを追加することで、常にアクティブなデスクトップで表示することができます。 ここでは、アクティブなデスクトップにウィンドウを移動させるという処理をおこなっています。
local spaces = require("hs._asm.undocumented.spaces") local activeSpace = spaces.activeSpace() local win = app:focusedWindow() win:spacesMoveTo(activeSpace)
これによって、デスクトップの移動が発生せず、アクティブなデスクトップで表示することができるようになりました。

ディスプレイの解像度を変更しても全画面表示する
自分は普段 4K ディスプレイにつないでクラムシェルモードで開発していますが、出社時などではディスプレイにつながずに開発する時があります。 そのため異なる解像度になってもシームレスに開発できるように、自動的にウィンドウのサイズが画面に合うようにしたいです。

次のようなコードを追加することで、自動的に画面に合うようにすることができます。 今回 kitty の設定でフルスクリーンで起動するようにしているため、ウィンドウの座標やサイズを変更するやり方は使うことが出来ませんでした。 そこで、フルスクリーン状態を解除→フルスクリーン化とすることで、自動的に画面に合わせるようにしました。 また、アドホックなコードですが画面を非表示にしてから focus することで、すぐに入力できるようにしています。
local win = app:focusedWindow()
win = win:toggleFullScreen()
win = win:toggleFullScreen()
app:hide()
win:focus()
これにより、画面に合わせてウィンドウが変更されるようになりました。

また、フルスクリーンで起動していないウィンドウの場合は、以下のように記述することで、ウィンドウサイズを画面に合わせることができます。 これは画面の幅や高さなどを取得し、それらをウィンドウにコピーすることで全画面表示しています。
local mainScreen = hs.screen.find(spaces.mainScreenUUID()) local winFrame = win:frame() local screenFrame = mainScreen:fullFrame() winFrame.w = screenFrame.w winFrame.h = screenFrame.h winFrame.y = screenFrame.y winFrame.x = screenFrame.x win:setFrame(winFrame, 0)
control 2 回押しで表示/非表示を切り替える
前述した通り、私は iTerm2 を使う時は control
2 回押しで表示/非表示をおこなっていたため、 kitty でも control
2 回押しで表示/非表示をおこないたいです。今回は、1 秒以内に control
が 2 回押された時に module.action
(表示/非表示をおこなう関数) を実行するようにしています。
local timer = require("hs.timer") local eventtap = require("hs.eventtap") local events = eventtap.event.types local module = {} local spaces = require("hs._asm.undocumented.spaces") -- double tap の間隔[s] module.timeFrame = 1 -- 画面を合わせてから、アクティブなディスプレイに移動させる(要件 1. 2. ) function MoveFullScreenWindow(app) local activeSpace = spaces.activeSpace() local win = app:focusedWindow() win = win:toggleFullScreen() win = win:toggleFullScreen() app:hide() win:spacesMoveTo(activeSpace) win:focus() end -- toggle で kitty を表示/非表示する module.action = function() local appName = "kitty" local app = hs.application.get(appName) if app == nil then hs.application.launchOrFocus(appName) elseif app:isFrontmost() then app:hide() else MoveFullScreenWindow(app) end end local timeFirstControl, firstDown, secondDown = 0, false, false local noFlags = function(ev) local result = true for _, v in pairs(ev:getFlags()) do if v then result = false break end end return result end -- control だけが押されているか確認 local onlyCtrl = function(ev) local result = ev:getFlags().ctrl for k, v in pairs(ev:getFlags()) do if k ~= "ctrl" and v then result = false break end end return result end -- module.timeFrame 秒以内に 2 回 control を押した時に module.action を実行する module.eventWatcher = eventtap.new({events.flagsChanged, events.keyDown}, function(ev) if (timer.secondsSinceEpoch() - timeFirstControl) > module.timeFrame then timeFirstControl, firstDown, secondDown = 0, false, false end if ev:getType() == events.flagsChanged then if noFlags(ev) and firstDown and secondDown then if module.action then module.action() end timeFirstControl, firstDown, secondDown = 0, false, false elseif onlyCtrl(ev) and not firstDown then firstDown = true timeFirstControl = timer.secondsSinceEpoch() elseif onlyCtrl(ev) and firstDown then secondDown = true elseif not noFlags(ev) then timeFirstControl, firstDown, secondDown = 0, false, false end else timeFirstControl, firstDown, secondDown = 0, false, false end return false end):start()
これで快適に kitty を使用できるようになりました! iTerm2 の時に比べると動作が軽快になって個人的にかなり嬉しかったです。
さいごに
本記事では、 iTerm2 から kitty に移行する際におこなったことをまとめました。 特に後半の Hammerspoon の話は、 kitty を使わないユーザにも役に立ちそうな話なので、参考になる点があれば幸いです。余談ですが Slack や Chrome も Hotkey で呼び出すようにしたところ割と快適になりました*3。 Hammerspoon を使えば、 BetterTouchTool や Karabiner-Elements などでおこなっていることを一元化することができるかもしれませんね。
次回は上村さんの『ニュース記事配信のパーソナライズロジックのオフライン実験では何を見ているのか?』という記事です。 楽しみですね。