LoginSignup
3

More than 5 years have passed since last update.

macOS Sierraでオレオレ装飾キーを使う

Last updated at Posted at 2017-06-18

MacbookProを購入した。
…と思ったら即日メインのWindowsPCがぶっ壊れる始末。
何なの…

概要

Windowsでは、AHKを使って無変換と変換キーをオレオレ装飾キーとして扱い、ダイヤモンドカーソル風の移動などを実現していた。例えば、無変換 + J無変換 + Kといった具合である。

Macオンリーな生活を余儀なくされたので、Macでも同様のオレオレ装飾キーが使えないか再現を試みた。満足のいく結果が得られたので、調査した内容や実装をまとめる。

要点

SierraではKarabiner(旧KeyRemap4MacBook)が使えない。KeyRemap4MacBook入れれば終いや!と久しぶりMacな自分は高を括ってたのだが…(KarabinerのSierra版はまだ開発途中、キーリマップだけならKarabiner-Elementsというアプリがリリースされている)

いろいろ調べたり試したりした結果、Hammerspoonというアプリを使えばLuaでかなり柔軟にキー入力の処理などを記述できそうである。キーリマップだけなら、前述のKarabiner-Elementsや英かなでも可能なのだが、今回の要件は満たせない。他にも、keyhacというアプリもあるようだが、不具合があるという記述や、調べた限り同様の目的での採用数が少なそうだったので見送った。

最終的には、Karabiner-Elementsで既存のキーを効果のないキーにリマップし、Hammerspoonを使って処理を実装するという形になった。

実装

  • なるべく簡単にキーリマップを設定・変更出来るように、汎用的な処理にリマップデータを流し込む形にした
  • 装飾キーは、Karabiner-Elementsを使って英数かなF19にして使用している(厳密には特にしなくても良い設定だが)
  • hs.hotkey.bind()だと、非装飾キー(ctrlなどでないキー)を装飾キーのように扱えなかったので、キーダウンとキーアップイベントを乗っ取る形で実現
  • 装飾キーを単体押下したときの処理も設定できるようにした
  • キー入力のリマップ以外にも、押下時に独自の処理を行うなどにも対応(コード中のextendKeyListで、outKeyを空白にしfunction()を設定しておけばキー入力は行われずfunction()のみが呼ばれる)

グローバル変数部分

init.lua
----------------------------------------------------------------
-- Hammerspoon 設定
----------------------------------------------------------------

-- キーコード
local eisuu = 0x66
local kana = 0x68
local minus = '-'
local caret = '='
local extendKey = 'f19'

-- グローバル
local normalizedKeyMap = {}

イベントハンドラ部分

init.lua
---------------------------------------------------------
-- キー入力監視
---------------------------------------------------------

-- 押下している独自修飾キー
local downedKeyMap = {}
-- 他のキーと組み合わせ済みの独自修飾キー(単独押し判定用)
local combinedKeyMap = {}

-- キーリマップリストの組み変え(アクセスし易いように)
-- {
--     "modKey": {
--         "single": "function(mods)",
--         "keys": {
--             "inKey": {"mods": ["outMods", ..], "key": "outKey", "func": "function(mods, events, self)"},
--             ..
--         }
--     },
--     ..
-- }
local function normalizeKeylist(list)
    local out = {}

    for i, val in pairs(list) do
        if out[val['mod']] == nil then out[val['mod']] = {} end
        local map = out[val['mod']]

        map['single'] = val['single']

        if map['keys'] == nil then map['keys'] = {} end
        for j, key in pairs(val['keys']) do
            map['keys'][key[1]] = {
                mods = key[2],
                key =  key[3],
                func = key[4]
            }
        end
    end

    return out
end

-- キー入力ハンドラー
local keyHandler = function(ev)
    local keyCode = ev:getKeyCode()
    local eventType = ev:getType()
    local key = hs.keycodes.map[keyCode]

    -- キーダウン時
    if eventType == hs.eventtap.event.types.keyDown then
        --  独自修飾キーとの組み合わせ処理(独自修飾キーが押下済みで独自修飾キー以外のダウンイベント)
        for downedKey, v in pairs(downedKeyMap) do
            if downedKey ~= key and normalizedKeyMap[downedKey] ~= nil then
                if combinedKeyMap[downedKey] == nil then combinedKeyMap[downedKey] = {} end

                if normalizedKeyMap[downedKey]['keys'][key] ~= nil then
                    local mods = {}
                    for k, v in pairs(ev:getFlags()) do
                        table.insert(mods, k)
                    end

                    -- 代替イベント(キーダウン)
                    local events = {}
                    local replacement = normalizedKeyMap[downedKey]['keys'][key]
                    if replacement['func'] ~= nil then replacement['func'](mods, events, replacement) end
                    if replacement['key'] ~= '' then
                        for i, mod in pairs(replacement['mods']) do table.insert(mods, mod) end
                        table.insert(events, hs.eventtap.event.newKeyEvent(mods, replacement['key'], true))
                    end

                    -- 組み合わせたキーと内容を保持しておく
                    combinedKeyMap[downedKey][key] = replacement
                    return true, events
                end
            end
        end

        -- 独自修飾キーの処理(独自修飾キーのダウンイベント)
        if normalizedKeyMap[key] ~= nil then
            if downedKeyMap[key] == nil then
                downedKeyMap[key] = true
            end
            -- キー自体のイベントは無効化(キーリピートも含まれるため)
            return true
        end

    -- キーアップ時
    elseif eventType == hs.eventtap.event.types.keyUp then
        -- 独自修飾キーの処理(独自修飾キーのアップイベント)
        if normalizedKeyMap[key] ~= nil then
            downedKeyMap[key] = nil

            -- 単体押し
            if combinedKeyMap[key] == nil then
                local mods = {}
                for k, v in pairs(ev:getFlags()) do
                    table.insert(mods, k)
                end
                -- 関数を呼ぶ
                if normalizedKeyMap[key]['single'] ~= nil then normalizedKeyMap[key]['single'](mods) end
                return true

            -- 組み合わせ済みの場合(独自修飾キーよりも先に組み合わせキーが離された場合)
            else
                -- 強制的に代替イベントのキーアップを呼ぶ
                local events = {}
                for k, val in pairs(combinedKeyMap[key]) do
                    table.insert(events, hs.eventtap.event.newKeyEvent(val['mods'], val['key'], false))
                end
                combinedKeyMap[key] = nil
                return true, events
            end
        end

        --  独自修飾キーとの組み合わせ処理(独自修飾キーが押下済みで独自修飾キー以外のアップイベント)
        for downedKey, v in pairs(downedKeyMap) do
            if downedKey ~= key and normalizedKeyMap[downedKey] ~= nil then
                if normalizedKeyMap[downedKey]['keys'][key] ~= nil then
                    -- 代替イベント(キーアップ)
                    local events = {}
                    local replacement = normalizedKeyMap[downedKey]['keys'][key]
                    if replacement['key'] ~= '' then
                        table.insert(events, hs.eventtap.event.newKeyEvent(replacement['mods'], replacement['key'], false))
                    end

                    -- 保持している処理を削除
                    if combinedKeyMap[downedKey] ~= nil then combinedKeyMap[downedKey][key] = nil end
                    return true, events
                end
            end
        end
    end
end

eventtap = hs.eventtap.new({hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, keyHandler)
eventtap:start()

キーリマップ設定部分

init.lua
---------------------------------------------------------
-- キーカスタマイズ設定
---------------------------------------------------------
-- {
--     "mod": "modKey",
--     "single": "function(mods)",
--     "keys": [
--         ["inKey", ["outMods", ..], "outKey", "function(mods, events, self)"],
--         ..
--     ]
-- }
local extendKeyList = {
    {
        mod = extendKey,
        single = function(mods)
            -- ターミナルにフォーカス
            hs.application.launchOrFocus('Terminal')
        end,
        keys = {
            -- IME切り替え
            -- ctrl + space だと意図通りに動かない…
            {'space', {}, eisuu, function(mods, events, self)
                -- 自身の設定を組み替える
                if self['key'] == eisuu then
                    self['key'] = kana
                else
                    self['key'] = eisuu
                end
            end},
            -- ディスプレイ切り替え
            {'left', {'ctrl'}, 'left'},
            {'down', {'ctrl'}, 'down'},
            {'up', {'ctrl'}, 'up'},
            {'right', {'ctrl'}, 'right'},
            -- カーソル
            {'j', {}, 'left'},
            {'k', {}, 'down'},
            {'l', {}, 'up'},
            {';', {}, 'right'},
            {'u', {}, 'home'},
            {'i', {}, 'pagedown'},
            {'o', {}, 'pageup'},
            {'p', {}, 'end'},
            -- ファンクションキー
            {'1', {}, 'f1'},
            {'2', {}, 'f2'},
            {'3', {}, 'f3'},
            {'4', {}, 'f4'},
            {'5', {}, 'f5'},
            {'6', {}, 'f6'},
            {'7', {}, 'f7'},
            {'8', {}, 'f8'},
            {'9', {}, 'f9'},
            {'0', {}, 'f10'},
            {minus, {}, 'f11'},
            {caret, {}, 'f12'},
            -- その他
            {'delete', {'ctrl'}, 'd'}
        }
    }
}

normalizedKeyMap = normalizeKeylist(extendKeyList)

hs.alert.show("Config Loaded!!")

まとめ

概ね満足のいく結果を得られた。Hammerspoonを使えば、かなりいろいろできそうである。

思わぬところでLuaに再入門することになった。とはいえ、Tableオブジェクトの扱いに詰まった程度で、これくらいのコードなら書くことが出来た。最初はアプリごとに有効無効も設定できるようにと考えていたが、めんどくさくなって断念、使わないしいいや。

補足

そもそもなぜKarabinerが動かなくなったのかという理由は以下のサイトに書いてあった。

仮想化雑記帳: 【余談】Hammerspoon に移行

KEXTというカーネル拡張モジュールを読み込ませて動作していたが、OSのアップデートによってKEXTの動作に制限が掛かったのが原因とのこと。KEXT自体は、デバイスドライバなどをカーネルの再コンパイルなしに 有効化したり無効化するための仕組みであるらしい。

今後もカーネルへの操作をMacが規制してくるなら、同じ仕組みである限りKarabinerがフル動作するのを期待するのは難しいのかも。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3