ターミナルや iTerm2 で AquaSKK を使う場合

2023年3月10日

最近、iTerm2 を使いはじめた。
 これ格好よくて、ctrl キーを二回連打すると、しゅばってターミナルのスクリーンが降りてくるのである。

設定は iTerm2 プリファレンスの「Keys」>「Hotkey」タブ>「Create a Dedicated Hotkey Window…」で行なえる。
 なお「システム環境設定」>「セキュリティとプライバシー」>「プライバシー」タブ>「アクセシビリティ」でアプリケーションに許可を与えてやる必要がある。

ctrl + j

本稿の目的はそういうことではなく、ターミナルや iTerm で AquaSKK を使う時の話だ。
 AquaSKK というのは日本語入力インプットメソッドである。日本語入力モードにする時、ctrl + j というキーコンビネーションを使う。
 しかし、ターミナルや iTerm で ctrl + j を押すと、改行されてしまうのである。

この ctrl + j を抑制するなら、karabiner-elements を使うのがいいみたいだ。
 下記のブログさまが json を公開されている。

🔗 Karabinar-ElementsでiTerm2 + AquaSKK環境下でのCtrl-J問題を解決する – nil.nu

下のほうにインストール用のリンクまであって非常に便利。

Hammerspoon で対応する

Hammerspoon でもできるはずなので、コードを書いてみた。

📝 ターミナルで ctrl + j

-- ターミナルでctrl + j
local function terminalEvent(name, event, app)
  if event == hs.application.watcher.activated then
    if name == 'ターミナル' or name == 'iTerm2' then
        hs.hotkey.bind({"ctrl"}, "j", function()
            hs.eventtap.keyStroke({}, 104, 0)
        end)
    else
        hs.hotkey.disableAll("ctrl", "j")
    end
  end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

hs.application.watcher は、アプリケーションが起動したり終了したり、アクティブになったり、非アクティブになったりするのを監視する。
 アプリを選択したり隠したりするごとに、三つの引数を引き受けてめちゃめちゃ監視している。その三つというのはアプリの名前、イベント(起動した、アクティブになった、など)、アプリのオブジェクト、である。

if event == hs.application.watcher.activatedというのはすなわち、なにかのアプリケーションがアクティブになったら、という意味だ。その下でアプリの名前で識別している。
 if name == 'ターミナル' or name == 'iTerm2'
 アクティブになったアプリの名前が「ターミナル」や「iTerm2」なら、という意味である。
 ターミナルや iTerm ならキーバインドを 104 にする。
 104 というのは「かなキー」のキーコードである。

AquaSKK における iTerm 上の L キーの問題

問題は L キーだ。
 AquaSKK 統合では、日本語入力モードの時 L キーを押すと英数入力になる。
 ターミナルは何事もなくその通りに動作する。
 しかし iTerm だと「 l 」が入力されてしまうのだ。l が入力されて英数入力になる。

これを抑制したいのだが難しい。
 まず現在の入力ソースを調べる。日本語入力モードなら、L キーの入力を監視する。
 L キーが押されたら入力を抑制する、という流れが考えられる。

現在の入力ソースを調べるにはどうすればいいか。ターミナルで、
 defaults read com.apple.HIToolbox
 を実行すると、AppleSelectedInputSources という項目に現在の入力ソースが表示される。

AppleSelectedInputSources =     (
                {
            "Bundle ID" = "jp.sourceforge.inputmethod.aquaskk";
            "Input Mode" = "com.apple.inputmethod.Japanese";
            InputSourceKind = "Input Mode";
        }
    );

Hammerspoon ではコマンドを実行できるので(hs.execute(“コマンド"))、なんとかなりそうに思える。
 しかし AquaSKK 統合では、日本語入力モードだろうが英数入力だろうが、この部分に変化は出ないのである。
 つまり、どうやって AquaSKK の入力モードを調べればいいのかわからない。
 初手から挫折なわけだ。

次に考えたのが、かなキーが押された時に true になる変数を設置する方法だ。
 かなキーが押されたなら、間違いなく日本語入力モードに入ってると考える。
 その変数が true の時だけは、L キーを押しても l が入力されず、かわりに「英数キー」を押したことにする。
 実際に書いたのが以下のようなコード。
 変数の名前は aquaHira とした。

aquaHira = false
local function lkeyWatch(ev)
    local c = ev:getKeyCode()
    if c == 104 then
        aquaHira = true
    end
    if c == 37 and aquaHira then
        ev:setKeyCode(-1)
        hs.eventtap.keyStroke({}, 102, 0)
        aquaHira = false
    end
end

aquaL = hs.eventtap.new({hs.eventtap.event.types.keyDown}, lkeyWatch)

local function terminalEvent(name, event, app)
  if event == hs.application.watcher.activated then
    if name == 'ターミナル' or name == 'iTerm2' then
        hs.hotkey.bind({"ctrl"}, "j", function()
            hs.eventtap.keyStroke({}, 104, 0)
        end)
    else
        hs.hotkey.disableAll("ctrl", "j")
    end
    if name == 'iTerm2' then
        hs.eventtap.keyStroke({}, 102, 0)
        aquaHira = false
        aquaL:start()
    else
        aquaL:stop()
    end
  end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

上記コードを init.lua に記述してリロードすると、ちゃんと l の入力が抑制されるようになる。
 ちなみにキーコード 102 は「英数キー」、キーコード 37 は「L キー」だ。

このコードの問題点は、かなキーを押さない限り aquaHira が true にならないところだ。
 つまり AquaSKK が日本語入力モードの状態で iTerm を前面に出すと aquaHira は false のまま。L キーの文字入力は抑制されない。
 この問題をふせぐため、もう iTerm がアクティブになったらすぐさま「英数キー」を送信することにした。iTerm が前面に出たら、それまでの入力モードがなんだろうと、むりやり英数入力にしちゃうのである。だいぶ力づくの解決法だ。

が、iTerm をたちあげて、いきなり日本語を入力するシチュエーションっていうのも、あまりないんじゃねーかな、と思う。
 けっしてスマートなやり方じゃないけど、まぁしょうがないので、しばらくはこれでいくつもりだ。

追記 19.12.28

AquaSKK はユーザの config である程度、キーマップを変更できる。
 これを利用しない手はないことに気づいた。
 Keymap.conf は ユーザの「ライブラリ/Application support/AquaSKK/」に入っている。文法は以下に書いてある。

🔗 keymap.confの文法 – AquaSKK Wiki – AquaSKK – OSDN

ここで、押しやすさとかは考慮せず、他のキーボード・ショートカットとかぶらないキーの組み合わせを考えて、SwitchToAscii に割り当てるわけだ。
 筆者は ctrl + shift + k にした。

# ======================================
# attribute section(for SKK_CHAR)
# ======================================
ToggleKana        q
ToggleJisx0201Kana    ctrl::q
SwitchToAscii        l||ctrl::shift::k # ←ここを変更した

これで、[ L ] キーか、あるいは [ctrl] + [shift] + [ k ] で AquaSKK の英数モードに出来るようになった(パイプ二本、「 || 」で区切ることで複数のキーを設定できる)。

あとは上述した Hammerspoon のスクリプトで [英数]キーを送っていた部分を [ctrl] + [shift] + [ k ] に書き換えればいい。具体的には以下のようになる。

aquaHira = false
local function lkeyWatch(ev)
    local c = ev:getKeyCode()
    if c == 104 then
        aquaHira = true
    end
    if c == 37 and aquaHira then
        ev:setKeyCode(-1)
        hs.eventtap.keyStroke({"ctrl","shift"}, "k", 0)
        aquaHira = false
    end
end

aquaL = hs.eventtap.new({hs.eventtap.event.types.keyDown}, lkeyWatch)

local function terminalEvent(name, event, app)
  if event == hs.application.watcher.activated then
    if name == 'ターミナル' or name == 'iTerm2' then
        hs.hotkey.bind({"ctrl"}, "j", function()
            hs.eventtap.keyStroke({}, 104, 0)
        end)
    else
        hs.hotkey.disableAll("ctrl", "j")
    end
    if name == 'iTerm2' then
        hs.eventtap.keyStroke({}, 104, 0)
        hs.eventtap.keyStroke({"ctrl","shift"}, "k", 0)
        aquaHira = false
        aquaL:start()
    else
        aquaL:stop()
    end
  end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

こっちのほうが素直な感じだな。

追記 2020.12.26

筆者はかなりのアホンダラだった。hammerspoon にはインプットメソッドを扱うやつがちゃんとあったみたい。知らんかったよ。
 ということでコードを修正したんだけど、目覚ましく良くなったというわけではない。

local function lkeyWatch(ev)
    local c = ev:getKeyCode()
    if c == 37 then
        local method = hs.keycodes.currentMethod()
        if method == 'AquaSKK 統合' then
            ev:setKeyCode(-1)
            hs.keycodes.setLayout("U.S.")
        end
    end
end

aquaL = hs.eventtap.new({hs.eventtap.event.types.keyDown}, lkeyWatch)

local function terminalEvent(name, event, app)
   if event == hs.application.watcher.activated then
    if name == 'ターミナル' or name == 'iTerm2' then
        hs.hotkey.bind({"ctrl"}, "j", function()
            hs.eventtap.keyStroke({}, 104, 0)
            --hs.keycodes.setMethod("AquaSKK 統合")
        end)
    else
        hs.hotkey.disableAll("ctrl", "j")
    end
    if name == 'iTerm2' then
        aquaL:start()
    else
        aquaL:stop()
    end
  end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

hs.keycodes.currentMethod()で現在のインプットメソッド名を調べられる。
 hs.keycodes.setMethod("AquaSKK 統合")とやれば、インプットメソッドを AquaSKK に変更できるんだけど、今回のケースではうまく動かないので、今まで通り「かなキー」を押すことにした。

入力ソースを U.S. にする場合は、hs.keycodes.setLayout("U.S.")というのを使うのだが、注意が必要だ。
 筆者は英語配列のキーボードを使っており、日本語キーボードの場合はここの記述が違ってくるかもしれない。

入力ソースを U.S. にしてからターミナルで、

defaults read com.apple.HIToolbox

を実行し、最後の項目「AppleSelectedInputSources」に注目する。
 KeyboardLayout Nameというところに書いてあるレイアウト名が「U.S.」だったらいいんだけど、違う場合は七行目の"U.S."を書き直せば、たぶんだけど大丈夫だと思う。

追記 2021.01.16

筆者のアホさ加減は天井知らずだった。
 この問題、どうも駄目っぽい。というか完全に問題を取り違えていた。
 なにが問題だったか思い出してみる。

  • AquaSKK 統合で[ command ] + [ j ]を iTerm2 で実行すると改行されてしまう。
    • これは解決できたと思う。
  • [ l ]キーで英数に変えようとすると、「 l 」が入力されてしまう。
    • これも解決したと思ってた。

[ l ]キー問題、解決してなかった。今までなんで気づかなかったのか不思議だが、筆者は AZIK で日本語を入力している。AZIK においては、けっこう[ l ]キーを使うのだ。
 [ l ]キーは、前の子音にあわせて「on」を入力してくれる。
 kl なら「こん」。rl なら「ろん」という感じ。
 筆者のスクリプトだと、[ l ]キーを押すや否や、前置された子音とか関係なく、入力ソースが変更され、英数になってしまうのだ。

おれ本当、なにやってんだろ。もう全裸に剥いて通りにさらしてくれ。

それでも、なにか方法がないか考えてみた。「AquaSKK 統合」では無理だが、AquaSKK の「ひらかな」と「ASCII」を入力ソースに追加すればなんとかなるかもしれない。

問題は、[ l ]キーで英数にしようとすると、「 l 」が入力されてしまう、というもの。だったら、入力された l を削除してしまえばいい。
 hammerspoon にはインプットメソッドの変更を検知する関数がある。
 hs.keycodes.inputSourceChangedである。

map = hs.keycodes.map
hs.keycodes.inputSourceChanged(function()
    local method = hs.keycodes.currentMethod()
    local app = hs.application.frontmostApplication()
    local appname = app:name()
    if appname == 'iTerm2' then
        if method == 'ASCII' then
            hs.eventtap.keyStroke({}, map['delete'], 0)
        end
    end
end)

アクティブなアプリが iTerm2 で、変更されたインプットメソッドが ASCII なら [ delete ]キーを押す、というスクリプトだ。

これでいけるはず、と思ったが、どうもうまくいかない。
 インプットメソッドの変更を二度、三度と検知してしまい、どんどん文字が削除されてしまう。
 また、別のウィンドウがアクティブになるたびにインプットメソッドの変更が発生するらしい。これは「書類ごとに入力ソースを自動的に切り替える」という設定が関係しているものと思われる。

deleteキーが二度入力されている

どうであれ、変数を設定して制御する必要がある。

📝 init.lua

map = hs.keycodes.map
lkey = false

local function terminalEvent(name, event, app)
   if event == hs.application.watcher.activated then
      --hs.alert.show(name)
    if name == 'ターミナル' or name == 'iTerm2' then
        hs.hotkey.bind({"ctrl"}, "j", function()
            hs.eventtap.keyStroke({}, map['kana'], 0)
        end)
    else
        hs.hotkey.disableAll("ctrl", "j")
    end
    if name == 'iTerm2' then
        lkey = true
        hs.timer.doAfter(0.5, function()
            lkey = false
        end)
    end
  end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

hs.keycodes.inputSourceChanged(function()
    local method = hs.keycodes.currentMethod()
    local app = hs.application.frontmostApplication()
    local appname = app:name()
    if appname == 'iTerm2' then
        if method == 'ASCII' and lkey == false then
            hs.eventtap.keyStroke({}, map['delete'], 0)
            lkey = true
        end
    end
    hs.timer.doAfter(0.5, function()
        lkey = false
    end)
end)

lkey という変数を設定して、これが true の時は、デリートを実行しない。
 最初の一回の検知のとき、一度だけデリートして入力された「 l 」を削除し、ただちに lkey を true にする。
 true になった lkey はhs.timer.doAfterという関数で、0.5 秒後に false に戻る、という仕掛けだ。

deleteキーの入力が一度になった

今のところ良好に動作してっけど、また不具合が明かになるんだろうなぁ。
 なんど書き直すんだよ、この話。
 そもそも「AquaSKK 統合」でやりたかったはずなのに、妥協しちまったし。
 とんだクソ記事になっちまったよ。読んでくれた方、どうもすみません。

追記 2021.02.01

筆者の阿呆さ加減は海より深い。
 今回は L キーの話じゃなく、[ command ] + [ j ]だ。

筆者はhs.hotkey.bindでもって[ command ] + [ j ]を日本語入力にしていた。
 アプリが「ターミナル」か「iTerm2」なら上記ホットキーを有効にし、そうでない場合はホットキーを無効にしていた。
 これ、アプリを切り替えるたびに、コンソールにメッセージが吐き出されていて、ちょっと気持ち悪かったのである。どこが間違っているのか、といわれれば、まだちょっとわからないんだが、こういう目的のために使うべき物じゃない、というか。

Hammerspoon にはhs.hotkey.modalというものがあり、こっちのほうが今回の目的に合致していそうだ。
 なんらかのキーコンビネーションを押すと、モードに入る、という物なんである。
 そのモードの状態でのみ、たとえば[ a ]キーを押すと設定した動作を起こせる、という感じだ。
 今回はターミナルか iTerm2 がアクティブになった時を起点にモードに入る、ということにすればいい。以下の記事で勉強させていただきました。

🔗 Hammerspoonで特定のアプリでだけショートカットキーを有効にする – Qiita

🔨 init.lua

map = hs.keycodes.map
lkey = false
itermMode = hs.hotkey.modal.new()
itermMode:bind({'ctrl'}, 'j', function()
    hs.eventtap.keyStroke({}, map['kana'], 0)
    -- 2022.10.17追加
    lkey = true
end)
function terminalEvent(name, event, app)
    if event == hs.application.watcher.activated then
        if name == 'ターミナル' or name == 'iTerm2' then
            itermMode:enter()
        else
            itermMode:exit()
        end
        if name == 'iTerm2' then
            lkey = true
            hs.timer.doAfter(0.5, function()
                lkey = false
            end)
        end
    end
end

terminalWatch = hs.application.watcher.new(terminalEvent)
terminalWatch:start()

hs.keycodes.inputSourceChanged(function()
    local method = hs.keycodes.currentMethod()
    local app = hs.application.frontmostApplication()
    local appname = app:name()
    if appname == 'iTerm2' then
        if method == 'ASCII' and lkey == false then
            hs.eventtap.keyStroke({}, map['delete'], 0)
            lkey = true
        elseif method == 'ひらかな' then
            lkey = true
        end
    end
     hs.timer.doAfter(0.5, function()
         lkey = false
     end)
end)

これだと、いまのところコンソールにメッセージが出ない。
 なお、hs.hotkey.modalは、ドキュメントに使い方の例が掲載されている。

🔗 Hammerspoon docs: hs.hotkey.modal

※追記 2022.10.17
 M2 MacBook Air にしたら挙動がおかしかったので、ctrl + j を押すごとに、変数 lkey を true にすることにした。