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
KeymapManages 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:
platform: “windows” or “mac” - branch on this where the two OSes genuinely differ.editor: The editor edit_config() opens the configuration file with: an application name or path the OS can resolve, or a callable receiving the path. Empty (the default) picks a platform default.replay_buffer: The keystroke buffer behind the keyboard macro actions.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.
The ClipboardHistory object (None while running without one, e.g. under –no-ui).
The configuration script this run loads.
lazydocs: ignore
extensions/ beside config.py: on sys.path, and re-imported on every reload.
lazydocs: ignore
Portable snapshot of the current keyboard focus (a Focus), or None before the first key event.
Whether the endpoint is currently listening.
lazydocs: ignore
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.
Keymap.call_on_main_threadcall_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:
callback: Called with no arguments.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.
Keymap.configureconfigure() → 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.
Keymap.define_keytabledefine_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:
name: Name of the key table. A multi-stroke table shows it in the balloon while armed.focus_path_pattern: Focus path pattern with wildcards, e.g. “/AXTextArea()”. Watch the console’s “Focus path” field for the live value.custom_condition_func: A function receiving the current Focus and returning whether the table applies.app: Application name pattern - process/exe base name on Windows (the “.exe” is optional), localized application name on macOS.title: Window title pattern.class_name: Win32 window class name pattern (Windows only).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.
Keymap.define_modifierdefine_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:
key: Key to use as the modifier, as a key name or a virtual key code.mod: Modifier the key produces - “User0”..”User3”, or a standard modifier such as “LCtrl” to give that modifier a second key.Keymap.edit_configedit_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.
Keymap.find_windowfind_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:
app: Application name pattern.title: Window title pattern.class_name: Win32 window class name pattern (Windows only).Returns: A Window, or None when nothing matches.
Note:
UI-thread only.
Keymap.get_active_windowget_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().
Keymap.get_ime_statusget_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.
Keymap.get_input_contextget_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:
replay: Re-evaluate the injected events through the keymap (what the keyboard macro playback uses).Returns: An InputContext, to be used as a context manager.
Keymap.get_instanceget_instance() → Keymap
Get the Keymap singleton.
Returns: The Keymap instance, or None before one has been created.
Keymap.list_windowslist_windows() → list
List the visible top-level windows.
Returns: Window objects, front-most first where the OS says so.
Note:
UI-thread only.
Keymap.reload_configreload_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.
Keymap.replace_keyreplace_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:
src: Key to replace, as a key name or a virtual key code.dst: Key it is replaced with.Keymap.screen_framesscreen_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().
Keymap.screen_work_framesscreen_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.
Keymap.set_ime_statusset_ime_status(on: bool) → bool
Turn the IME on or off for whatever holds the input focus.
Args:
on: True to turn the IME on, False to turn it off.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
InputContextonly queues for the application. Wrapping asend_keybatch in “off … back on” therefore does not work: the restore lands before the keys do and they are composed anyway. UseInputContext.send_textfor 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.
Keymap.window_frameswindow_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.
KeyTableDict-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:
name: Name given at definition time, shown in the balloon while the table is armed as a multi-stroke prefix.KeyConditionA 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:
vk: Virtual key code.mod: Modifier bit mask.down: True for a key-down condition, False for key-up.oneshot: True for a one-shot (“O-“) condition.KeyCondition.from_strfrom_str(s: str) → KeyCondition
Parse a key expression.
Args:
s: A key expression such as “Ctrl-X”, “O-RCmd”, “U-Fn-Space” or the short form “C-A”. Case-insensitive.Returns: The KeyCondition it describes.
Raises:
ValueError: The expression names an unknown modifier or key.FocusConditionCondition 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. |
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:
focus_path_pattern: Focus path pattern with wildcards.custom_condition_func: A function receiving the current Focus and returning whether the condition holds.app: Application name pattern (“.exe” optional on Windows).title: Window title pattern.class_name: Win32 window class name pattern (Windows only).InputContextA 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")
InputContext.send_keysend_key(s: str) → None
Send a key stroke from a key expression.
Args:
s: A key expression, e.g. “Cmd-Left”, “D-Shift” (key down only) or “U-Shift” (key up only). Modifiers go out as their left-side keys.Raises:
ValueError: Used outside the context, or the expression names an unknown modifier or key.InputContext.send_key_by_vksend_key_by_vk(vk: int, down: bool = True) → None
Send a key stroke by virtual key code.
Args:
vk: Virtual key code.down: True for key down, False for key up.Raises:
ValueError: Used outside the context.InputContext.send_mouse_buttonsend_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:
button: “left”, “right” or “middle”.down: True to press, False to release, None to click.Raises:
ValueError: Used outside the context, or an unknown button name.InputContext.send_mouse_horizontal_wheelsend_mouse_horizontal_wheel(notches: float) → None
Turn the horizontal mouse wheel.
Args:
notches: Wheel notches; positive = right, 1.0 = one notch.Raises:
ValueError: Used outside the context.InputContext.send_mouse_movesend_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:
dx: Horizontal offset in pixels, positive = right.dy: Vertical offset in pixels, positive = down.Raises:
ValueError: Used outside the context.InputContext.send_mouse_wheelsend_mouse_wheel(notches: float) → None
Turn the vertical mouse wheel.
Held modifiers are released first, like send_mouse_button.
Args:
notches: Wheel notches; positive = away from you, 1.0 = one notch.Raises:
ValueError: Used outside the context.InputContext.send_textsend_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:
s: The text to type.Raises:
ValueError: Used outside the context.FocusPortable snapshot of the current keyboard focus.
Available as keymap.focus, and passed to every custom_condition_func.
Attributes:
app_name: Process/exe base name without extension (Windows), or the localized application name (macOS).pid: Process id of the focused application.window_title: Title of the focused window. On macOS it is captured during the focus-path walk and carries the path’s transliteration of fnmatch special characters (“(“ and “[” become “<”, “)” and"]" become ">", and "/", "*", "?", ": “ each become “-“); on Windows it is the raw title. A title= pattern containing one of those characters must match the escaped spelling on macOS - or use a “*” wildcard across it, which works on both.class_name: Win32 window class name (Windows only; None on macOS).path: Focus path string - on macOS the AX focus path (“/AXApplication(Xcode)/AXWindow(…)…”), on Windows a synthesized “/{app_name}/{class_name}({title})” (provisional format).element: The focused semantic element - an AX UIElement (macOS) or a UI Automation UIElement (Windows). Same shape on both (get_attribute_names(), get_attribute_value(), get_action_names(), perform_action(), parent()), but each uses its own OS’s vocabulary of attribute names, “AXRole” versus “ControlType”. Portable code uses app_name / window_title / class_name and the focus path instead.native: The platform power object - a UIElement (macOS) or NativeWindow, an HWND wrapper (Windows).KeyEventA normalized key event delivered by the OS hook.
Attributes:
vk: Virtual key code.down: True for key down, False for key up.kind: “real” for physical input (or input injected by other apps), “replay” for input Keyhac injected in replay mode, which the keymap re-evaluates. Events Keyhac injects in normal (translated) mode are filtered out by the platform layer and never arrive here.WindowA 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().
Process base name without extension (Windows) / localized application name (macOS).
Win32 window class. None on macOS, which has no such concept.
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.
The underlying platform object (HWND wrapper / AX UIElement).
Process id of the application owning the window.
The window’s title.
Window.activateactivate() → bool
Bring this window and its application to the front.
Returns: Whether the activation succeeded.
Window.get_frameget_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.
Window.is_minimizedis_minimized() → bool
Whether the window is currently minimized.
Window.minimizeminimize() → bool
Minimize the window.
Returns: Whether the window was minimized.
Window.restorerestore() → bool
Un-minimize the window.
Returns: Whether the window was restored.
Window.set_frameset_frame(x: float, y: float, w: float = None, h: float = None) → bool
Move the window, and optionally resize it.
Args:
x: New left edge.y: New top edge.w: New width; None keeps the current one.h: New height; None keeps the current one.Returns: Whether the window accepted the change.
ThreadedActionBase 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}")
The running Keymap, so an action need not import and look it up.
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().
ThreadedAction.cancelledcancelled() → 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.
ThreadedAction.check_cancelledcheck_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.
ThreadedAction.finishedfinished(result: Any) → None
Called on the event-loop thread once run() has returned.
Main-thread-only APIs are allowed here too.
Args:
result: Whatever run() returned.ThreadedAction.runrun() → Any
Called in the thread pool; may block.
Returns: Anything; it is handed to finished().
ThreadedAction.startingstarting() → 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.
InputTextType a literal string into the focused application.
InputText.__init____init__(text: str)
Build the action.
Args:
text: The text to type; any characters, not just ones the keyboard can produce.LaunchApplicationLaunch (or activate) an application by name.
LaunchApplication.__init____init__(app_name: str)
Build the action.
Args:
app_name: Application to launch, named the way the OS resolvesit: “Terminal.app” on macOS, an executable name or path on Windows.The running Keymap, so an action need not import and look it up.
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().
ActivateWindowBring 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")
ActivateWindow.__init____init__(app: str)
Build the action.
Args:
app: Application name pattern, matched like define_keytable’s app= - case-insensitive, fnmatch wildcards, “ |
” alternation, “.exe” optional. |
The running Keymap, so an action need not import and look it up.
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().
MoveWindowMove 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)
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:
x: Deprecated since keyhac-mac v1.64; use direction and distance.y: Deprecated since keyhac-mac v1.64; use direction and distance.direction: “left”, “right”, “up” or “down”.distance: How far to move, in pixels (default 10). Pass a large value together with window_edge / screen_edge to travel until something stops it.window_edge: Stop at the edges of other windows (default False).screen_edge: Stop at the edge of the screen (default True).The running Keymap, so an action need not import and look it up.
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().
SnapWindowSnap 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")
SnapWindow.__init____init__(position: str, ratio: float = 0.5)
Build the action.
Args:
position: “left”, “right”, “top”, “bottom” or “full”.ratio: Fraction of the work area the window covers along the snap axis, between 0.1 and 1.0 (default 0.5 = half the screen). Ignored for “full”.Raises:
ValueError: Unknown position, or a ratio outside [0.1, 1.0] - reported when the configuration loads, not when the key is pressed.MouseMoveMove 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).
MouseMove.__init____init__(dx: int, dy: int)
Build the action.
Args:
dx: Horizontal offset in pixels, positive = right.dy: Vertical offset in pixels, positive = down.MouseButtonDownPress 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).
MouseButtonDown.__init____init__(button: str = 'left')
Build the action.
Args:
button: “left”, “right” or “middle”.Raises:
ValueError: Unknown button name - reported when the configuration loads, not when the key is pressed.MouseButtonUpRelease a held mouse button (keyhac-win MouseButtonUpCommand).
MouseButtonUp.__init____init__(button: str = 'left')
Build the action.
Args:
button: “left”, “right” or “middle”.Raises:
ValueError: Unknown button name - reported when the configuration loads, not when the key is pressed.MouseButtonClickClick a mouse button.
Held modifiers are released first, and rapid synthetic clicks register as double-clicks (keyhac-win MouseButtonClickCommand).
MouseButtonClick.__init____init__(button: str = 'left')
Build the action.
Args:
button: “left”, “right” or “middle”.Raises:
ValueError: Unknown button name - reported when the configuration loads, not when the key is pressed.MouseWheelTurn the vertical mouse wheel (keyhac-win MouseWheelCommand).
MouseWheel.__init____init__(wheel: float)
Build the action.
Args:
wheel: Wheel notches; positive = away from you, 1.0 = one notch.MouseHorizontalWheelTurn the horizontal mouse wheel (keyhac-win MouseHorizontalWheelCommand).
MouseHorizontalWheel.__init____init__(wheel: float)
Build the action.
Args:
wheel: Wheel notches; positive = right, 1.0 = one notch.StartRecordingKeysStart recording keystrokes into the replay buffer.
Bind it to a key; the recording is played back by PlaybackRecordedKeys.
StopRecordingKeysStop recording and normalize the buffer.
ToggleRecordingKeysToggle keystroke recording.
PlaybackRecordedKeysPlay back the recorded keystrokes.
The replayed keys run back through the keymap, so recorded bindings expand again on playback.
ClipboardHistoryAutomatically captures historical clipboard text.
Reached from a configuration as keymap.clipboard_history, and shown by the ShowClipboardHistory action.
Attributes:
max_items: Maximum entries kept (default 1000).max_label_length: Maximum length of item labels (default 4096).max_data_size: Maximum size of a single captured item (default 10 MB).max_persist_data_size: Maximum size of an item written to disk (default 64 KB).persist: Whether the history is saved across restarts (default True; set False to keep it in memory only).filename: Where it is saved (default ~/.keyhac/clipboard.json).ClipboardHistory.add_itemadd_item(s: str) → None
Add text to the history without touching the OS clipboard.
Args:
s: The text to add. A duplicate moves to the front; anything larger than max_data_size is dropped.ClipboardHistory.get_currentget_current() → str | None
Get the most recent clipboard text.
Returns: The newest history entry, or None when the history is empty.
ClipboardHistory.itemsitems()
Iterate the history.
Yields: (text, label) pairs, latest first. The label is the text collapsed onto one line for display.
ClipboardHistory.set_currentset_current(s: str) → None
Set text to the OS clipboard and the front of the history.
Args:
s: The text to put on the clipboard.ChooserActionBase 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])
ChooserAction.list_itemslist_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.
ChooserAction.on_chosenon_chosen(item, modifier_flags: int) → None
Handle the chosen item. Override this.
Args:
item: The tuple list_items() produced for the chosen row.modifier_flags: Modifiers held at selection time, as a bit mask - the clipboard choosers read it to tell Enter from Shift-Enter.ShowCandidatesOpen 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.
ShowCandidates.__init____init__(sources, on_chosen=None, matcher=None, activates=None)
Build the action.
Args:
sources: A CandidateSource, a plain callable returning candidates, or a list of either. A callable is wrapped, so anything that can produce a list can be a source without subclassing. A list of Scope objects instead gives the window a cycle Tab and Shift-Tab move along, keeping the query as they go.on_chosen: Called as on_chosen(candidate, modifier_flags) for rows whose source does not say what to do itself - which is every row when the source is a bare callable.matcher: How the filter text is matched; the default is case-insensitive substring unioned with Migemo.activates: Whether the window takes OS keyboard focus. Leave it alone unless the filter field genuinely needs an input method
- see ChooserAction.activates.CandidateOne row a source offers a view.
Attributes:
match_text: What the matcher runs against. Defaults to display.display: What the user sees, which may differ from the match text - a file candidate can match on its full path and display its basename.payload: What the consumer wants back: a string to paste, a UINode, a callable, a window handle.identity: Stable across invocations where the source can manage it, so a view assigning short labels can keep giving the same candidate the same label. None when the source has nothing stable to offer.icon: A short glyph shown before the display text.rect: Screen rectangle (x, y, w, h) in puikit’s portable top-left coordinates, for views that draw over the real element.provenance: Where display came from, when that is not simply the element’s name - "description", "help", "identifier", "position". UINode.name_source is where an accessibility source gets this.action: What choosing this row does, as action(modifier_flags).Usually left None: a source declares one on_chosen for everything it yields, since candidates from one source almost always do the same kind of thing. Set it per candidate for a source whose rows genuinely differ - and for the unified window, where rows from several sources sit in one list and Enter has to mean whatever that row means.extras: Anything else the source and its view agree on (a key expression, a role hint).Icon and display text as one line, the way a list view draws it.
Candidate.from_itemfrom_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.
CandidateSourceA 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.
CandidateSource.badgebadge(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.
CandidateSource.candidatescandidates()
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.
CandidateSource.on_chosenon_chosen(
candidate: keyhac.core.candidate.Candidate,
modifier_flags: int
) → None
Act on the chosen row. Override this.
Args:
candidate: The row the user picked.modifier_flags: Modifiers held at the moment of choosing, as a bit mask - Shift-Enter is how the clipboard sources tell “copy this” from “paste it”.CallableSourceA 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.
ScopeA 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]),
])
Scope.__init____init__(name: str, sources)
Build a scope.
Args:
name: Shown in the window while this scope is the current one.sources: The sources it draws from - CandidateSource objects, plain callables, or a mix.ActionsSourceEvery 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.
ActionsSource.__init____init__(name: str = None)
Build the source.
Args:
name: What a shared window shows beside these rows.ClipboardHistorySourceEverything the clipboard has held, most recent first.
KeyBindingsSourceEvery 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.
KeyBindingsSource.__init____init__(name: str = None)
Build the source.
Args:
name: What a shared window shows beside these rows.MenuItemsSourceEvery 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.
MenuItemsSource.__init____init__(name: str = None)
Build the source.
Args:
name: What a shared window shows beside these rows.WindowControlsSourceEverything 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.
WindowControlsSource.__init____init__(name: str = None)
Build the source.
Args:
name: What a shared window shows beside these rows.SnippetsSourceFixed text you paste often.
SnippetsSource([("📧", "me@example.com"), ("🕒", "Date", DateTimeSnippet("%Y-%m-%d"))])
SnippetsSource.__init____init__(snippets, name: str = None)
Build the source.
Args:
snippets: Sequence of (icon, text), (icon, label, text) or (icon, label, callable) tuples. A callable is invoked when the snippet is chosen and its return value is pasted; returning None pastes nothing.name: What the unified window shows beside these rows.ClipboardToolsSourceTransformations applied to whatever the clipboard holds now.
ClipboardToolsSource.__init____init__(tools, name: str = None)
Build the source.
Args:
tools: Sequence of (icon, label, callable) tuples; the callable takes the current clipboard text and returns the replacement.name: What the unified window shows beside these rows.ShowClipboardHistoryShow 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.
ShowClipboardSnippetsShow 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.
ShowClipboardSnippets.__init____init__(snippets)
Build the action.
Args:
snippets: Sequence of (icon, text), (icon, label, text) or (icon, label, callable) tuples. A callable is invoked when the snippet is chosen and its return value is pasted; returning None pastes nothing.ShowClipboardToolsShow 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.
ShowClipboardTools.__init____init__(tools)
Build the action.
Args:
tools: Sequence of (icon, label, callable) tuples; the callable takes the current clipboard text and returns the replacement.ShowClipboardTools.quotequote(s)
Prefix every line with quote_mark.
Args:
s: Current clipboard text.Returns: The quoted text.
ShowClipboardTools.to_full_widthto_full_width(s)
Convert half-width characters to their full-width forms.
Args:
s: Current clipboard text.Returns: The converted text.
ShowClipboardTools.to_half_widthto_half_width(s)
Convert full-width characters to their half-width forms.
Args:
s: Current clipboard text.Returns: The converted text.
ShowClipboardTools.to_plainto_plain(s)
Return the text unchanged (the identity converter).
Args:
s: Current clipboard text.Returns: The same text.
ShowClipboardTools.unindentunindent(s)
Remove the common leading whitespace from every line.
Args:
s: Current clipboard text.Returns: The dedented text.
DateTimeSnippetA ShowClipboardSnippets value that produces the current date and time.
ShowClipboardSnippets([("🕒", "Date", DateTimeSnippet("%Y-%m-%d"))])
DateTimeSnippet.__init____init__(fmt: str)
Build the snippet.
Args:
fmt: A strftime format string, e.g. “%Y-%m-%d”.getLoggergetLogger(name: str) → Logger
Get a logger wired to the Keyhac console.
logger = getLogger("Config")
logger.info("loaded")
Args:
name: Logger name, shown in brackets on each line.Returns: A standard logging.Logger whose output lands in the console window (and on stderr).
ConsoleThe 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:
max_lines: How many lines the ring buffer keeps (default 1000).Console.get_instanceget_instance() → Console
Get the Console singleton.
Returns: The Console instance, creating it on first use.
Console.lineslines() → 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.