Keyhac
Keyhac とは
Keyhac とは、Python を使ってキーカスタマイズが行えるアプリケーションです.
https://sites.google.com/site/craftware/keyhac-ja
定型文入力をスニペットのように入力できるツールとして、 aText を使っています.無料でも単純な定型文は入力することができますが、フル機能を使うためにはProバージョンにしなければならず、有料です.そんなに頻繁に利用するわけでもないですし、とりあえず他の方法でできないかと探していたところ、Keyhacにたどり着きました.
Keyhac のいいところは Python でカスタマイズできるところにあります.以前は Charu3 を使っていました.マクロは用意されていたりプラグインもあるので、ある程度カスタマイズができるのですが、やはり限界があります.Python が使えるなら、テキスト処理以外のこともいろいろできるので汎用性は高いです.
Keyhacの設定
Keyhacの設定方法を解説します.環境は Windows です.
まず、Keyhacを起動するとシステムトレイに常駐します.システムトレイからKeyhacアイコンを右クリックして、設定の編集
を選択すると、設定ファイル(config.py
)がエディタで開かれます.この設定ファイルを編集して、システムトレイから右クリック、設定のリロード
で反映されます.
コンソール画面
設定ファイルに問題があったり、実行したときにエラーが発生した場合はコンソール画面が表示されます.設定ファイルから print
すると、このコンソール画面が表示されます.画面が表示されていない場合、システムトレイのKeyhacアイコンをダブルクリックすると表示されます.コンソール画面をクリアしたい場合、システムトレイからKeyhacアイコンを右クリックして端末のクリア
を選択します.また、内部ログをON
を選択すると、ログ情報がコンソール画面に出力されます.無効にしたいときは内部ログをOFF
を選択します.
configure関数
Keyhacを起動すると、設定ファイル(config.py
)にあるconfigure
関数が呼ばれます.ここに処理を記述してきます.
エディタの設定
設定ファイルの編集に使うエディタを設定します.Visual Studio Codeは開く場合は次のようにします.
keymap.editor = "code"
フォントの設定
コンソールなどのウィンドウに表示されるテキストのフォントを指定します.好きなフォントを指定してください.フォントを指定すると、起動に少し時間がかかるようになります.
keymap.setFont("Iosevka", 16)
テーマの設定
標準では white
と black
が用意されています.
keymap.setTheme("black")
キーの再登録
Keyhacではキーを別のキーに割り当てることができます.それにはkeymap.replaceKey
メソッドを使います.私の場合、無変換
キーと変換キー
を適当な場所に再割り当てし、ユーザー修飾キーとしてそれらを登録します.ユーザー修飾キーの登録は keymap.defineModifier
メソッドを使います.
# Key replacement
keymap.replaceKey("(29)", 235) # 無変換
keymap.replaceKey("(28)", 236) # 変換
# User modifier key definition
keymap.defineModifier(235, "User0")
keymap.defineModifier(236, "User1")
グローバルキーマップ
Keyhacではウィンドウごとにキーカスタマイズを行うことができますし、すべてのウィンドウに対してキーカスタマイズできます.後者の場合、keymap.defineWindowKeymap
メソッドで取得したオブジェクトで設定します.
# Global keymap which affects any windows
keymap_global = keymap.defineWindowKeymap()
例えば、Shift
キーとZ
キーを同時に押したときの処理を設定したい場合、次のようになります.
keymap_global["S-Z"] = closure
キーに対応したクロージャを指定します.Keyhacはキーが押されたときに対応したクロージャを呼び出します.
指定できるキーやモディファイアについてはドキュメントを参照してください.
初期の設定
Keyhacでは標準でいくつか設定されています.まずは全部消すか、コメントアウトしておきましょう.
Keyhacモジュール
pyauto
とkeyhac
はKeyhacに用意されたモジュールです.pyauto
は低レベルOS機能で、keyhac
はそれ以外のものです.例えば、クリップボードを操作する場合、keyhac
の setClipboardText
, getClipboardText
を使います.
from keyhac import setClipboardText, getClipboardText
面倒なら、一括でインポートしても構いません.
from keyhac import *
これらのモジュールについて、詳しくはドキュメントを参照してください.
Keyhacを使って emacs ライクなキー操作を実現している方がいます.設定ファイルも参考になると思います.
https://github.com/smzht/fakeymacs
実装した機能の紹介
それでは、次から具体的な設定をしていきます.といっても、あまり使いこなしている訳ではありませんので悪しからず. ヘル パー関数は、それぞれ初出した場所に明記しています.
カーソルをスクリーン中央に移動する
Shift
キーと右Ctrl
キーを押したときに実行します.
def set_cursor_pos(x, y):
keymap.beginInput()
keymap.input_seq.append(pyauto.MouseMove(x, y))
keymap.endInput()
def cursor_to_center():
wnd = keymap.getTopLevelWindow()
wnd_left, wnd_top, wnd_right, wnd_bottom = wnd.getRect()
to_x = int((wnd_left + wnd_right) / 2)
to_y = int((wnd_bottom + wnd_top) / 2)
set_cursor_pos(to_x, to_y)
keymap_global["S-RCtrl"] = cursor_to_center
ウィンドウをスクリーン中央に移動する
左右のCtrl
キーを押したときに実行します.
def delay(sec=0.05):
time.sleep(sec)
def get_monitor_areas():
monitors = pyauto.Window.getMonitorInfo()
main_monitor_first = sorted(monitors, key=lambda x: x[2], reverse=True)
non_taskbar_areas = list(map(lambda x: x[1], main_monitor_first))
return non_taskbar_areas
def set_window_rect(rect):
wnd = keymap.getTopLevelWindow()
if list(wnd.getRect()) == rect:
wnd.maximize()
else:
if wnd.isMaximized():
wnd.restore()
delay()
wnd.setRect(rect)
def window_to_center():
wnd = keymap.getTopLevelWindow()
if wnd.isMaximized():
return None
wnd_left, wnd_top, wnd_right, wnd_bottom = wnd.getRect()
width = wnd_right - wnd_left
height = wnd_bottom - wnd_top
mntr_left, mntr_top, mntr_right, mntr_bottom = get_monitor_areas()[0]
center_h = (mntr_right - mntr_left) / 2
center_v = (mntr_bottom - mntr_top) / 2
lx = int(center_h - width / 2)
ly = int(center_v - height / 2)
to_rect = (lx, ly, lx + width, ly + height)
set_window_rect(to_rect)
keymap_global["C-RCtrl"] = window_to_center
もし、ウィンドウを最大化していた場合、先に通常のウィンドウに戻す必要があります.その場合、次のようにします.
def window_to_center_force():
wnd = keymap.getTopLevelWindow()
if wnd.isMaximized():
wnd.restore()
delay()
window_to_center()
ウィンドウをカーソル位置に移動する
無変換
キーと右Ctrl
キーを押したときに実行します.
def window_to_cursor():
wnd = keymap.getTopLevelWindow()
if wnd.isMaximized():
return None
wnd_left, wnd_top, wnd_right, wnd_bottom = wnd.getRect()
width = wnd_right - wnd_left
height = wnd_bottom - wnd_top
x, y = pyauto.Input.getCursorPos()
to_rect = (x, y, x + width, y + height)
set_window_rect(to_rect)
keymap_global["U0-RCtrl"] = window_to_cursor
ウィンドウを切り替える
Alt+Tab
やWin+Tab
のような機能です.Keyhacではリストを表示するウィンドウ機能がありますので、そちらを使います.今回は、無変換
キーとスペース
キーを押したときに実行します.
# import re
# from keyhac import cblister_FixedPhrase
debug_mode = False # デバッグ出力を有効にする場合は True にする
def dbg(text):
if debug_mode:
print("dbg: " + text)
def truncate(string, length, ellipsis="..."):
return string[:length] + (ellipsis if string[length:] else "")
def truncate_cjk(string, length, ellipsis="..."):
# http://www.unicode.org/reports/tr11/
count = 0
text = ""
for c in string:
if unicodedata.east_asian_width(c) in "FWA":
count += 2
else:
count += 1
if count > length:
text += ellipsis
break
text += c
return text
def switch_windows():
dbg(">>>>> switch_windows <<<<<")
def popWindowList():
# If the list window is already opened, just close it
if keymap.isListWindowOpened():
keymap.cancelListWindow()
return
def getWindowList(wnd, arg):
if not wnd.isVisible():
return True
# if not wnd.getOwner():
# return True
if wnd.getText() == "":
return True
dbg(wnd.getProcessName())
dbg(" " + wnd.getClassName())
dbg(" " + wnd.getText())
if re.match(
r"(keyhac|SystemSettings|ApplicationFrameHost|TextInputHost|explorer|onenoteim)\.exe",
wnd.getProcessName(),
):
dbg("(pass)")
return True
# if re.match(r"chrome", wnd.getClassName()):
# window_list.append(wnd)
window_list.append(wnd)
return True
window_list = []
Window.enum(getWindowList, None)
popup_list = [
("{:>20s} :: {}".format(truncate(i.getProcessName()[:-4], 17), truncate_cjk(i.getText(), 45)), i)
for i in sorted(window_list, key=lambda x: x.getProcessName())
]
if mysetting.debug:
for i in popup_list:
dbg(i[0])
listers = [("Windows", cblister_FixedPhrase(popup_list))]
item, mod = keymap.popListWindow(listers)
if item:
item[1].setForeground()
# Because the blocking procedure cannot be executed in the key-hook,
# delayed-execute the procedure by delayedCall().
keymap.delayedCall(popWindowList, 0)
keymap_global["U0-Space"] = switch_windows
実行結果は次のようになります.
正規表現(Line:47)を使って表示されるウィンドウを制限することもできます.
ちなみに、以前はTascherというのを使っていたのですが、Keyhacに置き換えました.
Keyhacのウィンドウ機能は、実は複数のリストを内部で持つことができます.以下のコードにあるように、listers
に配列を設定しています.
listers = [("Windows", cblister_FixedPhrase(popup_list))]
複数指定した場合、キーボードの左右キーで切り替えることができます.
ブラウザ(Chrome)を切り替える
Chromeウィンドウを次々に切り替える機能です.無変換
キーとF7
キーに割り当てています.
# import time
# from keyhac import Window
chrome_history = []
def next_chrome_window():
dbg(">>>>> next_chrome_window <<<<<")
def getWindowList(wnd, arg):
if not wnd.isVisible():
return True
# if not wnd.getOwner():
# return True
if wnd.getText() == "":
return True
# dbg(wnd.getProcessName())
if re.match(r"chrome", wnd.getProcessName()):
window_list.append(wnd)
# if re.match(r"chrome", wnd.getClassName()):
# window_list.append(wnd)
# window_list.append(wnd)
return True
now = time.time_ns() / 1_000_000 # ms
history = sorted(chrome_history, key=lambda x: x[1], reverse=True)
if len(history) > 0:
dbg("{}".format(now - history[0][1]))
if now - history[0][1] > 5_000: # 5s
dbg("clear chrome history")
chrome_history = []
history = []
if debug:
for entry in history:
dbg(f"{entry[0]}, {entry[1]}")
next_window = (None, -1)
window_list = []
Window.enum(getWindowList, None)
current_wnd_text = keymap.getTopLevelWindow().getText()
dbg(f"[current] {current_wnd_text}")
for wnd in window_list:
text = wnd.getText()
if current_wnd_text == text:
continue
idx = [i for i, e in enumerate(history) if e[0] == text]
if len(idx) > 0:
if next_window[1] < idx[0]:
dbg(f"found in history: {text}({idx[0]})")
next_window = (wnd, idx[0])
else:
dbg(f"not found in history: {text}")
next_window = (wnd, len(history))
break
if next_window[0]:
dbg("[next] " + next_window[0].getText())
chrome_history.append((next_window[0].getText(), now))
next_window[0].setForeground()
keymap_global["U0-F7"] = next_chrome_window
アプリケーションを実行する
keymap.ShellExecuteCommand
メソッドを使います.
keymap.ShellExecuteCommand(verb, file, param, directory, swmode);
私は次のようなヘルパー関数を定義して使っています.
def shell(s, arg=None):
return keymap.ShellExecuteCommand(None, s, arg, None)
def shell_mini(s, arg=None):
return keymap.ShellExecuteCommand(None, s, arg, None, "minimized")
例えば、Chromeで特定のURLを開きたい場合は次のようにします.
keymap_global["U0-F9"] = shell(
"C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe",
"--new-window https://www.google.com/webhp?hl=ja",
)
VSCodeの選択範囲拡大(x2)
Visual Studio Code で Shift+Alt+右
キーは選択範囲の拡大になります.ちょうど2回実行すると、いい感じに選択できることが多いため重宝しています.しかし、Shift+Alt+右
キーは押しづらいです.そこで、無変換
キーと右
キー押しで2回分のShift+Alt+右
キーを押すようにします.
def ikey(*keys):
return keymap.InputKeyCommand(*keys)
keymap_global["U0-Right"] = ikey("S-A-Right", "S-A-Right")
マルチストロークキー
Keyhacではマルチストロークキーをサポートしています.keymap.defineMultiStrokeKeymap
で最初のキーを登録します.例えば、無変換
キーとD
キーを押して、日付をフォーマットを指定して入力したい場合、次のようになります.
def send_keys(*keys):
keymap.beginInput()
for key in keys:
keymap.setInput_FromString(str(key))
keymap.endInput()
keymap._fixFunnyModifierState()
def set_ime(mode):
if keymap.getWindow().getImeStatus() != mode:
send_keys("(243)")
delay(0.01)
def input_date(fmt):
def _input_data():
d = datetime.datetime.today()
date_str = d.strftime(fmt)
set_ime(0)
send_input(date_str, 0)
return _input_data
keymap_global["U0-D"] = keymap.defineMultiStrokeKeymap("DATE: [1]YMD, [2]Y/M/D, [3]Y-M-D")
for key, args in {
"1": ["%Y%m%d"],
"2": ["%Y/%m/%d"],
"3": ["%Y-%m-%d"],
}.items():
keymap_global["U0-D"][key] = input_date(*args)
実行すると、アクティブなウィンドウの下に DATE: [1]YMD, [2]Y/M/D, [3]Y-M-D
が表示され、次のキー入力待ちになります.ここでは 1
、2
、3
キーのどれかを押すと、それぞれの書式に対応した日付が入力されます.
他に、括弧の入力支援は次のようになります.(無変換
キーとP
キー)
def send_string(s):
keymap.beginInput()
keymap.setInput_Modifier(0)
for c in s:
keymap.input_seq.append(pyauto.Char(c))
keymap.endInput()
def send_input(sequence, sleep=0.01):
for elem in sequence:
delay(sleep)
try:
send_keys(elem)
except:
send_string(elem)
def ime_input0(*sequence):
def _ime_input0():
set_ime(0)
send_input(sequence)
return _ime_input0
def ime_input1(*sequence):
def _ime_input1():
set_ime(1)
send_input(sequence)
return _ime_input1
keymap_global["U0-P"] = keymap.defineMultiStrokeKeymap("PARENTHES: [1]《》 [2]〈〉 [3]〔〕 [4]『』 [5]【】 [6]() [7]()")
for key, args in {
"1": ["《》", "Left"],
"2": ["〈〉", "Left"],
"3": ["〔〕", "Left"],
"4": ["『』", "Left"],
"5": ["【】", "Left"],
"6": ["()", "Left"],
"7": ["()", "Left"],
}.items():
keymap_global["U0-P"][key] = ime_input1(*args)
日本語入力時に半角の括弧を入れる
通常は全角の括弧(()
)ですが、半角を入れるようにできます.
def ime_context(func):
def _ime_context():
mode = keymap.getWindow().getImeStatus()
func()
set_ime(mode)
return _ime_context
keymap_global["U0-8"] = ime_context(ime_input0("S-8"))
keymap_global["U0-9"] = ime_context(ime_input0("S-9"))
無変換
キーと8
キーで(
が、無変換
キーと9
キーで)
が日本語入力時に入れられます.
同じやり方で、/
を/
、「」
を[]
と入力できます.
keymap_global["U0-OpenBracket"] = ime_context(ime_input0("OpenBracket"))
keymap_global["U0-CloseBracket"] = ime_context(ime_input0("CloseBracket"))
keymap_global["U0-Slash"] = ime_context(ime_input0("Slash"))
インクリメンタルサーチ
Keyhacはウィンドウを表示したときにインクリメンタルサーチを使えるのですが、検索モード(f
キー)に入らないと使えないので不便です.そこで、ウィンドウを表示したときに自動で検索モードに入るようにしたいのですができません.また、ウィンドウはスクリーンの中央に表示してほしいのですが、標準ではスクリーンの原点(つまり左上)に表示されます.これがとても不満だったのですが、なんとか妥協できるレベルにできました.それは、ウィンドウを表示するキーを2回連続で押すことで、ウィンドウを中央に移動して、検索モードに入るようにすることです.次のようにします.
def lw_search():
ikey("F")()
lw_search_mode = True
def lw_exit():
if lw_search_mode:
ikey("Esc", "Esc")()
else:
ikey("Esc")()
def is_list_window(window):
if window.getClassName() == "KeyhacWindowClass" and window.getText() != "Keyhac":
lw_search_mode = False
return True
else:
return False
keymap_lw = keymap.defineWindowKeymap(check_func=is_list_window)
keymap_lw["U0-N"] = ikey("Down")
keymap_lw["U0-P"] = ikey("Up")
keymap_lw["U0-AtMark"] = lambda: [window_to_center(), lw_search()]
keymap_lw["U0-Space"] = lambda: [window_to_center(), lw_search()]
keymap_lw["U0-Semicolon"] = lambda: [window_to_center(), lw_search()]
keymap_lw["Enter"] = ikey("Enter", "Enter")
keymap_lw["Escape"] = lw_exit