⌨️

SKKを意識したkeyhac設計

2024/02/02に公開

SKK(CorvusSKK)を使い始めました。keyhacと組み合わせることでさらに快適な日本語入力ライフを送れるようになったので備忘録です。

最新状況:

https://github.com/AWtnb/keyhac

以前に書いたkeyhacの紹介記事:

https://zenn.dev/awtnb/books/adf6c5162a9f08


詳細は上記のリポジトリや紹介記事を見ていただければと思いますが、keyhacは基本的に configure 関数の中に処理を書いていきます。
以下のコードはすべてこの関数内に記述していきます(インデントを省略しているのでコピペ時にはご注意ください)。

def configure(keymap):
    # この中に処理を書いていく

今回の記事はSKKとの連携に絞ります。
設定ファイル編集用のエディタや基本的なリマップなどは上記のリポジトリをご覧ください。

キー・文字入力部分を作る

公式に InputTextCommandInputKeyCommand が用意されていますが、ソースを読んでみるとさらに分解できそうだったので VirtualFinger クラスのメソッドとして作り直します。

# 遅延させる関数
def delay(msec: int = 50) -> None:
    time.sleep(msec / 1000)

# 「仮想指」
class VirtualFinger:
    def __init__(self, keymap: Keymap, inter_stroke_pause: int = 10) -> None:
        self._keymap = keymap
        self._inter_stroke_pause = inter_stroke_pause

    def _prepare(self) -> None:
        self._keymap.setInput_Modifier(0)
        self._keymap.beginInput()

    def _finish(self) -> None:
        self._keymap.endInput()

    def type_keys(self, *keys) -> None:
        self._prepare()
        for key in keys:
            delay(self._inter_stroke_pause)
            self._keymap.setInput_FromString(str(key))
        self._finish()

    def type_text(self, s: str) -> None:
        self._prepare()
        for c in str(s):
            delay(self._inter_stroke_pause)
            self._keymap.input_seq.append(pyauto.Char(c))
        self._finish()

    def type_smart(self, *sequence) -> None:
        for elem in sequence:
            try:
                self.type_keys(elem)
            except:
                self.type_text(elem)

キーストロークを入力させるときは type_keys を、文字列を入力させるときは type_text を使用します。
両者が混じることもあるので type_smart を作りました。ただ、やっていることはtry-exceptで雑に振り分けているだけなのでもう少し丁寧にしたほうがいいかもしれません。

あまりに速くキー入力を実行するとアプリによっては受け付けてくれないことがあるので、inter_stroke_pause で少し待つようにもできます。

IME制御部分を作る

SKKの基本的な制御を行うクラスを作ります。

IMEのオンオフは getImeStatus()setImeStatus() で簡単に制御できるので、それにVirtualFingerクラスによる自動キー入力を組み合わせています。

class ImeControl:
    kana_key = "C-J"
    latin_key = "L"
    cancel_key = "C-G"
    reconv_key = "LWin-Slash" # Windows標準の再変換ショートカットキー
    abbrev_key = "Slash"

    def __init__(
        self,
        keymap: Keymap,
    ) -> None:
        self._keymap = keymap
        self._finger = VirtualFinger(self._keymap, 0)

    def get_status(self) -> int:
        return self._keymap.getWindow().getImeStatus()

    def set_status(self, mode: int) -> None:
        self._keymap.getWindow().setImeStatus(mode)

    def is_enabled(self) -> bool:
        return self.get_status() == 1

    def enable(self) -> None:
        self.set_status(1)

    def enable_skk(self) -> None:
        self.enable()
        self._finger.type_keys(self.kana_key)

    def to_skk_latin(self) -> None:
        self.enable_skk()
        self._finger.type_keys(self.latin_key)

    def reconvert_with_skk(self) -> None:
        self.enable_skk()
        self._finger.type_keys(self.reconv_key, self.cancel_key)

    def disable(self) -> None:
        self.set_status(0)

全体で使い回すのでグローバル変数にしておきます。

IME_CONTROL = ImeControl(keymap)

キーに割り当てる

以下、どのアプリ上でも有効になるキーマップを keymap_global として定義しているものとします(公式サンプルのママです)。
ここでは無変換キーを U0 、変換キーを U1 としてカスタム修飾キーにしています。

# 変換+でSKKかなモード有効化、無変換+FでSKK英数モード、変換+I(もしくはR)で再変換
keymap_global["U1-J"] = IME_CONTROL.enable_skk
keymap_global["U0-F"] = IME_CONTROL.to_skk_latin
keymap_global["U1-I"] = IME_CONTROL.reconvert_with_skk
keymap_global["U1-R"] = IME_CONTROL.reconvert_with_skk

# 変換キー(ここでは仮想キーコード236に事前に割り当てているとする)単押しでabbrevモード
keymap_global["O-(236)"] = IME_CONTROL.abbrev_key

変換キー単押しですぐにabbrevモードに入れるのは個人的にかなり気にいっています。

keyhacはキー入力に関数を割り当てて、入力をトリガーに実行します。
なので割り当て部分で () は記載しません( IME_CONTROL.enable_skk() とすると config.py のリロード時に関数が実行されてしまって場合によってはエラーになる)。

SKKによる入力カスタマイズ(基本編)

SKKのモード制御にVirtualFingerによる文字入力を組み合わせます。

class SimpleSKK:
    def __init__(
        self,
        keymap: Keymap,
        inter_stroke_pause: int = 0,
    ) -> None:
        self._finger = VirtualFinger(keymap, inter_stroke_pause)
        self._control = ImeControl(keymap)

    # かなモードで入力する関数を作る
    def under_kanamode(self, *sequence) -> Callable:
        def _send() -> None:
            self._control.enable_skk()
            self._finger.type_smart(*sequence)

        return _send

    # 英数モードで入力する関数を作る
    def under_latinmode(self, *sequence) -> Callable:
        def _send() -> None:
            self._control.to_skk_latin()
            self._finger.type_smart(*sequence)

        return _send

キーに割り当てる

SIMPLE_SKK = SimpleSKK(keymap)

# 間違って英数モードで入力を始めてしまったときに1文字前を選択してかなモードに入る
keymap_global["U1-B"] = SIMPLE_SKK.under_kanamode("S-Left")

# 間違ってかなモードで入力を始めてしまったときに1文字前を選択して英数モードに入る
keymap_global["LS-U1-B"] = SIMPLE_SKK.under_latinmode("S-Left")

 # 間違ってかなモードで入力を始めてしまったときに1文字前を選択してabbrevモードに入る
keymap_global["U1-N"] = SIMPLE_SKK.under_kanamode("S-Left", ImeControl.abbrev_key)

うっかりモードを意識せずに入力を始めてしまったときのリカバリ用コマンドを作っています。

SKKによる入力カスタマイズ(応用編)

もう少し複雑な処理のためにクラスを分けました。
記号を直接入力するときに、

  1. 英数モードに入る
  2. VirtualFinger.type_smart() でキーストローク・文字を入力
  3. (必要なら)かなモードに戻る

というステップを踏みます。

class SKK:
    def __init__(self, keymap: Keymap, finish_with_kanamode: bool = True) -> None:
        self._base_skk = SimpleSKK(keymap)

        # 最終的にかなモードに戻る場合はTrueとする
        self._finish_with_kanamode = finish_with_kanamode

    # 記号を入力する関数を作る
    def send(self, *sequence) -> Callable:
        if self._finish_with_kanamode:
            sequence = list(sequence) + [ImeControl.kana_key]
        return self._base_skk.under_latinmode(*sequence)

    # 括弧類など、ペアになっている記号を入力する関数を作る
    def send_pair(self, pair: list) -> Callable:
        _, suffix = pair
        sequence = pair + ["Left"] * len(suffix)
        if self._finish_with_kanamode:
            sequence = sequence + [ImeControl.kana_key]
        return self._base_skk.under_latinmode(*sequence)

    # キー割り当て用
    def apply(self, km: WindowKeymap, mapping_dict: dict) -> None:
        for key, sent in mapping_dict.items():
            km[key] = self.send(sent)

    # キー割り当て用(ペア)
    def apply_pair(self, km: WindowKeymap, mapping_dict: dict) -> None:
        for key, sent in mapping_dict.items():
            km[key] = self.send_pair(sent)

キーに割り当てる

SKK_TO_KANAMODE = SKK(keymap, True)
SKK_TO_LATINMODE = SKK(keymap, False)


# markdown リスト類の入力
keymap_global["S-U0-8"] = SKK_TO_KANAMODE.send("- ")
keymap_global["U1-1"] = SKK_TO_KANAMODE.send("1. ")

# 特定のキー入力で約物を入力してかなモードに戻る
SKK_TO_KANAMODE.apply(
    keymap_global,
    {
        "S-U0-Colon": "\uff1a",  # FULLWIDTH COLON
        "S-U0-Comma": "\uff0c",  # FULLWIDTH COMMA
        "S-U0-Minus": "\u3000\u2015\u2015",
        "S-U0-Period": "\uff0e",  # FULLWIDTH FULL STOP
        "S-U0-U": "S-BackSlash",
        "U0-Minus": "\u2015\u2015",  # HORIZONTAL BAR * 2
        "U0-P": "\u30fb",  # KATAKANA MIDDLE DOT
        "S-U0-SemiColon": "+ ",
    },
)

# 特定のキー入力で約物(ペア)を入力してかなモードに戻る
SKK_TO_KANAMODE.apply_pair(
    keymap_global,
    {
        "U0-8": ["\u300e", "\u300f"],  # WHITE CORNER BRACKET 『』
        "U0-9": ["\u3010", "\u3011"],  # BLACK LENTICULAR BRACKET 【】
        "U0-OpenBracket": ["\u300c", "\u300d"],  # CORNER BRACKET 「」
        "U1-2": ["\u201c", "\u201d"],  # DOUBLE QUOTATION MARK “”
        "U1-7": ["\u2018", "\u2019"],  # SINGLE QUOTATION MARK ‘’
        "U0-T": ["\u3014", "\u3015"],  # TORTOISE SHELL BRACKET 〔〕
        "U1-8": ["\uff08", "\uff09"],  # FULLWIDTH PARENTHESIS ()
        "U1-OpenBracket": ["\uff3b", "\uff3d"],  # FULLWIDTH SQUARE BRACKET []
    },
)

# 〃英数モードに戻る
SKK_TO_LATINMODE.apply(
    keymap_global,
    {
        "U0-1": "S-1",
        "U0-4": "$_",
        "U0-Colon": "Colon",
        "U0-Comma": "Comma",
        "U0-Period": "Period",
        "U0-Slash": "Slash",
        "U1-Minus": "Minus",
        "U0-SemiColon": "SemiColon",
        "U1-SemiColon": "+:",
    },
)
SKK_TO_LATINMODE.apply_pair(
    keymap_global,
    {
        "U0-CloseBracket": ["[", "]"],
        "U1-9": ["(", ")"],
        "U1-CloseBracket": ["{", "}"],
        "U0-Caret": ["~~", "~~"],
    },
)

(一部は修行で補える部分も大きいですが)かなり快適になりました。もうこの環境から離れられない……!

Discussion