Skip to content

Commit

Permalink
new keymap/conditional API, now supports device conditionals
Browse files Browse the repository at this point in the history
  • Loading branch information
joshgoebel committed Jun 5, 2022
1 parent cae3135 commit 6f3e1a5
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 85 deletions.
40 changes: 34 additions & 6 deletions tests/test_keymap_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ def setup_function(module):
reset_transform()

@pytest.mark.looptime(False)
async def test_multiple_keys_at_once():
async def test_OLD_API_multiple_keys_at_once():

window("Firefox")
keymap(re.compile("Firefox"),{
define_keymap(re.compile("Firefox"),{
K("C-M-j"): K("C-TAB"),
K("C-M-k"): K("C-Shift-TAB"),
})
Expand All @@ -55,15 +55,43 @@ async def test_multiple_keys_at_once():
(RELEASE, Key.LEFT_CTRL),
]

@pytest.mark.looptime(False)
async def test_multiple_keys_at_once():

window("Firefox")
conditional(lambda ctx: re.compile("Firefox").search(ctx.wm_class),
keymap("Firefox",{
K("C-M-j"): K("C-TAB"),
K("C-M-k"): K("C-Shift-TAB"),
})
)

boot_config()

press(Key.LEFT_CTRL)
press(Key.LEFT_ALT)
press(Key.J)
release(Key.J)
release(Key.LEFT_ALT)
release(Key.LEFT_CTRL)
assert _out.keys() == [
(PRESS, Key.LEFT_CTRL),
(PRESS, Key.TAB),
(RELEASE, Key.TAB),
(RELEASE, Key.LEFT_CTRL),
]

@pytest.mark.looptime(False)
async def test_multiple_combos_without_releasing_all_nonsticky():
# NOTE: if we were sticky then techcanily the C on the output
# should probalby be held without release
window("Firefox")
keymap(re.compile("Firefox"),{
K("C-M-j"): K("C-TAB"),
K("C-M-k"): K("C-Shift-TAB"),
})
conditional(lambda ctx: re.compile("Firefox").search(ctx.wm_class),
keymap("Firefox",{
K("C-M-j"): K("C-TAB"),
K("C-M-k"): K("C-Shift-TAB"),
})
)

boot_config()

Expand Down
103 changes: 56 additions & 47 deletions xkeysnail/config_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .key import Action, Combo, Key, Modifier
from .lib.modmap import Modmap
from .lib.keymap import Keymap
from sys import exit
from .logger import *

Expand Down Expand Up @@ -169,41 +170,45 @@ def modmap(name, mappings):
return mm


def define_conditional_modmap(condition, mappings):
"""Defines conditional modmap (keycode translation)
Example:
define_conditional_modmap(re.compile(r'Emacs'), {
Key.CAPSLOCK: Key.LEFT_CTRL
})
"""
def old_style_condition_to_fn(condition):
condition_fn = None
def re_search(re):
def fn(ctx):
print("running re_search", ctx)
return re.search(ctx["wm_class"])
return re.search(ctx.wm_class)
return fn

def wm_class(wm_class_fn):
def fn(ctx):
return wm_class_fn(ctx["wm_class"])
return wm_class_fn(ctx.wm_class)
return fn

def wm_class_and_device(cond_fn):
def fn(ctx):
return cond_fn(ctx["wm_class"], ctx["device_name"])
return cond_fn(ctx.wm_class, ctx.device_name)
return fn


name = "define_conditional_modmap (old API)"
if hasattr(condition, 'search'):
condition_fn = re_search(condition)
elif callable(condition):
if len(signature(condition).parameters) == 1:
condition_fn = wm_class(condition)
elif len(signature(condition).parameters) == 2:
condition_fn = wm_class_and_device(condition)

return condition_fn

def define_conditional_modmap(condition, mappings):
"""Defines conditional modmap (keycode translation)
Example:
define_conditional_modmap(re.compile(r'Emacs'), {
Key.CAPSLOCK: Key.LEFT_CTRL
})
"""

condition_fn = old_style_condition_to_fn(condition)
name = "define_conditional_modmap (old API)"

if not callable(condition_fn):
raise ValueError('condition must be a function or compiled regexp')
Expand Down Expand Up @@ -251,8 +256,7 @@ def define_conditional_multipurpose_modmap(condition, multipurpose_remappings):

# ============================================================ #


def define_keymap(condition, mappings, name="Anonymous keymap"):
def keymap(name, mappings):
global _toplevel_keymaps

# Expand not L/R-specified modifiers
Expand All @@ -272,43 +276,48 @@ def define_keymap(condition, mappings, name="Anonymous keymap"):
# K("LC-d"): Key.C,
# K("RC-d"): Key.C}}
def expand(target):
if isinstance(target, dict):
expanded_mappings = {}
keys_for_deletion = []
for k, v in target.items():
# Expand children
expand(v)

if isinstance(k, Combo):
expanded_modifiers = []
for modifier in k.modifiers:
if not modifier.is_specified():
expanded_modifiers.append([modifier.to_left(), modifier.to_right()])
else:
expanded_modifiers.append([modifier])

# Create a Cartesian product of expanded modifiers
expanded_modifier_lists = itertools.product(*expanded_modifiers)
# Create expanded mappings
for modifiers in expanded_modifier_lists:
expanded_mappings[Combo(set(modifiers), k.key)] = v
keys_for_deletion.append(k)

# Delete original mappings whose key was expanded into expanded_mappings
for key in keys_for_deletion:
del target[key]
# Merge expanded mappings into original mappings
target.update(expanded_mappings)
if not isinstance(target, dict):
return None
expanded_mappings = {}
keys_for_deletion = []
for k, v in target.items():
# Expand children
expand(v)

if isinstance(k, Combo):
expanded_modifiers = []
for modifier in k.modifiers:
if not modifier.is_specified():
expanded_modifiers.append([modifier.to_left(), modifier.to_right()])
else:
expanded_modifiers.append([modifier])

# Create a Cartesian product of expanded modifiers
expanded_modifier_lists = itertools.product(*expanded_modifiers)
# Create expanded mappings
for modifiers in expanded_modifier_lists:
expanded_mappings[Combo(set(modifiers), k.key)] = v
keys_for_deletion.append(k)

# Delete original mappings whose key was expanded into expanded_mappings
for key in keys_for_deletion:
del target[key]
# Merge expanded mappings into original mappings
target.update(expanded_mappings)

expand(mappings)

_toplevel_keymaps.append((condition, mappings, name))
return mappings
km = Keymap(name, mappings)
_toplevel_keymaps.append(km)
return km

def define_keymap(condition, mappings, name="Anonymous keymap"):
condition_fn = old_style_condition_to_fn(condition)
return conditional(condition_fn, keymap(name, mappings))

# aliases

timeout = define_timeout
keymap = define_keymap
conditional_modmap = define_conditional_modmap
multipurpose_modmap = define_multipurpose_modmap
conditional_multipurpose_modmap = define_conditional_multipurpose_modmap
Expand Down
3 changes: 3 additions & 0 deletions xkeysnail/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,9 @@ def __hash__(self):
def __str__(self):
return "-".join([str(mod) for mod in self.modifiers] + [self.key.name])

def __repr__(self):
return self.__str__()

def with_modifier(self, modifiers):
if isinstance(modifiers, Modifier):
modifiers = {modifiers}
Expand Down
17 changes: 17 additions & 0 deletions xkeysnail/lib/key_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ..xorg import get_active_window_wm_class

class KeyContext:
def __init__(self, device_name):
self._device_name = device_name
self._wm_class = None

def get_wm_class(self):
# cache this, think it might be expensive
self._wm_class = self._wm_class or get_active_window_wm_class()
return self._wm_class

def get_device_name(self):
return self._device_name

wm_class = property(fget=get_wm_class)
device_name = property(fget=get_device_name)
12 changes: 12 additions & 0 deletions xkeysnail/lib/keymap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Keymap:
def __init__(self, name, mappings):
self.name = name
self.mappings = mappings
self.conditional = None

def __contains__(self, key):
return key in self.mappings

def __getitem__(self, item):
return self.mappings[item]

Loading

0 comments on commit 6f3e1a5

Please sign in to comment.