← Keyhac crftwr/keyhac on GitHub · craftware

API reference

Every class and function a config.py can reach, generated from the docstrings. It answers “what are the arguments of X”; for “how do I do Y”, read Configuration first — it introduces these APIs in the order you meet them, with worked examples.

Contents: Keymap · KeyTable · KeyCondition · FocusCondition · InputContext · Focus · KeyEvent · Window · ThreadedAction · InputText · LaunchApplication · ActivateWindow · MoveWindow · SnapWindow · MouseMove · MouseButtonDown · MouseButtonUp · MouseButtonClick · MouseWheel · MouseHorizontalWheel · StartRecordingKeys · StopRecordingKeys · ToggleRecordingKeys · PlaybackRecordedKeys · ClipboardHistory · ChooserAction · ShowCandidates · Candidate · CandidateSource · CallableSource · Scope · ActionsSource · ClipboardHistorySource · KeyBindingsSource · MenuItemsSource · WindowControlsSource · SnippetsSource · ClipboardToolsSource · ShowClipboardHistory · ShowClipboardSnippets · ShowClipboardTools · DateTimeSnippet · getLogger · Console

class Keymap

Manages key tables and executes key action translations.

One Keymap exists per Keyhac process. The configuration file receives it as configure(keymap)’s argument; code outside configure() reaches the same object through Keymap.get_instance().

Attributes:


property Keymap.clipboard

The OS clipboard - get_text() / set_text(), or None if unwired.

The history’s provider, exposed directly because actions that paste need to read and restore the clipboard around what they do, which is not a history operation.


property Keymap.clipboard_history

The ClipboardHistory object (None while running without one, e.g. under –no-ui).


property Keymap.config_path

The configuration script this run loads.

lazydocs: ignore


property Keymap.extensions_dir

extensions/ beside config.py: on sys.path, and re-imported on every reload.

lazydocs: ignore


property Keymap.focus

Portable snapshot of the current keyboard focus (a Focus), or None before the first key event.


property Keymap.mcp_server_running

Whether the endpoint is currently listening.

lazydocs: ignore


property Keymap.ui

The action-facing UI API - see doc/action-api.md.

Reading and driving another application’s elements: finding windows, searching trees, waiting for the screen to change, filling fields. Deliberately a separate namespace from the configuration API, and deliberately method-style, so from keyhac import * does not acquire a dozen generic verbs that only mean something inside an action.


method Keymap.call_on_main_thread

call_on_main_thread(callback)  None

Run a callback on the thread that owns the event loop.

Thread-safe, and the supported way for a worker thread to reach anything main-thread-only: UI, window moves, AX writes. ThreadedAction.run() is the usual caller; finished() already arrives here, so it needs this only for work it defers further.

Args:

Note:

With no loop wired - Keyhac used as a library, or under test - the callback runs inline on the calling thread, which is what the code did everywhere before a dispatcher existed.


method Keymap.configure

configure()  None

Load (or reload) the configuration file and rebuild the keymap.

A configuration that fails to load leaves the previous keymap active and reports the traceback to the console.


method Keymap.define_keytable

define_keytable(
    name: str = None,
    focus_path_pattern: str = None,
    custom_condition_func: Callable[[keyhac.platform.base.Focus], bool] = None,
    app: str = None,
    title: str = None,
    class_name: str = None
)  KeyTable

Define a key table.

With any focus condition (focus_path_pattern / app / title / class_name / custom_condition_func) the table is added to the keymap and activates automatically whenever the condition is met. Every matching table is active at once, merged in definition order, so a table defined later overrides exactly the keys it binds.

With no condition the table is not added to the keymap: assign it to a key to make that key a multi-stroke prefix.

Args:

Returns: The KeyTable created.

Note:

app, title and class_name patterns are case-insensitive, take fnmatch wildcards (*, ?, []) and “ ” alternation, and all the conditions given must match.

method Keymap.define_modifier

define_modifier(key: str | int, mod: str | int)  None

Define a user modifier key.

While defined, the key loses its original meaning entirely: a User0..User3 modifier is never emitted, so assignments hanging off it cannot collide with anything an application understands.

A Windows key cannot be one, and the call is refused with an error in the log. Defining it does not take the key away from the OS: Keyhac consumes the key-down, so no application ever receives it and the Start menu stays shut, but anything watching the keyboard ahead of Keyhac still sees the physical key held - the Xbox Game Bar opens on Win+G either way, and it swallows that keystroke, including one Keyhac itself injected. A modifier that is invisible to applications but not to the shell is not what this promises, so it is not offered.

Any other key may be redefined, including one that already is a modifier - define_modifier("RAlt", "RUser0") works - but prefer a key that is not one: the key stops being Alt (or Ctrl, or Shift) for everything, everywhere, and that is a large thing to give up by accident. Redefining a modifier is noted in the log.

Args:


method Keymap.edit_config

edit_config()  None

Open the configuration file in a text editor.

keymap.editor chooses the editor: an application name or path the OS can resolve, or a callable receiving the config path. Left empty, a platform default is used (Visual Studio Code / Xcode / TextEdit on macOS, Notepad on Windows). The tray menu’s “Edit Config” item calls this.


method Keymap.find_window

find_window(app: str = None, title: str = None, class_name: str = None)

Find the first visible window matching the given patterns.

Matching is exactly define_keytable’s: case-insensitive, fnmatch wildcards, “ ” alternation, “.exe” optional, and all the conditions given must match.

Args:

Returns: A Window, or None when nothing matches.

Note:

UI-thread only.


method Keymap.get_active_window

get_active_window()

Get the frontmost window.

Returns: A Window, or None when there is none (or the platform has no window support).

Note:

UI-thread only, like everything on Window - never call it from a ThreadedAction.run().


method Keymap.get_ime_status

get_ime_status()  bool | None

Get whether the IME is on for whatever holds the input focus.

There is no window argument on purpose: macOS can only ever address the current input source, so naming a window would mean two different contracts on the two OSes.

Returns: True when the IME is on, False when it is off, or None when the state cannot be determined - no IME is installed or reachable, or (Windows) a TSF-only IME does not answer the IMM32 query.

Note:

UI-thread only. “Off” is the same answer for two different situations, on both OSes: an IME that is installed and closed, and no IME in the picture at all - a plain keyboard layout on Windows, a plain layout or an input method’s Roman mode on macOS.


method Keymap.get_input_context

get_input_context(replay: bool = False)  InputContext

Get a key input context to send a batch of virtual key events.

with keymap.get_input_context() as ctx:
     ctx.send_key("Ctrl-C")

Args:

Returns: An InputContext, to be used as a context manager.


method Keymap.get_instance

get_instance()  Keymap

Get the Keymap singleton.

Returns: The Keymap instance, or None before one has been created.


method Keymap.list_windows

list_windows()  list

List the visible top-level windows.

Returns: Window objects, front-most first where the OS says so.

Note:

UI-thread only.


method Keymap.reload_config

reload_config()  None

Reload the configuration file.

The keyhac-win name for configure(), kept because configurations and documentation refer to it. The tray menu’s “Reload Config” item calls this.


method Keymap.replace_key

replace_key(src: str | int, dst: str | int)  None

Replace a key with a different key.

The substitution runs before everything else, so the rest of the configuration only ever sees dst.

Args:


method Keymap.screen_frames

screen_frames()  list

Get the frame of every screen.

Returns: One (x, y, w, h) tuple per screen, primary first, in the shared top-left-origin coordinate space.

Note:

Thread-safe - callable from a ThreadedAction.run().


method Keymap.screen_work_frames

screen_work_frames()  list

Get the work area of every screen.

Returns: screen_frames() minus the menu bar and Dock (macOS) or the taskbar (Windows), in the same order.

Note:

UI-thread only - the macOS implementation is an AppKit query.


method Keymap.set_ime_status

set_ime_status(on: bool)  bool

Turn the IME on or off for whatever holds the input focus.

Args:

Returns: Whether the requested state was actually reached - the result is read back rather than assumed, so False means the IME declined or there was none to ask.

Note:

UI-thread only, and it takes effect at once - unlike key output, which InputContext only queues for the application. Wrapping a send_key batch in “off … back on” therefore does not work: the restore lands before the keys do and they are composed anyway. Use InputContext.send_text for literal text, which the IME does not intercept. The two OSes differ in how far “on” reaches: macOS selects a Japanese input source even from a US layout, while Windows only opens an IME that the focused window is already typing under - asking for “on” while a plain layout like en-US is active returns False rather than switching the input language, which is the user’s own Win+Space to give. Whether a change also affects other applications is the user’s OS setting (“Let me use a different input method for each app window” on Windows, “Automatically switch to a document’s input source” on macOS), not something Keyhac decides.


method Keymap.window_frames

window_frames()  list

Get the frames of all normal on-screen windows.

Returns: One (x, y, w, h) tuple per window, in the same coordinate space as screen_frames().

Note:

Thread-safe - callable from a ThreadedAction.run(). It is the geometry query to use there, since Window itself is not.


class KeyTable

Dict-like table assigning input key conditions to output actions.

Subscript it with a key expression to bind a key. Values may be:

keymap.define_keytable() creates them.

kt["Fn-J"] = "Left"                  # key -> key
kt["Fn-N"] = "Cmd-1", "Cmd-2"        # key -> sequence
kt["Fn-A"] = some_callable           # key -> function / action object
kt["Ctrl-X"] = kt_ctrlx              # key -> multi-stroke table

Attributes:


class KeyCondition

A single key stroke condition - the parsed form of a key expression.

Assigning to a key table parses the expression for you, so configurations rarely build one directly; KeyCondition.from_str() is the way in when they do.

Attributes:


method KeyCondition.from_str

from_str(s: str)  KeyCondition

Parse a key expression.

Args:

Returns: The KeyCondition it describes.

Raises:


class FocusCondition

Condition deciding whether a key table is active for the current focus.

keymap.define_keytable() builds one from the focus arguments it is given, so configurations do not normally construct it themselves.

All specified conditions must match (AND). Within app/title/ class_name patterns, “ ” separates alternatives (OR) and fnmatch wildcards (*, ?, []) are available.

method FocusCondition.__init__

__init__(
    focus_path_pattern: str = None,
    custom_condition_func: Callable[[keyhac.platform.base.Focus], bool] = None,
    app: str = None,
    title: str = None,
    class_name: str = None
)

Build a focus condition.

Args:


class InputContext

A context manager to send virtual key strokes.

Key events are accumulated and sent as one batch when the context exits. Physically held modifiers are released around the batch and restored afterwards, so ctx.send_key("Ctrl-C") works even while the modifiers of the binding that triggered it are still down.

Sending is where the batch ends, not where it arrives: the events are queued for the application to pick up later. So anything the action does after the context exits - changing the IME state, activating another window - takes effect while the keys are still in flight, and lands first. keymap.set_ime_status(False) around a send_key batch is the trap this makes: the matching restore wins the race and the keys are composed by the IME after all. Send literal text with send_text(), which the IME does not intercept, and leave IME changes standing rather than undoing them in the same action.

keymap.get_input_context() creates one. It is safe to use from a ThreadedAction worker thread.

with keymap.get_input_context() as ctx:
     ctx.send_key("Cmd-Left")
     ctx.send_key("Cmd-Shift-Right")

method InputContext.send_key

send_key(s: str)  None

Send a key stroke from a key expression.

Args:

Raises:


method InputContext.send_key_by_vk

send_key_by_vk(vk: int, down: bool = True)  None

Send a key stroke by virtual key code.

Args:

Raises:


method InputContext.send_mouse_button

send_mouse_button(button: str = 'left', down: bool | None = None)  None

Press, release or click a mouse button.

Held modifiers are released first and restored when the context exits, so a modifier-bound click does not turn into a modified click (keyhac-win behavior).

Args:

Raises:


method InputContext.send_mouse_horizontal_wheel

send_mouse_horizontal_wheel(notches: float)  None

Turn the horizontal mouse wheel.

Args:

Raises:


method InputContext.send_mouse_move

send_mouse_move(dx: int, dy: int)  None

Move the mouse cursor by a relative offset.

Injected as an absolute position, so pointer acceleration cannot distort the distance. Unlike buttons and wheels, held modifiers stay held (keyhac-win behavior).

Args:

Raises:


method InputContext.send_mouse_wheel

send_mouse_wheel(notches: float)  None

Turn the vertical mouse wheel.

Held modifiers are released first, like send_mouse_button.

Args:

Raises:


method InputContext.send_text

send_text(s: str)  None

Type a literal string, whatever characters it holds.

Like an unmodified send_key, held modifiers are released first (and restored when the context exits) - otherwise e.g. a physically held Fn turns the injected keystrokes into macOS system shortcuts (Fn/Globe-A opens the Dock).

Args:

Raises:


class Focus

Portable snapshot of the current keyboard focus.

Available as keymap.focus, and passed to every custom_condition_func.

Attributes:


class KeyEvent

A normalized key event delivered by the OS hook.

Attributes:


class Window

A top-level OS window.

The portable half of keyhac-win’s pyauto.Window and keyhac-mac’s AXWindow element: window operations unify cleanly across both OSes (find, activate, move, restore, title, process), unlike element introspection, whose attribute vocabularies do not - see Focus.element.

keymap.get_active_window(), list_windows() and find_window() hand these out; configurations never construct one.

Note:

Everything on this class is UI-thread only. On macOS these are Accessibility calls, and AX into our own process off the main thread crashes with SIGTRAP. A ThreadedAction therefore reads windows in starting(), computes in run(), and writes back in finished(); the thread-safe queries a run() may call are keymap.screen_frames() and keymap.window_frames().


property Window.app_name

Process base name without extension (Windows) / localized application name (macOS).


property Window.class_name

Win32 window class. None on macOS, which has no such concept.


property Window.element

This window as an element, for searching inside it.

The bridge from window operations to element introspection: an action finds a window portably (keymap.find_window) and then has to look into it, which until now meant reaching for a platform-specific entry point. macOS already holds the AX element; Windows resolves the HWND through UI Automation.


property Window.native

The underlying platform object (HWND wrapper / AX UIElement).


property Window.pid

Process id of the application owning the window.


property Window.title

The window’s title.


method Window.activate

activate()  bool

Bring this window and its application to the front.

Returns: Whether the activation succeeded.


method Window.get_frame

get_frame()  tuple[float, float, float, float] | None

Get the window’s frame.

Returns: (x, y, w, h) in global top-left-origin screen coordinates, or None when the window has no readable frame.


method Window.is_minimized

is_minimized()  bool

Whether the window is currently minimized.


method Window.minimize

minimize()  bool

Minimize the window.

Returns: Whether the window was minimized.


method Window.restore

restore()  bool

Un-minimize the window.

Returns: Whether the window was restored.


method Window.set_frame

set_frame(x: float, y: float, w: float = None, h: float = None)  bool

Move the window, and optionally resize it.

Args:

Returns: Whether the window accepted the change.


class ThreadedAction

Base class for time-consuming key actions.

Anything slow - network, subprocess, sleeping, heavy computation - must not run inline, because a bound function executes inside the keyboard hook’s deadline. Derive from this and implement starting(), run() and finished() instead.

Three threads, and which one you are on decides what you may touch. starting() and finished() run on the event-loop thread under the engine lock: main-thread-only APIs (UI, windows, AX) are allowed there, and they should stay light-weight because they hold the lock the keyboard hook needs. run() executes on a worker, where input contexts are allowed but windows and AX elements are not.

Actions run concurrently, so a run() that takes minutes no longer holds up every other one. What is still serialized is what has to be: injected keystrokes (one with ctx: batch at a time) and the clipboard save and restore around a paste.

The user can stop a running action with Esc, and an action needs to write nothing for that: wait_for raises ActionCancelled, and a long action spends nearly all its time waiting. Use check_cancelled() in a stretch of work that has no wait in it.

class Fetch(ThreadedAction):
     def starting(self):          # main thread, before run
         logger.info("fetching...")
     def run(self):               # worker thread - the slow part
         return do_network_call()
     def finished(self, result):  # main thread, after run
         logger.info(f"got {result}")

property ThreadedAction.keymap

The running Keymap, so an action need not import and look it up.


property ThreadedAction.ui

The action-facing UI API (keymap.ui) - see doc/action-api.md.

An action’s most-used object, so it is one attribute away rather than two lines of lookup at the top of every run().


method ThreadedAction.cancelled

cancelled()  bool

True once the user has asked this action to stop.

Check it in a loop that does not wait - wait_for already raises ActionCancelled on its own, and a loop built out of waits needs nothing else.


method ThreadedAction.check_cancelled

check_cancelled()  None

Raise ActionCancelled if the user has asked this action to stop.

For a stretch of work with no wait in it - a long parse, a big write - where cancellation would otherwise not be noticed until the next wait.


method ThreadedAction.finished

finished(result: Any)  None

Called on the event-loop thread once run() has returned.

Main-thread-only APIs are allowed here too.

Args:


method ThreadedAction.run

run()  Any

Called in the thread pool; may block.

Returns: Anything; it is handed to finished().


method ThreadedAction.starting

starting()  None

Called on the event-loop thread the moment the action triggers.

Main-thread-only APIs (UI, windows, AX) are allowed here; it runs under the engine lock, so keep it light.


class InputText

Type a literal string into the focused application.

method InputText.__init__

__init__(text: str)

Build the action.

Args:


class LaunchApplication

Launch (or activate) an application by name.

method LaunchApplication.__init__

__init__(app_name: str)

Build the action.

Args:


property LaunchApplication.keymap

The running Keymap, so an action need not import and look it up.


property LaunchApplication.ui

The action-facing UI API (keymap.ui) - see doc/action-api.md.

An action’s most-used object, so it is one attribute away rather than two lines of lookup at the top of every run().


class ActivateWindow

Bring an application’s window to the front, by name pattern.

Where the platform enumerates windows (Windows), this raises an actual window, so it can restore a minimized one and pick the front-most match. Otherwise (macOS today) it activates the matching application by pid.

ActivateWindow(app="code|Visual Studio Code")

method ActivateWindow.__init__

__init__(app: str)

Build the action.

Args:


property ActivateWindow.keymap

The running Keymap, so an action need not import and look it up.


property ActivateWindow.ui

The action-facing UI API (keymap.ui) - see doc/action-api.md.

An action’s most-used object, so it is one attribute away rather than two lines of lookup at the top of every run().


class MoveWindow

Move the focused window.

It nudges the window by distance pixels, or - with window_edge / screen_edge - travels until it meets another window’s edge or the edge of the screen. A window already at the screen edge hops to the adjacent monitor instead.

MoveWindow(direction="left", distance=20)
MoveWindow(direction="left", distance=9999, window_edge=True)

method MoveWindow.__init__

__init__(
    x: int = None,
    y: int = None,
    direction: str = '',
    distance: float = 10,
    window_edge: bool = False,
    screen_edge: bool = True
)

Build the action.

Args:


property MoveWindow.keymap

The running Keymap, so an action need not import and look it up.


property MoveWindow.ui

The action-facing UI API (keymap.ui) - see doc/action-api.md.

An action’s most-used object, so it is one attribute away rather than two lines of lookup at the top of every run().


class SnapWindow

Snap the focused window to a region of its screen (tiling).

The region is the screen’s work area, so the menu bar and Dock (macOS) and the taskbar (Windows) stay uncovered. “Its screen” is the one the window overlaps most, so repeated snaps keep a window on the monitor it is already on.

SnapWindow("left")               # left half
SnapWindow("left", ratio=2/3)    # left two thirds
SnapWindow("full")

method SnapWindow.__init__

__init__(position: str, ratio: float = 0.5)

Build the action.

Args:

Raises:


class MouseMove

Move the mouse cursor by a relative offset.

Held modifiers stay held, unlike the button and wheel actions. The move is injected acceleration-proof, so the distance is exactly what you ask for (keyhac-win MouseMoveCommand).

method MouseMove.__init__

__init__(dx: int, dy: int)

Build the action.

Args:


class MouseButtonDown

Press a mouse button and hold it.

Held modifiers are released first, so a modifier-bound press does not become a modified one (keyhac-win MouseButtonDownCommand).

method MouseButtonDown.__init__

__init__(button: str = 'left')

Build the action.

Args:

Raises:


class MouseButtonUp

Release a held mouse button (keyhac-win MouseButtonUpCommand).

method MouseButtonUp.__init__

__init__(button: str = 'left')

Build the action.

Args:

Raises:


class MouseButtonClick

Click a mouse button.

Held modifiers are released first, and rapid synthetic clicks register as double-clicks (keyhac-win MouseButtonClickCommand).

method MouseButtonClick.__init__

__init__(button: str = 'left')

Build the action.

Args:

Raises:


class MouseWheel

Turn the vertical mouse wheel (keyhac-win MouseWheelCommand).

method MouseWheel.__init__

__init__(wheel: float)

Build the action.

Args:


class MouseHorizontalWheel

Turn the horizontal mouse wheel (keyhac-win MouseHorizontalWheelCommand).

method MouseHorizontalWheel.__init__

__init__(wheel: float)

Build the action.

Args:


class StartRecordingKeys

Start recording keystrokes into the replay buffer.

Bind it to a key; the recording is played back by PlaybackRecordedKeys.


class StopRecordingKeys

Stop recording and normalize the buffer.


class ToggleRecordingKeys

Toggle keystroke recording.


class PlaybackRecordedKeys

Play back the recorded keystrokes.

The replayed keys run back through the keymap, so recorded bindings expand again on playback.


class ClipboardHistory

Automatically captures historical clipboard text.

Reached from a configuration as keymap.clipboard_history, and shown by the ShowClipboardHistory action.

Attributes:


method ClipboardHistory.add_item

add_item(s: str)  None

Add text to the history without touching the OS clipboard.

Args:


method ClipboardHistory.get_current

get_current()  str | None

Get the most recent clipboard text.

Returns: The newest history entry, or None when the history is empty.


method ClipboardHistory.items

items()

Iterate the history.

Yields: (text, label) pairs, latest first. The label is the text collapsed onto one line for display.


method ClipboardHistory.set_current

set_current(s: str)  None

Set text to the OS clipboard and the front of the history.

Args:


class ChooserAction

Base class for actions that open the chooser window.

Derive from it to build your own popup: implement list_items() and on_chosen(), and inherit the whole open / filter / refocus flow. Only one chooser is open at a time - pressing the same action’s key again closes it, and a different chooser action replaces it.

class PickBranch(ChooserAction):
     def list_items(self):
         return [("🌱", name) for name in git_branches()]
     def on_chosen(self, item, modifier_flags):
         checkout(item[1])

method ChooserAction.list_items

list_items()

Build the list the chooser shows. Override this.

Returns: A list of (icon, label) or (icon, label, …) tuples. Anything after the label is yours; on_chosen() receives the whole tuple.


method ChooserAction.on_chosen

on_chosen(item, modifier_flags: int)  None

Handle the chosen item. Override this.

Args:


class ShowCandidates

Open the candidate window over one or more sources.

The hotkey is the scarce resource, not the code: an action class per kind of row means a key per kind of row, and there are only so many a person can hold. This takes sources as values, so several kinds share one key and one incremental search - and each row is labelled with where it came from, so a mixed list stays readable.

kt["Fn-V"] = ShowCandidates([ClipboardHistorySource(), SnippetsSource(mine)])
kt["Fn-B"] = ShowCandidates(git_branches, on_chosen=checkout)
kt["Fn-P"] = ShowCandidates([Scope("All", every), Scope("Clipboard", clip)])

Enter runs whatever the chosen row’s source says to do, so rows from different sources can mean different things in the same window - paste this, activate that, press the other.

method ShowCandidates.__init__

__init__(sources, on_chosen=None, matcher=None, activates=None)

Build the action.

Args:


class Candidate

One row a source offers a view.

Attributes:


property Candidate.label

Icon and display text as one line, the way a list view draws it.


method Candidate.from_item

from_item(item)  Candidate

Adapt the (icon, label, *payload) tuple ChooserAction.list_items returns. The whole tuple becomes the payload, so on_chosen still receives exactly what it received before.


class CandidateSource

A named set of candidates, and what choosing one does.

Named for what it is a source of: keyhac import * is flat, and a config writes class Branches(CandidateSource) with no surrounding call to say which kind of source is meant. Scope keeps the shorter name because it is only ever written inside ShowCandidates([...]), where the context is right there.


method CandidateSource.badge

badge(candidate: keyhac.core.candidate.Candidate)  str

What to show quietly at the right of this row, when the window is showing only this source.

With several sources the window shows which one a row came from, because that is the thing a mixed list hides. With one there is no such question, and the slot is free for whatever this source thinks annotates a row - the menu source puts the keyboard shortcut there, so choosing a command from the list twice teaches the key the third time.


method CandidateSource.candidates

candidates()

The rows this source offers right now. Override this.

Called on every invocation rather than cached: a source reading the screen - the windows that exist, the controls in the front window - is describing something that has already moved on by the time it is asked again.

Return a list, or - for a source with real work to do - yield. A generator is drained a slice at a time between renders, so its first rows are on screen while it is still finding the rest, and abandoning it (the window closed, the scope changed) simply stops pulling.

A generator runs on the main thread, in slices, and not on a worker. That is not a simplification: on macOS an accessibility call off the main thread crashes the process, and accessibility is what the sources needing this are made of. Yield often, and do not block - a slice that does not return holds the keyboard as surely as any other main-thread work would.


method CandidateSource.on_chosen

on_chosen(
    candidate: keyhac.core.candidate.Candidate,
    modifier_flags: int
)  None

Act on the chosen row. Override this.

Args:


class CallableSource

A source built from a plain callable, so anything that can produce a list can be one without subclassing - SSH hosts, git branches, records out of a line-of-business system.

branches = CallableSource(git_branches, "Branches", on_chosen=checkout)

The callable returns Candidate objects, or the (icon, label, *rest) tuples ChooserAction.list_items has always returned - those are adapted, and on_chosen then receives a candidate whose payload is the tuple.

A callable that yields is a streaming source and stays one; one that returns a list stays a list.


class Scope

A named set of sources the candidate window can switch between.

One key opens the window; Tab and Shift-Tab move along the cycle, and the query survives the move - type kensaku, then look for it somewhere else without retyping it. That is the thing a typed prefix (>, @) cannot do, and the reason the switch is a key rather than a sigil. The other reason is that with Migemo the query alphabet is exactly ASCII, so a sigil sits in the middle of what the user is trying to type.

Scopes are also how an expensive source stays affordable. A source that walks the accessibility tree costs a real traversal every time the window opens; put it in its own scope and it is paid for only when the user asks for it, instead of on every invocation of a merged everything-scope.

keymap_global["Fn-P"] = ShowCandidates([
     Scope("All", [clipboard, snippets, windows]),
     Scope("Clipboard", [clipboard, snippets]),
     Scope("Windows", [windows]),
])

method Scope.__init__

__init__(name: str, sources)

Build a scope.

Args:


class ActionsSource

Every action in ~/.keyhac/extensions/, startable without a key.

The half of the authoring loop a key binding never covered: a class lands in extensions/, works, and is runnable from here - the config.py edit that binds it to a key comes later, or never, for something used once a month. That is also the answer to running out of keys, from the other side to KeyBindingsSource: one asks what the keys do, this asks what there is to run.

Listing does not import. The catalogue is an AST parse (keyhac.mcp.extensions.discover), so a file is read and never executed to find out what is in it, and a module no config.py imports stays inert on disk. A class here runs at exactly one moment: when the operator picks it.

What is offered is a ThreadedAction subclass, transitively and across files - not every callable class. The main thread services the keyboard hook and every window, so a list whose rows might block it is a list that can freeze the keyboard.

A class needing constructor arguments is listed and says so rather than being hidden: an action missing from the list reads as Keyhac not seeing the file, which is a much worse thing to debug than a row that explains itself.

method ActionsSource.__init__

__init__(name: str = None)

Build the source.

Args:


class ClipboardHistorySource

Everything the clipboard has held, most recent first.


class KeyBindingsSource

Every key binding in effect right now, and a way to run one.

The one source nothing outside Keyhac can offer: it is the engine’s own tables, resolved the way the hook resolves them - the tables whose focus condition matches where the user is standing, merged in definition order, or the armed multi-stroke table when there is one. Re-deriving that from the configuration would be a second implementation of the rule, and the two would drift.

It is also the cheap one. There is no traversal and no other process to ask; the answer is a dict the engine already keeps up to date.

A multi-stroke prefix is expanded to its leaves, the way the menu source expands submenus - Fn-X › A is the sequence you would type, and those are exactly the bindings nobody remembers. Rows show what the binding does, with the keys themselves right-aligned, so the list reads as a reference: what can I press here, and what would it do.

Choosing a row runs it, which is the point rather than a bonus - a binding you can run from a list is one that does not need a key of its own, and running out of keys is what the candidate window exists to fix.

method KeyBindingsSource.__init__

__init__(name: str = None)

Build the source.

Args:


class MenuItemsSource

Every command in the front application’s menus, as one flat list.

This is the long tail the candidate window is for: the commands that have no keyboard shortcut, in an application whose menus you do not know by heart. Rows read as the path to them - File › Export › As PDF… - and carry the shortcut where there is one, so choosing from here twice teaches the key the third time.

Only leaves are offered. A row that merely opens another menu is not a command, and a list of them would be a worse menu bar rather than a better one. Disabled items are skipped: they are visible in the menu for the shape of it, and unchoosable here.

It costs a real traversal. Measured on macOS: 79 ms for a small application, 396 ms for Chrome, for 161 and 331 items - so this belongs in a Scope of its own, where it is paid for when asked for, rather than in a merged scope opened on every keystroke.

method MenuItemsSource.__init__

__init__(name: str = None)

Build the source.

Args:


class WindowControlsSource

Everything you could click in the front window, reachable by name.

Discussion #112’s original target, and the reason the window had to stop taking the keyboard focus: a list of “what is actionable here” that changes what is actionable by opening is no use to anybody.

It streams, because it is expensive. Measured on macOS: a heavy application’s tree is 3000 nodes and 460 ms, and filtering by role does not help - the walk is the cost, and reporting less of it changes nothing. So the walk yields as it goes and the first controls are on screen while the rest are still being found. Put it in a Scope of its own all the same; it has real work to do on every invocation.

Only controls with a name are offered. An icon-only button with no label, no description and no tooltip cannot be typed for - there is no text to filter on - so listing it would add a row nobody can reach. Where a name comes from is recorded on the candidate (provenance), because it decides what else can find the element: a control reachable only through its tooltip cannot be found by find(name=...) in an action either.

method WindowControlsSource.__init__

__init__(name: str = None)

Build the source.

Args:


class SnippetsSource

Fixed text you paste often.

SnippetsSource([("📧", "me@example.com"), ("🕒", "Date", DateTimeSnippet("%Y-%m-%d"))])

method SnippetsSource.__init__

__init__(snippets, name: str = None)

Build the source.

Args:


class ClipboardToolsSource

Transformations applied to whatever the clipboard holds now.

method ClipboardToolsSource.__init__

__init__(tools, name: str = None)

Build the source.

Args:


class ShowClipboardHistory

Show the clipboard history in the chooser window.

Type to filter, Enter pastes into the application you came from, Shift-Enter only sets the clipboard, Escape cancels.

A preset: ShowCandidates(ClipboardHistorySource()). Reach for ShowCandidates directly to put the history in one window alongside other sources rather than on a hotkey of its own.


class ShowClipboardSnippets

Show fixed snippets in the chooser window.

Choosing one pastes it, exactly like the clipboard history.

ShowClipboardSnippets([
     ("📧", "me@example.com"),                          # (icon, text)
     ("📮", "Mailing address", "400 Broad St, ..."),    # (icon, label, text)
     ("🕒", "Date", DateTimeSnippet("%Y-%m-%d")),       # (icon, label, callable)
])

A preset over SnippetsSource.

method ShowClipboardSnippets.__init__

__init__(snippets)

Build the action.

Args:


class ShowClipboardTools

Show clipboard conversion tools in the chooser window.

Each tool takes the current clipboard text and returns its replacement.

ShowClipboardTools([
     ("🔄", "Quote", ShowClipboardTools.quote),
     ("🔄", "Upper case", str.upper),
])

A preset over ClipboardToolsSource.

method ShowClipboardTools.__init__

__init__(tools)

Build the action.

Args:


method ShowClipboardTools.quote

quote(s)

Prefix every line with quote_mark.

Args:

Returns: The quoted text.


method ShowClipboardTools.to_full_width

to_full_width(s)

Convert half-width characters to their full-width forms.

Args:

Returns: The converted text.


method ShowClipboardTools.to_half_width

to_half_width(s)

Convert full-width characters to their half-width forms.

Args:

Returns: The converted text.


method ShowClipboardTools.to_plain

to_plain(s)

Return the text unchanged (the identity converter).

Args:

Returns: The same text.


method ShowClipboardTools.unindent

unindent(s)

Remove the common leading whitespace from every line.

Args:

Returns: The dedented text.


class DateTimeSnippet

A ShowClipboardSnippets value that produces the current date and time.

ShowClipboardSnippets([("🕒", "Date", DateTimeSnippet("%Y-%m-%d"))])

method DateTimeSnippet.__init__

__init__(fmt: str)

Build the snippet.

Args:


function getLogger

getLogger(name: str)  Logger

Get a logger wired to the Keyhac console.

logger = getLogger("Config")
logger.info("loaded")

Args:

Returns: A standard logging.Logger whose output lands in the console window (and on stderr).


class Console

The console window’s backing store: a ring buffer of log lines plus the named text slots it displays (“lastKey”, “focusPath”).

A configuration reaches the console through print() and getLogger(), not through this object.

Attributes:


classmethod Console.get_instance

get_instance()  Console

Get the Console singleton.

Returns: The Console instance, creating it on first use.


method Console.lines

lines()  list[tuple[str, int]]

Get the buffered console lines.

Returns: (text, log level) pairs, oldest first, up to max_lines of them.


Generated from the docstrings by make api-reference. Edit the docstrings, not this file.