Fakeymacs無きKeyhac2のconfig.pyの設定の例

日常的なファイルのコピペもCtrl+Y でやってしまうEmacsに手癖をカスタマイズされた人間としては、keyhac + Fakeymacs は非常に便利なツールでした。新しいPCをセットアップする際に改めてkeyhacのインストールをしようとすれば、なんとKeyhac2がリリースされていて、Microsoft Storeアプリとしてインストールできるようになったとな。使う側としては安心&アップデートにも便利。

https://crftwr.github.io/keyhac

そうなると、設定ファイルのFakeymacs ですが、こちらはまだKeyhac2には対応していない。というかAPIが変わっちゃったので、おそらくは別に作り直す必要が出てきそう。

https://github.com/smzht/fakeymacs

無いものは作るしかないので、とりあえず必要最低限の分を作ってみたので共有します。細かい使い心地の部分については”Fakeymacs-style key bindings”以下の部分をよしなに調整してみてください。
Winでしか試していないのでMacで変だったらゴメンです。


クリップボード履歴&定型文については、Alt+Y にバインドしてます。アプリランチャーはAlt+l に割り当ててますが、適当に変えて使ってください。クリップボード履歴のUIはkeihac2で結構変わってますので、まあ何となく近い操作感になればいいやくらいにしてます。

リージョン選択については、まだ未実装です。そのうち余裕ができたらやります。

困ったポイントとしては、Win+Ctrl+左右キーで仮想デスクトップの切り替えを行っていたのですが、こいつが動かなくなってしまい、結局、Winキーを"LUser0″に割り当てるのをやめました。LEADERは個別に設定するようにしたので問題無いと思いますが、"LUser0/User0″を使って設定を入れている人はご注意ください。たぶん動かなくなるか、挙動がおかしくなると思います。

"""Keyhac 2 configuration file.

This file is copied to ~/.keyhac/config.py on first run.  Edit that copy —
Keyhac reloads it from the tray menu or the console's hook toggle.

The same file runs on Windows and macOS.  Where the OSes genuinely differ,
branch on `keymap.platform`; the two constants set up at the top (LEADER and
MOD) absorb most of it, so the samples below rarely have to.

Everything here is a working example, not pseudo-code.  Delete what you do
not want.
"""

import json

from keyhac import *
from keyhac.core.keymap import Keymap
from keyhac.core.const import MODKEY_SHIFT
from keyhac.core.const import MODKEY_ALT
from keyhac.core.const import MODKEY_CTRL
from keyhac.core.const import MODKEY_WIN
from keyhac.core.const import MODKEY_CMD
from keyhac.core.const import MODKEY_FN
from keyhac.ui.chooser import ChooserWindow
from puikit.event import EventType

logger = getLogger("Config")

_EVENT_MODKEYS = {
    "shift": MODKEY_SHIFT,
    "ctrl": MODKEY_CTRL,
    "alt": MODKEY_ALT,
    "win": MODKEY_WIN,
    "cmd": MODKEY_CMD,
}


def configure(keymap):

    mac = keymap.platform == "mac"

    # ==================================================================
    # Setup
    # ==================================================================

    # --- user modifier -------------------------------------------------
    # Turn a key into User0, a modifier of your own that no application
    # sees.  User0-User3 are available; a key used this way is never
    # emitted, so it loses its original meaning while defined.
    if mac:
        # The right Option key; it stops acting as Option.
        keymap.define_modifier("RAlt", "RUser0")
    else:
        # The left Windows key; the Start menu no longer opens on a tap
        # (bind kt["O-LWin"] = "LWin" below if you want that back).
        # keymap.define_modifier("LWin", "LUser0")  # 仮想デスクトップ切り替えがうまくいかないので、Winキーを開放
        pass
    
    # --- the two portability constants ---------------------------------
    # LEADER: the modifier most samples below hang off.
    #   macOS   - the Fn key, which Windows does not expose to software
    #   Windows - User0, i.e. the left Windows key defined just above
    # LEADER = "Fn" if mac else "User0"  # 上記の変更に伴い、"User0"
    LEADER = "Fn" if mac else "LWin"

    # MOD: the OS's primary shortcut modifier, so one binding can mean
    # "Cmd-C" on macOS and "Ctrl-C" on Windows.
    MOD = "Cmd" if mac else "Ctrl"

    # --- swap a key entirely (uncomment to try) ------------------------
    # replace_key runs before any key table, so the rest of the config
    # only ever sees the replacement.
    # keymap.replace_key("CapsLock", "LCtrl")

    # --- text editor for "Edit Config" ---------------------------------
    # The tray menu's "Edit Config" opens this file.  Unset, a default is
    # picked (VS Code / Xcode / TextEdit on macOS, Notepad on Windows).
    # Name an editor application, or set a callable taking the path:
    # keymap.editor = "CotEditor" if mac else "notepad.exe"

    # --- clipboard history ---------------------------------------------
    keymap.clipboard_history.max_items = 500
    keymap.clipboard_history.max_data_size = 10 * 1024 * 1024

    # ==================================================================
    # Global key table (active everywhere)
    # ==================================================================

    kt = keymap.define_keytable(focus_path_pattern="*")

    # ==================================================================
    # Fakeymacs-style key bindings
    # ==================================================================

    # Basic Emacs cursor movement.
    kt["Ctrl-B"] = "Left"
    kt["Ctrl-F"] = "Right"
    kt["Ctrl-P"] = "Up"
    kt["Ctrl-N"] = "Down"
    kt["Ctrl-A"] = "Home"
    kt["Ctrl-E"] = "End"

    # Word movement.
    kt["Alt-B"] = "Ctrl-Left"
    kt["Alt-F"] = "Ctrl-Right"

    # Page movement.
    kt["Ctrl-V"] = "PageDown"
    kt["Alt-V"] = "PageUp"

    # Delete / kill.
    kt["Ctrl-D"] = "Delete"
    kt["Alt-D"] = "Ctrl-Delete"
    kt["Alt-Back"] = "Ctrl-Backspace"
    kt["Ctrl-K"] = "Shift-End", "Ctrl-C", "Delete"
    kt["Ctrl-H"] = "Back"

    # Clipboard.
    kt["Ctrl-W"] = "Ctrl-X"
    kt["Alt-W"] = "Ctrl-C"
    kt["Ctrl-Y"] = "Ctrl-V"

    # Newline / undo / cancel.
    kt["Ctrl-M"] = "Enter"
    kt["Ctrl-J"] = "Enter"
    kt["Ctrl-O"] = "Enter", "Left"
    kt["Ctrl-Slash"] = "Ctrl-Z"
    kt["Ctrl-G"] = "Esc"

    # Search.
    kt["Ctrl-S"] = "Ctrl-F"
    kt["Ctrl-R"] = "Ctrl-Shift-F3"

    # --------------------------------------------------------------
    # Fakeymacs-like C-x prefix
    # --------------------------------------------------------------
    kt_x_emacs = keymap.define_keytable(name="Emacs-C-X")
    kt["Ctrl-X"] = kt_x_emacs

    kt_x_emacs["H"] = "Ctrl-Home", "Ctrl-A"   # mark whole buffer
    kt_x_emacs["Ctrl-F"] = "Ctrl-O"           # open
    kt_x_emacs["Ctrl-S"] = "Ctrl-S"           # save
    kt_x_emacs["K"] = "Ctrl-F4"               # close tab/window
    kt_x_emacs["U"] = "Ctrl-Z"                # undo
    kt_x_emacs["Ctrl-C"] = "Alt-F4"           # quit

    # --------------------------------------------------------------
    # Clipboard history + categorized preset snippets
    #
    # Alt-Y opens a custom chooser.  Left/Right switches categories,
    # Up/Down selects an item, Enter pastes it, and Shift-Enter only
    # copies it to the clipboard.
    #
    # The categories below are intentionally easy to customize.
    # Each category is: (icon, label, text/callable).
    # --------------------------------------------------------------
    clipboard_categories = [
        ("📋 履歴", None),       # populated dynamically
        ("📧 メール", [
            ("📧", "メールアドレス", "me@example.com"),
            ("✍", "署名", "よろしくお願いいたします。"),
        ]),
        ("💻 開発", [
            ("🔗", "GitHub", "https://github.com/"),
            ("🐍", "Python", "```python\n\n```"),
            ("📝", "Markdown code block", "```\n\n```"),
        ]),
        ("🕒 日時", [
            ("📅", "YYYY-MM-DD", DateTimeSnippet("%Y-%m-%d")),
            ("🕒", "YYYY-MM-DD HH:MM:SS",
             DateTimeSnippet("%Y-%m-%d %H:%M:%S")),
            ("📁", "YYYYMMDD_HHMMSS",
             DateTimeSnippet("%Y%m%d_%H%M%S")),
        ]),
    ]

    class CategorizedChooserWindow(ChooserWindow):
        """ChooserWindow with Left/Right category switching."""

        def __init__(self, backend, categories, *args, **kwargs):
            self._categories = categories
            self._category_index = 0
            self._category_titles = [c[0] for c in categories]
            self._filter_text = ""
            super().__init__(backend, self._category_items(), *args, **kwargs)

        def _category_items(self):
            title, items = self._categories[self._category_index]
            return list(items)

        def _category_header(self):
            n = len(self._categories)
            title = self._category_titles[self._category_index]
            return f"← {title} →   ({self._category_index + 1}/{n})"

        def _labels(self):
            # Put the category indicator at the top of the chooser.
            return [self._category_header()] + [
                f"{item[0]} {item[1]}" if item[0] else item[1]
                for item in self._filtered
            ]

        def _on_filter_change(self, text: str) -> None:
            # Category header is not part of filtering.
            self._filter_text = text
            words = [w for w in text.lower().split() if w]
            items = self._category_items()
            self._filtered = [
                item for item in items
                if all(w in item[1].lower() for w in words)
            ]
            self._list.set_items(self._labels())
            self._list.selected = 1 if self._filtered else 0
            self.panel.render()

        def _switch_category(self, delta):
            self._category_index = (
                self._category_index + delta
            ) % len(self._categories)

            # Re-apply the current filter to the new category.  This
            # avoids relying on undocumented TextEdit methods.
            self._on_filter_change(self._filter_text)

        def _on_event(self, event) -> None:
            if event.type is EventType.KEY:
                if event.key == "left":
                    self._switch_category(-1)
                    return

                if event.key == "right":
                    self._switch_category(+1)
                    return

                # The first row is the category header.  Skip over it
                # when navigating with Up/Down.
                if event.key in ("up", "down", "pageup", "pagedown"):
                    delta = {
                        "up": -1,
                        "down": 1,
                        "pageup": -10,
                        "pagedown": 10,
                    }[event.key]

                    if self._filtered:
                        selected = self._list.selected - 1
                        selected = max(
                            0,
                            min(len(self._filtered) - 1, selected + delta),
                        )
                        self._list.selected = selected + 1
                        self.panel.render()
                    return

                if event.key == "enter":
                    # Don't select the category header itself.
                    index = self._list.selected - 1
                    if 0 <= index < len(self._filtered):
                        mod = 0
                        for name in event.modifiers:
                            mod |= _EVENT_MODKEYS.get(name, 0)
                        self._finish(self._filtered[index], mod)
                    return

            self.panel.dispatch_event(event)
            self.panel.render()


    class CategorizedClipboardChooser(ChooserAction):
        """Fakeymacs-style clipboard/snippet chooser with categories."""

        _open = None

        def __init__(self, categories):
            self.categories = categories

        def __repr__(self):
            return "CategorizedClipboardChooser()"

        def _make_categories(self):
            categories = []

            for title, items in self.categories:
                if items is None:
                    # Clipboard history: Keyhac 2 returns (text, label).
                    history = Keymap.get_instance().clipboard_history
                    history_items = [
                        ("&#x1f4cb;", label, text)
                        for text, label in history.items()
                    ]
                    categories.append((title, history_items))
                else:
                    categories.append((title, list(items)))

            return categories

        def __call__(self):
            from keyhac.ui import runtime
            from keyhac.ui.chooser import ChooserWindow

            if runtime.backend is None:
                logger.error(
                    "CategorizedClipboardChooser requires the Keyhac UI."
                )
                return

            keymap = Keymap.get_instance()

            # Close an already-open categorized chooser.
            if self._open is not None:
                old_window, old_pid = self._open
                old_window.dismiss()
                self._open = None
                if old_pid is not None and keymap.app_control is not None:
                    keymap.app_control.activate_pid(old_pid)
                return

            focus = keymap.focus
            original_pid = focus.pid if focus else None

            def refocus():
                if original_pid is not None and keymap.app_control is not None:
                    keymap.app_control.activate_pid(original_pid)

            def selected(item, modifier_flags):
                self._open = None
                refocus()

                value = item[2] if len(item) > 2 else item[1]
                if callable(value):
                    value = value()
                if value is None:
                    return

                keymap.clipboard_history.set_current(str(value))

                # Shift-Enter = clipboard only.
                if modifier_flags & MODKEY_SHIFT:
                    return

                # Normal Enter = paste into the original application.
                runtime.backend.call_later(
                    0.15,
                    lambda: self._paste(),
                )

            def canceled():
                self._open = None
                refocus()

            center_on = clamp_to = None
            active = keymap.get_active_window()
            if active is not None:
                center_on = active.get_frame()

            if center_on is not None and keymap.window_provider is not None:
                clamp_to = MoveWindow._get_best_screen(
                    center_on,
                    keymap.window_provider.screen_frames(),
                )

            categories = self._make_categories()

            chooser = CategorizedChooserWindow(
                runtime.backend,
                categories,
                on_selected=selected,
                on_canceled=canceled,
                title="Clipboard / Snippets",
                center_on=center_on,
                clamp_to=clamp_to,
            )

            self._open = (chooser, original_pid)

            if keymap.app_control is not None:
                import os
                keymap.app_control.activate_pid(os.getpid())

        def _paste(self):
            keymap = Keymap.get_instance()
            with keymap.get_input_context() as ctx:
                ctx.send_key(
                    "Cmd-V" if keymap.platform == "mac" else "Ctrl-V"
                )

    kt["Alt-Y"] = CategorizedClipboardChooser(clipboard_categories)

    # --------------------------------------------------------------
    # Application launcher
    #
    # Alt-L opens a selectable application list.
    # Add/remove entries here to match the applications you use.
    # --------------------------------------------------------------
    def show_application_launcher():
        applications = [
            ("&#x1f4dd;", "Notepad", "notepad.exe"),
            ("&#x1f4c1;", "Explorer", "explorer.exe"),
            ("&#x2328;", "Windows Terminal", "wt.exe"),
            ("&#x1f4bb;", "PowerShell", "powershell.exe"),
            ("&#x1f310;", "Microsoft Edge", "msedge.exe"),
            ("&#x1f310;", "Google Chrome", "chrome.exe"),
            ("&#x1f9d1;&#x200d;&#x1f4bb;", "Visual Studio Code", "code"),
        ]

        entries = []
        for icon, name, command in applications:
            def launch(command=command):
                LaunchApplication(command)()
            entries.append((icon, name, launch))

        ShowClipboardSnippets(entries)()

    kt["Alt-L"] = show_application_launcher


    # --- key -> key ----------------------------------------------------
    # IJKL as arrow keys while LEADER is held.
    kt[f"{LEADER}-I"] = "Up"
    kt[f"{LEADER}-J"] = "Left"
    kt[f"{LEADER}-K"] = "Down"
    kt[f"{LEADER}-L"] = "Right"
    kt[f"{LEADER}-U"] = "Home"
    kt[f"{LEADER}-O"] = "End"

    # --- key -> sequence of keys ---------------------------------------
    # Select the whole line: Home, then Shift-End.
    kt[f"{LEADER}-A"] = "Home", "Shift-End"

    # --- one-shot: tap for one key, hold to modify ---------------------
    # A one-shot key held down still works as its modifier; only a lone
    # tap-and-release fires the assignment.  Picked so a stray tap is
    # harmless (an Escape here, say, would cancel dialogs mid-typing).
    if mac:
        # The classic macOS setup: tap Left/Right Cmd alone for Eisu/Kana
        # (IME off/on) - a no-op unless a Japanese input source is
        # installed.  Held, they are still plain Cmd.
        kt["O-LCmd"] = "Eisu"
        kt["O-RCmd"] = "Kana"
    else:
        # Tap right Ctrl alone to open the Start-menu search; held, it is
        # still plain Ctrl.
        kt["O-RCtrl"] = "Win-S"

    # --- key -> your own function --------------------------------------
    def hello():
        # print() and the logger both reach the console window.
        print("Hello from config.py")
        logger.info(f"platform={keymap.platform}")

    kt[f"{LEADER}-H"] = hello

    # --- typing literal text -------------------------------------------
    kt[f"{LEADER}-Semicolon"] = InputText("me@example.com")

    # --- sending keys from your own function ---------------------------
    # get_input_context() batches virtual key input; it is safe to use from
    # a worker thread as well (see the ThreadedAction sample below).
    def wrap_in_quotes():
        with keymap.get_input_context() as ctx:
            ctx.send_key(f"{MOD}-C")

    kt[f"{LEADER}-Q"] = wrap_in_quotes

    # ==================================================================
    # Clipboard
    # ==================================================================

    # --- history, in a chooser window ----------------------------------
    # Enter pastes; Shift-Enter only sets the clipboard.  Type to filter.
    kt[f"{LEADER}-V"] = ShowClipboardHistory()

    # --- fixed snippets -------------------------------------------------
    # (icon, label) | (icon, label, text) | (icon, label, callable)
    kt[f"{LEADER}-Shift-V"] = ShowClipboardSnippets([
        ("&#x1f4e7;", "me@example.com"),
        ("&#x1f4ee;", "Mailing address", "400 Broad St, Seattle, WA 98109"),
        ("&#x1f552;", "Date", DateTimeSnippet("%Y-%m-%d")),
        ("&#x1f552;", "Timestamp", DateTimeSnippet("%Y-%m-%d %H:%M:%S")),
        ("&#x1f552;", "For filenames", DateTimeSnippet("%Y%m%d_%H%M%S")),
    ])

    # --- transform whatever is on the clipboard -------------------------
    # A tool takes the clipboard text and returns the replacement.
    def pretty_json(s):
        try:
            return json.dumps(json.loads(s), indent=4, ensure_ascii=False)




        except json.JSONDecodeError:
            logger.error("Clipboard content is not valid JSON.")
            return s

    kt[f"{LEADER}-Ctrl-V"] = ShowClipboardTools([
        ("&#x1f504;", "Quote", ShowClipboardTools.quote),
        ("&#x1f504;", "Unindent", ShowClipboardTools.unindent),
        ("&#x1f504;", "Upper case", str.upper),
        ("&#x1f504;", "Lower case", str.lower),
        ("&#x1f504;", "Half width", ShowClipboardTools.to_half_width),
        ("&#x1f504;", "Full width", ShowClipboardTools.to_full_width),
        ("&#x1f504;", "Pretty JSON", pretty_json),
    ])

    # ==================================================================
    # Windows and applications
    # ==================================================================

    # --- move the focused window ---------------------------------------
    # Apple keyboards translate Fn-Arrow into Home/End/PageUp/PageDown in
    # hardware (the Fn modifier itself still arrives), so with LEADER = Fn
    # the "...-Left" spellings would never fire on macOS - bind the keys
    # that actually arrive there.  Ctrl/Alt rather than Shift, because
    # Fn-Shift-Arrow is how you *select text* on a Mac laptop (it arrives
    # as Shift-Home etc.) - a Shift binding here would steal it.
    LEFT, RIGHT, UP, DOWN = (("Home", "End", "PageUp", "PageDown") if mac
                             else ("Left", "Right", "Up", "Down"))

    # Nudge by 20 px...
    kt[f"{LEADER}-Ctrl-{LEFT}"] = MoveWindow(direction="left", distance=20)
    kt[f"{LEADER}-Ctrl-{RIGHT}"] = MoveWindow(direction="right", distance=20)
    kt[f"{LEADER}-Ctrl-{UP}"] = MoveWindow(direction="up", distance=20)
    kt[f"{LEADER}-Ctrl-{DOWN}"] = MoveWindow(direction="down", distance=20)

    # ...or send it as far as it goes, stopping at other windows' edges
    # and screen edges (and hopping to the next monitor when already there).
    kt[f"{LEADER}-Alt-{LEFT}"] = MoveWindow(direction="left", distance=9999,
                                            window_edge=True, screen_edge=True)
    kt[f"{LEADER}-Alt-{RIGHT}"] = MoveWindow(direction="right", distance=9999,
                                             window_edge=True, screen_edge=True)
    kt[f"{LEADER}-Alt-{UP}"] = MoveWindow(direction="up", distance=9999,
                                          window_edge=True, screen_edge=True)
    kt[f"{LEADER}-Alt-{DOWN}"] = MoveWindow(direction="down", distance=9999,
                                            window_edge=True, screen_edge=True)

    # --- snap to screen regions (tiling) --------------------------------
    # Resizes to a half of the window's current screen, inside the work
    # area (menu bar, Dock and taskbar stay uncovered).  Same IJKL layout
    # as the arrows above.  ratio= picks a different split, e.g.
    # SnapWindow("left", ratio=2/3).
    kt[f"{LEADER}-Ctrl-J"] = SnapWindow("left")
    kt[f"{LEADER}-Ctrl-L"] = SnapWindow("right")
    kt[f"{LEADER}-Ctrl-I"] = SnapWindow("top")
    kt[f"{LEADER}-Ctrl-K"] = SnapWindow("bottom")
    kt[f"{LEADER}-F"] = SnapWindow("full")

    # --- minimize the focused window -------------------------------------
    def minimize_window():
        window = keymap.get_active_window()
        if window is not None:
            window.minimize()

    kt[f"{LEADER}-M"] = minimize_window

    # --- bring an application forward -----------------------------------
    # Matches like the focus conditions below: wildcards, "|" alternation,
    # case-insensitive, ".exe" optional.
    kt[f"{LEADER}-1"] = ActivateWindow(app="code|Visual Studio Code")
    kt[f"{LEADER}-2"] = ActivateWindow(app="chrome|Google Chrome")

    # --- launch an application ------------------------------------------
    if mac:
        kt[f"{LEADER}-T"] = LaunchApplication("Terminal.app")
    else:
        kt[f"{LEADER}-T"] = LaunchApplication("wt.exe")   # Windows Terminal

    # --- inspect windows yourself ---------------------------------------
    # keymap.get_active_window() / find_window() / list_windows() return
    # portable Window objects: title, app_name, pid, class_name (Windows),
    # get_frame(), set_frame(), activate(), minimize(), is_minimized(),
    # restore().  Screen geometry: keymap.screen_frames() (whole screens),
    # keymap.screen_work_frames() (minus menu bar / Dock / taskbar) and
    # keymap.window_frames().  Window objects and screen_work_frames() are
    # UI-thread only - never touch them from a ThreadedAction.run(); the
    # thread-safe pair there is screen_frames() / window_frames().
    def describe_window():
        window = keymap.get_active_window()
        if window is None:
            logger.warning("No active window.")
            return
        x, y, w, h = window.get_frame()
        logger.info(f"{window.app_name}: \"{window.title}\" "
                    f"at ({x:.0f},{y:.0f}) {w:.0f}x{h:.0f}")
        logger.info(f"{len(keymap.list_windows())} windows open on "
                    f"{len(keymap.screen_frames())} screen(s)")

    kt[f"{LEADER}-W"] = describe_window

    # --- activate, or launch if it is not running ------------------------
    def activate_or_launch_editor():
        window = keymap.find_window(app="code|Visual Studio Code")
        if window:
            window.activate()
        else:
            LaunchApplication("Visual Studio Code.app" if mac else "code")()

    kt[f"{LEADER}-E"] = activate_or_launch_editor

    # ==================================================================
    # Keyboard macros
    # ==================================================================

    kt[f"{LEADER}-OpenBracket"] = ToggleRecordingKeys()    # record on/off
    kt[f"{LEADER}-CloseBracket"] = PlaybackRecordedKeys()  # replay
    # StartRecordingKeys() / StopRecordingKeys() exist too, if you would
    # rather have separate keys than a toggle.

    # ==================================================================
    # Background work: ThreadedAction
    # ==================================================================

    # Anything slow (network, subprocess, sleeping) must not run inline -
    # it would block the keyboard hook.  ThreadedAction gives you a worker
    # thread; starting() and finished() run under the engine lock, run()
    # does not.
    class TypeSlowly(ThreadedAction):
        def __init__(self, text):
            self.text = text

        def starting(self):
            logger.info(f"Typing {self.text!r}...")

        def run(self):
            import time
            for char in self.text:
                time.sleep(0.05)
                with keymap.get_input_context() as ctx:
                    ctx.send_key(f"Shift-{char}" if char.isupper() else char)
            return len(self.text)

        def finished(self, result):
            logger.info(f"Typed {result} characters.")

        def __repr__(self):
            return f"TypeSlowly({self.text!r})"

    kt[f"{LEADER}-Y"] = TypeSlowly("keyhac")

    # ==================================================================
    # Multi-stroke key tables
    # ==================================================================

    # Press LEADER-X, then a second key.  A balloon shows the table's name
    # while it is armed.
    kt_x = keymap.define_keytable(name="LEADER-X")
    kt[f"{LEADER}-X"] = kt_x
    kt_x["C"] = f"{MOD}-C"
    kt_x["V"] = f"{MOD}-V"
    kt_x["S"] = f"{MOD}-S"

    # --- balloon messages of your own ------------------------------------
    def show_balloon():
        # Absent when running with --no-ui, so ask before using it.
        pop = getattr(keymap, "pop_balloon", None)
        if pop:
            pop("hello", "Keyhac is running", 2.0)

    kt[f"{LEADER}-B"] = show_balloon

    # ==================================================================
    # Application-specific key tables
    # ==================================================================
    # Tables are merged in definition order and later ones win, so anything
    # below overrides the global table for the apps it matches.

    # --- by application name (portable) ----------------------------------
    kt_browser = keymap.define_keytable(app="chrome|Google Chrome|firefox|Safari")
    kt_browser[f"{LEADER}-R"] = f"{MOD}-R"          # reload

    # --- by window title -------------------------------------------------
    # kt_docs = keymap.define_keytable(title="*Google Docs*")

    # --- by Win32 window class (Windows only) -----------------------------
    if not mac:
        kt_notepad = keymap.define_keytable(app="notepad", class_name="Edit")
        kt_notepad[f"{LEADER}-D"] = "Home", "Shift-Down", "Shift-End", "Delete"

    # --- by focus path ----------------------------------------------------
    # The focus path is the control hierarchy down to the focused element -
    # the AX tree on macOS, the UI Automation tree on Windows.  Watch the
    # console's "Focus path" field to see the live value, and use "*" freely
    # to skip levels.
    #   macOS   : /AXApplication(Xcode)/AXWindow(...)/.../AXTextArea()
    #   Windows : /Application(Code)/Window(...)/.../Edit(Message input)
    # Note the trailing "(*)": a component is "Role(Name)", and many controls
    # do carry a name, so "*/Edit()" would only match unnamed ones.
    kt_textarea = keymap.define_keytable(
        focus_path_pattern="*/AXTextArea(*)" if mac else "*/Edit(*)")
    kt_textarea[f"{LEADER}-Slash"] = InputText("# ")

    # --- by your own test -------------------------------------------------
    # custom_condition_func receives the portable Focus object: app_name,
    # window_title, class_name (Windows), path, and element - the focused
    # element in the OS's own vocabulary.
    def is_terminal(focus):
        if focus.app_name in ("Terminal", "iTerm2", "WindowsTerminal", "cmd",
                              "powershell", "pwsh"):
            return True
        # Element attributes differ per OS: AX names on macOS, UI Automation
        # names on Windows.  An unknown name simply reads back as None.
        element = focus.element
        if element is None:
            return False
        role = element.get_attribute_value("AXRole" if mac else "ControlType")
        return role in ("AXTextArea", "Document")

    kt_terminal = keymap.define_keytable(custom_condition_func=is_terminal)
    kt_terminal[f"{LEADER}-K"] = "Ctrl-K"     # clear, rather than "Down"

仮想デスクトップ切り替えに関しては、結構ハマったところで、元の設定ファイルには既存の割り当てがあったので、コメントアウトし、改めて定義してみたのですが動いてくれないんですよね。以下の例以外にもいろいろ試したのですが、結局ダメだったので冒頭に記載したように、Winキーを"LUser0″への割り当てをやめる方法でお茶を濁しています。何かいい感じのやり方を見つけたら、改めて更新します。

    # -------------------------------------------------------------
    # 既存のウィンドウ20px移動設定をコメントアウト
    # -------------------------------------------------------------
    # kt[f"{LEADER}-Ctrl-{LEFT}"] = MoveWindow(direction="left", distance=20)
    # kt[f"{LEADER}-Ctrl-{RIGHT}"] = MoveWindow(direction="right", distance=20)
    kt[f"{LEADER}-Ctrl-{UP}"] = MoveWindow(direction="up", distance=20)
    kt[f"{LEADER}-Ctrl-{DOWN}"] = MoveWindow(direction="down", distance=20)

    # -------------------------------------------------------------
    # 仮想デスクトップ切り替えを設定 → ダメだった・・・
    # -------------------------------------------------------------
    # Win + Ctrl + 左右キー で仮想デスクトップ切り替えを行う場合
    kt[f"{LEADER}-Ctrl-Left"] = "Win-Ctrl-Left"
    kt[f"{LEADER}-Ctrl-Right"] = "Win-Ctrl-Right"

技術・開発emacs,Windows

Posted by tomi