Last active
March 29, 2019 03:07
-
-
Save caspark/9c2c5e2853a14b6e28e9aa4f121164a6 to your computer and use it in GitHub Desktop.
Window switching grammar for Dragonfly
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""A grammar to swap windows by saying words in their title. | |
Uses a natlink timer to periodically load the list of open windows into a DictList, | |
so they can be referenced by the "switch window" command. | |
Commands: | |
"switch window <keyword>" -> switch to the window with the given word in its | |
title. If multiple windows have that word in | |
their title, then you can say more words in the | |
window's title to disambiguate which one you | |
mean. If you don't, the Natlink window will be | |
foregrounded instead with info on which windows | |
are ambiguously being matched by your keywords. | |
"refresh windows" -> manually reload the list of windows. Useful while | |
developing if you don't want to use the timer | |
"debug window switching" -> output debug information about which keywords can | |
be used on their own to switch windows and which | |
require multiple words. | |
There may be some bugs as I extracted it out from my own grammar, so I had to inline some util functions etc. | |
""" | |
from __future__ import print_function | |
import datetime | |
import dragonfly | |
import re | |
open_windows_dictlist = dragonfly.DictList("open_windows") | |
WORD_SPLITTER = re.compile('[^a-zA-Z0-9]+') | |
def lower_if_not_abbreviation(s): | |
if len(s) <= 4 and s.upper() == s: | |
return s | |
else: | |
return s.lower() | |
def find_window(window_matcher_func, timeout_ms=3000): | |
"""Returns a dragonfly Window matching the given matcher function, or raises an error otherwise""" | |
steps = timeout_ms / 100 | |
for i in range(steps): | |
for win in dragonfly.Window.get_all_windows(): | |
if window_matcher_func(win): | |
return win | |
time.sleep(0.1) | |
raise ValueError( | |
"no matching window found within {} ms".format(timeout_ms)) | |
def refresh_open_windows_dictlist(): | |
window_options = {} | |
for window in (x for x in dragonfly.Window.get_all_windows() if | |
x.is_valid and | |
x.is_enabled and | |
x.is_visible and | |
not x.executable.startswith("C:\\Windows") and | |
x.classname != "DgnResultsBoxWindow"): | |
for word in {lower_if_not_abbreviation(word) | |
for word | |
in WORD_SPLITTER.split(window.title) | |
if len(word)}: | |
if word in window_options: | |
window_options[word] += [window] | |
else: | |
window_options[word] = [window] | |
window_options = {k: v for k, | |
v in window_options.iteritems() if v is not None} | |
open_windows_dictlist.set(window_options) | |
def debug_window_switching(): | |
options = open_windows_dictlist.copy() | |
print("*** Windows known:\n", | |
"\n".join(sorted({w.title for list_of_windows in options.itervalues() for w in list_of_windows}))) | |
print("*** Single word switching options:\n", "\n".join( | |
"{}: '{}'".format( | |
k.ljust(20), "', '".join(window.title for window in options[k]) | |
) for k in sorted(options.iterkeys()) if len(options[k]) == 1)) | |
print("*** Ambiguous switching options:\n", "\n".join( | |
"{}: '{}'".format( | |
k.ljust(20), "', '".join(window.title for window in options[k]) | |
) for k in sorted(options.iterkeys()) if len(options[k]) > 1)) | |
def switch_window(windows): | |
matched_window_handles = {w.handle: w for w in windows[0]} | |
for window_options in windows[1:]: | |
matched_window_handles = { | |
w.handle: w for w in window_options if w.handle in matched_window_handles} | |
matched_windows = matched_window_handles.values() | |
if len(matched_windows) == 1: | |
window = matched_windows[0] | |
print("Switcher: switching window to", window.title) | |
window.set_foreground() | |
else: | |
try: | |
natlink_window = find_window( | |
lambda w: "Messages from Python Macros V15 - " in w.title, timeout_ms=100) | |
if natlink_window.is_minimized: | |
natlink_window.restore() | |
else: | |
natlink_window.set_foreground() | |
except ValueError: | |
# window didn't exist, it'll be created when we write some output | |
pass | |
print("Ambiguous switch command:\n", "\n".join( | |
"'{}' from {} (handle: {})".format(w.title, w.executable, w.handle) for w in matched_windows)) | |
class SwitcherRule(dragonfly.MappingRule): | |
mapping = { | |
"switch window <windows>": dragonfly.Function(switch_window), | |
"refresh windows": dragonfly.Function(refresh_open_windows_dictlist), | |
"debug window switching": dragonfly.Function(debug_window_switching), | |
} | |
extras = [ | |
dragonfly.Repetition(name="windows", min=1, max=5, | |
child=dragonfly.DictListRef("window_by_keyword", open_windows_dictlist)) | |
] | |
grammar = dragonfly.Grammar('window_switcher') | |
grammar.add_rule(SwitcherRule()) | |
grammar.load() | |
timer = dragonfly.get_engine().create_timer(refresh_open_windows_dictlist, 1) | |
print('Switcher grammar: loaded at ' + str(datetime.datetime.now())) | |
def unload(): | |
global grammar, timer | |
if timer: | |
timer.stop() | |
if grammar: | |
grammar.unload() | |
print('Switcher grammar: unloaded') | |
grammar = None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment