keyhac 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keyhac/__init__.py +70 -0
- keyhac/__main__.py +4 -0
- keyhac/_config.py +372 -0
- keyhac/actions.py +622 -0
- keyhac/core/__init__.py +0 -0
- keyhac/core/action.py +121 -0
- keyhac/core/clipboard_history.py +113 -0
- keyhac/core/config.py +29 -0
- keyhac/core/const.py +81 -0
- keyhac/core/focus.py +130 -0
- keyhac/core/input.py +203 -0
- keyhac/core/key.py +144 -0
- keyhac/core/keymap.py +546 -0
- keyhac/core/log.py +154 -0
- keyhac/core/replay.py +80 -0
- keyhac/core/settings.py +64 -0
- keyhac/core/vk.py +393 -0
- keyhac/main.py +181 -0
- keyhac/platform/__init__.py +0 -0
- keyhac/platform/base.py +307 -0
- keyhac/platform/fake.py +99 -0
- keyhac/platform/mac/__init__.py +26 -0
- keyhac/platform/mac/apps.py +70 -0
- keyhac/platform/mac/clipboard.py +27 -0
- keyhac/platform/mac/focus.py +102 -0
- keyhac/platform/mac/hook.py +586 -0
- keyhac/platform/mac/instance.py +76 -0
- keyhac/platform/mac/loop.py +40 -0
- keyhac/platform/mac/uielement.py +144 -0
- keyhac/platform/mac/window.py +214 -0
- keyhac/platform/win/__init__.py +24 -0
- keyhac/platform/win/apps.py +86 -0
- keyhac/platform/win/clipboard.py +95 -0
- keyhac/platform/win/focus.py +220 -0
- keyhac/platform/win/hook.py +425 -0
- keyhac/platform/win/instance.py +98 -0
- keyhac/platform/win/loop.py +90 -0
- keyhac/platform/win/uielement.py +547 -0
- keyhac/platform/win/window.py +350 -0
- keyhac/ui/__init__.py +0 -0
- keyhac/ui/assets/MenuExtraTemplate.png +0 -0
- keyhac/ui/assets/MenuExtraTemplate@2x.png +0 -0
- keyhac/ui/assets/keyhac.icns +0 -0
- keyhac/ui/assets/keyhac.ico +0 -0
- keyhac/ui/balloon.py +59 -0
- keyhac/ui/chooser.py +138 -0
- keyhac/ui/console.py +300 -0
- keyhac/ui/runtime.py +6 -0
- keyhac/ui/tray.py +55 -0
- keyhac-2.0.0.dist-info/METADATA +141 -0
- keyhac-2.0.0.dist-info/RECORD +55 -0
- keyhac-2.0.0.dist-info/WHEEL +5 -0
- keyhac-2.0.0.dist-info/entry_points.txt +2 -0
- keyhac-2.0.0.dist-info/licenses/LICENSE +21 -0
- keyhac-2.0.0.dist-info/top_level.txt +1 -0
keyhac/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Keyhac 2 - Python-scriptable keyboard customization for Windows and macOS.
|
|
2
|
+
|
|
3
|
+
Configuration files do `from keyhac import *` and get the user-facing API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "2.0.0"
|
|
7
|
+
|
|
8
|
+
from keyhac.core.keymap import Keymap
|
|
9
|
+
from keyhac.core.key import KeyCondition, KeyTable
|
|
10
|
+
from keyhac.core.focus import FocusCondition
|
|
11
|
+
from keyhac.core.input import InputContext
|
|
12
|
+
from keyhac.core.log import getLogger, Console
|
|
13
|
+
from keyhac.core.action import (
|
|
14
|
+
ThreadedAction, LaunchApplication, InputText,
|
|
15
|
+
StartRecordingKeys, StopRecordingKeys, ToggleRecordingKeys,
|
|
16
|
+
PlaybackRecordedKeys,
|
|
17
|
+
)
|
|
18
|
+
from keyhac.core.clipboard_history import ClipboardHistory
|
|
19
|
+
from keyhac.actions import (
|
|
20
|
+
ActivateWindow,
|
|
21
|
+
MouseButtonClick,
|
|
22
|
+
MouseButtonDown,
|
|
23
|
+
MouseButtonUp,
|
|
24
|
+
MouseHorizontalWheel,
|
|
25
|
+
MouseMove,
|
|
26
|
+
MouseWheel,
|
|
27
|
+
MoveWindow,
|
|
28
|
+
SnapWindow,
|
|
29
|
+
ChooserAction,
|
|
30
|
+
DateTimeSnippet,
|
|
31
|
+
ShowClipboardHistory,
|
|
32
|
+
ShowClipboardSnippets,
|
|
33
|
+
ShowClipboardTools,
|
|
34
|
+
)
|
|
35
|
+
from keyhac.platform.base import Focus, KeyEvent
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Keymap",
|
|
39
|
+
"KeyTable",
|
|
40
|
+
"KeyCondition",
|
|
41
|
+
"FocusCondition",
|
|
42
|
+
"InputContext",
|
|
43
|
+
"Focus",
|
|
44
|
+
"KeyEvent",
|
|
45
|
+
"ThreadedAction",
|
|
46
|
+
"LaunchApplication",
|
|
47
|
+
"InputText",
|
|
48
|
+
"StartRecordingKeys",
|
|
49
|
+
"StopRecordingKeys",
|
|
50
|
+
"ToggleRecordingKeys",
|
|
51
|
+
"PlaybackRecordedKeys",
|
|
52
|
+
"ClipboardHistory",
|
|
53
|
+
"ChooserAction",
|
|
54
|
+
"MouseButtonClick",
|
|
55
|
+
"MouseButtonDown",
|
|
56
|
+
"MouseButtonUp",
|
|
57
|
+
"MouseHorizontalWheel",
|
|
58
|
+
"MouseMove",
|
|
59
|
+
"MouseWheel",
|
|
60
|
+
"MoveWindow",
|
|
61
|
+
"SnapWindow",
|
|
62
|
+
"ActivateWindow",
|
|
63
|
+
"ShowClipboardHistory",
|
|
64
|
+
"ShowClipboardSnippets",
|
|
65
|
+
"ShowClipboardTools",
|
|
66
|
+
"DateTimeSnippet",
|
|
67
|
+
"getLogger",
|
|
68
|
+
"Console",
|
|
69
|
+
"__version__",
|
|
70
|
+
]
|
keyhac/__main__.py
ADDED
keyhac/_config.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Keyhac 2 configuration file.
|
|
2
|
+
|
|
3
|
+
This file is copied to ~/.keyhac/config.py on first run. Edit that copy —
|
|
4
|
+
Keyhac reloads it from the tray menu or the console's hook toggle.
|
|
5
|
+
|
|
6
|
+
The same file runs on Windows and macOS. Where the OSes genuinely differ,
|
|
7
|
+
branch on `keymap.platform`; the two constants set up at the top (LEADER and
|
|
8
|
+
MOD) absorb most of it, so the samples below rarely have to.
|
|
9
|
+
|
|
10
|
+
Everything here is a working example, not pseudo-code. Delete what you do
|
|
11
|
+
not want.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from keyhac import *
|
|
17
|
+
|
|
18
|
+
logger = getLogger("Config")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def configure(keymap):
|
|
22
|
+
|
|
23
|
+
mac = keymap.platform == "mac"
|
|
24
|
+
|
|
25
|
+
# ==================================================================
|
|
26
|
+
# Setup
|
|
27
|
+
# ==================================================================
|
|
28
|
+
|
|
29
|
+
# --- user modifier -------------------------------------------------
|
|
30
|
+
# Turn a key into User0, a modifier of your own that no application
|
|
31
|
+
# sees. User0-User3 are available; a key used this way is never
|
|
32
|
+
# emitted, so it loses its original meaning while defined.
|
|
33
|
+
if mac:
|
|
34
|
+
# The right Option key; it stops acting as Option.
|
|
35
|
+
keymap.define_modifier("RAlt", "RUser0")
|
|
36
|
+
else:
|
|
37
|
+
# The left Windows key; the Start menu no longer opens on a tap
|
|
38
|
+
# (bind kt["O-LWin"] = "LWin" below if you want that back).
|
|
39
|
+
keymap.define_modifier("LWin", "LUser0")
|
|
40
|
+
|
|
41
|
+
# --- the two portability constants ---------------------------------
|
|
42
|
+
# LEADER: the modifier most samples below hang off.
|
|
43
|
+
# macOS - the Fn key, which Windows does not expose to software
|
|
44
|
+
# Windows - User0, i.e. the left Windows key defined just above
|
|
45
|
+
LEADER = "Fn" if mac else "User0"
|
|
46
|
+
|
|
47
|
+
# MOD: the OS's primary shortcut modifier, so one binding can mean
|
|
48
|
+
# "Cmd-C" on macOS and "Ctrl-C" on Windows.
|
|
49
|
+
MOD = "Cmd" if mac else "Ctrl"
|
|
50
|
+
|
|
51
|
+
# --- swap a key entirely (uncomment to try) ------------------------
|
|
52
|
+
# replace_key runs before any key table, so the rest of the config
|
|
53
|
+
# only ever sees the replacement.
|
|
54
|
+
# keymap.replace_key("CapsLock", "LCtrl")
|
|
55
|
+
|
|
56
|
+
# --- text editor for "Edit Config" ---------------------------------
|
|
57
|
+
# The tray menu's "Edit Config" opens this file. Unset, a default is
|
|
58
|
+
# picked (VS Code / Xcode / TextEdit on macOS, Notepad on Windows).
|
|
59
|
+
# Name an editor application, or set a callable taking the path:
|
|
60
|
+
# keymap.editor = "CotEditor" if mac else "notepad.exe"
|
|
61
|
+
|
|
62
|
+
# --- clipboard history ---------------------------------------------
|
|
63
|
+
keymap.clipboard_history.max_items = 500
|
|
64
|
+
keymap.clipboard_history.max_data_size = 10 * 1024 * 1024
|
|
65
|
+
|
|
66
|
+
# ==================================================================
|
|
67
|
+
# Global key table (active everywhere)
|
|
68
|
+
# ==================================================================
|
|
69
|
+
|
|
70
|
+
kt = keymap.define_keytable(focus_path_pattern="*")
|
|
71
|
+
|
|
72
|
+
# --- key -> key ----------------------------------------------------
|
|
73
|
+
# IJKL as arrow keys while LEADER is held.
|
|
74
|
+
kt[f"{LEADER}-I"] = "Up"
|
|
75
|
+
kt[f"{LEADER}-J"] = "Left"
|
|
76
|
+
kt[f"{LEADER}-K"] = "Down"
|
|
77
|
+
kt[f"{LEADER}-L"] = "Right"
|
|
78
|
+
kt[f"{LEADER}-U"] = "Home"
|
|
79
|
+
kt[f"{LEADER}-O"] = "End"
|
|
80
|
+
|
|
81
|
+
# --- key -> sequence of keys ---------------------------------------
|
|
82
|
+
# Select the whole line: Home, then Shift-End.
|
|
83
|
+
kt[f"{LEADER}-A"] = "Home", "Shift-End"
|
|
84
|
+
|
|
85
|
+
# --- one-shot: tap for one key, hold to modify ---------------------
|
|
86
|
+
# A one-shot key held down still works as its modifier; only a lone
|
|
87
|
+
# tap-and-release fires the assignment. Picked so a stray tap is
|
|
88
|
+
# harmless (an Escape here, say, would cancel dialogs mid-typing).
|
|
89
|
+
if mac:
|
|
90
|
+
# The classic macOS setup: tap Left/Right Cmd alone for Eisu/Kana
|
|
91
|
+
# (IME off/on) - a no-op unless a Japanese input source is
|
|
92
|
+
# installed. Held, they are still plain Cmd.
|
|
93
|
+
kt["O-LCmd"] = "Eisu"
|
|
94
|
+
kt["O-RCmd"] = "Kana"
|
|
95
|
+
else:
|
|
96
|
+
# Tap right Ctrl alone to open the Start-menu search; held, it is
|
|
97
|
+
# still plain Ctrl.
|
|
98
|
+
kt["O-RCtrl"] = "Win-S"
|
|
99
|
+
|
|
100
|
+
# --- key -> your own function --------------------------------------
|
|
101
|
+
def hello():
|
|
102
|
+
# print() and the logger both reach the console window.
|
|
103
|
+
print("Hello from config.py")
|
|
104
|
+
logger.info(f"platform={keymap.platform}")
|
|
105
|
+
|
|
106
|
+
kt[f"{LEADER}-H"] = hello
|
|
107
|
+
|
|
108
|
+
# --- typing literal text -------------------------------------------
|
|
109
|
+
kt[f"{LEADER}-Semicolon"] = InputText("me@example.com")
|
|
110
|
+
|
|
111
|
+
# --- sending keys from your own function ---------------------------
|
|
112
|
+
# get_input_context() batches virtual key input; it is safe to use from
|
|
113
|
+
# a worker thread as well (see the ThreadedAction sample below).
|
|
114
|
+
def wrap_in_quotes():
|
|
115
|
+
with keymap.get_input_context() as ctx:
|
|
116
|
+
ctx.send_key(f"{MOD}-C")
|
|
117
|
+
|
|
118
|
+
kt[f"{LEADER}-Q"] = wrap_in_quotes
|
|
119
|
+
|
|
120
|
+
# ==================================================================
|
|
121
|
+
# Clipboard
|
|
122
|
+
# ==================================================================
|
|
123
|
+
|
|
124
|
+
# --- history, in a chooser window ----------------------------------
|
|
125
|
+
# Enter pastes; Shift-Enter only sets the clipboard. Type to filter.
|
|
126
|
+
kt[f"{LEADER}-V"] = ShowClipboardHistory()
|
|
127
|
+
|
|
128
|
+
# --- fixed snippets -------------------------------------------------
|
|
129
|
+
# (icon, label) | (icon, label, text) | (icon, label, callable)
|
|
130
|
+
kt[f"{LEADER}-Shift-V"] = ShowClipboardSnippets([
|
|
131
|
+
("📧", "me@example.com"),
|
|
132
|
+
("📮", "Mailing address", "400 Broad St, Seattle, WA 98109"),
|
|
133
|
+
("🕒", "Date", DateTimeSnippet("%Y-%m-%d")),
|
|
134
|
+
("🕒", "Timestamp", DateTimeSnippet("%Y-%m-%d %H:%M:%S")),
|
|
135
|
+
("🕒", "For filenames", DateTimeSnippet("%Y%m%d_%H%M%S")),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
# --- transform whatever is on the clipboard -------------------------
|
|
139
|
+
# A tool takes the clipboard text and returns the replacement.
|
|
140
|
+
def pretty_json(s):
|
|
141
|
+
try:
|
|
142
|
+
return json.dumps(json.loads(s), indent=4, ensure_ascii=False)
|
|
143
|
+
except json.JSONDecodeError:
|
|
144
|
+
logger.error("Clipboard content is not valid JSON.")
|
|
145
|
+
return s
|
|
146
|
+
|
|
147
|
+
kt[f"{LEADER}-Ctrl-V"] = ShowClipboardTools([
|
|
148
|
+
("🔄", "Quote", ShowClipboardTools.quote),
|
|
149
|
+
("🔄", "Unindent", ShowClipboardTools.unindent),
|
|
150
|
+
("🔄", "Upper case", str.upper),
|
|
151
|
+
("🔄", "Lower case", str.lower),
|
|
152
|
+
("🔄", "Half width", ShowClipboardTools.to_half_width),
|
|
153
|
+
("🔄", "Full width", ShowClipboardTools.to_full_width),
|
|
154
|
+
("🔄", "Pretty JSON", pretty_json),
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
# ==================================================================
|
|
158
|
+
# Windows and applications
|
|
159
|
+
# ==================================================================
|
|
160
|
+
|
|
161
|
+
# --- move the focused window ---------------------------------------
|
|
162
|
+
# Apple keyboards translate Fn-Arrow into Home/End/PageUp/PageDown in
|
|
163
|
+
# hardware (the Fn modifier itself still arrives), so with LEADER = Fn
|
|
164
|
+
# the "...-Left" spellings would never fire on macOS - bind the keys
|
|
165
|
+
# that actually arrive there. Ctrl/Alt rather than Shift, because
|
|
166
|
+
# Fn-Shift-Arrow is how you *select text* on a Mac laptop (it arrives
|
|
167
|
+
# as Shift-Home etc.) - a Shift binding here would steal it.
|
|
168
|
+
LEFT, RIGHT, UP, DOWN = (("Home", "End", "PageUp", "PageDown") if mac
|
|
169
|
+
else ("Left", "Right", "Up", "Down"))
|
|
170
|
+
|
|
171
|
+
# Nudge by 20 px...
|
|
172
|
+
kt[f"{LEADER}-Ctrl-{LEFT}"] = MoveWindow(direction="left", distance=20)
|
|
173
|
+
kt[f"{LEADER}-Ctrl-{RIGHT}"] = MoveWindow(direction="right", distance=20)
|
|
174
|
+
kt[f"{LEADER}-Ctrl-{UP}"] = MoveWindow(direction="up", distance=20)
|
|
175
|
+
kt[f"{LEADER}-Ctrl-{DOWN}"] = MoveWindow(direction="down", distance=20)
|
|
176
|
+
|
|
177
|
+
# ...or send it as far as it goes, stopping at other windows' edges
|
|
178
|
+
# and screen edges (and hopping to the next monitor when already there).
|
|
179
|
+
kt[f"{LEADER}-Alt-{LEFT}"] = MoveWindow(direction="left", distance=9999,
|
|
180
|
+
window_edge=True, screen_edge=True)
|
|
181
|
+
kt[f"{LEADER}-Alt-{RIGHT}"] = MoveWindow(direction="right", distance=9999,
|
|
182
|
+
window_edge=True, screen_edge=True)
|
|
183
|
+
kt[f"{LEADER}-Alt-{UP}"] = MoveWindow(direction="up", distance=9999,
|
|
184
|
+
window_edge=True, screen_edge=True)
|
|
185
|
+
kt[f"{LEADER}-Alt-{DOWN}"] = MoveWindow(direction="down", distance=9999,
|
|
186
|
+
window_edge=True, screen_edge=True)
|
|
187
|
+
|
|
188
|
+
# --- snap to screen regions (tiling) --------------------------------
|
|
189
|
+
# Resizes to a half of the window's current screen, inside the work
|
|
190
|
+
# area (menu bar, Dock and taskbar stay uncovered). Same IJKL layout
|
|
191
|
+
# as the arrows above. ratio= picks a different split, e.g.
|
|
192
|
+
# SnapWindow("left", ratio=2/3).
|
|
193
|
+
kt[f"{LEADER}-Ctrl-J"] = SnapWindow("left")
|
|
194
|
+
kt[f"{LEADER}-Ctrl-L"] = SnapWindow("right")
|
|
195
|
+
kt[f"{LEADER}-Ctrl-I"] = SnapWindow("top")
|
|
196
|
+
kt[f"{LEADER}-Ctrl-K"] = SnapWindow("bottom")
|
|
197
|
+
kt[f"{LEADER}-F"] = SnapWindow("full")
|
|
198
|
+
|
|
199
|
+
# --- minimize the focused window -------------------------------------
|
|
200
|
+
def minimize_window():
|
|
201
|
+
window = keymap.get_active_window()
|
|
202
|
+
if window is not None:
|
|
203
|
+
window.minimize()
|
|
204
|
+
|
|
205
|
+
kt[f"{LEADER}-M"] = minimize_window
|
|
206
|
+
|
|
207
|
+
# --- bring an application forward -----------------------------------
|
|
208
|
+
# Matches like the focus conditions below: wildcards, "|" alternation,
|
|
209
|
+
# case-insensitive, ".exe" optional.
|
|
210
|
+
kt[f"{LEADER}-1"] = ActivateWindow(app="code|Visual Studio Code")
|
|
211
|
+
kt[f"{LEADER}-2"] = ActivateWindow(app="chrome|Google Chrome")
|
|
212
|
+
|
|
213
|
+
# --- launch an application ------------------------------------------
|
|
214
|
+
if mac:
|
|
215
|
+
kt[f"{LEADER}-T"] = LaunchApplication("Terminal.app")
|
|
216
|
+
else:
|
|
217
|
+
kt[f"{LEADER}-T"] = LaunchApplication("wt.exe") # Windows Terminal
|
|
218
|
+
|
|
219
|
+
# --- inspect windows yourself ---------------------------------------
|
|
220
|
+
# keymap.get_active_window() / find_window() / list_windows() return
|
|
221
|
+
# portable Window objects: title, app_name, pid, class_name (Windows),
|
|
222
|
+
# get_frame(), set_frame(), activate(), minimize(), is_minimized(),
|
|
223
|
+
# restore(). Screen geometry: keymap.screen_frames() (whole screens),
|
|
224
|
+
# keymap.screen_work_frames() (minus menu bar / Dock / taskbar) and
|
|
225
|
+
# keymap.window_frames(). Window objects and screen_work_frames() are
|
|
226
|
+
# UI-thread only - never touch them from a ThreadedAction.run(); the
|
|
227
|
+
# thread-safe pair there is screen_frames() / window_frames().
|
|
228
|
+
def describe_window():
|
|
229
|
+
window = keymap.get_active_window()
|
|
230
|
+
if window is None:
|
|
231
|
+
logger.warning("No active window.")
|
|
232
|
+
return
|
|
233
|
+
x, y, w, h = window.get_frame()
|
|
234
|
+
logger.info(f"{window.app_name}: \"{window.title}\" "
|
|
235
|
+
f"at ({x:.0f},{y:.0f}) {w:.0f}x{h:.0f}")
|
|
236
|
+
logger.info(f"{len(keymap.list_windows())} windows open on "
|
|
237
|
+
f"{len(keymap.screen_frames())} screen(s)")
|
|
238
|
+
|
|
239
|
+
kt[f"{LEADER}-W"] = describe_window
|
|
240
|
+
|
|
241
|
+
# --- activate, or launch if it is not running ------------------------
|
|
242
|
+
def activate_or_launch_editor():
|
|
243
|
+
window = keymap.find_window(app="code|Visual Studio Code")
|
|
244
|
+
if window:
|
|
245
|
+
window.activate()
|
|
246
|
+
else:
|
|
247
|
+
LaunchApplication("Visual Studio Code.app" if mac else "code")()
|
|
248
|
+
|
|
249
|
+
kt[f"{LEADER}-E"] = activate_or_launch_editor
|
|
250
|
+
|
|
251
|
+
# ==================================================================
|
|
252
|
+
# Keyboard macros
|
|
253
|
+
# ==================================================================
|
|
254
|
+
|
|
255
|
+
kt[f"{LEADER}-OpenBracket"] = ToggleRecordingKeys() # record on/off
|
|
256
|
+
kt[f"{LEADER}-CloseBracket"] = PlaybackRecordedKeys() # replay
|
|
257
|
+
# StartRecordingKeys() / StopRecordingKeys() exist too, if you would
|
|
258
|
+
# rather have separate keys than a toggle.
|
|
259
|
+
|
|
260
|
+
# ==================================================================
|
|
261
|
+
# Background work: ThreadedAction
|
|
262
|
+
# ==================================================================
|
|
263
|
+
|
|
264
|
+
# Anything slow (network, subprocess, sleeping) must not run inline -
|
|
265
|
+
# it would block the keyboard hook. ThreadedAction gives you a worker
|
|
266
|
+
# thread; starting() and finished() run under the engine lock, run()
|
|
267
|
+
# does not.
|
|
268
|
+
class TypeSlowly(ThreadedAction):
|
|
269
|
+
def __init__(self, text):
|
|
270
|
+
self.text = text
|
|
271
|
+
|
|
272
|
+
def starting(self):
|
|
273
|
+
logger.info(f"Typing {self.text!r}...")
|
|
274
|
+
|
|
275
|
+
def run(self):
|
|
276
|
+
import time
|
|
277
|
+
for char in self.text:
|
|
278
|
+
time.sleep(0.05)
|
|
279
|
+
with keymap.get_input_context() as ctx:
|
|
280
|
+
ctx.send_key(f"Shift-{char}" if char.isupper() else char)
|
|
281
|
+
return len(self.text)
|
|
282
|
+
|
|
283
|
+
def finished(self, result):
|
|
284
|
+
logger.info(f"Typed {result} characters.")
|
|
285
|
+
|
|
286
|
+
def __repr__(self):
|
|
287
|
+
return f"TypeSlowly({self.text!r})"
|
|
288
|
+
|
|
289
|
+
kt[f"{LEADER}-Y"] = TypeSlowly("keyhac")
|
|
290
|
+
|
|
291
|
+
# ==================================================================
|
|
292
|
+
# Multi-stroke key tables
|
|
293
|
+
# ==================================================================
|
|
294
|
+
|
|
295
|
+
# Press LEADER-X, then a second key. A balloon shows the table's name
|
|
296
|
+
# while it is armed.
|
|
297
|
+
kt_x = keymap.define_keytable(name="LEADER-X")
|
|
298
|
+
kt[f"{LEADER}-X"] = kt_x
|
|
299
|
+
kt_x["C"] = f"{MOD}-C"
|
|
300
|
+
kt_x["V"] = f"{MOD}-V"
|
|
301
|
+
kt_x["S"] = f"{MOD}-S"
|
|
302
|
+
|
|
303
|
+
# --- balloon messages of your own ------------------------------------
|
|
304
|
+
def show_balloon():
|
|
305
|
+
# Absent when running with --no-ui, so ask before using it.
|
|
306
|
+
pop = getattr(keymap, "pop_balloon", None)
|
|
307
|
+
if pop:
|
|
308
|
+
pop("hello", "Keyhac is running", 2.0)
|
|
309
|
+
|
|
310
|
+
kt[f"{LEADER}-B"] = show_balloon
|
|
311
|
+
|
|
312
|
+
# ==================================================================
|
|
313
|
+
# Application-specific key tables
|
|
314
|
+
# ==================================================================
|
|
315
|
+
# Tables are merged in definition order and later ones win, so anything
|
|
316
|
+
# below overrides the global table for the apps it matches.
|
|
317
|
+
|
|
318
|
+
# --- by application name (portable) ----------------------------------
|
|
319
|
+
kt_browser = keymap.define_keytable(app="chrome|Google Chrome|firefox|Safari")
|
|
320
|
+
kt_browser[f"{LEADER}-R"] = f"{MOD}-R" # reload
|
|
321
|
+
|
|
322
|
+
# --- by window title -------------------------------------------------
|
|
323
|
+
# kt_docs = keymap.define_keytable(title="*Google Docs*")
|
|
324
|
+
|
|
325
|
+
# --- by Win32 window class (Windows only) -----------------------------
|
|
326
|
+
if not mac:
|
|
327
|
+
kt_notepad = keymap.define_keytable(app="notepad", class_name="Edit")
|
|
328
|
+
kt_notepad[f"{LEADER}-D"] = "Home", "Shift-Down", "Shift-End", "Delete"
|
|
329
|
+
|
|
330
|
+
# --- by focus path ----------------------------------------------------
|
|
331
|
+
# The focus path is the control hierarchy down to the focused element -
|
|
332
|
+
# the AX tree on macOS, the UI Automation tree on Windows. Watch the
|
|
333
|
+
# console's "Focus path" field to see the live value, and use "*" freely
|
|
334
|
+
# to skip levels.
|
|
335
|
+
# macOS : /AXApplication(Xcode)/AXWindow(...)/.../AXTextArea()
|
|
336
|
+
# Windows : /Application(Code)/Window(...)/.../Edit(Message input)
|
|
337
|
+
# Note the trailing "(*)": a component is "Role(Name)", and many controls
|
|
338
|
+
# do carry a name, so "*/Edit()" would only match unnamed ones.
|
|
339
|
+
kt_textarea = keymap.define_keytable(
|
|
340
|
+
focus_path_pattern="*/AXTextArea(*)" if mac else "*/Edit(*)")
|
|
341
|
+
kt_textarea[f"{LEADER}-Slash"] = InputText("# ")
|
|
342
|
+
|
|
343
|
+
# --- by your own test -------------------------------------------------
|
|
344
|
+
# custom_condition_func receives the portable Focus object: app_name,
|
|
345
|
+
# window_title, class_name (Windows), path, and element - the focused
|
|
346
|
+
# element in the OS's own vocabulary.
|
|
347
|
+
def is_terminal(focus):
|
|
348
|
+
if focus.app_name in ("Terminal", "iTerm2", "WindowsTerminal", "cmd",
|
|
349
|
+
"powershell", "pwsh"):
|
|
350
|
+
return True
|
|
351
|
+
# Element attributes differ per OS: AX names on macOS, UI Automation
|
|
352
|
+
# names on Windows. An unknown name simply reads back as None.
|
|
353
|
+
element = focus.element
|
|
354
|
+
if element is None:
|
|
355
|
+
return False
|
|
356
|
+
role = element.get_attribute_value("AXRole" if mac else "ControlType")
|
|
357
|
+
return role in ("AXTextArea", "Document")
|
|
358
|
+
|
|
359
|
+
kt_terminal = keymap.define_keytable(custom_condition_func=is_terminal)
|
|
360
|
+
kt_terminal[f"{LEADER}-K"] = "Ctrl-K" # clear, rather than "Down"
|
|
361
|
+
|
|
362
|
+
# ==================================================================
|
|
363
|
+
# More to explore
|
|
364
|
+
# ==================================================================
|
|
365
|
+
# keymap.focus - the current Focus (app_name, window_title,
|
|
366
|
+
# class_name, path, element, native)
|
|
367
|
+
# keymap.replay_buffer - the macro buffer behind the record actions
|
|
368
|
+
# keymap.clipboard_history.items() / set_current(text)
|
|
369
|
+
# Opening a URL or a file: import subprocess and run
|
|
370
|
+
# subprocess.Popen(["open", url] if mac else ["cmd", "/c", "start", url])
|
|
371
|
+
#
|
|
372
|
+
# Full reference: doc/configuration.md in the Keyhac 2 source tree.
|