Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permit keymap conditions to test against wm_name #129

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ application and takes one of the following forms:
- Regular expression (e.g., `re.compile("YYY")`)
- Activates the `mappings` if the pattern `YYY` matches the `WM_CLASS` of the application.
- Case Insensitivity matching against `WM_CLASS` via `re.IGNORECASE` (e.g. `re.compile('Gnome-terminal', re.IGNORECASE)`)
- `lambda wm_class: some_condition(wm_class)`
- Activates the `mappings` if the `WM_CLASS` of the application satisfies the condition specified by the `lambda` function.
- Case Insensitivity matching via `casefold()` or `lambda wm_class: wm_class.casefold()` (see example below to see how to compare to a list of names)
- Lambda based condition utilizing `WM_CLASS`, device name, and `WM_NAME`. The lambda may have 1, 2, or 3 arguments, e.g:
- `lambda wm_class: some_condition(...)`
- `lambda wm_class, device_name : some_condition(...)`
- `lambda wm_class, device_name, wm_name: some_condition(...)`
- Activates the `mappings` if the specified by the `lambda` function is satisfied.
- For example: case-insensitivity matching via `lambda wm_class: wm_class.casefold()` (see example below to see how to compare to a list of names)
- `None`: Refers to no condition. `None`-specified keymap will be a global keymap and is always enabled.

Argument `mappings` is a dictionary in the form of `{key: command, key2:
Expand Down
7 changes: 7 additions & 0 deletions example/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
Key.RIGHT_SHIFT: [Key.KPRIGHTPAREN, Key.RIGHT_SHIFT]
})

# Example keymap conditional on window name.
# Google Docs / Sheets browser applications use M-slash as an execute-extended-command binding
# Let's make it more emacsish
define_keymap(lambda wm_class, device_name, wm_name: any(app in wm_name for app in ["Google Docs", "Google Sheets"]), {
K("M-x"): K("M-slash")
}, "Google Suite")


# Keybindings for Firefox/Chrome
define_keymap(re.compile("Firefox|Google-chrome"), {
Expand Down
71 changes: 47 additions & 24 deletions xkeysnail/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,32 @@
import Xlib.display


def get_active_window_wm_class(display=Xlib.display.Display()):
"""Get active window's WM_CLASS"""
def get_active_window_wm_info(display=Xlib.display.Display()):
"""Get active window's WM_CLASS, WM_NAME"""
current_window = display.get_input_focus().focus
pair = get_class_name(current_window)
wmname = get_window_name(current_window, display)
if pair:
# (process name, class name)
return str(pair[1])
return str(pair[1]), str(wmname)
else:
return ""
return "", ""


def get_window_name(window, display):
"""Get window's name (recursively checks parents)"""
try:
wmname = window.get_full_text_property(
display.intern_atom('_NET_WM_NAME'),
display.get_atom('UTF8_STRING'))

if (wmname is None):
parent_window = window.query_tree().parent
if parent_window:
return get_window_name(parent_window, display)
return None
return wmname
except:
return None


def get_class_name(window):
Expand Down Expand Up @@ -377,57 +394,63 @@ def maybe_press_modifiers(multipurpose_map):
_last_key = key


def test_condition(condition, device_name=None, wm_class=None, wm_name=None):
# This is a little ugly, but backward compatible
params = [wm_class]
if len(signature(condition).parameters) == 2:
params = [wm_class, device_name]
if len(signature(condition).parameters) == 3:
params = [wm_class, device_name, wm_name]
if condition(*params):
# print(f'tested {params}')
return True
return False


def on_event(event, device_name, quiet):
key = Key(event.code)
action = Action(event.value)
wm_class = None
wm_name = None
# translate keycode (like xmodmap)
active_mod_map = _mod_map
if _conditional_mod_map:
wm_class = get_active_window_wm_class()
wm_class, wm_name = get_active_window_wm_info()
for condition, mod_map in _conditional_mod_map:
params = [wm_class]
if len(signature(condition).parameters) == 2:
params = [wm_class, device_name]

if condition(*params):
if test_condition(condition, device_name, wm_class, wm_name):
active_mod_map = mod_map
break
if active_mod_map and key in active_mod_map:
key = active_mod_map[key]

active_multipurpose_map = _multipurpose_map
if _conditional_multipurpose_map:
wm_class = get_active_window_wm_class()
wm_class, wm_name = get_active_window_wm_info()
for condition, mod_map in _conditional_multipurpose_map:
params = [wm_class]
if len(signature(condition).parameters) == 2:
params = [wm_class, device_name]

if condition(*params):
if test_condition(condition, device_name, wm_class, wm_name):
active_multipurpose_map = mod_map
break
if active_multipurpose_map:
multipurpose_handler(active_multipurpose_map, key, action)
if key in active_multipurpose_map:
return

on_key(key, action, wm_class=wm_class, quiet=quiet)
on_key(key, action, device_name=device_name, wm_class=wm_class, wm_name=wm_name, quiet=quiet)
update_pressed_keys(key, action)


def on_key(key, action, wm_class=None, quiet=False):
def on_key(key, action, device_name=None, wm_class=None, wm_name=None, quiet=False):
if key in Modifier.get_all_keys():
update_pressed_modifier_keys(key, action)
send_key_action(key, action)
elif not action.is_pressed():
if is_pressed(key):
send_key_action(key, action)
else:
transform_key(key, action, wm_class=wm_class, quiet=quiet)
transform_key(key, action, device_name=device_name, wm_class=wm_class, wm_name=wm_name, quiet=quiet)


def transform_key(key, action, wm_class=None, quiet=False):
def transform_key(key, action, device_name=None, wm_class=None, wm_name=None, quiet=False):
global _mode_maps
global _toplevel_keymaps

Expand All @@ -445,16 +468,16 @@ def transform_key(key, action, wm_class=None, quiet=False):
is_top_level = True
_mode_maps = []
if wm_class is None:
wm_class = get_active_window_wm_class()
wm_class, wm_name = get_active_window_wm_info()
keymap_names = []
for condition, mappings, name in _toplevel_keymaps:
if (callable(condition) and condition(wm_class)) \
if (callable(condition) and test_condition(condition, device_name, wm_class, wm_name)) \
or (hasattr(condition, "search") and condition.search(wm_class)) \
or condition is None:
_mode_maps.append(mappings)
keymap_names.append(name)
if not quiet:
print("WM_CLASS '{}' | active keymaps = [{}]".format(wm_class, ", ".join(keymap_names)))
print("WM_CLASS '{}' WM_NAME '{}' DEVICE_NAME '{}'| active keymaps = [{}]".format(wm_class, wm_name, device_name, ", ".join(keymap_names)))

if not quiet:
print(combo)
Expand Down