Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

iTerm2 から kitty に移行した話 〜Hammerspoon で快適ターミナルライフ〜

こんにちは、 Gunosy Tech Lab AdsML チームで広告のロジック改善をしている m-hamashita です。昨年 FlexiSpot E6ErgoDox 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 にマッピングされています。 他にもハッシュ値やファイルパスを取得したり、デフォルトアプリケーションで開いたりすることができます。

Hints を使って URL を開く様子

インストールは以下のコマンドでおこなうことができ、 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 を作成し、おこないたい処理を記述していきます。

www.hammerspoon.org

今回 Hotkey の設定で求める要件は次のようになります。

  1. アクティブなデスクトップで表示する
  2. ディスプレイの解像度が変わっても、全画面表示する
  3. 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 などでおこなっていることを一元化することができるかもしれませんね。

次回は上村さんの『ニュース記事配信のパーソナライズロジックのオフライン実験では何を見ているのか?』という記事です。 楽しみですね。

*1:最近 nsh という POSIX に準拠している Rust 製 shell を見つけたので、 fish からの移行を試みています。

*2: kitty の前に Alacritty を検討していましたが、当時一部フォントのレンダリングがうまくいかなかったのと、日本語のインライン入力ができないことから使用を止めていました。しかし、今年 7 月に出た PR によって、日本語のインライン入力ができるようになるため、次のバージョンのリリース時に移行するかもしれません。

*3:それまでは Alfred を使って呼び出していました。Alfred も非常に強力なのでおすすめです。