FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

HammerspoonとKarabiner-Elementsを使って、キーボードでカーソルとウィンドウを操作する

FLINTERSブログ祭りの記事です。テーマはキーボードです。

概要

FLINTERSでエンジニアをしている吉田です。

自分は趣味で、キーボード配列を変更したり、キーボードショートカットを最適化したりを日々行っています。 今回は自分が使用しているHaummerspoonとKarabiner-Elementsを使ったキーボードショートカットについて解説したいと思います。

自分はなるべくキーボードの操作のみでPC操作を完結させたいと考えており、テキストエディターやアプリケーションの操作は極力キーボードで行うようにしています。

そうしたとき、複数ウィンドウにおけるアクティブウィンドウの切り替えについて考えます。これをキーボードで行うことを考えたとき、Alt Tabのようなツールでウィンドウ切り替えを行う方法もありますが(自分も適宜使っています)、今回はもう一つの方法として、HammerspoonとKarabiner Elementを使ったカーソル移動とウィンドウ操作の方法を紹介したいと思います。

作るもの

今回実装するものは以下になります。 キーボードショートカットとして、キーボードに以下のような割当を行います。 キーボードは ThinkPad トラックポイント キーボード IIの日本語配列、OSはMac OSを想定しています。

ThinkpadキーボードにはAlt(Mac OSではOptionキーと認識される)キーが2つあるので、左側のAltキーに独自のショートカットを割り当てます。

MV1~MV4

カーソルをウィンドウの特定の位置に移動します。 移動する位置はHammerSpoon上で指定します。 自分はデュアルディスプレイを使用しているので、MV1~MV4にそれぞれ左ディスプレイの左側中央、右側中央、右側ディスプレイの左側中央、右側中央に移動するように設定しています。 かつ、カーソルの下にウィンドウがあった場合、そのウィンドウをアクティブにします。 デュアルディスプレイでドキュメントを適宜参照しつつ、メインの作業をもう片方のウィンドウで操作する際など、カーソルの移動を伴って直感的にアクティブウィンドウの切り替えができます。

Move to Active Center

アクティブなウィンドウの中央にカーソルを移動します。 カーソルを見失ったときなどに活用できます。

Activate Second Top Window

二番目に上に来ているウィンドウをアクティブにします。 一画面で、2つの重なったウィンドウを交互に参照したい際などに直感的に操作できます。

動作イメージは以下のようになります。

実装

Hammerspoonでは、両側にあるOptionを同じものとして扱うため、右Optionと左Optionにそれぞれ分けてショートカットを設定することはできません。 そのため、左右のキーを別のものとして割当が可能なKarabiner Elementsを使って、一旦別のキーに割り当て、そのキーに対してHammerspoonでウィンドウ操作のショートカットを実装をしていきます。

Karabiner-Elementsによる実装

Karabiner-Elementsの概要と使用方法については他に素晴らしい記事がいくつもあるので、今回は実装するものについてのみ記載します。 以下をKarabiner.jsonに貼り付けて、使用したいキーにOptionと異なる別のキーバインドを割り当てます。

{
    "description": "Replace Right Option Key To Original Move Cursor Function Key",
    "manipulators": [
        {
            "from": {
                "key_code": "a",
                "modifiers": {
                    "mandatory": [
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "a",
                    "modifiers": [
                        "left_option",
                        "command",
                        "control"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "s",
                "modifiers": {
                    "mandatory": [
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "s",
                    "modifiers": [
                        "left_option",
                        "command",
                        "control"
                    ]
                }
            ],
            "type": "basic"
        },
        // 同様の実装を割当たいキーの数分行う
    ],
    "type": "basic"
}

Hammerspoonによる実装

Hammerspoonについても、概要や使い方は割愛し、実装するコードのみ記載します。 Hammerspoonのinit.luaに以下を貼り付け、設定を更新します。

--[[
    カーソル移動とウィンドウ切り替えを行うための関数
]]--

function moveCursorToScreenPosition(screen_number, ratio)
    hs.mouse.setAbsolutePosition(getPositionBasedOnScreen(screen_number, ratio))
end

function getPositionBasedOnScreen(screen_number, ratio)
    local screens = hs.screen.allScreens()
    table.sort(screens, function(a, b) return a:frame().x < b:frame().x end)

    local rightScreen = screens[screen_number]

    if rightScreen then
        local frame = rightScreen:frame()
        local targetX = frame.x + frame.w * ratio
        local targetY = frame.y + frame.h / 2
        return {x = targetX, y = targetY}
    end
end

function getWindowsUnderPos(mousePoint)
    return hs.fnutils.filter(hs.window.orderedWindows(), function(win)
        local frame = win:frame()
        return mousePoint.x >= frame.x and mousePoint.x <= frame.x + frame.w
               and mousePoint.y >= frame.y and mousePoint.y <= frame.y + frame.h
    end)
end

function activateWindowBasedOnScreenPos(screen_number, ratio)
    local pos = getPositionBasedOnScreen(screen_number, ratio)
    local windowsUnderCursor = getWindowsUnderPos(pos)[1]
    if windowsUnderCursor then
        windowsUnderCursor:focus() 
    end
end

function activateSecondWindowUnderCursor()
    local mousePoint = hs.mouse.getAbsolutePosition()
    local windowsUnderCursor = getWindowsUnderPos(mousePoint) 

    -- 二番目に前面にあるウィンドウを特定し、アクティブにする
    if #windowsUnderCursor >= 2 then
        local secondWindow = windowsUnderCursor[2]
        secondWindow:focus()
    end
end

-- アクティブなウィンドウの中心にカーソルを移動する関数
function moveCursorToCenterOfActiveWindow()
    local win = hs.window.focusedWindow() 
    if win then
        -- ウィンドウのフレーム(位置とサイズ)を取得
        local f = win:frame() 

        -- ウィンドウの中心点を計算
        local center = { x = f.x + f.w / 2, y = f.y + f.h / 2 }

        -- カーソルをウィンドウの中心に移動
        hs.mouse.setAbsolutePosition(center) 
    end
end

--[[
    キーバインドの設定
]]--
[f:id:kobita777:20240602143739g:plain]
-- 'left alt + a'
hs.hotkey.bind({"alt", "command", "ctrl"}, "a", function()
    activateSecondWindowUnderCursor()
end)

-- 'left alt +s'
hs.hotkey.bind({"alt", "command", "ctrl"}, "s", function()
    moveCursorToScreenPosition(1, 1/4)  
    activateWindowBasedOnScreenPos(1, 1/4)
end)

-- 'left alt +d'
hs.hotkey.bind({"alt", "command", "ctrl"}, "d", function()
    moveCursorToScreenPosition(1, 3/4)  
    activateWindowBasedOnScreenPos(1, 3/4)
end)

-- 'left alt +f'
hs.hotkey.bind({"alt", "command", "ctrl"}, "f", function()
    moveCursorToScreenPosition(2, 1/4)  
    activateWindowBasedOnScreenPos(2, 1/4)
end)

-- 'left alt + g'
hs.hotkey.bind({"alt", "command", "ctrl"}, "g", function()
    moveCursorToScreenPosition(2, 3/4) 
    activateWindowBasedOnScreenPos(2, 3/4)
end)

-- 'left alt + e'
hs.hotkey.bind({"alt", "command", "ctrl"}, "e", function()
    moveCursorToCenterOfActiveWindow()
end)

これで、上記Gif画像のような操作が可能になったと思います。

おわりに

今回はHammerspoonとKarabiner-Elementsを使って、カーソル移動とウィンドウのアクティブ化をキーボードで行う方法について紹介しました。

これらツールを用いたキーボードのカスタマイズは非常に奥が深く、さまざまな設定ができます。 例えば単体で押下したときは変換キーとして振る舞い、他のキーと組み合わせたときはCtrlとして振る舞う、といったことや(One Shot KeysやOne Shot Mode Keyなどと呼ばれています)、あるキーをレイヤーキーとして、キーを押下することでキーボードの設定の切り替えを行うといったことも可能です。

また、例えばEscキーやEnterキー、標準の位置では使用頻度に比べ、遠くにあると思いませんか? CommandキーやOptionキーをOneShotKeyとして利用することで、Commandを単体押下した際はEscキー、Optionを押下した際はEnterキーにするといったことで、ホームポジションから離れずにこれらキーを押すことが可能です。 こういったものを利用して自分にあった配列、キーボードショートカットを採用することで、生産性を底上げすることが可能だと考えています。

皆さんもキーボードをカスタマイズして、快適なPC操作ライフを送りましょう。