├── setup.cfg
├── easy_karabiner
├── __init__.py
├── def_tag_map.py
├── __main__.py
├── data
│ ├── def_tag_map.data
│ └── alias.data
├── fucking_string.py
├── query.pyi
├── parse.pyi
├── config.py
├── alias.py
├── exception.py
├── filter.py
├── keycombo.py
├── factory.pyi
├── generator.py
├── basexml.py
├── util.py
├── parse.py
├── definition.py
├── osxkit.py
├── main.py
├── query.py
├── keymap.py
└── factory.py
├── MANIFEST.in
├── .landscape.yml
├── examples
├── swap_alt_super_when_device.py
├── launcher_mode.py
├── basic.py
├── test.py
├── swap_alt_super_when_device.xml
├── launcher_mode.xml
├── basic.xml
├── test.xml
├── myconfig.py
└── myconfig.xml
├── .travis.yml
├── tests
├── test_main.py
├── test_query.py
├── test_filter.py
├── test_generator.py
├── test_parse.py
├── test_definition.py
└── test_factory.py
├── tox.ini
├── .gitignore
├── LICENSE.txt
├── setup.py
└── README.md
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
--------------------------------------------------------------------------------
/easy_karabiner/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.5.2'
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include easy_karabiner/data *
2 | recursive-exclude tests *
3 |
--------------------------------------------------------------------------------
/.landscape.yml:
--------------------------------------------------------------------------------
1 | pylint:
2 | disable:
3 | - unused-argument
4 | - redefined-builtin
5 | - arguments-differ
--------------------------------------------------------------------------------
/examples/swap_alt_super_when_device.py:
--------------------------------------------------------------------------------
1 | DEFINITIONS = {
2 | 'DeviceVendor::CHERRY': '0x046a',
3 | 'DeviceProduct::3494' : '0x0011',
4 | }
5 |
6 | MAPS = [
7 | ['alt', 'cmd', ('CHERRY', '3494')],
8 | ['cmd', 'alt', ['CHERRY', '3494']],
9 | ]
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | only:
3 | - master
4 | language: python
5 | python:
6 | - "2.7"
7 | - "3.3"
8 | - "3.4"
9 | - "3.5"
10 | install:
11 | - pip install lxml click
12 | cache:
13 | directories:
14 | - $HOME/.cache/pip
15 | script:
16 | - python setup.py install
17 |
--------------------------------------------------------------------------------
/examples/launcher_mode.py:
--------------------------------------------------------------------------------
1 | DEFINITIONS = {
2 | 'LAUNCHER_MODE_V2_EXTRA': [
3 | ['__KeyDownUpToKey__', 'C', 'Maps'],
4 | ['__KeyDownUpToKey__', 'V', 'FaceTime'],
5 | ]
6 | }
7 |
8 | MAPS = [
9 | ['__KeyOverlaidModifier__', 'O', 'config_sync_keydownup_notsave_launcher_mode_v2 launcher_mode_v2', 'O'],
10 | ]
11 |
--------------------------------------------------------------------------------
/examples/basic.py:
--------------------------------------------------------------------------------
1 | DEFINITIONS = {
2 | 'EMACS_IGNORE_APP': ['X11', 'GOOGLE_CHROME'],
3 | }
4 |
5 | MAPS = [
6 | ['ctrl B', 'left' , ['!EMACS_IGNORE_APP']],
7 | ['ctrl B', 'left' , ['!EMACS_IGNORE_APP']],
8 | ['cmd D', 'cmd_r D', ['VIRTUALMACHINE', 'X11']],
9 | ['_double_' , 'fn' , 'f12'],
10 | ['_double_' , 'fn' , 'cmd alt I' , ['GOOGLE_CHROME']],
11 | ['_holding_', 'esc', 'cmd_r ctrl_r alt_r shift_r', ['GOOGLE_CHROME']],
12 | ]
13 |
--------------------------------------------------------------------------------
/easy_karabiner/def_tag_map.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from . import util
4 | from . import config
5 |
6 |
7 | def _get_def_tag_maps():
8 | if not hasattr(_get_def_tag_maps, 'maps'):
9 | maps = util.read_python_file(config.get_data_path('def_tag_map.data'))['DEF_TAG_MAP']
10 | _get_def_tag_maps.maps = maps
11 | return _get_def_tag_maps.maps
12 |
13 |
14 | def _get(def_tag):
15 | return _get_def_tag_maps().get(def_tag, [None, None])
16 |
17 |
18 | def get_name_tag_name(def_tag):
19 | return _get(def_tag)[0]
20 |
21 |
22 | def get_filter_class_name(def_tag):
23 | return _get(def_tag)[1]
24 |
--------------------------------------------------------------------------------
/easy_karabiner/__main__.py:
--------------------------------------------------------------------------------
1 | # Used for test purpose
2 | if __name__ == '__main__':
3 | import os
4 | import sys
5 | from easy_karabiner.main import *
6 |
7 | args_num = len(sys.argv) - 1
8 |
9 | if args_num == 0:
10 | inpath = os.path.join(os.path.dirname(__file__), '..', 'examples/test.py')
11 | else:
12 | inpath = sys.argv[1]
13 |
14 | if args_num >= 2:
15 | outpath = sys.argv[2]
16 | string = False
17 | else:
18 | outpath = None
19 | string = True
20 |
21 | try:
22 | main.callback(inpath, outpath, verbose=True, string=string)
23 | except SystemExit as e:
24 | print(e.code)
25 |
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner.main import *
3 |
4 |
5 | def test_main():
6 | def cmd(*args, **kwargs):
7 | try:
8 | main.callback(*args, **kwargs)
9 | return 0
10 | except SystemExit as e:
11 | return e.code
12 |
13 | inpath = 'examples/test.py'
14 | outpath = 'examples/test.xml'
15 |
16 | assert(cmd(inpath, outpath, verbose=True, string=True) == 0)
17 | assert(cmd(inpath, outpath, help=True) == 0)
18 | assert(cmd(inpath, outpath, reload=True) == 0)
19 | assert(cmd(inpath, outpath, version=True) == 0)
20 | assert(cmd(inpath, outpath, list_peripherals=True) == 0)
21 |
--------------------------------------------------------------------------------
/easy_karabiner/data/def_tag_map.data:
--------------------------------------------------------------------------------
1 | DEF_TAG_MAP = {
2 | 'appdef' : ('appname', 'Filter'),
3 | 'replacementdef' : ('replacementname', 'ReplacementFilter'),
4 | 'devicevendordef' : ('vendorname', 'DeviceVendorFilter'),
5 | 'deviceproductdef' : ('productname', 'DeviceProductFilter'),
6 | 'windownamedef' : ('name', 'WindowNameFilter'),
7 | 'inputsourcedef' : ('name', 'InputSourceFilter'),
8 | 'vkchangeinputsourcedef': ('name', 'InputSourceFilter'),
9 | 'vkopenurldef' : ('name', None),
10 | 'modifierdef' : (None, 'ModifierFilter'),
11 | 'uielementroledef' : (None, 'UIElementRoleFilter'),
12 | }
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27,py35
3 |
4 | [testenv]
5 | passenv =
6 | COVERALLS_REPO_TOKEN
7 | deps =
8 | nose
9 | coveralls
10 | commands =
11 | nosetests --with-coverage --cover-package=easy_karabiner
12 | python easy_karabiner examples/basic.py examples/basic.xml
13 | python easy_karabiner examples/swap_alt_super_when_device.py examples/swap_alt_super_when_device.xml
14 | easy_karabiner --verbose examples/launcher_mode.py examples/launcher_mode.xml
15 | easy_karabiner --verbose examples/test.py examples/test.xml
16 | easy_karabiner --verbose examples/myconfig.py examples/myconfig.xml
17 | coveralls
18 |
--------------------------------------------------------------------------------
/examples/test.py:
--------------------------------------------------------------------------------
1 | DEFINITIONS = {
2 | 'error': 'because value not valid',
3 | # app
4 | 'BILIBILI': 'com.typcn.Bilibili',
5 | # replacement
6 | '可以是中文': ('比如', 'hello', 'Xee³'),
7 | # device
8 | 'CHERRY_3494': ['0x046a', '0x0011'],
9 | 'UIElementRole::自定义UI组件': '用作 filter',
10 | # modifierdef
11 | 'Modifier::KEYLOCK': '',
12 | }
13 |
14 | MAPS = [
15 | ['cmd', 'alt'],
16 | ['_double_', 'shift', '#! osascript -e \'display notification "test1"\''],
17 | ['alt mouse_left', 'mouse_left "#! osascript -e \'display notification \\"test2\\"\'"'],
18 | ['alt E', 'Finder', ['UIElementRole::自定义UI组件']],
19 | ['alt X', 'Xee³.app', ['Finder', 'cmd']],
20 | ['__FlipScrollWheel__', 'flipscrollwheel_vertical', ['Xee³.app', 'built_in_keyboard_and_trackpad']],
21 | ['ctrl cmd F', 'cmd F', ['VIRTUALMACHINE']],
22 | ['alt', 'none', ['KEYLOCK']],
23 | ['cmd', 'none', ['Modifier::KEYLOCK']],
24 | ]
25 |
--------------------------------------------------------------------------------
/easy_karabiner/fucking_string.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | import sys
4 | import codecs
5 |
6 | __all__ = ['ensure_utf8', 'u', 'write_utf8_to', 'is_string_type']
7 |
8 |
9 | # if python 3.x
10 | if sys.version_info[0] == 2:
11 | def write_utf8_to(s, outpath):
12 | with codecs.open(outpath, 'w', encoding='utf8') as fp:
13 | fp.write(s)
14 | else:
15 | basestring = str
16 | unicode = str
17 |
18 | def write_utf8_to(s, outpath):
19 | with open(outpath, 'w') as fp:
20 | fp.write(s)
21 |
22 |
23 | def is_string_type(s):
24 | return isinstance(s, (basestring, unicode, str))
25 |
26 |
27 | def ensure_utf8(s):
28 | # convert from any object to `unicode`
29 | if not isinstance(s, basestring):
30 | s = unicode(s)
31 |
32 | if isinstance(s, unicode):
33 | s = s.encode('utf-8')
34 | return unicode(s, encoding='utf-8')
35 |
36 |
37 | u = ensure_utf8
38 |
--------------------------------------------------------------------------------
/examples/swap_alt_super_when_device.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.5.2
4 | -
5 | Easy-Karabiner
6 |
7 | 3494
8 | 0x0011
9 |
10 |
11 | CHERRY
12 | 0x046a
13 |
14 |
-
15 | Enable
16 | private.easy_karabiner
17 |
18 |
19 | DeviceVendor::CHERRY,
20 | DeviceProduct::3494
21 |
22 | __KeyToKey__
23 | KeyCode::OPTION_L,
24 | KeyCode::COMMAND_L
25 |
26 | __KeyToKey__
27 | KeyCode::COMMAND_L,
28 | KeyCode::OPTION_L
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/easy_karabiner/query.pyi:
--------------------------------------------------------------------------------
1 | from typing import Iterable as Iter
2 | from .definition import DefinitionBase
3 |
4 |
5 | def query_filter_class_names(value: str, scope: str) -> Iter[str]: ...
6 |
7 |
8 | class BaseTypeQuery(object):
9 | @classmethod
10 | def query(cls, value: str) -> str: ...
11 |
12 | def is_in(self, type: str, value: str) -> bool: ...
13 |
14 |
15 | class DefinitionTypeQuery(BaseTypeQuery):
16 | def get_data(self, type: str, filepath: str) -> Iter[str]: ...
17 |
18 |
19 | class DefinitionBucket(object):
20 | @classmethod
21 | def get_instance(cls, reset: bool = False): ...
22 |
23 | @classmethod
24 | def get_all_definitions(cls) -> Iter[DefinitionBase]: ...
25 |
26 | @classmethod
27 | def put(cls, category: str, name: str, definitions: Iter[DefinitionBase]): ...
28 |
29 | @classmethod
30 | def get(cls, category: str, name: str) -> Iter[DefinitionBase]: ...
31 |
32 | @classmethod
33 | def has(cls, category: str, name: str) -> bool: ...
34 |
--------------------------------------------------------------------------------
/tests/test_query.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner.query import *
3 |
4 |
5 | def test_alias():
6 | aliases_table = {
7 | 'MODIFIER_ALIAS': {
8 | 'command': 'COMMAND_R',
9 | },
10 | 'KEY_ALIAS': {
11 | 'scroll_flip': 'FLIPSCROLLWHEEL_VERTICAL',
12 | 'none': 'ModifierFlag::None',
13 | }
14 | }
15 | update_aliases(aliases_table)
16 | assert(get_def_alias('open') == 'VKOpenURL')
17 | assert(get_key_alias('command') == 'COMMAND_R')
18 | assert(get_keymap_alias('press_modifier') == 'KeyOverlaidModifier')
19 |
20 | assert(query_filter_class_names('EMACS') == ['Filter'])
21 | assert(query_filter_class_names('ModifierFlag::COMMAND_R') == ['ModifierFilter'])
22 | assert(query_filter_class_names('ctrl') == ['ModifierFilter'])
23 | assert(query_filter_class_names('COMMAND_R') == ['ModifierFilter'])
24 |
25 | assert(KeyHeaderQuery.query('FN') == 'ModifierFlag')
26 | assert(DefinitionTypeQuery.query('EMACS') == 'appdef')
27 | assert(DefinitionTypeQuery.query('FN') == 'modifierdef')
28 |
--------------------------------------------------------------------------------
/easy_karabiner/parse.pyi:
--------------------------------------------------------------------------------
1 | from typing import TypeVar, Dict, Tuple, List
2 | from .parse import Block
3 | from .definition import DefinitionBase
4 |
5 | RawEntry = TypeVar('RawEntry', str, List)
6 | RawDefinitions = Dict[str:RawEntry]
7 | RawMap = List[RawEntry]
8 | RawMaps = List[RawMap]
9 | RawKeymap = TypeVar('RawKeymap',
10 | Tuple[str],
11 | Tuple[str, Tuple[str]],
12 | Tuple[str, Tuple[str], Tuple[str]],
13 | Tuple[str, Tuple[str], Tuple[str], Tuple[str]])
14 | RawFilters = TypeVar('RawFilters', Tuple[str])
15 |
16 |
17 | def parse(maps: RawMaps, definitions: RawDefinitions) -> Tuple[List[Block], List[DefinitionBase]]: ...
18 | def ensure_definitions_utf8(definitions: RawDefinitions) -> RawDefinitions: ...
19 | def ensure_maps_utf8(maps: RawMaps) -> RawMaps: ...
20 | def organize_maps(maps: RawMaps) -> Dict[RawFilters, List[RawKeymap]]: ...
21 | def separate_keymap_filters(raw_map: RawMap) -> Dict[RawKeymap, RawFilters]: ...
22 | def create_blocks(filters_keymaps_table: Dict[RawFilters, List[RawKeymap]]) -> List[Block]: ...
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | cleanup.sh
3 | __pycache__/
4 | *.ipynb
5 | *.py[cod]
6 | *$py.class
7 | .idea
8 | venv*/
9 | env2/
10 | env3/
11 | .vscode/
12 | browse.VC.db
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Distribution / packaging
18 | .Python
19 | env/
20 | build/
21 | develop-eggs/
22 | dist/
23 | downloads/
24 | eggs/
25 | .eggs/
26 | lib/
27 | lib64/
28 | parts/
29 | sdist/
30 | var/
31 | *.egg-info/
32 | .installed.cfg
33 | *.egg
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *,cover
54 | .hypothesis/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | #Ipython Notebook
70 | .ipynb_checkpoints
71 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Fangzheng Long
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/easy_karabiner/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | KARABINER_APP_PATH = '/Applications/Karabiner.app'
5 | DEFAULT_CONFIG_PATH = '~/.easy_karabiner.py'
6 | DEFAULT_OUTPUT_PATH = '~/Library/Application Support/Karabiner/private.xml'
7 |
8 | command_options = {}
9 |
10 |
11 | def set(options):
12 | command_options.update(options)
13 |
14 |
15 | def get(key):
16 | return command_options.get(key)
17 |
18 |
19 | def get_default_config_path():
20 | return os.path.expanduser(DEFAULT_CONFIG_PATH)
21 |
22 |
23 | def get_default_output_path():
24 | return os.path.expanduser(DEFAULT_OUTPUT_PATH)
25 |
26 |
27 | def get_data_dir():
28 | return os.path.join(os.path.dirname(__file__), 'data')
29 |
30 |
31 | def get_data_path(filename):
32 | return os.path.join(get_data_dir(), filename)
33 |
34 |
35 | def get_karabiner_app_path():
36 | return KARABINER_APP_PATH
37 |
38 |
39 | def _get_path_relate_to_karabiner(relative_path):
40 | return os.path.join(get_karabiner_app_path(), relative_path)
41 |
42 |
43 | def get_karabiner_bin_dir():
44 | return _get_path_relate_to_karabiner('Contents/Library/bin')
45 |
46 |
47 | def get_karabiner_resources_dir():
48 | return _get_path_relate_to_karabiner('Contents/Resources')
49 |
50 |
51 | def get_karabiner_bin(filename):
52 | return os.path.join(get_karabiner_bin_dir(), filename)
53 |
--------------------------------------------------------------------------------
/easy_karabiner/alias.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from . import util
4 | from . import config
5 |
6 |
7 | def _apply_if_is_alias(vars, func):
8 | result = {}
9 |
10 | for alias_name in vars.keys():
11 | alias = vars[alias_name]
12 | if hasattr(alias, '__iter__') \
13 | and alias_name.endswith('ALIAS') \
14 | and not alias_name.startswith('_'):
15 | result[alias_name] = func(vars, alias_name)
16 |
17 | return result
18 |
19 |
20 | def _get_aliases():
21 | if not hasattr(_get_aliases, 'aliases'):
22 | aliases = util.read_python_file(config.get_data_path('alias.data'))
23 | _get_aliases.aliases = _apply_if_is_alias(aliases, lambda d, k: d[k])
24 |
25 | return _get_aliases.aliases
26 |
27 |
28 | # alias is case-insensitive
29 | def get_alias(alias_name, value):
30 | def get(aname, avalue):
31 | return _get_aliases()[aname].get(avalue.lower())
32 |
33 | if alias_name == 'KEY_ALIAS':
34 | return get('KEY_ALIAS', value) or get('MODIFIER_ALIAS', value)
35 | else:
36 | return get(alias_name, value)
37 |
38 |
39 | def update_alias(alias_name, new_aliases):
40 | _get_aliases().setdefault(alias_name, {}).update(new_aliases)
41 |
42 |
43 | def update_aliases(aliases_table):
44 | _apply_if_is_alias(aliases_table, lambda d, k: update_alias(k, d[k]))
45 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | from easy_karabiner import __version__
3 |
4 | setup(
5 | name='easy_karabiner',
6 | version=__version__,
7 | description='A tool to simplify key-remapping configuration for Karabiner',
8 | author='loggerhead',
9 | author_email='i@loggerhead.me',
10 | url='https://github.com/loggerhead/Easy-Karabiner',
11 | keywords=('Karabiner', 'configer', 'remap', 'key'),
12 | packages=find_packages(exclude=("tests",)),
13 | include_package_data=True,
14 | install_requires=[
15 | 'lxml >= 3.0.0, < 4.0.0',
16 | 'click >= 6.0.0, < 7.0.0',
17 | ],
18 | entry_points='''
19 | [console_scripts]
20 | easy_karabiner=easy_karabiner.main:main
21 | ''',
22 | license='MIT',
23 | classifiers=[
24 | 'Development Status :: 4 - Beta',
25 | 'License :: OSI Approved :: MIT License',
26 | 'Intended Audience :: Developers',
27 | 'Intended Audience :: End Users/Desktop',
28 | 'Operating System :: MacOS',
29 | 'Programming Language :: Python',
30 | 'Programming Language :: Python :: 2',
31 | 'Programming Language :: Python :: 2.7',
32 | 'Programming Language :: Python :: 3',
33 | 'Programming Language :: Python :: 3.3',
34 | 'Programming Language :: Python :: 3.4',
35 | 'Programming Language :: Python :: 3.5',
36 | 'Topic :: Utilities',
37 | ]
38 | )
39 |
--------------------------------------------------------------------------------
/examples/launcher_mode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.5.2
4 | -
5 | Easy-Karabiner
6 |
7 | LAUNCHER_MODE_V2_EXTRA
8 |
9 | __KeyDownUpToKey__
10 | KeyCode::C,
11 | @begin
12 | KeyCode::VK_OPEN_URL_Maps
13 | @end
14 | @begin
15 | KeyCode::C
16 | @end
17 |
18 | __KeyDownUpToKey__
19 | KeyCode::V,
20 | @begin
21 | KeyCode::VK_OPEN_URL_FaceTime
22 | @end
23 | @begin
24 | KeyCode::V
25 | @end
26 | ]]>
27 |
28 |
29 |
30 | KeyCode::VK_OPEN_URL_FaceTime
31 | /Applications/FaceTime.app
32 |
33 |
34 | KeyCode::VK_OPEN_URL_Maps
35 | /Applications/Maps.app
36 |
37 |
-
38 | Enable
39 | private.easy_karabiner
40 |
41 | __KeyOverlaidModifier__
42 | KeyCode::O,
43 | @begin
44 | KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_launcher_mode_v2, ModifierFlag::LAUNCHER_MODE_V2
45 | @end
46 | @begin
47 | KeyCode::O
48 | @end
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/easy_karabiner/exception.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from .fucking_string import ensure_utf8
4 |
5 |
6 | class NeedOverrideError(NotImplementedError):
7 | def __init__(self, errmsg='You need override this method'):
8 | super(NeedOverrideError, self).__init__(self, errmsg)
9 |
10 |
11 | class ConfigError(Exception):
12 | pass
13 |
14 |
15 | class ConfigWarning(Exception):
16 | pass
17 |
18 |
19 | class InvalidDefinition(ConfigWarning):
20 | pass
21 |
22 |
23 | class InvalidKeymapException(ConfigWarning):
24 | pass
25 |
26 |
27 | class UndefinedFilterException(ConfigWarning):
28 | pass
29 |
30 |
31 | class UndefinedKeyException(ConfigWarning):
32 | pass
33 |
34 |
35 | class ExceptionRegister(object):
36 | """This class is used to record the occurrence of any exceptions,
37 | so we can provide a user friendly error message.
38 | """
39 | error_table = {}
40 | raw_maps_table = {}
41 |
42 | @classmethod
43 | def put(cls, k, raw_map):
44 | k = ensure_utf8(k)
45 | if k not in cls.raw_maps_table:
46 | cls.raw_maps_table[k] = raw_map
47 |
48 | @classmethod
49 | def record_by(cls, k, exception):
50 | k = ensure_utf8(k)
51 | raw_map = cls.raw_maps_table[k]
52 | cls.record(raw_map, exception)
53 |
54 | @classmethod
55 | def record(cls, k, exception):
56 | k = ensure_utf8(k)
57 | if k not in cls.error_table:
58 | cls.error_table[k] = exception
59 |
60 | @classmethod
61 | def get_all_records(cls):
62 | return cls.error_table.items()
63 |
--------------------------------------------------------------------------------
/examples/basic.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.5.2
4 | -
5 | Easy-Karabiner
6 |
7 | EMACS_IGNORE_APP
8 | X11, GOOGLE_CHROME
9 |
10 |
-
11 | Enable
12 | private.easy_karabiner
13 |
14 | {{EMACS_IGNORE_APP}}
15 | __KeyToKey__
16 | KeyCode::B, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
17 | KeyCode::CURSOR_LEFT
18 |
19 | __KeyToKey__
20 | KeyCode::B, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
21 | KeyCode::CURSOR_LEFT
22 |
23 |
24 |
25 |
26 | VIRTUALMACHINE,
27 | X11
28 |
29 | __KeyToKey__
30 | KeyCode::D, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
31 | KeyCode::D, ModifierFlag::COMMAND_R
32 |
33 |
34 |
35 | __DoublePressModifier__
36 | KeyCode::FN,
37 | @begin
38 | KeyCode::FN
39 | @end
40 | @begin
41 | KeyCode::F12
42 | @end
43 |
44 |
45 |
46 | GOOGLE_CHROME
47 | __DoublePressModifier__
48 | KeyCode::FN,
49 | @begin
50 | KeyCode::FN
51 | @end
52 | @begin
53 | KeyCode::I, ModifierFlag::COMMAND_L, ModifierFlag::OPTION_L
54 | @end
55 |
56 | __HoldingKeyToKey__
57 | KeyCode::ESCAPE,
58 | @begin
59 | KeyCode::ESCAPE
60 | @end
61 | @begin
62 | KeyCode::COMMAND_R, ModifierFlag::CONTROL_R, ModifierFlag::OPTION_R, ModifierFlag::SHIFT_R
63 | @end
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/tests/test_filter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner import util
3 | from easy_karabiner.filter import *
4 |
5 |
6 | def test_basic_filter():
7 | f1 = Filter('GOOGLE_CHROME')
8 | s = ''' GOOGLE_CHROME '''
9 | util.assert_xml_equal(f1, s)
10 |
11 | f2 = ReplacementFilter('{{EMACS_IGNORE_APP}}', type='not')
12 | s = ''' {{EMACS_IGNORE_APP}} '''
13 | util.assert_xml_equal(f2, s)
14 |
15 | try:
16 | f1 + f2
17 | assert(False)
18 | except:
19 | pass
20 |
21 |
22 | def test_device_filter():
23 | f = DeviceFilter('DeviceVendor::APPLE_COMPUTER', 'DeviceProduct::ANY')
24 | s = '''
25 |
26 | DeviceVendor::APPLE_COMPUTER,
27 | DeviceProduct::ANY
28 | '''
29 | util.assert_xml_equal(f, s)
30 |
31 | f = DeviceFilter('DeviceVendor::APPLE_COMPUTER', 'DeviceProduct::ANY',
32 | 'DeviceVendor::Apple_Bluetooth', 'DeviceProduct::ANY',
33 | type='not')
34 | s = '''
35 |
36 | DeviceVendor::APPLE_COMPUTER, DeviceProduct::ANY,
37 | DeviceVendor::Apple_Bluetooth, DeviceProduct::ANY
38 | '''
39 | util.assert_xml_equal(f, s)
40 |
41 |
42 | def test_windowname_filter():
43 | f = WindowNameFilter('Gmail')
44 | s = ''' Gmail '''
45 | util.assert_xml_equal(f, s)
46 |
47 |
48 | def test_uielementrole_filter():
49 | f = UIElementRoleFilter('AXTextField', 'AXTextArea')
50 | s = ''' AXTextField, AXTextArea '''
51 | util.assert_xml_equal(f, s)
52 |
53 |
54 | def test_inputsource_filter():
55 | f = InputSourceFilter('UKRAINIAN')
56 | s = ''' UKRAINIAN '''
57 | util.assert_xml_equal(f, s)
58 |
59 | f = InputSourceFilter('SWISS_FRENCH', 'SWISS_GERMAN', type='not')
60 | s = ''' SWISS_FRENCH, SWISS_GERMAN '''
61 | util.assert_xml_equal(f, s)
62 |
63 |
64 | def test_modifier_filter():
65 | f = ModifierFilter('ModifierFlag::COMMAND_L')
66 | s = ''' ModifierFlag::COMMAND_L '''
67 | util.assert_xml_equal(f, s)
68 |
69 | f = ModifierFilter('ModifierFlag::CONTROL_R', type='not')
70 | s = ''' ModifierFlag::CONTROL_R '''
71 | util.assert_xml_equal(f, s)
72 |
--------------------------------------------------------------------------------
/easy_karabiner/data/alias.data:
--------------------------------------------------------------------------------
1 | DEF_ALIAS = {
2 | 'open' : 'VKOpenURL',
3 | 'window' : 'WindowName',
4 | 'inputsource': 'ChangeInputSource',
5 | }
6 |
7 | KEYMAP_ALIAS = {
8 | 'double' : 'DoublePressModifier',
9 | 'holding' : 'HoldingKeyToKey',
10 | 'press_modifier': 'KeyOverlaidModifier',
11 | }
12 |
13 | MODIFIER_ALIAS = {
14 | "shift" : "SHIFT_L",
15 | "shift_l": "SHIFT_L",
16 | "shift_r": "SHIFT_R",
17 | "cmd" : "COMMAND_L",
18 | "command": "COMMAND_L",
19 | "cmd_l" : "COMMAND_L",
20 | "cmd_r" : "COMMAND_R",
21 | "opt" : "OPTION_L",
22 | "option" : "OPTION_L",
23 | "opt_l" : "OPTION_L",
24 | "opt_r" : "OPTION_R",
25 | "alt" : "OPTION_L",
26 | "alt_l" : "OPTION_L",
27 | "alt_r" : "OPTION_R",
28 | "ctrl" : "CONTROL_L",
29 | "control": "CONTROL_L",
30 | "ctrl_l" : "CONTROL_L",
31 | "ctrl_r" : "CONTROL_R",
32 | "caps" : "CAPSLOCK",
33 | "fn" : "FN",
34 |
35 | "launcher_mode_v2": "ModifierFlag::LAUNCHER_MODE_V2",
36 | }
37 |
38 | KEY_ALIAS = {
39 | "whitespace": "SPACE",
40 | "sp" : "SPACE",
41 | "del" : "DELETE",
42 | "fdel" : "FORWARD_DELETE",
43 | "esc" : "ESCAPE",
44 | "left" : "CURSOR_LEFT",
45 | "right" : "CURSOR_RIGHT",
46 | "down" : "CURSOR_DOWN",
47 | "up" : "CURSOR_UP",
48 | "`" : "BACKQUOTE",
49 | "1" : "KEY_1",
50 | "2" : "KEY_2",
51 | "3" : "KEY_3",
52 | "4" : "KEY_4",
53 | "5" : "KEY_5",
54 | "6" : "KEY_6",
55 | "7" : "KEY_7",
56 | "8" : "KEY_8",
57 | "9" : "KEY_9",
58 | "0" : "KEY_0",
59 | "-" : "MINUS",
60 | "=" : "EQUAL",
61 | "[" : "BRACKET_LEFT",
62 | "]" : "BRACKET_RIGHT",
63 | "\\" : "BACKSLASH",
64 | ";" : "SEMICOLON",
65 | "'" : "QUOTE",
66 | "," : "COMMA",
67 | "." : "DOT",
68 | "/" : "SLASH",
69 |
70 | "mouse_left" : "PointingButton::LEFT",
71 | "mouse_right" : "PointingButton::RIGHT",
72 | "mouse_middle": "PointingButton::MIDDLE",
73 | "mouse1" : "PointingButton::LEFT",
74 | "mouse2" : "PointingButton::RIGHT",
75 | "mouse3" : "PointingButton::MIDDLE",
76 |
77 | "scroll_up" : "ScrollWheel::UP",
78 | "scroll_down" : "ScrollWheel::DOWN",
79 | "scroll_left" : "ScrollWheel::left",
80 | "scroll_right": "ScrollWheel::right",
81 |
82 | "config_sync_keydownup_notsave_launcher_mode_v2": "KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_launcher_mode_v2",
83 | }
--------------------------------------------------------------------------------
/easy_karabiner/filter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from .basexml import BaseXML
4 |
5 |
6 | class FilterBase(BaseXML):
7 | """A object represent filter XML node in Karabiner.
8 | For example, the following XML is a typical filter.
9 |
10 | SKIM
11 | """
12 | def __init__(self, *vals, **kwargs):
13 | """
14 | :param vals: List[str]
15 | :param kwargs: Dict
16 | """
17 | self.type = kwargs.get('type', 'only')
18 | self.vals = vals
19 | self.kwargs = kwargs
20 |
21 | def get_vals(self):
22 | return self.vals
23 |
24 | def get_tag_name(self):
25 | header = self.get_class_name().lower().rsplit('filter', 1)[0]
26 | tag_name = '%s_%s' % (header, self.type)
27 | return tag_name
28 |
29 | def to_xml(self):
30 | xml_tree = self.create_tag(self.get_tag_name())
31 | text = ',\n'.join(self.get_vals())
32 | self.assign_text_attribute(xml_tree, text)
33 | return xml_tree
34 |
35 | def __add__(self, another):
36 | if self.get_tag_name() == another.get_tag_name():
37 | self.vals += another.vals
38 | return self
39 | else:
40 | tagname1 = self.get_tag_name()
41 | tagname2 = another.get_tag_name()
42 | errmsg = "Cannot add %s with %s" % (tagname1, tagname2)
43 | raise TypeError(errmsg)
44 |
45 | @property
46 | def id(self):
47 | return self.get_tag_name(), tuple(sorted(self.get_vals()))
48 |
49 | def __hash__(self):
50 | return hash(self.id)
51 |
52 | def __eq__(self, other):
53 | return self.__class__ == other.__class__ and self.id == other.id
54 |
55 |
56 | class Filter(FilterBase):
57 | """
58 | >>> print(Filter('SKIM'))
59 | SKIM
60 | >>> print(Filter('SKIM', type='not'))
61 | SKIM
62 | """
63 |
64 | def get_tag_name(self):
65 | return self.type
66 |
67 |
68 | class ReplacementFilter(Filter):
69 | pass
70 |
71 |
72 | class DeviceFilter(FilterBase):
73 | def get_tag_name(self):
74 | tag_name = 'device_%s' % self.type
75 | return tag_name
76 |
77 |
78 | class DeviceProductFilter(DeviceFilter):
79 | pass
80 |
81 |
82 | class DeviceVendorFilter(DeviceFilter):
83 | pass
84 |
85 |
86 | class WindowNameFilter(FilterBase):
87 | pass
88 |
89 |
90 | class UIElementRoleFilter(FilterBase):
91 | pass
92 |
93 |
94 | class InputSourceFilter(FilterBase):
95 | pass
96 |
97 |
98 | class ModifierFilter(FilterBase):
99 | def get_tag_name(self):
100 | tag_name = 'modifier_%s' % self.type
101 | return tag_name
102 |
103 |
104 | if __name__ == "__main__":
105 | import doctest
106 | doctest.testmod()
107 |
--------------------------------------------------------------------------------
/easy_karabiner/keycombo.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 |
4 |
5 | class KeyCombo(object):
6 | """Convert key list to Karabiner's favorite format
7 |
8 | >>> print(KeyCombo(['ModifierFlag::SHIFT_L', 'ModifierFlag::COMMAND_L']))
9 | KeyCode::SHIFT_L, ModifierFlag::COMMAND_L
10 |
11 | >>> print(KeyCombo(['ModifierFlag::SHIFT_L', 'ModifierFlag::COMMAND_L', 'KeyCode::K'], has_modifier_none=True))
12 | KeyCode::K, ModifierFlag::SHIFT_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE
13 | """
14 |
15 | def __init__(self, keys=None, has_modifier_none=False, keep_first_keycode=False):
16 | self.has_modifier_none = has_modifier_none
17 | self.keep_first_keycode = keep_first_keycode
18 | self.keys = self.parse(keys or [])
19 |
20 | def parse(self, keys):
21 | """
22 | :param keys: List[str]
23 | :return: List[str]
24 | """
25 | # we need adjust position of keys, because the first key cannot be modifier key in most case
26 | keys = self.rearrange_keys(keys)
27 |
28 | if len(keys) > 0:
29 | if not self.keep_first_keycode and self.is_modifier_key(keys[0]):
30 | # change the header of first key to `KeyCode`
31 | keys = self.regularize_first_key(keys)
32 | if self.has_modifier_none and self.is_modifier_key(keys[-1]):
33 | keys = self.add_modifier_none(keys)
34 |
35 | return keys
36 |
37 | def rearrange_keys(self, keys):
38 | tmp = []
39 | last = 0
40 |
41 | for i in range(len(keys)):
42 | if not self.is_modifier_key(keys[i]):
43 | tmp.append(keys[i])
44 | while last < i:
45 | tmp.append(keys[last])
46 | last += 1
47 | last = i + 1
48 |
49 | tmp.extend(keys[last:])
50 | return tmp
51 |
52 | @classmethod
53 | def is_modifier_key(cls, key):
54 | return key.lower().startswith('modifier')
55 |
56 | @classmethod
57 | def regularize_first_key(cls, keys):
58 | parts = keys[0].split('::', 1)
59 | keys[0] = 'KeyCode::' + parts[-1]
60 | return keys
61 |
62 | # Append 'ModifierFlag::NONE' if you want to change from this key
63 | # For more information about 'ModifierFlag::NONE', see https://pqrs.org/osx/karabiner/xml.html.en
64 | @classmethod
65 | def add_modifier_none(cls, keys):
66 | keys.append('ModifierFlag::NONE')
67 | return keys
68 |
69 | def to_str(self):
70 | return ', '.join(self.keys)
71 |
72 | def __add__(self, another):
73 | res = KeyCombo()
74 | res.keys = self.keys + another.keys
75 | res.has_modifier_none = self.has_modifier_none or another.has_modifier_none
76 | res.keep_first_keycode = self.keep_first_keycode or another.keep_first_keycode
77 | return res
78 |
79 | def __str__(self):
80 | return self.to_str()
81 |
82 |
83 | if __name__ == "__main__":
84 | import doctest
85 | doctest.testmod()
86 |
--------------------------------------------------------------------------------
/easy_karabiner/factory.pyi:
--------------------------------------------------------------------------------
1 | from typing import TypeVar, Dict, Tuple, List
2 | from .definition import DefinitionBase
3 | from .filter import FilterBase
4 | from .keymap import KeyToKeyBase
5 |
6 | Iter = TypeVar('Iter', List, Tuple)
7 | RawEntry = TypeVar('RawEntry', str, Iter)
8 | RawDefinitions = Dict[str:RawEntry]
9 | RawMap = Iter[RawEntry]
10 | RawMaps = Iter[RawMap]
11 | RawKeymap = TypeVar('RawKeymap',
12 | Iter[str],
13 | Iter[str, Iter[str]],
14 | Iter[str, Iter[str], Iter[str]],
15 | Iter[str, Iter[str], Iter[str], Iter[str]])
16 | RawFilters = TypeVar('RawFilters', Tuple[str])
17 |
18 |
19 | def define_filters(raw_filters: RawFilters) -> None: ...
20 | def define_keymaps(raw_keymaps: Iter[RawKeymap]) -> None: ...
21 | def create_filters(raw_filters: RawFilters) -> Iter[FilterBase]: ...
22 | def create_definitions(definitions: RawDefinitions) -> Iter[DefinitionBase]: ...
23 |
24 |
25 | class FilterCreater(object):
26 | @classmethod
27 | def define(cls, val: str) -> None: ...
28 |
29 | @classmethod
30 | def create(cls, raw_val: str) -> Iter[FilterBase]: ...
31 |
32 | @classmethod
33 | def escape_filter_name(cls, class_name: str, name_val: str) -> str: ...
34 |
35 |
36 | class KeymapCreater(object):
37 | @classmethod
38 | def define_key(cls, val: str) -> None: ...
39 |
40 | @classmethod
41 | def create(cls, raw_keymap: RawKeymap) -> KeyToKeyBase: ...
42 |
43 | @classmethod
44 | def get_karabiner_format_key(cls, key: str, key_header: str = None) -> str: ...
45 |
46 |
47 | class DefinitionCreater(object):
48 | @classmethod
49 | def create(cls, raw_name: str, vals: Iter[str]) -> Iter[DefinitionBase]: ...
50 |
51 | @classmethod
52 | def define(cls, class_name: str,
53 | raw_name: str,
54 | def_name: str,
55 | vals: Iter[str],
56 | escape_def_name: bool) -> Iter[DefinitionBase]: ...
57 |
58 | @classmethod
59 | def define_replacement(cls, raw_name: str, vals: Iter[str]) -> Iter[DefinitionBase]: ...
60 |
61 | @classmethod
62 | def define_device(cls, raw_name: str, vals: Iter[str]) -> Iter[DefinitionBase]: ...
63 |
64 | @classmethod
65 | def define_app(cls, raw_name: str, bundle_id: str) -> Iter[DefinitionBase]: ...
66 |
67 | @classmethod
68 | def define_open(cls, val: str, index: str) -> Iter[DefinitionBase]: ...
69 |
70 | @classmethod
71 | def escape_def_name(cls, def_name: str, class_name: str) -> str: ...
72 |
73 |
74 | class DefinitionDetector(object):
75 | @classmethod
76 | def is_device(cls, vals: Iter[str]) -> bool: ...
77 |
78 | @classmethod
79 | def is_vkopenurl(cls, val: str) -> bool: ...
80 |
81 | @classmethod
82 | def is_uielementrole(cls, val: str) -> bool: ...
83 |
84 | @classmethod
85 | def is_app(cls, val: str) -> bool: ...
86 |
87 | @classmethod
88 | def is_all_app(cls, vals: Iter[str]) -> bool: ...
89 |
90 | @classmethod
91 | def is_replacement(cls, val: str) -> bool: ...
92 |
93 | @classmethod
94 | def is_keymap(cls, val: str) -> bool: ...
95 |
96 | @classmethod
97 | def get_definition_caregory(cls, def_class: DefinitionBase) -> str: ...
98 |
--------------------------------------------------------------------------------
/easy_karabiner/generator.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from . import __version__
4 | from . import parse
5 | from . import config
6 | from .basexml import BaseXML
7 | from .util import print_info
8 |
9 |
10 | class Generator(BaseXML):
11 | """Construct Karabiner favorite XML tree
12 |
13 | >>> g = Generator()
14 | >>> s = '''
15 | ...
16 | ... {version}
17 | ... -
18 | ... Easy-Karabiner
19 | ...
-
20 | ... Enable
21 | ... private.easy_karabiner
22 | ...
23 | ...
24 | ...
25 | ... '''.format(version=__version__)
26 | >>> util.assert_xml_equal(g, s)
27 | """
28 |
29 | ITEM_IDENTIFIER = 'private.easy_karabiner'
30 |
31 | def __init__(self, maps=None, definitions=None):
32 | self.maps = maps or []
33 | self.definitions = definitions or {}
34 |
35 | def init_xml_tree(self, xml_root):
36 | """
37 | {version}
38 | -
39 | Easy-Karabiner
40 |
41 |
42 | """
43 | version_tag = BaseXML.create_tag('Easy-Karabiner', __version__)
44 | item_tag = BaseXML.create_tag('item')
45 | item_tag.append(BaseXML.create_tag('name', 'Easy-Karabiner'))
46 | xml_root.append(version_tag)
47 | xml_root.append(item_tag)
48 | return item_tag
49 |
50 | def init_subitem_tag(self, item_tag):
51 | """
52 | -
53 | Easy-Karabiner
54 |
-
55 | Enable
56 | private.easy_karabiner
57 |
58 |
59 |
60 | """
61 | subitem_tag = BaseXML.create_tag('item')
62 | name_tag = BaseXML.create_tag('name', 'Enable')
63 | identifier_tag = BaseXML.create_tag('identifier', self.ITEM_IDENTIFIER)
64 | item_tag.append(subitem_tag)
65 | subitem_tag.append(name_tag)
66 | subitem_tag.append(identifier_tag)
67 | return subitem_tag
68 |
69 | def to_xml(self):
70 | if config.get('verbose'):
71 | print_info("parsing user configuration")
72 | blocks, definitions = parse.parse(self.maps, self.definitions)
73 |
74 | if config.get('verbose'):
75 | print_info("constructing XML file")
76 | # construct XML tree
77 | xml_tree = BaseXML.create_tag('root')
78 |
79 | item_tag = self.init_xml_tree(xml_tree)
80 | for definition in definitions:
81 | item_tag.append(definition.to_xml())
82 |
83 | subitem_tag = self.init_subitem_tag(item_tag)
84 | for block in blocks:
85 | subitem_tag.append(block.to_xml())
86 |
87 | return xml_tree
88 |
89 |
90 | if __name__ == '__main__':
91 | import doctest
92 | from . import util
93 | doctest.testmod(extraglobs={
94 | '__version__': __version__,
95 | 'util': util,
96 | })
97 |
--------------------------------------------------------------------------------
/easy_karabiner/basexml.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | import lxml.etree as etree
4 | import xml.dom.minidom as minidom
5 | import xml.sax.saxutils as saxutils
6 | from . import exception
7 | from .fucking_string import ensure_utf8
8 |
9 |
10 | class BaseXML(object):
11 | xml_parser = etree.XMLParser(strip_cdata=False)
12 |
13 | @classmethod
14 | def unescape(cls, s):
15 | return saxutils.unescape(s, {
16 | """: '"',
17 | "'": "'",
18 | })
19 |
20 | @classmethod
21 | def parse(cls, filepath):
22 | return etree.parse(filepath).getroot()
23 |
24 | @classmethod
25 | def parse_string(cls, xml_str):
26 | return etree.fromstring(xml_str, cls.xml_parser)
27 |
28 | @classmethod
29 | def get_class_name(cls):
30 | return cls.__name__
31 |
32 | @classmethod
33 | def is_cdata_text(cls, text):
34 | return text.startswith('')
35 |
36 | @classmethod
37 | def remove_cdata_mark(cls, text):
38 | return text[len('')]
39 |
40 | @classmethod
41 | def create_cdata_text(cls, text):
42 | # do NOT use `etree.CDATA`
43 | return '' % text
44 |
45 | @classmethod
46 | def assign_text_attribute(cls, etree_element, text):
47 | if text is not None:
48 | etree_element.text = ensure_utf8(text)
49 | else:
50 | etree_element.text = text
51 |
52 | @classmethod
53 | def create_tag(cls, name, text=None, **kwargs):
54 | et = etree.Element(name, **kwargs)
55 | cls.assign_text_attribute(et, text)
56 | return et
57 |
58 | @classmethod
59 | def pretty_text(cls, elem, indent=" ", level=0):
60 | """WARNING: This method would change the construct of XML tree"""
61 | i = "\n" + level * indent
62 |
63 | if len(elem) == 0:
64 | if elem.text is not None:
65 | lines = elem.text.split('\n')
66 | if len(lines) > 1:
67 | if not lines[0].startswith(' '):
68 | lines[0] = (i + indent) + lines[0]
69 | if lines[-1].strip() == '':
70 | lines.pop()
71 | elem.text = (i + indent).join(lines) + i
72 | else:
73 | for subelem in elem:
74 | BaseXML.pretty_text(subelem, indent, level + 1)
75 |
76 | return elem
77 |
78 | @classmethod
79 | def to_format_str(cls, xml_tree, pretty_text=True):
80 | indent = " "
81 | if pretty_text:
82 | BaseXML.pretty_text(xml_tree, indent=indent)
83 | xml_string = etree.tostring(xml_tree)
84 | xml_string = minidom.parseString(xml_string).toprettyxml(indent=indent)
85 | xml_string = cls.unescape(xml_string)
86 | return xml_string
87 |
88 | def to_xml(self):
89 | """NOTICE: This method must be a REENTRANT function, which means
90 | it should NOT change status or modify any member of `self` object.
91 | Because other methods may change the construct of the XML tree.
92 | """
93 | raise exception.NeedOverrideError()
94 |
95 | def to_str(self, pretty_text=True, remove_first_line=False):
96 | xml_str = self.to_format_str(self.to_xml(), pretty_text=pretty_text)
97 |
98 | if remove_first_line:
99 | lines = xml_str.split('\n')
100 | if len(lines[-1].strip()) == 0:
101 | # remove last blank line
102 | lines = lines[1:-1]
103 | else:
104 | lines = lines[1:]
105 | xml_str = '\n'.join(lines)
106 |
107 | return xml_str
108 |
109 | def __str__(self):
110 | # `remove_first_line=True` is used to remove version tag in the first line
111 | return self.to_str(remove_first_line=True)
112 |
--------------------------------------------------------------------------------
/easy_karabiner/util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | import shlex
4 | import click
5 | from hashlib import sha1
6 | from . import config
7 | from .basexml import BaseXML
8 | from .fucking_string import ensure_utf8, is_string_type
9 |
10 |
11 | def read_python_file(pypath):
12 | vars = {}
13 | with open(pypath, 'rb') as fp:
14 | exec(compile(fp.read(), pypath, 'exec'), {}, vars)
15 | return vars
16 |
17 |
18 | def get_checksum(s):
19 | return sha1(ensure_utf8(s).encode('utf-8')).hexdigest()[:7]
20 |
21 |
22 | def escape_string(s):
23 | """Keep char unchanged if char is number or letter or unicode"""
24 | chs = []
25 | for ch in s:
26 | ch = ch if ord(ch) > 128 or ch.isalnum() else ' '
27 | chs.append(ch)
28 |
29 | # remove multiple whitespaces and replace whitespace with '_'
30 | return '_'.join(''.join(chs).split())
31 |
32 |
33 | def encode_with_utf8(o):
34 | """Encode object `o` with UTF-8 recursively"""
35 | if is_string_type(o):
36 | return ensure_utf8(o)
37 |
38 | if is_list_or_tuple(o):
39 | return type(o)([encode_with_utf8(item) for item in o])
40 | elif isinstance(o, dict):
41 | for k in o.keys():
42 | o[encode_with_utf8(k)] = encode_with_utf8(o.pop(k))
43 | return o
44 | else:
45 | raise TypeError('Cannot encode %s with UTF-8' % o.__repr__())
46 |
47 |
48 | def is_hex(s):
49 | try:
50 | int(s, 16)
51 | return True
52 | except ValueError:
53 | return False
54 |
55 |
56 | def is_list_or_tuple(obj):
57 | return isinstance(obj, (list, tuple))
58 |
59 |
60 | def split_ignore_quote(s):
61 | return shlex.split(s)
62 |
63 |
64 | def remove_all_space(s):
65 | return ''.join(s.split())
66 |
67 |
68 | def is_xml_element_equal(node1, node2):
69 | if len(node1) != len(node2):
70 | return False
71 | if node1.tag != node2.tag:
72 | return False
73 | if node1.attrib != node2.attrib:
74 | return False
75 |
76 | text1 = '' if node1.text is None else remove_all_space(node1.text)
77 | text2 = '' if node2.text is None else remove_all_space(node2.text)
78 | return text1 == text2
79 |
80 |
81 | def is_xml_tree_equal(tree1, tree2, ignore_tags=tuple()):
82 | if tree1.tag == tree2.tag and tree1.tag in ignore_tags:
83 | return True
84 | elif is_xml_element_equal(tree1, tree2):
85 | elems1 = list(tree1)
86 | elems2 = list(tree2)
87 |
88 | for i in range(len(elems1)):
89 | if not is_xml_tree_equal(elems1[i], elems2[i], ignore_tags=ignore_tags):
90 | return False
91 | return True
92 | else:
93 | return False
94 |
95 |
96 | def assert_xml_equal(xml_tree1, xml_tree2, ignore_tags=tuple()):
97 | if isinstance(xml_tree1, BaseXML):
98 | xml_tree1 = xml_tree1.__str__()
99 | if isinstance(xml_tree2, BaseXML):
100 | xml_tree2 = xml_tree2.__str__()
101 |
102 | nospaces1 = ''.join(xml_tree1.split())
103 | nospaces2 = ''.join(xml_tree2.split())
104 | xml_tree1 = BaseXML.parse_string(xml_tree1)
105 | xml_tree2 = BaseXML.parse_string(xml_tree2)
106 |
107 | if nospaces1 != nospaces2:
108 | assert(is_xml_tree_equal(xml_tree1, xml_tree2, ignore_tags=ignore_tags))
109 |
110 |
111 | def print_message(msg, color=None, err=False):
112 | """Seems `click.echo` has fixed the problem of UnicodeDecodeError when redirecting (See
113 | https://stackoverflow.com/questions/4545661/unicodedecodeerror-when-redirecting-to-file
114 | for detail). As a result, the below code used to solve the problem is conflict with `click.echo`.
115 | To avoid the problem, you should always use `print` with below code or `click.echo` in `__main__.py`
116 |
117 | if sys.version_info[0] == 2:
118 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout)
119 | else:
120 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
121 | """
122 | if not is_string_type(msg):
123 | msg = str(msg)
124 | click.secho(msg, fg=color, err=err)
125 |
126 |
127 | def print_error(msg, print_stack=False):
128 | print_message(msg, color='red', err=True)
129 | if config.get('verbose'):
130 | import traceback
131 | traceback.print_exc()
132 |
133 |
134 | def print_warning(msg):
135 | print_message(msg, color='yellow', err=True)
136 |
137 |
138 | def print_info(msg):
139 | print_message(msg, color='green')
140 |
--------------------------------------------------------------------------------
/examples/test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.5.2
4 | -
5 | Easy-Karabiner
6 |
7 | BILIBILI
8 | com.typcn.Bilibili
9 |
10 |
11 | Finder
12 | com.apple.finder
13 |
14 |
15 | Xee³
16 | cx.c3.Xee3
17 |
18 |
19 | Xee³_app
20 | cx.c3.Xee3
21 |
22 |
23 | CHERRY_3494_PRODUCT
24 | 0x0011
25 |
26 |
27 | built_in_keyboard_and_trackpad_PRODUCT
28 | 0x0259
29 |
30 |
31 | CHERRY_3494_VENDOR
32 | 0x046a
33 |
34 |
35 | built_in_keyboard_and_trackpad_VENDOR
36 | 0x05ac
37 |
38 | KEYLOCK
39 |
40 | 可以是中文
41 | 比如, hello, Xee³
42 |
43 | 自定义UI组件
44 |
45 | KeyCode::VK_OPEN_URL_760efb2
46 |
47 |
48 |
49 | KeyCode::VK_OPEN_URL_Finder
50 | /System/Library/CoreServices/Finder.app
51 |
52 |
53 | KeyCode::VK_OPEN_URL_Xee³_app
54 | /Applications/Xee³.app
55 |
56 |
57 | KeyCode::VK_OPEN_URL_aedd86e
58 |
59 |
60 |
-
61 | Enable
62 | private.easy_karabiner
63 |
64 | __KeyToKey__
65 | KeyCode::COMMAND_L,
66 | KeyCode::OPTION_L
67 |
68 | __DoublePressModifier__
69 | KeyCode::SHIFT_L,
70 | @begin
71 | KeyCode::SHIFT_L
72 | @end
73 | @begin
74 | KeyCode::VK_OPEN_URL_aedd86e
75 | @end
76 |
77 | __KeyToKey__
78 | PointingButton::LEFT, ModifierFlag::OPTION_L, ModifierFlag::NONE,
79 | PointingButton::LEFT, KeyCode::VK_OPEN_URL_760efb2
80 |
81 |
82 |
83 | 自定义UI组件
84 | __KeyToKey__
85 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
86 | KeyCode::VK_OPEN_URL_Finder
87 |
88 |
89 |
90 | ModifierFlag::COMMAND_L
91 | Finder
92 | __KeyToKey__
93 | KeyCode::X, ModifierFlag::OPTION_L, ModifierFlag::NONE,
94 | KeyCode::VK_OPEN_URL_Xee³_app
95 |
96 |
97 |
98 |
99 | DeviceVendor::built_in_keyboard_and_trackpad_VENDOR,
100 | DeviceProduct::built_in_keyboard_and_trackpad_PRODUCT
101 |
102 | Xee³_app
103 | __FlipScrollWheel__
104 | Option::FLIPSCROLLWHEEL_VERTICAL
105 |
106 |
107 |
108 | VIRTUALMACHINE
109 | __KeyToKey__
110 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
111 | KeyCode::F, ModifierFlag::COMMAND_L
112 |
113 |
114 |
115 | ModifierFlag::KEYLOCK
116 | __KeyToKey__
117 | KeyCode::OPTION_L,
118 | KeyCode::NONE
119 |
120 | __KeyToKey__
121 | KeyCode::COMMAND_L,
122 | KeyCode::NONE
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/tests/test_generator.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner import __version__
3 | from easy_karabiner import util
4 | from easy_karabiner import query
5 | from easy_karabiner.generator import *
6 |
7 |
8 | def test_generator():
9 | query.DefinitionBucket.clear()
10 | KEYMAP_ALIAS = {
11 | 'flip': 'FlipScrollWheel',
12 | }
13 | DEFINITIONS = {
14 | 'BILIBILI': 'com.typcn.Bilibili',
15 | 'DeviceVendor::CHERRY': '0x046a',
16 | 'DeviceProduct::3494' : '0x0011',
17 | }
18 | MAPS = [
19 | ['cmd', 'alt'],
20 | ['alt', 'cmd', ['CHERRY', 'BILIBILI', '3494']],
21 | ['_flip_', 'flipscrollwheel_vertical', ['!APPLE_COMPUTER', '!ANY']],
22 | ]
23 | query.update_aliases({'KEYMAP_ALIAS': KEYMAP_ALIAS})
24 | g = Generator(maps=MAPS, definitions=DEFINITIONS)
25 | s = '''
26 |
27 | {version}
28 | -
29 | Easy-Karabiner
30 |
31 | BILIBILI
32 | com.typcn.Bilibili
33 |
34 |
35 | 3494
36 | 0x0011
37 |
38 |
39 | CHERRY
40 | 0x046a
41 |
42 |
-
43 | Enable
44 | private.easy_karabiner
45 |
46 | __KeyToKey__ KeyCode::COMMAND_L, KeyCode::OPTION_L
47 |
48 |
49 | DeviceVendor::CHERRY, DeviceProduct::3494
50 | BILIBILI
51 | __KeyToKey__ KeyCode::OPTION_L, KeyCode::COMMAND_L
52 |
53 |
54 | DeviceVendor::APPLE_COMPUTER, DeviceProduct::ANY
55 | __FlipScrollWheel__ Option::FLIPSCROLLWHEEL_VERTICAL
56 |
57 |
58 |
59 |
60 | '''.format(version=__version__)
61 | util.assert_xml_equal(g, s)
62 | # test for reentrant of `BaseXML` methods
63 | assert(str(g) == str(g))
64 | query.DefinitionBucket.clear()
65 |
66 |
67 | DEFINITIONS = {
68 | 'APP_FINDER': '/Applications/Finder.app',
69 | 'Open::Calculator': '/Applications/Calculator.app',
70 | }
71 | MAPS = [
72 | ['alt', 'cmd', ['fn']],
73 | ['ctrl alt F', 'APP_FINDER', ['!ModifierFlag::NONE']],
74 | ['cmd', 'alt', ['fn']],
75 | ['ctrl shift C', 'Open::Calculator', ['!none']],
76 | ]
77 | g = Generator(maps=MAPS, definitions=DEFINITIONS)
78 | s = '''
79 |
80 | {version}
81 | -
82 | Easy-Karabiner
83 |
84 | KeyCode::VK_OPEN_URL_APP_FINDER
85 | /Applications/Finder.app
86 |
87 |
88 | KeyCode::VK_OPEN_URL_Calculator
89 | /Applications/Calculator.app
90 |
91 |
-
92 | Enable
93 | private.easy_karabiner
94 |
95 | ModifierFlag::FN
96 | __KeyToKey__ KeyCode::OPTION_L, KeyCode::COMMAND_L
97 | __KeyToKey__ KeyCode::COMMAND_L, KeyCode::OPTION_L
98 |
99 |
100 | ModifierFlag::NONE
101 | __KeyToKey__
102 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
103 | KeyCode::VK_OPEN_URL_APP_FINDER
104 |
105 | __KeyToKey__
106 | KeyCode::C, ModifierFlag::CONTROL_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
107 | KeyCode::VK_OPEN_URL_Calculator
108 |
109 |
110 |
111 |
112 |
113 | '''.format(version=__version__)
114 | util.assert_xml_equal(g, s)
115 |
--------------------------------------------------------------------------------
/tests/test_parse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner.query import DefinitionBucket
3 | from easy_karabiner.parse import *
4 |
5 |
6 | def test_alias():
7 | definitions = {
8 | 'BILIBILI': 'com.typcn.Bilibili',
9 | 'CHERRY_3494': ['0x046a', '0x0011'],
10 | 'UIElementRole::custom_ui': 'used as filter',
11 | 'error': 'because value not valid',
12 | 'replacement_test': ['for', 'example', 'Xee'],
13 | }
14 |
15 | maps = [
16 | ['cmd', 'alt'],
17 | ['_double_', 'shift', '#! osascript -e \'display notification "test1"\''],
18 | ['alt mouse_left', 'mouse_left "#! osascript -e \'display notification \\"test2\\"\'"'],
19 | ['alt E', 'Finder', ['UIElementRole::custom_ui']],
20 | ['__FlipScrollWheel__', 'flipscrollwheel_vertical', ['Finder', 'cmd', 'built_in_keyboard_and_trackpad']],
21 | ['ctrl cmd F', 'cmd F', ['VIRTUALMACHINE']],
22 | ]
23 |
24 | result = '''
25 |
26 | BILIBILI
27 | com.typcn.Bilibili
28 |
29 |
30 | Finder
31 | com.apple.finder
32 |
33 |
34 | CHERRY_3494_PRODUCT
35 | 0x0011
36 |
37 |
38 | built_in_keyboard_and_trackpad_PRODUCT
39 | 0x0259
40 |
41 |
42 | CHERRY_3494_VENDOR
43 | 0x046a
44 |
45 |
46 | built_in_keyboard_and_trackpad_VENDOR
47 | 0x05ac
48 |
49 |
50 | replacement_test
51 | for, example, Xee
52 |
53 | custom_ui
54 |
55 | KeyCode::VK_OPEN_URL_760efb2
56 |
57 |
58 |
59 | KeyCode::VK_OPEN_URL_Finder
60 | /System/Library/CoreServices/Finder.app
61 |
62 |
63 | KeyCode::VK_OPEN_URL_aedd86e
64 |
65 |
66 |
67 | __KeyToKey__
68 | KeyCode::COMMAND_L,
69 | KeyCode::OPTION_L
70 |
71 | __DoublePressModifier__
72 | KeyCode::SHIFT_L,
73 | @begin
74 | KeyCode::SHIFT_L
75 | @end
76 | @begin
77 | KeyCode::VK_OPEN_URL_aedd86e
78 | @end
79 |
80 | __KeyToKey__
81 | PointingButton::LEFT, ModifierFlag::OPTION_L, ModifierFlag::NONE,
82 | PointingButton::LEFT, KeyCode::VK_OPEN_URL_760efb2
83 |
84 |
85 |
86 | custom_ui
87 | __KeyToKey__
88 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
89 | KeyCode::VK_OPEN_URL_Finder
90 |
91 |
92 |
93 |
94 | DeviceVendor::built_in_keyboard_and_trackpad_VENDOR,
95 | DeviceProduct::built_in_keyboard_and_trackpad_PRODUCT
96 |
97 | ModifierFlag::COMMAND_L
98 | Finder
99 | __FlipScrollWheel__
100 | Option::FLIPSCROLLWHEEL_VERTICAL
101 |
102 |
103 |
104 | VIRTUALMACHINE
105 | __KeyToKey__
106 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
107 | KeyCode::F, ModifierFlag::COMMAND_L
108 |
109 |
110 | '''
111 |
112 | DefinitionBucket.clear()
113 | block_objs, definition_objs = parse(maps=maps, definitions=definitions)
114 | s = ''.join(obj.to_str(remove_first_line=True) for obj in (definition_objs + block_objs))
115 | s = ''.join(s.split())
116 | result = ''.join(result.split())
117 | assert(s == result)
118 |
--------------------------------------------------------------------------------
/easy_karabiner/parse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from collections import OrderedDict
4 | from operator import add
5 | from functools import reduce
6 |
7 | from . import util
8 | from . import query
9 | from . import config
10 | from . import osxkit
11 | from . import factory
12 | from . import exception
13 | from .basexml import BaseXML
14 | from .util import print_info
15 |
16 |
17 | def parse(maps, definitions):
18 | """Parse and convert user Easy-Karabiner config to XML objects."""
19 | if config.get('verbose'):
20 | print_info("encoding configuration with UTF-8")
21 | definitions = util.encode_with_utf8(definitions)
22 | maps = util.encode_with_utf8(maps)
23 |
24 | if config.get('verbose'):
25 | print_info("creating definitions")
26 | factory.create_definitions(definitions)
27 | if config.get('verbose'):
28 | print_info("organizing keymaps")
29 | filters_keymaps_table = organize_maps(maps)
30 | if config.get('verbose'):
31 | print_info("creating XML block from keymaps")
32 | block_objs = create_blocks(filters_keymaps_table)
33 |
34 | if config.get('verbose'):
35 | print_info("taking out all relative definitions")
36 | definition_objs = query.DefinitionBucket.get_all_definitions()
37 | return block_objs, definition_objs
38 |
39 |
40 | def organize_maps(maps):
41 | """Convert `maps` to a `OrderedDict` with the consistent type.
42 | The items in `filters_keymaps_table` is ordered by the first present of `filters`.
43 | """
44 | filters_keymaps_table = OrderedDict()
45 |
46 | for raw_map in maps:
47 | if util.is_list_or_tuple(raw_map) and len(raw_map) > 0:
48 | try:
49 | raw_keymap, raw_filters = separate_keymap_filters(raw_map)
50 | filters_keymaps_table.setdefault(raw_filters, []).append(raw_keymap)
51 | except exception.ConfigWarning as e:
52 | exception.ExceptionRegister.record(raw_map, e)
53 | else:
54 | if raw_filters:
55 | exception.ExceptionRegister.put(raw_filters, raw_map)
56 | if raw_keymap:
57 | exception.ExceptionRegister.put(raw_keymap, raw_map)
58 | else:
59 | raise exception.ConfigError('Map must be a list: %s' % raw_map.__repr__())
60 |
61 | return filters_keymaps_table
62 |
63 |
64 | def separate_keymap_filters(raw_map):
65 | """Convert `raw_map` to a `tuple` with consistent type."""
66 | # if last element in `raw_map` is a filter
67 | if util.is_list_or_tuple(raw_map[-1]):
68 | raw_filters = tuple(raw_map[-1])
69 | raw_map = raw_map[:-1]
70 | else:
71 | raw_filters = tuple()
72 |
73 | if len(raw_map) == 0 or len(raw_map[0]) == 0:
74 | raise exception.ConfigWarning("Cannot found keymap")
75 | else:
76 | # if first part in `raw_map` is command marker
77 | if len(raw_map[0]) > 2 and raw_map[0].startswith('_') and raw_map[0].endswith('_'):
78 | command = raw_map[0]
79 | raw_keycombos = raw_map[1:]
80 | else:
81 | command = '__KeyToKey__'
82 | raw_keycombos = raw_map
83 |
84 | # ((key1, key2, ...), (key1, key2, ...))
85 | keycombos = []
86 | for keycombo_str in raw_keycombos:
87 | if factory.DefinitionDetector.is_vkopenurl(keycombo_str) or osxkit.get_app_info(keycombo_str):
88 | keycombos.append((keycombo_str,))
89 | else:
90 | keycombos.append(tuple(util.split_ignore_quote(keycombo_str)))
91 |
92 | raw_keymap = (command,) + tuple(keycombos)
93 | return raw_keymap, raw_filters
94 |
95 |
96 | def create_blocks(filters_keymaps_table):
97 | tmp = OrderedDict()
98 |
99 | for raw_filters in filters_keymaps_table:
100 | raw_keymaps = filters_keymaps_table[raw_filters]
101 |
102 | try:
103 | factory.define_filters(raw_filters)
104 | factory.define_keymaps(raw_keymaps)
105 |
106 | filter_objs = factory.create_filters(raw_filters)
107 | keymap_objs = factory.create_keymaps(raw_keymaps)
108 | tmp.setdefault(filter_objs, []).append(keymap_objs)
109 | except exception.UndefinedFilterException as e:
110 | exception.ExceptionRegister.record_by(raw_filters, e)
111 |
112 | blocks = []
113 | for filter_objs in tmp:
114 | keymap_objs = reduce(add, tmp[filter_objs])
115 | block = Block(keymap_objs, filter_objs)
116 | blocks.append(block)
117 | return blocks
118 |
119 |
120 | class Block(BaseXML):
121 | """Block is a kind of XML node similar to `item` in Karabiner.
122 | For example, the following XML is a typical `block`.
123 |
124 |
125 | VIRTUALMACHINE
126 | __KeyToKey__
127 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
128 | KeyCode::F, ModifierFlag::COMMAND_L
129 |
130 |
131 | """
132 | def __init__(self, keymaps, filters=None):
133 | self.keymaps = keymaps
134 | self.filters = filters or tuple()
135 |
136 | def to_xml(self):
137 | xml_tree = self.create_tag('block')
138 |
139 | for f in self.filters:
140 | xml_tree.append(f.to_xml())
141 | for k in self.keymaps:
142 | xml_tree.append(k.to_xml())
143 |
144 | return xml_tree
145 |
--------------------------------------------------------------------------------
/tests/test_definition.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner import util
3 | from easy_karabiner.definition import *
4 |
5 |
6 | def test_appdef():
7 | d = App('BILIBILI', 'com.typcn.Bilibili')
8 | s = '''
9 |
10 | BILIBILI
11 | com.typcn.Bilibili
12 | '''
13 | util.assert_xml_equal(d, s)
14 |
15 | d = App('PQRS', 'org.pqrs.aaa', 'org.pqrs.ccc.', '.local')
16 | s = '''
17 |
18 | PQRS
19 | org.pqrs.aaa
20 | org.pqrs.ccc.
21 | .local
22 | '''
23 | util.assert_xml_equal(d, s)
24 |
25 |
26 | def test_windownamedef():
27 | d = WindowName('Google_Search', ' - Google Search$', 'Search$')
28 | s = '''
29 |
30 | Google_Search
31 | - Google Search$
32 | Search$
33 | '''
34 | util.assert_xml_equal(d, s)
35 |
36 |
37 | def test_devicedef():
38 | d = DeviceVendor('HEWLETT_PACKARD', '0x03f0')
39 | s = '''
40 |
41 | HEWLETT_PACKARD
42 | 0x03f0
43 | '''
44 | util.assert_xml_equal(d, s)
45 |
46 | d = DeviceProduct('MY_HP_KEYBOARD', '0x0224')
47 | s = '''
48 |
49 | MY_HP_KEYBOARD
50 | 0x0224
51 | '''
52 | util.assert_xml_equal(d, s)
53 |
54 |
55 | def test_inputsourcedef():
56 | d = InputSource('AZERTY',
57 | 'com.apple.keylayout.ABC-AZERTY',
58 | 'com.apple.keylayout.French')
59 | s = '''
60 |
61 | AZERTY
62 | com.apple.keylayout.ABC-AZERTY
63 | com.apple.keylayout.French
64 | '''
65 | util.assert_xml_equal(d, s)
66 |
67 | d = InputSource('CHINESE', 'zh-Hans', 'zh-Hant')
68 | s = '''
69 |
70 | CHINESE
71 | zh-Hans
72 | zh-Hant
73 | '''
74 | util.assert_xml_equal(d, s)
75 |
76 | d = InputSource('DVORAK',
77 | 'com.apple.keylayout.Dvorak.',
78 | 'com.apple.keylayout.DVORAK.')
79 | s = '''
80 |
81 | DVORAK
82 | com.apple.keylayout.Dvorak
83 | com.apple.keylayout.DVORAK
84 | '''
85 | util.assert_xml_equal(d, s)
86 |
87 | d = VKChangeInputSource('TEST_ALL', 'com.example.equal', 'com.example.prefix.', 'en-US')
88 | s = '''
89 |
90 | TEST_ALL
91 | com.example.equal
92 | com.example.prefix
93 | en-US
94 | '''
95 | util.assert_xml_equal(d, s)
96 |
97 |
98 | def test_vkopenurldef():
99 | d = VKOpenURL('KeyCode::VK_OPEN_URL_karabiner', 'https://pqrs.org/osx/karabiner/')
100 | s = '''
101 |
102 | KeyCode::VK_OPEN_URL_karabiner
103 | https://pqrs.org/osx/karabiner/
104 | '''
105 | util.assert_xml_equal(d, s)
106 |
107 | d = VKOpenURL('KeyCode::VK_OPEN_URL_FINDER', '/Applications/Finder.app', background=True)
108 | s = '''
109 |
110 | KeyCode::VK_OPEN_URL_FINDER
111 | /Applications/Finder.app
112 |
113 | '''
114 | util.assert_xml_equal(d, s)
115 |
116 | d = VKOpenURL('KeyCode::VK_OPEN_URL_Calculator', '/Applications/Calculator.app')
117 | s = '''
118 |
119 | KeyCode::VK_OPEN_URL_Calculator
120 | /Applications/Calculator.app
121 | '''
122 | util.assert_xml_equal(d, s)
123 |
124 | d = VKOpenURL('KeyCode::VK_OPEN_URL_date_pbcopy', '#! /bin/date | /usr/bin/pbcopy')
125 | s = '''
126 |
127 | KeyCode::VK_OPEN_URL_date_pbcopy
128 |
129 | '''
130 | util.assert_xml_equal(d, s)
131 |
132 |
133 | def test_replacementdef():
134 | d = Replacement('EMACS_IGNORE_APP',
135 | 'ECLIPSE', 'EMACS', 'TERMINAL',
136 | 'REMOTEDESKTOPCONNECTION', 'VI', 'X11',
137 | 'VIRTUALMACHINE', 'TERMINAL', 'SUBLIMETEXT')
138 | s = '''
139 |
140 | EMACS_IGNORE_APP
141 |
142 | ECLIPSE, EMACS, TERMINAL,
143 | REMOTEDESKTOPCONNECTION, VI, X11,
144 | VIRTUALMACHINE, TERMINAL, SUBLIMETEXT
145 |
146 | '''
147 | util.assert_xml_equal(d, s)
148 |
149 |
150 | def test_uielementrole():
151 | d = UIElementRole('AXTextField')
152 | s = ''' AXTextField '''
153 | util.assert_xml_equal(d, s)
154 |
--------------------------------------------------------------------------------
/easy_karabiner/definition.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from . import exception
4 | from . import def_tag_map
5 | from .basexml import BaseXML
6 |
7 |
8 | class DefinitionBase(BaseXML):
9 | """A object represent definition XML node in Karabiner.
10 | For example, the following XML is a typical definition.
11 |
12 |
13 | BILIBILI
14 | com.typcn.Bilibili
15 |
16 | """
17 | def __init__(self, name, *tag_vals, **kwargs):
18 | self.name = name
19 | self.tag_vals = tag_vals
20 | self.kwargs = kwargs
21 |
22 | def get_name(self):
23 | return self.name
24 |
25 | @classmethod
26 | def get_def_tag_name(cls):
27 | return '%sdef' % cls.get_class_name().lower()
28 |
29 | @classmethod
30 | def get_name_tag_name(cls):
31 | return def_tag_map.get_name_tag_name(cls.get_def_tag_name())
32 |
33 | def get_tag_val_pair(self, val):
34 | raise exception.NeedOverrideError()
35 |
36 | def get_tag_val_pairs(self, tag_vals):
37 | return [self.get_tag_val_pair(tag_val) for tag_val in tag_vals]
38 |
39 | @classmethod
40 | def split_name_and_attrs(cls, tag_name):
41 | """Because some tag names contain attributes,
42 | so we need separate it into two parts.
43 | """
44 | # transform `key="value"` to `(key, value)`
45 | def to_pair(s):
46 | removed_quote = ''.join(ch for ch in s if ch not in ('"', "'"))
47 | return removed_quote.split('=')
48 |
49 | name_parts = tag_name.split()
50 | name = name_parts[0]
51 | raw_attrs = [w for w in name_parts if "=" in w]
52 | tag_attrs = dict(to_pair(w) for w in raw_attrs)
53 | return name, tag_attrs
54 |
55 | def to_xml(self):
56 | xml_tree = self.create_tag(self.get_def_tag_name())
57 | name_tag = self.create_tag(self.get_name_tag_name(), self.name)
58 | xml_tree.append(name_tag)
59 |
60 | tag_val_pairs = self.get_tag_val_pairs(self.tag_vals)
61 |
62 | for tag_name, tag_val in tag_val_pairs:
63 | if len(tag_name) > 0:
64 | tag_name, tag_attrs = self.split_name_and_attrs(tag_name)
65 | tag = self.create_tag(tag_name, tag_val, attrib=tag_attrs)
66 | xml_tree.append(tag)
67 |
68 | return xml_tree
69 |
70 | @property
71 | def id(self):
72 | return self.get_def_tag_name(), self.get_name()
73 |
74 | def __hash__(self):
75 | return hash(self.id)
76 |
77 | def __eq__(self, other):
78 | return self.id == other.id
79 |
80 |
81 | class NoNameTagDefinitionBase(DefinitionBase):
82 | def to_xml(self):
83 | xml_tree = self.create_tag(self.get_def_tag_name(), self.name)
84 | return xml_tree
85 |
86 |
87 | class App(DefinitionBase):
88 | """
89 | >>> d = App('BILIBILI', 'com.typcn.Bilibili')
90 | >>> s = '''
91 | ...
92 | ... BILIBILI
93 | ... com.typcn.Bilibili
94 | ...
95 | ... '''
96 | >>> util.assert_xml_equal(d, s)
97 | """
98 |
99 | def get_tag_val_pair(self, val):
100 | if len(val) == 0:
101 | return ()
102 | if val[0] == '.':
103 | return ('suffix', val)
104 | elif val[-1] == '.':
105 | return ('prefix', val)
106 | else:
107 | return ('equal', val)
108 |
109 |
110 | class WindowName(DefinitionBase):
111 | def get_tag_val_pair(self, val):
112 | return ('regex', val)
113 |
114 |
115 | class DeviceVendor(DefinitionBase):
116 | def get_tag_val_pair(self, val):
117 | return ('vendorid', val)
118 |
119 |
120 | class DeviceProduct(DefinitionBase):
121 | def get_tag_val_pair(self, val):
122 | return ('productid', val)
123 |
124 |
125 | class InputSource(DefinitionBase):
126 | @classmethod
127 | def is_host(cls, val):
128 | parts = val.split('.')
129 | return len(parts) >= 3 and all(len(part) for part in parts)
130 |
131 | def get_tag_val_pair(self, val):
132 | if len(val) == 0:
133 | return ()
134 | if val[-1] == '.':
135 | return ('inputsourceid_prefix', val[:-1])
136 | elif self.is_host(val):
137 | return ('inputsourceid_equal', val)
138 | else:
139 | return ('languagecode', val)
140 |
141 |
142 | class VKChangeInputSource(InputSource):
143 | pass
144 |
145 |
146 | class VKOpenURL(DefinitionBase):
147 | def get_tag_val_pair(self, val):
148 | if len(val) == 0:
149 | return ()
150 | elif val.startswith('/'):
151 | return ('url type="file"', val)
152 | elif val.startswith('#!'):
153 | if val.startswith('#! '):
154 | val = val[3:]
155 | script = self.create_cdata_text(val)
156 | return ('url type="shell"', script)
157 | else:
158 | return ('url', val)
159 |
160 | def to_xml(self):
161 | xml_tree = super(VKOpenURL, self).to_xml()
162 | if self.kwargs.get('background', False):
163 | background_tag = self.create_tag('background')
164 | xml_tree.append(background_tag)
165 | return xml_tree
166 |
167 |
168 | class Replacement(DefinitionBase):
169 | def get_tag_val_pair(self, val):
170 | return ('replacementvalue', val)
171 |
172 | def get_tag_val_pairs(self, tag_vals):
173 | vals = []
174 |
175 | for tag_val in tag_vals:
176 | val = tag_val.strip()
177 | if self.is_cdata_text(val):
178 | val = self.create_cdata_text(self.remove_cdata_mark(val))
179 | else:
180 | val = tag_val
181 |
182 | vals.append(val)
183 |
184 | return [self.get_tag_val_pair(', '.join(vals))]
185 |
186 |
187 | class UIElementRole(NoNameTagDefinitionBase):
188 | pass
189 |
190 |
191 | class Modifier(NoNameTagDefinitionBase):
192 | pass
193 |
194 |
195 | if __name__ == "__main__":
196 | import doctest
197 | from . import util
198 | doctest.testmod(extraglobs={'util': util})
199 |
--------------------------------------------------------------------------------
/examples/myconfig.py:
--------------------------------------------------------------------------------
1 | DEFINITIONS = {
2 | 'cherry_3494': ['0x046a', '0x0011'],
3 | 'emacs_ignore_app': ['ECLIPSE', 'EMACS', 'TERMINAL',
4 | 'REMOTEDESKTOPCONNECTION', 'VI', 'X11',
5 | 'VIRTUALMACHINE', 'TERMINAL', 'Sublime Text'],
6 | }
7 |
8 | MAPS = [
9 | ['__FlipScrollWheel__', 'flipscrollwheel_vertical', ['!APPLE_COMPUTER', '!ANY']],
10 | ['_holding_', 'esc', 'cmd_r ctrl_r alt_r shift_r'],
11 | ['_double_' , 'fn' , 'F12'],
12 | ['_double_' , 'fn' , 'cmd alt I', ['Google Chrome']],
13 | ['_press_modifier_', 'ctrl', 'esc'],
14 |
15 | ['alt mouse_left', 'mouse_left "#! osascript /usr/local/bin/copy_finder_path"'],
16 |
17 | ['F5', 'brightness_down', ['!PyCharm CE']],
18 | ['F6', 'brightness_up', ['!PyCharm CE']],
19 | ['F10', 'volume_mute', ['!PyCharm CE']],
20 | ['F11', 'volume_down', ['!PyCharm CE']],
21 | ['F12', 'volume_up', ['!PyCharm CE']],
22 |
23 | ['alt A' , 'iTerm'],
24 | ['alt E' , 'Finder'],
25 | ['alt C' , 'Google Chrome'],
26 | ['alt S' , 'Sublime Text'],
27 | ['alt P' , 'PyCharm CE'],
28 | ['ctrl cmd del', 'Activity Monitor'],
29 | ['ctrl cmd ,' , 'System Preferences'],
30 |
31 | ['alt', 'cmd', ['cherry_3494']],
32 | ['cmd', 'alt', ['cherry_3494']],
33 |
34 | ['cmd K', 'up ' * 6 , ['Skim']],
35 | ['cmd J', 'down ' * 6 , ['Skim']],
36 | ['alt L', 'ctrl_r tab' , ['Skim']],
37 | ['alt H', 'ctrl_r shift_r tab' , ['Skim']],
38 |
39 | ['cmd K', 'up ' * 30 , ['Google Chrome']],
40 | ['cmd J', 'down ' * 30 , ['Google Chrome']],
41 | ['alt L', 'ctrl_r tab' , ['Google Chrome']],
42 | ['alt H', 'ctrl_r shift_r tab' , ['Google Chrome']],
43 | ['ctrl l', 'cmd_r l' , ['Google Chrome']],
44 |
45 | ['ctrl P' , 'up ' * 6 , ['Skim']],
46 | ['ctrl N' , 'down ' * 6 , ['Skim']],
47 | ['alt shift ,' , 'fn left' , ['Skim']],
48 | ['alt shift .' , 'fn right' , ['Skim']],
49 |
50 | ['ctrl D' , 'cmd_r del' , ['Xee³']],
51 | ['ctrl P' , 'cmd_r left' , ['Xee³']],
52 | ['ctrl N' , 'cmd_r right' , ['Xee³']],
53 |
54 | ['alt shift ,' , 'alt_r up' , ['Finder']],
55 | ['alt shift .' , 'alt_r down' , ['Finder']],
56 |
57 | ['alt shift ,' , 'cmd_r up' , ['Sublime Text']],
58 | ['alt shift .' , 'cmd_r down' , ['Sublime Text']],
59 | ['ctrl P' , 'up' , ['Sublime Text']],
60 | ['ctrl N' , 'down' , ['Sublime Text']],
61 |
62 | ['ctrl P' , 'up' , ['!emacs_ignore_app', '!Skim', '!Xee³']],
63 | ['ctrl N' , 'down' , ['!emacs_ignore_app', '!Skim', '!Xee³']],
64 | ['ctrl D' , 'fdel' , ['!emacs_ignore_app', '!Skim', '!Xee³']],
65 |
66 | ['alt shift ,' , 'cmd_r up' , ['!emacs_ignore_app', '!Skim', '!Finder', '!Sublime Text']],
67 | ['alt shift .' , 'cmd_r down' , ['!emacs_ignore_app', '!Skim', '!Finder', '!Sublime Text']],
68 |
69 | ['ctrl B' , 'left' , ['!emacs_ignore_app']],
70 | ['ctrl F' , 'right' , ['!emacs_ignore_app']],
71 | ['alt B' , 'alt_r left' , ['!emacs_ignore_app']],
72 | ['alt F' , 'alt_r right' , ['!emacs_ignore_app']],
73 | ['ctrl A' , 'cmd_r left' , ['!emacs_ignore_app']],
74 | ['ctrl E' , 'cmd_r right' , ['!emacs_ignore_app']],
75 | ['ctrl H' , 'del' , ['!emacs_ignore_app']],
76 | ['alt D' , 'alt_r fdel' , ['!emacs_ignore_app']],
77 | ['ctrl U' , 'cmd_r right cmd_r shift_r left del del norepeat', ['!emacs_ignore_app']],
78 |
79 | ['ctrl cmd F' , 'cmd_r return' , ['TERMINAL']],
80 | ['ctrl cmd F' , 'cmd_r shift_r F cmd_r shift_r -', ['Skim', 'Kindle']],
81 | ['ctrl cmd F' , 'cmd F' , ['VIRTUALMACHINE']],
82 |
83 | ['alt R' , 'cmd_r R' , ['VIRTUALMACHINE', 'X11']],
84 | ['alt E' , 'cmd_r E' , ['VIRTUALMACHINE', 'X11']],
85 | ['cmd D' , 'cmd_r D' , ['VIRTUALMACHINE', 'X11']],
86 |
87 | ['ctrl H' , 'del' , ['VIRTUALMACHINE', 'X11']],
88 | ['ctrl D' , 'fdel' , ['VIRTUALMACHINE', 'X11']],
89 | ['ctrl U' , 'end shift home del del norepeat' , ['VIRTUALMACHINE', 'X11']],
90 |
91 | ['ctrl alt del' , 'ctrl_r del' , ['VIRTUALMACHINE', 'X11']],
92 | ['ctrl alt D' , 'ctrl_r fdel' , ['VIRTUALMACHINE', 'X11']],
93 | ['ctrl alt F' , 'ctrl_r right' , ['VIRTUALMACHINE', 'X11']],
94 | ['ctrl alt B' , 'ctrl_r left' , ['VIRTUALMACHINE', 'X11']],
95 |
96 | ['cmd Q' , 'alt_r F4' , ['VIRTUALMACHINE', 'X11']],
97 | ['cmd R' , 'ctrl_r R' , ['VIRTUALMACHINE', 'X11']],
98 | ['cmd L' , 'ctrl_r L' , ['VIRTUALMACHINE', 'X11']],
99 | ['cmd C' , 'ctrl_r C' , ['VIRTUALMACHINE', 'X11']],
100 | ['cmd V' , 'ctrl_r V' , ['VIRTUALMACHINE', 'X11']],
101 | ['cmd X' , 'ctrl_r X' , ['VIRTUALMACHINE', 'X11']],
102 | ['cmd Z' , 'ctrl_r Z' , ['VIRTUALMACHINE', 'X11']],
103 | ['cmd A' , 'ctrl_r A' , ['VIRTUALMACHINE', 'X11']],
104 | ['cmd F' , 'ctrl_r F' , ['VIRTUALMACHINE', 'X11']],
105 | ['cmd S' , 'ctrl_r S' , ['VIRTUALMACHINE', 'X11']],
106 | ['cmd W' , 'ctrl_r W' , ['VIRTUALMACHINE', 'X11']],
107 | ['cmd T' , 'ctrl_r T' , ['VIRTUALMACHINE', 'X11']],
108 | ['ctrl A' , 'home' , ['VIRTUALMACHINE', 'X11']],
109 | ['cmd left' , 'home' , ['VIRTUALMACHINE', 'X11']],
110 | ['ctrl E' , 'end' , ['VIRTUALMACHINE', 'X11']],
111 | ['cmd right' , 'end' , ['VIRTUALMACHINE', 'X11']],
112 | ['ctrl P' , 'up' , ['VIRTUALMACHINE', 'X11']],
113 | ['ctrl N' , 'down' , ['VIRTUALMACHINE', 'X11']],
114 | ['ctrl F' , 'right' , ['VIRTUALMACHINE', 'X11']],
115 | ['ctrl B' , 'left' , ['VIRTUALMACHINE', 'X11']],
116 |
117 | ['ctrl tab' , 'cmd_r alt_r right', ['Bilibili']],
118 | ['ctrl shift tab', 'cmd_r alt_r left' , ['Bilibili']],
119 |
120 | ['left' , 'cmd_r left' , ['Xee³']],
121 | ['up' , 'cmd_r left' , ['Xee³']],
122 | ['H' , 'cmd_r left' , ['Xee³']],
123 | ['K' , 'cmd_r left' , ['Xee³']],
124 | ['right', 'cmd_r right', ['Xee³']],
125 | ['down' , 'cmd_r right', ['Xee³']],
126 | ['J' , 'cmd_r right', ['Xee³']],
127 | ['L' , 'cmd_r right', ['Xee³']],
128 |
129 | ['cmd P', 'cmd_r alt_r G', ['Skim']],
130 | ]
131 |
--------------------------------------------------------------------------------
/easy_karabiner/osxkit.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import print_function
3 | import os
4 | import subprocess
5 | from . import util
6 | from .fucking_string import ensure_utf8
7 |
8 | __all__ = ['get_app_info', 'get_all_app_info',
9 | 'get_peripheral_info', 'get_all_peripheral_info']
10 |
11 |
12 | def call(cmd, **kwargs):
13 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, **kwargs)
14 | output = proc.stdout.read().decode('utf-8')
15 | return output
16 |
17 |
18 | def contains_any_keywords(s, *keywords):
19 | words = set(w.lower() for w in s.split())
20 | keywords = set(w.lower() for w in keywords)
21 | return not words.isdisjoint(keywords)
22 |
23 |
24 | def get_name_without_suffix(path):
25 | return os.path.splitext(os.path.basename(path))[0]
26 |
27 |
28 | def get_all_app_info():
29 | """Return dict contains applications information. Item in the dict has format like:
30 |
31 | 'basename / display_name' : (absolute_path, bundle_identifier)
32 |
33 | NOTICE: `basename` or `display_name` already removed '.app' suffix
34 | """
35 | # list absolute path of all applications
36 | LIST_APP_PATH_CMD = ('mdfind', '-0', 'kMDItemContentType==com.apple.application-bundle')
37 | # get Bundle-Identifier and Display-Name of applications by absolute path
38 | LIST_APP_INFO_CMD = ('mdls', '-raw',
39 | '-name', 'kMDItemCFBundleIdentifier',
40 | '-name', 'kMDItemDisplayName')
41 |
42 | raw_paths = call(LIST_APP_PATH_CMD).split('\x00')
43 | app_paths = [p for p in raw_paths if p.strip()]
44 |
45 | raw_infos = call(LIST_APP_INFO_CMD + tuple(app_paths))
46 | info_lines = raw_infos.split('\x00')
47 |
48 | app_ids = info_lines[0::2]
49 | app_infos = tuple(zip(app_paths, app_ids))
50 | # display name not always equal to basename,
51 | # because system language and application name maybe not English
52 | display_names = tuple(info_lines[1::2])
53 | basenames = tuple(get_name_without_suffix(p) for p in app_paths)
54 |
55 | name_map = tuple(zip(display_names + basenames, app_infos + app_infos))
56 | return dict(name_map)
57 |
58 |
59 | def get_app_info(name):
60 | if not hasattr(get_app_info, 'infos'):
61 | get_app_info.infos = get_all_app_info()
62 |
63 | if name.endswith('.app'):
64 | name = name[:-4]
65 | name = ensure_utf8(name)
66 | return get_app_info.infos.get(name)
67 |
68 |
69 | def lines2tree(lines):
70 | def do_lines2tree(root, lines, i=0, level=-1):
71 | while i < len(lines):
72 | line = lines[i]
73 | indent_level = len(line) - len(line.lstrip())
74 |
75 | if indent_level > level:
76 | key, val = line.split(':', 1)
77 | key = key.strip()
78 | val = val.strip()
79 |
80 | if line.endswith(':'):
81 | root[key] = {}
82 | i = do_lines2tree(root[key], lines, i + 1, indent_level)
83 | else:
84 | root[key] = val
85 | i += 1
86 | else:
87 | break
88 | return i
89 |
90 | tree = {}
91 | do_lines2tree(tree, lines)
92 | return tree
93 |
94 |
95 | def get_devices(item, conditions):
96 | devices = {}
97 | for k in item:
98 | if isinstance(item[k], dict) and any(fn(k) for fn in conditions):
99 | devices[k] = item[k]
100 | return devices
101 |
102 |
103 | def get_display_name(key, brand, product_id):
104 | id_num = product_id[-4:]
105 |
106 | if key == 'Apple Internal Keyboard / Trackpad':
107 | name = 'built-in keyboard and trackpad'
108 | elif contains_any_keywords(key, 'Device'):
109 | name = '%s_%s' % (brand, id_num)
110 | else:
111 | name = '%s_%s' % (key, id_num)
112 |
113 | return util.escape_string(name)
114 |
115 |
116 | def get_all_peripheral_info():
117 | """Return dict contains devices information. Item in the dict has format like:
118 |
119 | 'display_name' : (vendor_id, product_id)
120 | """
121 | # list information about USB and Bluetooth devices
122 | CMD = ('system_profiler', '-detailLevel', 'mini', '-timeout', '1',
123 | 'SPUSBDataType', 'SPBluetoothDataType')
124 |
125 | output = call(CMD)
126 | lines = [line for line in output.split('\n') if line.strip()]
127 | info_tree = lines2tree(lines)
128 |
129 | # Bluetooth Devices
130 | items = [v for k, v in info_tree['Bluetooth'].items() if k.startswith('Devices')]
131 | if len(items) > 0:
132 | item = items[0]
133 | conditions = [
134 | lambda k: k == 'Apple Internal Keyboard / Trackpad',
135 | lambda k: item[k].get('Major Type') == 'Peripheral',
136 | ]
137 | devices = get_devices(item, conditions)
138 | else:
139 | devices = {}
140 |
141 | # USB Devices
142 | for item in info_tree['USB'].values():
143 | conditions = [
144 | lambda k: contains_any_keywords(k, 'Keyboard', 'Trackpad', 'Mouse'),
145 | lambda k: item[k].get('Built-In') != 'Yes',
146 | ]
147 | devices.update(get_devices(item, conditions))
148 |
149 | # only keep (Vendor ID, Product ID) properties
150 | for k in sorted(devices.keys()):
151 | device = devices.pop(k)
152 | if 'Product ID' in device and 'Vendor ID' in device:
153 | product_id = device['Product ID']
154 | vendor_parts = device['Vendor ID'].split(' ', 1)
155 | vendor_id = vendor_parts[0]
156 | brand = device.get('Manufacturer', vendor_parts[-1].strip()[1:-1])
157 | # construct display name from properties
158 | name = ensure_utf8(get_display_name(k, brand, product_id))
159 |
160 | devices[name] = (vendor_id, product_id)
161 |
162 | return devices
163 |
164 |
165 | def get_peripheral_info(name):
166 | if not hasattr(get_peripheral_info, 'infos'):
167 | get_peripheral_info.infos = get_all_peripheral_info()
168 |
169 | name = ensure_utf8(name)
170 | return get_peripheral_info.infos.get(name)
171 |
172 |
173 | # used for test purpose
174 | if __name__ == '__main__':
175 | app_names = ['Finder', 'Xee³',
176 | '虾米音乐', '坚果云', '地图',
177 | 'xiami', 'Nutstore', 'Maps']
178 |
179 | peripheral_names = ['“fz”的键盘_0255', 'Lenovo_USB_Optical_Mouse_6019',
180 | 'built_in_keyboard_and_trackpad', 'CHERRY_GmbH_0011']
181 |
182 | print(' Application Info '.center(80, '-'))
183 | for app_name in app_names:
184 | print('%s \t%s' % (app_name, get_app_info(app_name)))
185 |
186 | print(' Peripheral Info '.center(80, '-'))
187 | for peripheral_name in peripheral_names:
188 | print('%-42s\t%s' % (peripheral_name, get_peripheral_info(peripheral_name)))
189 |
--------------------------------------------------------------------------------
/easy_karabiner/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """A tool to generate key remap configuration for Karabiner
3 |
4 | Usage:
5 | easy_karabiner [-evrl] [SOURCE] [TARGET | --string]
6 | easy_karabiner [--help | --version]
7 | """
8 | from __future__ import print_function
9 | import os
10 | import sys
11 | import lxml
12 | import click
13 | from subprocess import call
14 | from . import __version__
15 | from . import util
16 | from . import query
17 | from . import config
18 | from . import exception
19 | from .osxkit import get_all_peripheral_info
20 | from .basexml import BaseXML
21 | from .generator import Generator
22 | from .fucking_string import ensure_utf8, write_utf8_to, is_string_type
23 | from .util import print_error, print_warning, print_info, print_message
24 |
25 |
26 | @click.command()
27 | @click.help_option('--help', '-h')
28 | @click.version_option(__version__, '--version', '-v', message='%(version)s')
29 | @click.argument('inpath', default=config.get_default_config_path(), type=click.Path())
30 | @click.argument('outpath', default=config.get_default_output_path(), type=click.Path())
31 | @click.option('--verbose', '-V', help='Print more text.', is_flag=True)
32 | @click.option('--string', '-s', help='Output as string.', is_flag=True)
33 | @click.option('--reload', '-r', help='Reload Karabiner.', is_flag=True)
34 | @click.option('--no-reload', help='Opposite of --reload', is_flag=True)
35 | @click.option('--edit', '-e', help='Edit default config file.', is_flag=True)
36 | @click.option('--list-peripherals', '-l', help='List name of all peripherals.', is_flag=True)
37 | def main(inpath, outpath, **options):
38 | """
39 | \b
40 | $ easy_karabiner
41 | $ easy_karabiner input.py output.xml
42 | $ easy_karabiner input.py --string
43 | """
44 | config.set(options)
45 |
46 | if config.get('help') or config.get('version'):
47 | return
48 | elif config.get('edit'):
49 | edit_config_file()
50 | elif config.get('list_peripherals'):
51 | list_peripherals()
52 | elif config.get('reload'):
53 | reload_karabiner()
54 | else:
55 | try:
56 | configs = read_config_file(inpath)
57 | xml_str = gen_config(configs)
58 |
59 | if config.get('string'):
60 | print_message(xml_str)
61 | else:
62 | try:
63 | if not is_generated_by_easy_karabiner(outpath):
64 | backup_file(outpath)
65 | except IOError:
66 | pass
67 | finally:
68 | write_generated_xml(outpath, xml_str)
69 | if is_need_reload(config.get('reload'), config.get('no_reload'), outpath):
70 | reload_karabiner()
71 |
72 | show_config_warnings()
73 | except exception.ConfigError as e:
74 | print_error(e)
75 | sys.exit(1)
76 | except IOError as e:
77 | print_error("%s not exist" % e.filename, print_stack=True)
78 | sys.exit(1)
79 | except Exception as e:
80 | print_error(e, print_stack=True)
81 | sys.exit(1)
82 |
83 | sys.exit(0)
84 |
85 |
86 | def read_config_file(config_path):
87 | if config.get('verbose'):
88 | print_info('executing "%s"' % config_path)
89 | return util.read_python_file(config_path)
90 |
91 |
92 | def write_generated_xml(outpath, content):
93 | if config.get('verbose'):
94 | print_info('writing XML to "%s"' % outpath)
95 | write_utf8_to(content, outpath)
96 |
97 |
98 | def edit_config_file():
99 | click.edit(filename=config.get_default_config_path())
100 |
101 |
102 | def is_need_reload(reload, no_reload, outpath):
103 | if no_reload:
104 | return False
105 | else:
106 | return reload or (outpath == config.get_default_output_path())
107 |
108 |
109 | def reload_karabiner():
110 | NOTIFICATION_MSG = "Enabled generated configuration"
111 | NOTIFICATION_CMD = ('/usr/bin/osascript', '-e',
112 | 'display notification "%s" with title "Karabiner Reloaded"' % NOTIFICATION_MSG)
113 | KARABINER_CMD = config.get_karabiner_bin('karabiner')
114 |
115 | if config.get('verbose'):
116 | print_info("reloading Karabiner config")
117 | call([KARABINER_CMD, 'enable', 'private.easy_karabiner'])
118 | call([KARABINER_CMD, 'reloadxml'])
119 | call(NOTIFICATION_CMD)
120 |
121 |
122 | def list_peripherals():
123 | peripheral_names = [ensure_utf8(name) for name in get_all_peripheral_info().keys()]
124 | for name in sorted(peripheral_names):
125 | print_message(name)
126 |
127 |
128 | def gen_config(configs):
129 | maps = configs.get('MAPS')
130 | definitions = configs.get('DEFINITIONS')
131 | if config.get('verbose'):
132 | print_info('update aliases from user configuration')
133 | query.update_aliases(configs)
134 | if config.get('verbose'):
135 | print_info("generating XML configuration")
136 | return Generator(maps, definitions).to_str()
137 |
138 |
139 | def is_generated_by_easy_karabiner(filepath):
140 | try:
141 | tag = BaseXML.parse(filepath).find('Easy-Karabiner')
142 | return tag is not None
143 | except lxml.etree.XMLSyntaxError:
144 | return False
145 |
146 |
147 | def backup_file(filepath, new_path=None):
148 | with open(filepath, 'rb') as fp:
149 | if new_path is None:
150 | # private.xml -> private.941f123.xml
151 | checksum = util.get_checksum(fp.read())
152 | parts = os.path.basename(filepath).split('.')
153 | parts.insert(-1, checksum)
154 | new_name = '.'.join(parts)
155 |
156 | if config.get('verbose'):
157 | print_info("backup original XML config file")
158 | new_path = os.path.join(os.path.dirname(filepath), new_name)
159 |
160 | os.rename(filepath, new_path)
161 | return new_path
162 |
163 |
164 | def show_config_warnings():
165 | records = exception.ExceptionRegister.get_all_records()
166 | for raw_data, e in records:
167 | exception_class = type(e)
168 |
169 | if exception_class == exception.UndefinedFilterException:
170 | msg = 'Undefined filter'
171 | elif exception_class == exception.UndefinedKeyException:
172 | msg = 'Undefined key'
173 | elif exception_class == exception.InvalidDefinition:
174 | msg = 'Invalid definition'
175 | elif exception_class == exception.InvalidKeymapException:
176 | msg = 'Invalid keymap'
177 | else:
178 | msg = exception_class.__name__
179 |
180 | if len(e.args) == 0:
181 | print_warning('%s: %s' % (msg, raw_data))
182 | elif len(e.args) == 1:
183 | print_warning('%s: `%s` in %s' % (msg, e.args[0], raw_data))
184 | else:
185 | print_warning('%s: %s in %s' % (msg, e.args, raw_data))
186 |
--------------------------------------------------------------------------------
/easy_karabiner/query.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | import os
4 | import glob
5 | from itertools import chain
6 | from . import alias
7 | from . import config
8 | from . import exception
9 | from . import definition
10 | from . import def_tag_map
11 | from .basexml import BaseXML
12 |
13 |
14 | def is_defined_filter(val):
15 | return query_filter_class_names(val, scope='all')
16 |
17 |
18 | def is_defined_key(val):
19 | if get_key_alias(val.lower()):
20 | return True
21 | else:
22 | for k in [val, val.upper(), val.lower()]:
23 | if KeyHeaderQuery.query(k):
24 | return True
25 |
26 | return DefinitionBucket.has('key', val)
27 |
28 |
29 | def is_predefined_modifier(key):
30 | key = get_key_alias(key.lower()) or key
31 |
32 | for k in [key, key.upper(), key.lower()]:
33 | if KeyHeaderQuery.query(k) == 'ModifierFlag':
34 | return True
35 |
36 | parts = key.split('::', 1)
37 | return len(parts) == 2 and parts[0] == 'ModifierFlag'
38 |
39 |
40 | def query_filter_class_names(value, scope='all'):
41 | """Get `Filter` class name by `value` in `scope`."""
42 | definitions = DefinitionBucket.get('filter', value)
43 | def_tag_name = DefinitionTypeQuery.query(value)
44 | filter_class_names = []
45 |
46 | if scope in ('all', 'user') and definitions:
47 | for d in definitions:
48 | filter_class_name = def_tag_map.get_filter_class_name(d.get_def_tag_name())
49 | if filter_class_name:
50 | filter_class_names.append(filter_class_name)
51 | else:
52 | return []
53 | elif scope in ('all', 'predefined'):
54 | if not def_tag_name:
55 | if is_predefined_modifier(value):
56 | def_tag_name = definition.Modifier.get_def_tag_name()
57 | else:
58 | return []
59 |
60 | filter_class_name = def_tag_map.get_filter_class_name(def_tag_name)
61 | if filter_class_name:
62 | filter_class_names.append(filter_class_name)
63 |
64 | return filter_class_names
65 |
66 |
67 | def get_def_alias(value):
68 | return alias.get_alias('DEF_ALIAS', value)
69 |
70 |
71 | def get_key_alias(value):
72 | return alias.get_alias('KEY_ALIAS', value)
73 |
74 |
75 | def get_keymap_alias(value):
76 | return alias.get_alias('KEYMAP_ALIAS', value)
77 |
78 |
79 | def update_aliases(aliases_table):
80 | alias.update_aliases(aliases_table)
81 |
82 |
83 | class BaseTypeQuery(object):
84 | """This class is used to get corresponding type of `value` by searching in XML files.
85 | Because there is a possibility that multiple types associated with the same value,
86 | for avoid this problem, we specify the order of types and return the first occurrence as the query result.
87 | """
88 | DATA_DIR = config.get_karabiner_resources_dir()
89 |
90 | def __init__(self):
91 | self.data = {}
92 |
93 | @classmethod
94 | def get_instance(cls):
95 | if hasattr(cls, '_instance'):
96 | return cls._instance
97 | else:
98 | cls._instance = cls()
99 | cls._instance.load_data()
100 | return cls._instance
101 |
102 | @classmethod
103 | def query(cls, value):
104 | self = cls.get_instance()
105 | for type in self.orders:
106 | if self.is_in(type, value):
107 | return type
108 | return None
109 |
110 | def is_in(self, type, value):
111 | return type if value in self.data.get(type, []) else None
112 |
113 | @property
114 | def orders(self):
115 | raise exception.NeedOverrideError()
116 |
117 | def load_data(self):
118 | raise exception.NeedOverrideError()
119 |
120 |
121 | class KeyHeaderQuery(BaseTypeQuery):
122 | RESOURCE_FILE = 'symbol_map.xml'
123 |
124 | @property
125 | def orders(self):
126 | return [
127 | 'ModifierFlag',
128 | 'ConsumerKeyCode',
129 | 'KeyCode',
130 | 'Option',
131 | 'InputSource',
132 | 'PointingButton',
133 | 'DeviceProduct',
134 | 'DeviceVendor',
135 | 'KeyboardType',
136 | ]
137 |
138 | def load_data(self):
139 | filepath = os.path.join(self.DATA_DIR, self.RESOURCE_FILE)
140 |
141 | xml_tree = BaseXML.parse(filepath)
142 | for symbol_map in xml_tree:
143 | type = symbol_map.get('type')
144 | value = symbol_map.get('name')
145 | self.data.setdefault(type, set()).add(value)
146 |
147 |
148 | class DefinitionTypeQuery(BaseTypeQuery):
149 | @property
150 | def orders(self):
151 | return [
152 | 'appdef',
153 | 'replacementdef',
154 | 'modifierdef',
155 | 'devicevendordef',
156 | 'deviceproductdef',
157 | 'uielementroledef',
158 | 'windownamedef',
159 | 'vkopenurldef',
160 | 'inputsourcedef',
161 | 'vkchangeinputsourcedef',
162 | ]
163 |
164 | @classmethod
165 | def query(cls, value):
166 | result = super(DefinitionTypeQuery, cls).query(value)
167 | if result:
168 | return result
169 | elif KeyHeaderQuery.query(value) == 'ModifierFlag':
170 | return 'modifierdef'
171 |
172 | def load_data(self):
173 | for filepath in glob.glob(os.path.join(self.DATA_DIR, '*def.xml')):
174 | type, _ = os.path.splitext(os.path.basename(filepath))
175 | self.data[type] = self.get_data(type, filepath)
176 |
177 | def get_data(self, type, filepath):
178 | name_tag = def_tag_map.get_name_tag_name(type)
179 | xml_tree = BaseXML.parse(filepath)
180 |
181 | if name_tag == '':
182 | tags = xml_tree.findall(type)
183 | else:
184 | tags = xml_tree.findall('%s/%s' % (type, name_tag))
185 |
186 | return set(tag.text for tag in tags)
187 |
188 |
189 | class DefinitionBucket(object):
190 | """This class is used to store global `Definition` objects,
191 | so we can create a `Definition` object from anywhere,
192 | and found it by the original value used to define.
193 | """
194 | def __init__(self):
195 | self.buckets = {
196 | 'filter': {},
197 | 'key': {},
198 | }
199 |
200 | @classmethod
201 | def get_instance(cls, reset=False):
202 | if not hasattr(cls, '_instance') or reset:
203 | cls._instance = cls()
204 | return cls._instance
205 |
206 | @classmethod
207 | def get_all_definitions(cls):
208 | list_of_defos = [d.values() for d in cls.get_instance().buckets.values()]
209 | defos = set(chain.from_iterable(chain.from_iterable(list_of_defos)))
210 | return sorted(defos, key=lambda f: (f.get_def_tag_name(), f.get_name()))
211 |
212 | @classmethod
213 | def put(cls, category, name, definitions):
214 | cls.get_instance().buckets[category][name] = definitions
215 |
216 | @classmethod
217 | def get(cls, category, name):
218 | return cls.get_instance().buckets[category].get(name)
219 |
220 | @classmethod
221 | def has(cls, category, name):
222 | return name in cls.get_instance().buckets[category]
223 |
224 | @classmethod
225 | def clear(cls):
226 | cls.get_instance(reset=True)
227 |
--------------------------------------------------------------------------------
/easy_karabiner/keymap.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 | from . import exception
4 | from .basexml import BaseXML
5 | from .keycombo import KeyCombo
6 | from .fucking_string import u
7 |
8 |
9 | class KeyToKeyBase(BaseXML):
10 | """A object represent `autogen` XML node in Karabiner.
11 | For example, the following XML is a typical `autogen`.
12 |
13 | __KeyToKey__
14 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
15 | KeyCode::E, ModifierFlag::COMMAND_L
16 |
17 | """
18 | MULTI_KEYS_FMT = u('@begin\n{key}\n@end')
19 | AUTOGEN_FMT = u(' {type}\n{keys_str}\n')
20 |
21 | def __init__(self, *keycombos, **kwargs):
22 | self.keys_str = self.parse(*keycombos, **kwargs)
23 |
24 | def parse(self, *keycombos, **kwargs):
25 | """
26 | :param keycombos: List[List[str]]
27 | :param kwargs: Dict
28 | :return: str
29 | """
30 | raise exception.NeedOverrideError()
31 |
32 | def get_type(self):
33 | return '__%s__' % self.get_class_name()
34 |
35 | def to_xml(self):
36 | text = self.AUTOGEN_FMT.format(type=self.get_type(),
37 | keys_str=self.keys_str)
38 | return self.create_tag('autogen', text)
39 |
40 |
41 | class UniversalKeyToKey(KeyToKeyBase):
42 | def __init__(self, type, *keycombos, **kwargs):
43 | self.type = type
44 | super(UniversalKeyToKey, self).__init__(*keycombos, **kwargs)
45 |
46 | def get_type(self):
47 | return self.type
48 |
49 | def parse(self, *keycombos, **kwargs):
50 | keys = [KeyCombo(k, keep_first_keycode=True, has_modifier_none=False) for k in keycombos]
51 | return ',\n'.join(str(k) for k in keys)
52 |
53 |
54 | class _KeyToMultiKeys(KeyToKeyBase):
55 | KEYS_FMT = u('{from_key},\n'
56 | '@begin\n'
57 | '{to_key}\n'
58 | '@end\n'
59 | '@begin\n'
60 | '{additional_key}\n'
61 | '@end')
62 |
63 | def parse(self, from_key, to_key, additional_key, has_modifier_none=True):
64 | from_key = KeyCombo(from_key, has_modifier_none)
65 | to_key = KeyCombo(to_key)
66 | additional_key = KeyCombo(additional_key)
67 |
68 | return self.KEYS_FMT.format(from_key=from_key.to_str(),
69 | to_key=to_key.to_str(),
70 | additional_key=additional_key.to_str())
71 |
72 |
73 | class _KeyToAdditionalKey(_KeyToMultiKeys):
74 | def parse(self, from_key, to_key=None, additional_key=None, has_modifier_none=True):
75 | if to_key is None and additional_key is None:
76 | raise exception.InvalidKeymapException(from_key)
77 | elif to_key and additional_key is None:
78 | additional_key = to_key
79 | to_key = from_key
80 |
81 | return super(_KeyToAdditionalKey, self).parse(from_key,
82 | to_key,
83 | additional_key,
84 | has_modifier_none)
85 |
86 |
87 | class _OneKeyEvent(KeyToKeyBase):
88 | def parse(self, key, has_modifier_none=False):
89 | return KeyCombo(key, has_modifier_none).to_str()
90 |
91 |
92 | class _ZeroKeyEvent(KeyToKeyBase):
93 | def parse(self):
94 | return ''
95 |
96 |
97 | class _MultiKeys(UniversalKeyToKey):
98 | def __init__(self, *keys, **kwargs):
99 | super(_MultiKeys, self).__init__(self.get_type(), *keys, **kwargs)
100 |
101 | def get_type(self):
102 | return '__%s__' % self.get_class_name()
103 |
104 |
105 | class KeyToKey(KeyToKeyBase):
106 | """
107 | >>> keymap = KeyToKey(['ModifierFlag::OPTION_L', 'KeyCode::E'],
108 | ... ['ModifierFlag::COMMAND_L', 'KeyCode::E'])
109 | >>> s = '''
110 | ... __KeyToKey__
111 | ... KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
112 | ... KeyCode::E, ModifierFlag::COMMAND_L
113 | ...
114 | ... '''
115 | >>> util.assert_xml_equal(keymap, s)
116 | """
117 | KEYS_FMT = u('{from_key},\n{to_key}')
118 |
119 | def parse(self, from_key, to_key, has_modifier_none=True):
120 | from_key = KeyCombo(from_key, has_modifier_none=has_modifier_none).to_str()
121 | to_key = KeyCombo(to_key).to_str()
122 |
123 | return self.KEYS_FMT.format(from_key=from_key, to_key=to_key)
124 |
125 |
126 | class DropAllKeys(KeyToKeyBase):
127 | KEYS_FMT = u('{from_key},\n{options}')
128 |
129 | def parse(self, from_modifier, options=None):
130 | from_key = KeyCombo(from_modifier, keep_first_keycode=True)
131 |
132 | if options is None:
133 | return from_key.to_str()
134 | else:
135 | options = KeyCombo(options, keep_first_keycode=True)
136 | return self.KEYS_FMT.format(from_key=from_key.to_str(),
137 | options=options.to_str())
138 |
139 |
140 | class SimultaneousKeyPresses(KeyToKeyBase):
141 | KEYS_FMT = u('@begin\n'
142 | '{from_key}\n'
143 | '@end\n'
144 | '@begin\n'
145 | '{to_key}\n'
146 | '@end')
147 |
148 | def parse(self, from_key, to_key):
149 | from_key = KeyCombo(from_key, False)
150 | to_key = KeyCombo(to_key, False)
151 |
152 | return self.KEYS_FMT.format(from_key=from_key.to_str(),
153 | to_key=to_key.to_str())
154 |
155 |
156 | class DoublePressModifier(_KeyToAdditionalKey):
157 | pass
158 |
159 |
160 | class HoldingKeyToKey(_KeyToAdditionalKey):
161 | pass
162 |
163 |
164 | class KeyOverlaidModifier(_KeyToAdditionalKey):
165 | pass
166 |
167 |
168 | class KeyDownUpToKey(_KeyToMultiKeys):
169 | def parse(self, from_key, immediately_key, interrupted_key=None, has_modifier_none=True):
170 | return super(KeyDownUpToKey, self).parse(from_key,
171 | immediately_key,
172 | interrupted_key or from_key,
173 | has_modifier_none)
174 |
175 |
176 | class BlockUntilKeyUp(_OneKeyEvent):
177 | pass
178 |
179 |
180 | class DropKeyAfterRemap(_OneKeyEvent):
181 | pass
182 |
183 |
184 | class PassThrough(_ZeroKeyEvent):
185 | pass
186 |
187 |
188 | class DropPointingRelativeCursorMove(_ZeroKeyEvent):
189 | pass
190 |
191 |
192 | # NOTICE: because below `autogen` command not yet documented,
193 | # so those implements maybe changed in future
194 | class DropScrollWheel(_MultiKeys):
195 | pass
196 |
197 |
198 | class FlipPointingRelative(_MultiKeys):
199 | pass
200 |
201 |
202 | class FlipScrollWheel(_MultiKeys):
203 | pass
204 |
205 |
206 | class IgnoreMultipleSameKeyPress(_MultiKeys):
207 | pass
208 |
209 |
210 | class StripModifierFromScrollWheel(_MultiKeys):
211 | pass
212 |
213 |
214 | class ScrollWheelToScrollWheel(_MultiKeys):
215 | pass
216 |
217 |
218 | class ScrollWheelToKey(_MultiKeys):
219 | pass
220 |
221 |
222 | class PointingRelativeToScroll(_MultiKeys):
223 | pass
224 |
225 |
226 | class PointingRelativeToKey(_MultiKeys):
227 | pass
228 |
229 |
230 | class ForceNumLockOn(_MultiKeys):
231 | pass
232 |
233 |
234 | class ShowStatusMessage(KeyToKeyBase):
235 | def parse(self, *keys, **kwargs):
236 | return ' '.join(keys)
237 |
238 |
239 | class SetKeyboardType(_OneKeyEvent):
240 | pass
241 |
242 |
243 | if __name__ == "__main__":
244 | import doctest
245 | from . import util
246 | doctest.testmod(extraglobs={'util': util})
247 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Easy-Karabiner
2 |
3 | [](https://travis-ci.org/loggerhead/Easy-Karabiner)
4 | [](https://landscape.io/github/loggerhead/Easy-Karabiner/master)
5 | [](https://coveralls.io/github/loggerhead/Easy-Karabiner)
6 | [](https://pypi.python.org/pypi/easy_karabiner)
7 |
8 | [Karabiner](https://pqrs.org/osx/karabiner/index.html.en) is a great tool and I love it! But it's configuration is too complicated for newbies. For making life simpler, I have invented Easy-Karabiner, which is a tool to generate xml configuration for Karabiner from simple python script.
9 |
10 | # Installation
11 |
12 | ```bash
13 | pip install easy_karabiner
14 | ```
15 |
16 | Or you can install manually
17 |
18 | ```bash
19 | git clone https://github.com/loggerhead/Easy-Karabiner.git
20 | cd Easy-Karabiner
21 | python setup.py install
22 | ```
23 |
24 | # Usage
25 |
26 | Generate xml configuration from default path (`~/.easy_karabiner.py`), save it to `~/Library/Application Support/Karabiner/private.xml`, and reload Karabiner.
27 |
28 | ```bash
29 | easy_karabiner
30 | ```
31 |
32 | Same as above, except generating xml configuration from `./myconfig.py`.
33 |
34 | ```bash
35 | easy_karabiner myconfig.py
36 | ```
37 |
38 | Print generated xml configuration from a specific file.
39 |
40 | ```bash
41 | easy_karabiner -s myconfig.py
42 | ```
43 |
44 | See `easy_karabiner --help` for detailed options.
45 |
46 | # How to write `~/.easy_karabiner.py`
47 |
48 | Easy-Karabiner attempts to simplified the most commonly used configurations of Karabiner as well as possible, but there still exists some things you should know first.
49 |
50 | Or if you don't care about it and/or want to try it right now, [examples](https://github.com/loggerhead/Easy-Karabiner/tree/master/examples) are a good start :-)
51 |
52 | ## Basics
53 |
54 | Karabiner is a context-aware key-remapping software, and Easy-Karabiner has simplified it's context and key-remapping to the combination of three components:
55 |
56 | * Map
57 | * Definition
58 | * Filter
59 |
60 | Let me show you a simple example.
61 |
62 | ```python
63 | MAPS = [
64 | # Remap 1 press of 'Left Command'+'K' to 30 press of 'Up'
65 | # only when the active application is 'Google Chrome'
66 | ['cmd K', 'up ' * 30, ['Google Chrome']],
67 | ['cmd J', 'down ' * 30, ['Google Chrome']],
68 | # Swap 'Left Alt' to 'Left Command' only
69 | # when the input keyborad is 'Cherry G80-3494'
70 | ['alt', 'cmd', ['CHERRY_GmbH_0011']],
71 | # You can get the peripheral name from `easy_karabiner -l`
72 | ['cmd', 'alt', ['CHERRY_GmbH_0011']],
73 | # Press 'Left Alt'+'C' to open 'Google Chrome'
74 | ['alt C', 'Google Chrome'],
75 | ]
76 | ```
77 |
78 | In most simple situation, you don't need to define any thing, just write a `MAPS` by your intuition.
79 |
80 | ## Map
81 |
82 | `Map` is consist of three parts: `Map_Command`, `KeyCombo`, `Filter`, none of these parts are necessary.
83 |
84 | ```python
85 | ['_Map_Command_', 'KeyCombo1', 'KeyCombo2', ..., ['Filter1', 'Filter2', ...]]
86 | ```
87 |
88 | The number of `KeyCombo` could be changed if `Map_Command` changed, but in most situations, there are only one or two `KeyCombo`; and if you ignored `Map_Command`, then only two `KeyCombo` is allowed.
89 |
90 | ### Map_Command
91 |
92 | `Map_Command` is used to tell Karabiner what kind of key-remapping it is. For example
93 |
94 | ```python
95 | # Remapping double pressed 'fn' to 'F12'
96 | # it keeps unchanged when single pressed
97 | ['_double_', 'fn', 'F12'],
98 | # Remapping 'esc' to 'cmd_r ctrl_r alt_r shift_r' when holding it
99 | # it keeps unchanged when single pressed or other situations
100 | ['_holding_', 'esc', 'cmd_r ctrl_r alt_r shift_r']
101 | ```
102 |
103 | Easy-Karabiner provide some aliases to help you remeber Karabiner `Map_Command`, include
104 |
105 | | Alias | Original |
106 | | ---------------- | --------------------- |
107 | | `double` | `DoublePressModifier` |
108 | | `holding` | `HoldingKeyToKey` |
109 | | `press_modifier` | `KeyOverlaidModifier` |
110 |
111 | You can also use the original Karabiner `Map_Command`, For example
112 |
113 | ```python
114 | # Reverse scroll direction if not Apple trackpad
115 | ['__FlipScrollWheel__', 'flipscrollwheel_vertical', ['!APPLE_COMPUTER', '!ANY']]
116 | ```
117 |
118 | ### KeyCombo
119 |
120 | `KeyCombo` has the same format, they are composed by space-separated `Key`, and used to represent a combo of normal keys or actions. Easy-Karabiner predefined some aliases for reducing tedious typing. You can found the predefined aliases [here](https://github.com/loggerhead/Easy-Karabiner/blob/master/easy_karabiner/alias.py).
121 |
122 | Here is some example about `KeyCombo`
123 |
124 | ```python
125 | # A single key
126 | 'alt'
127 | # A shortcut
128 | 'alt C'
129 |
130 | # A special key-combo which represent one action--open application
131 | 'Google Chrome'
132 |
133 | # A special key-combo which represent one action--exectue script
134 | # script must start with '#! ' to tell Easy-Karabiner it's a script
135 | '#! osascript /usr/local/bin/copy_finder_path'
136 |
137 | # A special key-combo which represent two actions:
138 | # 1. Click left mouse button
139 | # 2. Execute script
140 | # NOTICE: if a action contain space, you should use quote to tell
141 | # Easy-Karabiner that is a entirety.
142 | 'mouse_left "#! osascript /usr/local/bin/copy_finder_path"'
143 | ```
144 |
145 | Because Easy-Karabiner verify a `Key` by check predefined XML file, but there exists some predefined `Key` not exists in any data file, so Easy-Karabiner will not prevent you to use a unknown `Key` but give you a warning.
146 |
147 | ### Filter
148 |
149 | `Filter` is used to tell Karabiner when/where the key-remapping working or not working. For example
150 |
151 | ```python
152 | # Remapping works only when current application is NOT
153 | # 'Skim' or any applications defined in 'EMACS_IGNORE_APP' predefined replacement
154 | ['ctrl P', 'up', ['!EMACS_IGNORE_APP', '!Skim']]
155 | # '!' before a filter means NOT, otherwise means ONLY
156 | # NOTICE: How you define a thing, then how you use it in Easy-Karabiner.
157 | # So, you don't need add '{{' and '}}' around a replacement.
158 |
159 | # Remapping works only when 'Skim' or 'Kindle'
160 | # NOTICE: You don't need to define a filter if it is application filter,
161 | # Easy-Karabiner will do this job for you.
162 | ['ctrl cmd F', 'cmd_r shift_r F cmd_r shift_r -', ['Skim', 'Kindle']]
163 | ```
164 |
165 | To distinguish `KeyCombo` and `Filter`, you must use brackets to tell Easy-Karabiner whether last part of a `Map` is a list of `Filter` or not.
166 |
167 | ## Definition
168 |
169 | `Definition` is used to define `Filter` or `Key`, so you can use it in `Map`. Because Easy-Karabiner auto-defined most things for you, so you don't need it in most situation. `Definition` has several forms
170 |
171 | ```python
172 | DEFINITIONS = {
173 | # `NAME` is a symbol to represent the ground-truth value which used in `MAPS`
174 | 'NAME': 'VALUE',
175 | # A `NAME` can represents multiple values
176 | 'NAME': ['VALUE1', 'VALUE2', ...],
177 | # Same as above, except `DEF_TYPE` specified `Definition` type
178 | 'DEF_TYPE::NAME': 'VALUE',
179 | 'DEF_TYPE::NAME': ['VALUE1', 'VALUE2', ...],
180 | }
181 | ```
182 |
183 | The key of `Definition` is how you define it, how you use it; So you don't need to around a defined replacement with `{{` and `}}`.
184 |
185 | For your convenience, Easy-Karabiner would use predefined `Definition`, so you don't need to define everything and just use it in `Filter`.
186 |
187 | ### Replacement
188 |
189 | `Replacement` is a special `Definition` which will replaced by values when used. It has the form below
190 |
191 | ```python
192 | 'NAME': ['VALUE1', 'VALUE2', ...]
193 | ```
194 |
195 | If Easy-Karabiner cannot guess `DEF_TYPE` from `VALUE`, then a `Definition` with above form will be defined as a `Replacement`. Easy-Karabiner will try to define any undefined values in `Replacement` to keep consistency with other parts, for example
196 |
197 | ```python
198 | # Two keymap will be defined
199 | 'LAUNCHER_MODE_V2_EXTRA': [
200 | ['__KeyDownUpToKey__', 'C', 'Maps'],
201 | ['__KeyDownUpToKey__', 'V', 'FaceTime'],
202 | ]
203 |
204 | # 'Sublime Text' and 'Visual Studio Code' will be defined
205 | 'emacs_ignore_app': [
206 | 'ECLIPSE', 'EMACS', 'TERMINAL',
207 | 'REMOTEDESKTOPCONNECTION', 'VI', 'X11',
208 | 'VIRTUALMACHINE', 'TERMINAL', 'Sublime Text',
209 | 'Visual Studio Code',
210 | ]
211 | ```
212 |
213 | # LICENSE
214 |
215 | MIT
216 |
--------------------------------------------------------------------------------
/tests/test_factory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from easy_karabiner.factory import *
3 |
4 |
5 | def test_create_keymap():
6 | raw_keymap = [
7 | 'KeyToKey',
8 | ['ctrl'],
9 | ['f12'],
10 | ]
11 | k = KeymapCreater.create(raw_keymap)
12 | s = '''
13 | __KeyToKey__
14 | KeyCode::CONTROL_L,
15 | KeyCode::F12
16 | '''
17 | util.assert_xml_equal(k, s)
18 |
19 | raw_keymap = [
20 | 'KeyToKey',
21 | ['ctrl', 'U'],
22 | ['end', 'shift_r', 'home', 'del', 'del', 'norepeat'],
23 | ]
24 | k = KeymapCreater.create(raw_keymap)
25 | s = '''
26 | __KeyToKey__
27 | KeyCode::U, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
28 | KeyCode::END, KeyCode::HOME, ModifierFlag::SHIFT_R,
29 | KeyCode::DELETE, KeyCode::DELETE, Option::NOREPEAT
30 | '''
31 | util.assert_xml_equal(k, s)
32 |
33 | raw_keymap = [
34 | 'KeyToKey',
35 | ['alt', 'shift', ','],
36 | ['fn', 'left'],
37 | ]
38 | k = KeymapCreater.create(raw_keymap)
39 | s = '''
40 | __KeyToKey__
41 | KeyCode::COMMA, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
42 | KeyCode::CURSOR_LEFT, ModifierFlag::FN
43 | '''
44 | util.assert_xml_equal(k, s)
45 |
46 | raw_keymap = [
47 | 'DropAllKeys',
48 | ['ModifierFlag::MY_VI_MODE'],
49 | ['DROPALLKEYS_DROP_KEY', 'DROPALLKEYS_DROP_CONSUMERKEY', 'DROPALLKEYS_DROP_POINTINGBUTTON'],
50 | ]
51 | k = KeymapCreater.create(raw_keymap)
52 | s = '''
53 | __DropAllKeys__
54 | ModifierFlag::MY_VI_MODE,
55 | Option::DROPALLKEYS_DROP_KEY,
56 | Option::DROPALLKEYS_DROP_CONSUMERKEY,
57 | Option::DROPALLKEYS_DROP_POINTINGBUTTON
58 | '''
59 | util.assert_xml_equal(k, s)
60 |
61 | raw_keymap = [
62 | 'SimultaneousKeyPresses',
63 | ['9', '0', '9', 'shift'],
64 | ['shift', '0', 'left']
65 | ]
66 | k = KeymapCreater.create(raw_keymap)
67 | s = '''
68 | __SimultaneousKeyPresses__
69 | @begin
70 | KeyCode::KEY_9, KeyCode::KEY_0, KeyCode::KEY_9, ModifierFlag::SHIFT_L
71 | @end
72 |
73 | @begin
74 | KeyCode::KEY_0, ModifierFlag::SHIFT_L, KeyCode::CURSOR_LEFT
75 | @end
76 | '''
77 | util.assert_xml_equal(k, s)
78 |
79 | raw_keymap = [
80 | 'DoublePressModifier',
81 | ['fn'],
82 | ['cmd', 'alt', 'I'],
83 | ]
84 | k = KeymapCreater.create(raw_keymap)
85 | s = '''
86 | __DoublePressModifier__
87 | KeyCode::FN,
88 | @begin
89 | KeyCode::FN
90 | @end
91 | @begin
92 | KeyCode::I, ModifierFlag::COMMAND_L, ModifierFlag::OPTION_L
93 | @end
94 | '''
95 | util.assert_xml_equal(k, s)
96 |
97 | raw_keymap = [
98 | 'DoublePressModifier',
99 | ['fn'],
100 | ['F11'],
101 | ['F12'],
102 | ]
103 | k = KeymapCreater.create(raw_keymap)
104 | s = '''
105 | __DoublePressModifier__
106 | KeyCode::FN,
107 | @begin
108 | KeyCode::F11
109 | @end
110 | @begin
111 | KeyCode::F12
112 | @end
113 | '''
114 | util.assert_xml_equal(k, s)
115 |
116 | raw_keymap = [
117 | 'HoldingKeyToKey',
118 | ['esc'],
119 | ['cmd_r', 'ctrl_r', 'alt_r', 'shift_r'],
120 | ]
121 | k = KeymapCreater.create(raw_keymap)
122 | s = '''
123 | __HoldingKeyToKey__
124 | KeyCode::ESCAPE,
125 | @begin
126 | KeyCode::ESCAPE
127 | @end
128 | @begin
129 | KeyCode::COMMAND_R, ModifierFlag::CONTROL_R, ModifierFlag::OPTION_R, ModifierFlag::SHIFT_R
130 | @end
131 | '''
132 | util.assert_xml_equal(k, s)
133 |
134 | raw_keymap = [
135 | 'KeyOverlaidModifier',
136 | ['caps'],
137 | ['ctrl'],
138 | ['esc'],
139 | ]
140 | k = KeymapCreater.create(raw_keymap)
141 | s = '''
142 | __KeyOverlaidModifier__
143 | KeyCode::CAPSLOCK,
144 | @begin
145 | KeyCode::CONTROL_L
146 | @end
147 | @begin
148 | KeyCode::ESCAPE
149 | @end
150 | '''
151 | util.assert_xml_equal(k, s)
152 |
153 | raw_keymap = [
154 | 'KeyDownUpToKey',
155 | ['cmd', ','],
156 | ['cmd', 'shift', 'left'],
157 | ['cmd', 'left'],
158 | ]
159 | k = KeymapCreater.create(raw_keymap)
160 | s = '''
161 | __KeyDownUpToKey__
162 | KeyCode::COMMA, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
163 | @begin
164 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_L, ModifierFlag::SHIFT_L
165 | @end
166 | @begin
167 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_L
168 | @end
169 | '''
170 | util.assert_xml_equal(k, s)
171 |
172 | raw_keymap = [
173 | 'BlockUntilKeyUp',
174 | ['sp']
175 | ]
176 | k = KeymapCreater.create(raw_keymap)
177 | s = '''
178 | __BlockUntilKeyUp__
179 | KeyCode::SPACE
180 | '''
181 | util.assert_xml_equal(k, s)
182 |
183 | raw_keymap = [
184 | 'DropKeyAfterRemap',
185 | ['mission_control', 'MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_SHIFT']
186 | ]
187 | k = KeymapCreater.create(raw_keymap)
188 | s = '''
189 | __DropKeyAfterRemap__
190 | KeyCode::MISSION_CONTROL,
191 | MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_SHIFT
192 | '''
193 | util.assert_xml_equal(k, s)
194 |
195 | raw_keymap = [
196 | 'PassThrough',
197 | ]
198 | k = KeymapCreater.create(raw_keymap)
199 | s = ' __PassThrough__ '
200 | util.assert_xml_equal(k, s)
201 |
202 | raw_keymap = [
203 | 'double',
204 | ['cmd', 'K'],
205 | ['up'] * 6,
206 | ]
207 | k = KeymapCreater.create(raw_keymap)
208 | s = '''
209 | __DoublePressModifier__
210 | KeyCode::K, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
211 |
212 | @begin
213 | KeyCode::K, ModifierFlag::COMMAND_L
214 | @end
215 |
216 | @begin
217 | KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP,
218 | KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP
219 | @end
220 | '''
221 | util.assert_xml_equal(k, s)
222 |
223 | raw_keymap = [
224 | 'DoublePressModifier',
225 | ['cmd', 'J'],
226 | ['down'] * 6,
227 | ]
228 | k = KeymapCreater.create(raw_keymap)
229 | s = '''
230 | __DoublePressModifier__
231 | KeyCode::J, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
232 |
233 | @begin
234 | KeyCode::J, ModifierFlag::COMMAND_L
235 | @end
236 |
237 | @begin
238 | KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN,
239 | KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN
240 | @end
241 | '''
242 | util.assert_xml_equal(k, s)
243 |
244 | raw_keymap = [
245 | 'KeyToKey',
246 | ['alt', 'E'],
247 | ['KeyCode::VK_OPEN_URL_FINDER'],
248 | ]
249 | k = KeymapCreater.create(raw_keymap)
250 | s = '''
251 | __KeyToKey__
252 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
253 | KeyCode::VK_OPEN_URL_FINDER
254 | '''
255 | util.assert_xml_equal(k, s)
256 |
257 | raw_keymap = [
258 | 'FlipScrollWheel',
259 | ['FLIPSCROLLWHEEL_HORIZONTAL', 'FLIPSCROLLWHEEL_VERTICAL'],
260 | ]
261 | k = KeymapCreater.create(raw_keymap)
262 | s = '''
263 | __FlipScrollWheel__
264 | Option::FLIPSCROLLWHEEL_HORIZONTAL,
265 | Option::FLIPSCROLLWHEEL_VERTICAL
266 | '''
267 | util.assert_xml_equal(k, s)
268 |
269 |
270 | def test_create_definition():
271 | d = DefinitionCreater.create('KINDLE', ['com.amazon.Kindle'])
272 | s = '''
273 |
274 | KINDLE
275 | com.amazon.Kindle
276 | '''
277 | util.assert_xml_equal(d[0], s)
278 |
279 | d = DefinitionCreater.create('EMACS_IGNORE_APP', [
280 | 'ECLIPSE', 'EMACS', 'TERMINAL',
281 | 'REMOTEDESKTOPCONNECTION', 'VI', 'X11',
282 | 'VIRTUALMACHINE', 'TERMINAL', 'SUBLIMETEXT',
283 | ])
284 | s = '''
285 |
286 | EMACS_IGNORE_APP
287 |
288 | ECLIPSE, EMACS, TERMINAL,
289 | REMOTEDESKTOPCONNECTION, VI, X11,
290 | VIRTUALMACHINE, TERMINAL, SUBLIMETEXT
291 |
292 | '''
293 | util.assert_xml_equal(d[0], s)
294 |
295 | d1, d2 = DefinitionCreater.create('CHERRY_3494', ['0x046a', '0x0011'])
296 | s1 = '''
297 |
298 | CHERRY_3494_VENDOR
299 | 0x046a
300 |
301 | '''
302 | s2 = '''
303 |
304 | CHERRY_3494_PRODUCT
305 | 0x0011
306 |
307 | '''
308 | util.assert_xml_equal(d1, s1)
309 | util.assert_xml_equal(d2, s2)
310 |
311 | d = DefinitionCreater.create('Open::FINDER', ['/Applications/Finder.app'])
312 | s = '''
313 |
314 | KeyCode::VK_OPEN_URL_FINDER
315 | /Applications/Finder.app
316 | '''
317 | util.assert_xml_equal(d[0], s)
318 |
319 |
320 | def test_create_filter():
321 | f = FilterCreater.create('LOGITECH')
322 | s = ''' DeviceVendor::LOGITECH '''
323 | util.assert_xml_equal(f[0], s)
324 |
325 | f = FilterCreater.create('!EMACS_MODE_IGNORE_APPS')
326 | s = ''' {{EMACS_MODE_IGNORE_APPS}} '''
327 | util.assert_xml_equal(f[0], s)
328 |
329 |
330 | def test_create_keymaps():
331 | raw_keymaps = [
332 | ['KeyToKey', ['Cmd'], ['Alt']],
333 | ['double', ['fn'], ['f12']],
334 | ['holding', ['ctrl'], ['esc'], ['cmd', 'alt', 'ctrl']],
335 | ]
336 | outputs = [
337 | '''
338 | __KeyToKey__
339 | KeyCode::COMMAND_L,
340 | KeyCode::OPTION_L
341 |
342 | ''',
343 | '''
344 | __DoublePressModifier__
345 | KeyCode::FN,
346 | @begin KeyCode::FN @end
347 | @begin KeyCode::F12 @end
348 |
349 | ''',
350 | '''
351 | __HoldingKeyToKey__
352 | KeyCode::CONTROL_L,
353 | @begin KeyCode::ESCAPE @end
354 | @begin KeyCode::COMMAND_L, ModifierFlag::OPTION_L, ModifierFlag::CONTROL_L @end
355 |
356 | ''',
357 | ]
358 | keymap_objs = create_keymaps(raw_keymaps)
359 | assert(len(keymap_objs) == len(outputs))
360 | for i in range(len(outputs)):
361 | util.assert_xml_equal(keymap_objs[i], outputs[i])
362 |
363 |
364 | def test_create_definitions():
365 | definitions = {
366 | 'BILIBILI': 'com.typcn.Bilibili',
367 | 'CHERRY_3494': ['0x046a', '0x0011'],
368 | 'UIElementRole::custom_ui': 'used as a filter',
369 | 'replacement_example': ['for', 'example', 'Xee'],
370 | }
371 | outputs = [
372 | '''
373 |
374 | BILIBILI
375 | com.typcn.Bilibili
376 |
377 | ''',
378 | '''
379 |
380 | CHERRY_3494_VENDOR
381 | 0x046a
382 |
383 | ''',
384 | '''
385 |
386 | CHERRY_3494_PRODUCT
387 | 0x0011
388 |
389 | ''',
390 | '''
391 | custom_ui
392 | ''',
393 | '''
394 |
395 | replacement_example
396 | for, example, Xee
397 |
398 | ''',
399 | ]
400 | definition_objs = create_definitions(definitions)
401 | assert(len(definition_objs) == len(outputs))
402 | for i in range(len(outputs)):
403 | util.assert_xml_equal(definition_objs[i], outputs[i])
404 |
405 |
406 | def test_create_filters():
407 | f = create_filters(['LOGITECH', 'LOGITECH_USB_RECEIVER'])
408 | s = '''
409 |
410 | DeviceVendor::LOGITECH,
411 | DeviceProduct::LOGITECH_USB_RECEIVER
412 | '''
413 | util.assert_xml_equal(f[0], s)
414 |
415 | f = create_filters(['!EMACS_MODE_IGNORE_APPS', '!FINDER', '!SKIM'])
416 | s = '''
417 |
418 | {{EMACS_MODE_IGNORE_APPS}}, FINDER, SKIM
419 | '''
420 | util.assert_xml_equal(f[0], s)
421 |
--------------------------------------------------------------------------------
/easy_karabiner/factory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*
2 | from __future__ import print_function
3 | import operator
4 | from functools import reduce
5 | from itertools import groupby
6 |
7 | from . import util
8 | from . import query
9 | from . import osxkit
10 | from . import filter
11 | from . import keymap
12 | from . import definition
13 | from . import exception
14 | from . import def_tag_map
15 | from .basexml import BaseXML
16 | from .fucking_string import is_string_type
17 |
18 |
19 | def define_filters(raw_filters):
20 | """Found undefined `Filter` in `raw_filters`, and create a `Definition` by it."""
21 | for raw_filter in raw_filters:
22 | if raw_filter.startswith('!'):
23 | val = raw_filter[1:]
24 | else:
25 | val = raw_filter
26 |
27 | if not query.is_defined_filter(val):
28 | FilterCreater.define(val)
29 |
30 |
31 | def define_keymaps(raw_keymaps):
32 | """Found undefined `Key` in `raw_keymaps`, and create a `Definition` by it."""
33 | # raw_keymap: (str, (str), ...)
34 | for raw_keymap in raw_keymaps:
35 | try:
36 | # raw_keycombo: (str)
37 | for raw_keycombo in raw_keymap[1:]:
38 | # raw_key: str
39 | for raw_key in raw_keycombo:
40 | if not query.is_defined_key(raw_key):
41 | KeymapCreater.define_key(raw_key)
42 | except exception.UndefinedKeyException as e:
43 | exception.ExceptionRegister.record_by(raw_keymap, e)
44 |
45 |
46 | def create_filters(raw_filters):
47 | """Create `Filter` object from `raw_filters`,
48 | `Filter` objects with the same class has been merged to one `Filter` object.
49 | """
50 | def filter_sort_key(f):
51 | return f.get_tag_name()
52 |
53 | filter_objs = []
54 |
55 | for raw_filter in raw_filters:
56 | objs = FilterCreater.create(raw_filter)
57 | filter_objs.extend(objs)
58 |
59 | grouped = groupby(sorted(filter_objs, key=filter_sort_key), filter_sort_key)
60 | # merge filters in the same group
61 | filter_objs = tuple(reduce(operator.add, group) for _, group in grouped)
62 | return filter_objs
63 |
64 |
65 | def create_keymaps(raw_keymaps):
66 | """Create `Keymap` object from `raw_keymaps`."""
67 | keymap_objs = []
68 |
69 | for raw_keymap in raw_keymaps:
70 | try:
71 | keymap_obj = KeymapCreater.create(raw_keymap)
72 | keymap_objs.append(keymap_obj)
73 | except exception.InvalidKeymapException as e:
74 | exception.ExceptionRegister.record_by(raw_keymap, e)
75 |
76 | return keymap_objs
77 |
78 |
79 | def create_definitions(definitions):
80 | definition_objs = []
81 |
82 | for name in sorted(definitions.keys()):
83 | # make sure `vals` is iterable
84 | if util.is_list_or_tuple(definitions[name]):
85 | vals = definitions[name]
86 | else:
87 | vals = [definitions[name]]
88 |
89 | try:
90 | tmp = DefinitionCreater.create(name, vals)
91 | definition_objs.extend(tmp)
92 | except exception.InvalidDefinition as e:
93 | k = {name: definitions[name]}
94 | exception.ExceptionRegister.record(k, e)
95 |
96 | return definition_objs
97 |
98 |
99 | class FilterCreater(object):
100 | @classmethod
101 | def define(cls, val):
102 | device_ids = osxkit.get_peripheral_info(val)
103 | # if `val` is a peripheral name
104 | if device_ids:
105 | return DefinitionCreater.define_device(val, device_ids)
106 | else:
107 | app_info = osxkit.get_app_info(val)
108 | # if `val` is a application name
109 | if app_info:
110 | bundle_id = app_info[1]
111 | return DefinitionCreater.define_app(val, bundle_id)
112 | else:
113 | raise exception.UndefinedFilterException(val)
114 |
115 | @classmethod
116 | def create(cls, raw_val):
117 | """Create a list of `Filter` object from `raw_val` string."""
118 | if raw_val.startswith('!'):
119 | val = raw_val[1:]
120 | type = 'not'
121 | else:
122 | val = raw_val
123 | type = 'only'
124 |
125 | class_names = query.query_filter_class_names(val, scope='all')
126 | # if `val` has been defined
127 | if class_names:
128 | filter_classes = [filter.__dict__.get(c) for c in class_names]
129 | definition_objs = query.DefinitionBucket.get('filter', val)
130 |
131 | # if `val` is user defined
132 | if definition_objs:
133 | filter_names = [d.get_name() for d in definition_objs]
134 | # if `val` is predefined
135 | else:
136 | filter_names = [val]
137 |
138 | filter_objs = []
139 | for i in range(len(filter_classes)):
140 | filter_class = filter_classes[i]
141 | filter_name = cls.escape_filter_name(filter_class.__name__, filter_names[i])
142 | filter_obj = filter_class(filter_name, type=type)
143 | filter_objs.append(filter_obj)
144 | return filter_objs
145 | else:
146 | raise exception.UndefinedFilterException(raw_val)
147 |
148 | @classmethod
149 | def escape_filter_name(cls, class_name, name_val):
150 | if class_name == 'ReplacementFilter':
151 | name_val = '{{%s}}' % name_val
152 | elif class_name == 'DeviceVendorFilter':
153 | name_val = 'DeviceVendor::%s' % name_val
154 | elif class_name == 'DeviceProductFilter':
155 | name_val = 'DeviceProduct::%s' % name_val
156 | elif class_name == 'ModifierFilter':
157 | name_val = KeymapCreater.get_karabiner_format_key(name_val, key_header='ModifierFlag')
158 | return name_val
159 |
160 |
161 | class KeymapCreater(object):
162 | @classmethod
163 | def define_key(cls, val):
164 | app_info = osxkit.get_app_info(val)
165 |
166 | # if `val` is a application name
167 | if app_info:
168 | app_path = app_info[0]
169 | return DefinitionCreater.define_open(app_path, index=val)
170 | # if `val` is `VKOpenURL` format
171 | elif DefinitionDetector.is_vkopenurl(val):
172 | return DefinitionCreater.define_open(val)
173 | else:
174 | raise exception.UndefinedKeyException(val)
175 |
176 | @classmethod
177 | def create(cls, raw_keymap):
178 | """Create a `Keymap` object from `raw_keymap`."""
179 | # found the `Keymap` constructor by the command marker
180 | command = raw_keymap[0].strip('_')
181 | command = query.get_keymap_alias(command) or command
182 | keymap_class = keymap.__dict__.get(command)
183 |
184 | new_keycombos = []
185 |
186 | # Translate key string to `Header::Value` format if it has been defined,
187 | # otherwise, keep it unchanged.
188 | for raw_keycombo in raw_keymap[1:]:
189 | new_keycombo = []
190 |
191 | for raw_key in raw_keycombo:
192 | definition_objs = query.DefinitionBucket.get('key', raw_key)
193 | if definition_objs:
194 | new_keys = [d.get_name() for d in definition_objs]
195 | else:
196 | new_keys = [query.get_key_alias(raw_key) or raw_key]
197 |
198 | for new_key in new_keys:
199 | new_key = cls.get_karabiner_format_key(new_key)
200 | new_keycombo.append(new_key)
201 |
202 | new_keycombos.append(new_keycombo)
203 |
204 | try:
205 | if keymap_class:
206 | return keymap_class(*new_keycombos)
207 | # if can't found the `Keymap` constructor
208 | else:
209 | return keymap.UniversalKeyToKey(command, *new_keycombos)
210 | except TypeError:
211 | raise exception.InvalidKeymapException(raw_keymap)
212 |
213 | @classmethod
214 | def get_karabiner_format_key(cls, key, key_header=None):
215 | key_parts = key.split('::', 1)
216 |
217 | if len(key_parts) < 2:
218 | key = query.get_key_alias(key.lower()) or key
219 |
220 | for k in [key, key.upper(), key.lower()]:
221 | predefined_header = query.KeyHeaderQuery.query(k)
222 | if predefined_header:
223 | return "%s::%s" % (predefined_header, k)
224 |
225 | if key_header:
226 | return "%s::%s" % (key_header, key_parts[-1])
227 | else:
228 | return key
229 |
230 |
231 | class DefinitionCreater(object):
232 | @classmethod
233 | def create(cls, raw_name, vals):
234 | """Create a list of `Definition` object by found out the relevant constructor intelligently."""
235 | name_parts = raw_name.split('::', 1)
236 | # { name : [val] }
237 | # if no explicit `DefinitionType`, then look at `vals` to figure it out
238 | if len(name_parts) == 1:
239 | # { name : val }
240 | if len(vals) == 1:
241 | val = vals[0]
242 | if DefinitionDetector.is_vkopenurl(val):
243 | return cls.define('VKOpenURL', raw_name, raw_name, vals)
244 | elif DefinitionDetector.is_uielementrole(val):
245 | return cls.define('UIElementRole', raw_name, raw_name, vals)
246 | elif DefinitionDetector.is_app(val):
247 | return cls.define('App', raw_name, raw_name, vals)
248 | elif DefinitionDetector.is_replacement(val):
249 | return cls.define_replacement(raw_name, vals)
250 | else:
251 | raise exception.InvalidDefinition(raw_name)
252 | # { name : [val] }
253 | elif len(vals) > 1:
254 | if all(is_string_type(val) for val in vals):
255 | # { name : [DeviceVendorID, DeviceProductID] }
256 | if DefinitionDetector.is_device(vals):
257 | return cls.define_device(raw_name, vals)
258 | elif DefinitionDetector.is_all_app(vals):
259 | return cls.define('App', raw_name, raw_name, vals)
260 |
261 | return cls.define_replacement(raw_name, vals)
262 | # { DefHeader::DefName : [val] }
263 | else:
264 | def_header, def_name = name_parts
265 | class_name = query.get_def_alias(def_header) or def_header
266 | definition_objs = cls.define(class_name, raw_name, def_name, vals)
267 | if definition_objs:
268 | return definition_objs
269 | else:
270 | raise exception.InvalidDefinition(raw_name)
271 |
272 | @classmethod
273 | def define(cls, class_name, raw_name, def_name, vals, escape_def_name=True):
274 | def_class = definition.__dict__.get(class_name)
275 | if def_class:
276 | if escape_def_name:
277 | def_name = cls.escape_def_name(def_name, class_name)
278 | definition_obj = def_class(def_name, *vals)
279 |
280 | def_category = DefinitionDetector.get_definition_caregory(def_class)
281 | query.DefinitionBucket.put(def_category, raw_name, [definition_obj])
282 | query.DefinitionBucket.put(def_category, def_name, [definition_obj])
283 | return [definition_obj]
284 | else:
285 | return []
286 |
287 | @classmethod
288 | def define_replacement(cls, raw_name, vals):
289 | # if `vals` is `Keymap` format, define and create it
290 | if all(DefinitionDetector.is_keymap(val) for val in vals):
291 | from .parse import parse
292 |
293 | block_objs, _ = parse(vals, {})
294 |
295 | keymap_strs = ''
296 | for block in block_objs:
297 | keymap_strs += '\n'.join(str(o) for o in block.keymaps)
298 |
299 | after_defined = [BaseXML.create_cdata_text(keymap_strs)]
300 | else:
301 | after_defined = []
302 |
303 | for val in vals:
304 | # if `val` is application name, define it and replace with define name
305 | app_info = osxkit.get_app_info(val)
306 | if app_info:
307 | bundle_id = app_info[1]
308 | DefinitionCreater.define_app(val, bundle_id)
309 | after_defined.append(cls.escape_def_name(val, 'App'))
310 | else:
311 | after_defined.append(val)
312 |
313 | return cls.define('Replacement', raw_name, raw_name, after_defined)
314 |
315 | @classmethod
316 | def define_device(cls, raw_name, vals):
317 | def_name = cls.escape_def_name(raw_name)
318 | vdef_name = '%s_VENDOR' % def_name
319 | pdef_name = '%s_PRODUCT' % def_name
320 |
321 | [vdefinition_obj] = cls.define('DeviceVendor', raw_name, vdef_name, [vals[0]], escape_def_name=False)
322 | [pdefinition_obj] = cls.define('DeviceProduct', raw_name, pdef_name, [vals[1]], escape_def_name=False)
323 | definition_objs = (vdefinition_obj, pdefinition_obj)
324 |
325 | query.DefinitionBucket.put('filter', raw_name, definition_objs)
326 | return definition_objs
327 |
328 | @classmethod
329 | def define_app(cls, raw_name, bundle_id):
330 | return cls.define('App', raw_name, raw_name, [bundle_id])
331 |
332 | @classmethod
333 | def define_open(cls, val, index=None):
334 | if index:
335 | return cls.define('VKOpenURL', index, index, [val])
336 | else:
337 | def_name = util.get_checksum(val)
338 | return cls.define('VKOpenURL', val, def_name, [val])
339 |
340 | @classmethod
341 | def escape_def_name(cls, def_name, class_name=None):
342 | if class_name == 'VKOpenURL' and not def_name.startswith('KeyCode::VK_OPEN_URL_'):
343 | def_name = util.escape_string(def_name)
344 | return 'KeyCode::VK_OPEN_URL_%s' % def_name
345 | else:
346 | return util.escape_string(def_name)
347 |
348 |
349 | class DefinitionDetector(object):
350 | @classmethod
351 | def is_device(cls, vals):
352 | return len(vals) == 2 and all(util.is_hex(val) for val in vals)
353 |
354 | @classmethod
355 | def is_vkopenurl(cls, val):
356 | # if `val` is script, application or file, or url
357 | return any([val.startswith('#! '),
358 | val.endswith('.app') or val.startswith('/'),
359 | val.startswith('http://') or val.startswith('https://')])
360 |
361 | @classmethod
362 | def is_uielementrole(cls, val):
363 | return val.startswith('AX') and val.isalpha()
364 |
365 | @classmethod
366 | def is_app(cls, val):
367 | """`appdef` has format like:
368 |
369 | equal: com.example.application
370 | prefix: com.example.
371 | suffix: .example.application
372 | """
373 | words = val.split('.')
374 | if len(words) < 3:
375 | return False
376 | else:
377 | if words[0] == '':
378 | words.pop(0)
379 | elif words[-1] == '':
380 | words.pop()
381 | return all(w.isalpha() for w in words)
382 |
383 | @classmethod
384 | def is_all_app(cls, vals):
385 | return all(cls.is_app(val) for val in vals)
386 |
387 | @classmethod
388 | def is_replacement(cls, val):
389 | if BaseXML.is_cdata_text(val):
390 | return True
391 | elif cls.is_keymap(val):
392 | return True
393 | else:
394 | return False
395 |
396 | @classmethod
397 | def is_keymap(cls, val):
398 | return util.is_list_or_tuple(val)
399 |
400 | @classmethod
401 | def get_definition_caregory(cls, def_class):
402 | def_tag_name = def_class.get_def_tag_name()
403 | if def_tag_map.get_filter_class_name(def_tag_name):
404 | return 'filter'
405 | else:
406 | return 'key'
407 |
--------------------------------------------------------------------------------
/examples/myconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.5.2
4 | -
5 | Easy-Karabiner
6 |
7 | Bilibili
8 | com.typcn.Bilibili
9 |
10 |
11 | Finder
12 | com.apple.finder
13 |
14 |
15 | Google_Chrome
16 | com.google.Chrome
17 |
18 |
19 | Kindle
20 | com.amazon.Kindle
21 |
22 |
23 | PyCharm_CE
24 | com.jetbrains.pycharm
25 |
26 |
27 | Skim
28 | net.sourceforge.skim-app.skim
29 |
30 |
31 | Sublime_Text
32 | com.sublimetext.3
33 |
34 |
35 | Xee³
36 | cx.c3.Xee3
37 |
38 |
39 | cherry_3494_PRODUCT
40 | 0x0011
41 |
42 |
43 | cherry_3494_VENDOR
44 | 0x046a
45 |
46 |
47 | emacs_ignore_app
48 | ECLIPSE, EMACS, TERMINAL, REMOTEDESKTOPCONNECTION, VI, X11, VIRTUALMACHINE, TERMINAL, Sublime_Text
49 |
50 |
51 | KeyCode::VK_OPEN_URL_814dbb8
52 |
53 |
54 |
55 | KeyCode::VK_OPEN_URL_Activity_Monitor
56 | /Applications/Utilities/Activity Monitor.app
57 |
58 |
59 | KeyCode::VK_OPEN_URL_Finder
60 | /System/Library/CoreServices/Finder.app
61 |
62 |
63 | KeyCode::VK_OPEN_URL_Google_Chrome
64 | /Applications/Google Chrome.app
65 |
66 |
67 | KeyCode::VK_OPEN_URL_PyCharm_CE
68 | /Applications/PyCharm CE.app
69 |
70 |
71 | KeyCode::VK_OPEN_URL_Sublime_Text
72 | /Applications/Sublime Text.app
73 |
74 |
75 | KeyCode::VK_OPEN_URL_System_Preferences
76 | /Applications/System Preferences.app
77 |
78 |
79 | KeyCode::VK_OPEN_URL_iTerm
80 | /Applications/iTerm.app
81 |
82 |
-
83 | Enable
84 | private.easy_karabiner
85 |
86 |
87 | DeviceVendor::APPLE_COMPUTER,
88 | DeviceProduct::ANY
89 |
90 | __FlipScrollWheel__
91 | Option::FLIPSCROLLWHEEL_VERTICAL
92 |
93 |
94 |
95 | __HoldingKeyToKey__
96 | KeyCode::ESCAPE,
97 | @begin
98 | KeyCode::ESCAPE
99 | @end
100 | @begin
101 | KeyCode::COMMAND_R, ModifierFlag::CONTROL_R, ModifierFlag::OPTION_R, ModifierFlag::SHIFT_R
102 | @end
103 |
104 | __DoublePressModifier__
105 | KeyCode::FN,
106 | @begin
107 | KeyCode::FN
108 | @end
109 | @begin
110 | KeyCode::F12
111 | @end
112 |
113 | __KeyOverlaidModifier__
114 | KeyCode::CONTROL_L,
115 | @begin
116 | KeyCode::CONTROL_L
117 | @end
118 | @begin
119 | KeyCode::ESCAPE
120 | @end
121 |
122 | __KeyToKey__
123 | PointingButton::LEFT, ModifierFlag::OPTION_L, ModifierFlag::NONE,
124 | PointingButton::LEFT, KeyCode::VK_OPEN_URL_814dbb8
125 |
126 | __KeyToKey__
127 | KeyCode::A, ModifierFlag::OPTION_L, ModifierFlag::NONE,
128 | KeyCode::VK_OPEN_URL_iTerm
129 |
130 | __KeyToKey__
131 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
132 | KeyCode::VK_OPEN_URL_Finder
133 |
134 | __KeyToKey__
135 | KeyCode::C, ModifierFlag::OPTION_L, ModifierFlag::NONE,
136 | KeyCode::VK_OPEN_URL_Google_Chrome
137 |
138 | __KeyToKey__
139 | KeyCode::S, ModifierFlag::OPTION_L, ModifierFlag::NONE,
140 | KeyCode::VK_OPEN_URL_Sublime_Text
141 |
142 | __KeyToKey__
143 | KeyCode::P, ModifierFlag::OPTION_L, ModifierFlag::NONE,
144 | KeyCode::VK_OPEN_URL_PyCharm_CE
145 |
146 | __KeyToKey__
147 | KeyCode::DELETE, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
148 | KeyCode::VK_OPEN_URL_Activity_Monitor
149 |
150 | __KeyToKey__
151 | KeyCode::COMMA, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
152 | KeyCode::VK_OPEN_URL_System_Preferences
153 |
154 |
155 |
156 | Google_Chrome
157 | __DoublePressModifier__
158 | KeyCode::FN,
159 | @begin
160 | KeyCode::FN
161 | @end
162 | @begin
163 | KeyCode::I, ModifierFlag::COMMAND_L, ModifierFlag::OPTION_L
164 | @end
165 |
166 | __KeyToKey__
167 | KeyCode::K, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
168 | KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP
169 |
170 | __KeyToKey__
171 | KeyCode::J, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
172 | KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN
173 |
174 | __KeyToKey__
175 | KeyCode::L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
176 | KeyCode::TAB, ModifierFlag::CONTROL_R
177 |
178 | __KeyToKey__
179 | KeyCode::H, ModifierFlag::OPTION_L, ModifierFlag::NONE,
180 | KeyCode::TAB, ModifierFlag::CONTROL_R, ModifierFlag::SHIFT_R
181 |
182 | __KeyToKey__
183 | KeyCode::L, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
184 | KeyCode::L, ModifierFlag::COMMAND_R
185 |
186 |
187 |
188 | PyCharm_CE
189 | __KeyToKey__
190 | KeyCode::F5,
191 | ConsumerKeyCode::BRIGHTNESS_DOWN
192 |
193 | __KeyToKey__
194 | KeyCode::F6,
195 | ConsumerKeyCode::BRIGHTNESS_UP
196 |
197 | __KeyToKey__
198 | KeyCode::F10,
199 | ConsumerKeyCode::VOLUME_MUTE
200 |
201 | __KeyToKey__
202 | KeyCode::F11,
203 | ConsumerKeyCode::VOLUME_DOWN
204 |
205 | __KeyToKey__
206 | KeyCode::F12,
207 | ConsumerKeyCode::VOLUME_UP
208 |
209 |
210 |
211 |
212 | DeviceVendor::cherry_3494_VENDOR,
213 | DeviceProduct::cherry_3494_PRODUCT
214 |
215 | __KeyToKey__
216 | KeyCode::OPTION_L,
217 | KeyCode::COMMAND_L
218 |
219 | __KeyToKey__
220 | KeyCode::COMMAND_L,
221 | KeyCode::OPTION_L
222 |
223 |
224 |
225 | Skim
226 | __KeyToKey__
227 | KeyCode::K, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
228 | KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP
229 |
230 | __KeyToKey__
231 | KeyCode::J, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
232 | KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN
233 |
234 | __KeyToKey__
235 | KeyCode::L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
236 | KeyCode::TAB, ModifierFlag::CONTROL_R
237 |
238 | __KeyToKey__
239 | KeyCode::H, ModifierFlag::OPTION_L, ModifierFlag::NONE,
240 | KeyCode::TAB, ModifierFlag::CONTROL_R, ModifierFlag::SHIFT_R
241 |
242 | __KeyToKey__
243 | KeyCode::P, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
244 | KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP, KeyCode::CURSOR_UP
245 |
246 | __KeyToKey__
247 | KeyCode::N, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
248 | KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN, KeyCode::CURSOR_DOWN
249 |
250 | __KeyToKey__
251 | KeyCode::COMMA, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
252 | KeyCode::CURSOR_LEFT, ModifierFlag::FN
253 |
254 | __KeyToKey__
255 | KeyCode::DOT, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
256 | KeyCode::CURSOR_RIGHT, ModifierFlag::FN
257 |
258 | __KeyToKey__
259 | KeyCode::P, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
260 | KeyCode::G, ModifierFlag::COMMAND_R, ModifierFlag::OPTION_R
261 |
262 |
263 |
264 | Xee³
265 | __KeyToKey__
266 | KeyCode::D, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
267 | KeyCode::DELETE, ModifierFlag::COMMAND_R
268 |
269 | __KeyToKey__
270 | KeyCode::P, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
271 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
272 |
273 | __KeyToKey__
274 | KeyCode::N, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
275 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
276 |
277 | __KeyToKey__
278 | KeyCode::CURSOR_LEFT,
279 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
280 |
281 | __KeyToKey__
282 | KeyCode::CURSOR_UP,
283 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
284 |
285 | __KeyToKey__
286 | KeyCode::H,
287 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
288 |
289 | __KeyToKey__
290 | KeyCode::K,
291 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
292 |
293 | __KeyToKey__
294 | KeyCode::CURSOR_RIGHT,
295 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
296 |
297 | __KeyToKey__
298 | KeyCode::CURSOR_DOWN,
299 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
300 |
301 | __KeyToKey__
302 | KeyCode::J,
303 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
304 |
305 | __KeyToKey__
306 | KeyCode::L,
307 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
308 |
309 |
310 |
311 | Finder
312 | __KeyToKey__
313 | KeyCode::COMMA, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
314 | KeyCode::CURSOR_UP, ModifierFlag::OPTION_R
315 |
316 | __KeyToKey__
317 | KeyCode::DOT, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
318 | KeyCode::CURSOR_DOWN, ModifierFlag::OPTION_R
319 |
320 |
321 |
322 | Sublime_Text
323 | __KeyToKey__
324 | KeyCode::COMMA, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
325 | KeyCode::CURSOR_UP, ModifierFlag::COMMAND_R
326 |
327 | __KeyToKey__
328 | KeyCode::DOT, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
329 | KeyCode::CURSOR_DOWN, ModifierFlag::COMMAND_R
330 |
331 | __KeyToKey__
332 | KeyCode::P, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
333 | KeyCode::CURSOR_UP
334 |
335 | __KeyToKey__
336 | KeyCode::N, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
337 | KeyCode::CURSOR_DOWN
338 |
339 |
340 |
341 |
342 | {{emacs_ignore_app}},
343 | Skim,
344 | Xee³
345 |
346 | __KeyToKey__
347 | KeyCode::P, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
348 | KeyCode::CURSOR_UP
349 |
350 | __KeyToKey__
351 | KeyCode::N, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
352 | KeyCode::CURSOR_DOWN
353 |
354 | __KeyToKey__
355 | KeyCode::D, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
356 | KeyCode::FORWARD_DELETE
357 |
358 |
359 |
360 |
361 | {{emacs_ignore_app}},
362 | Skim,
363 | Finder,
364 | Sublime_Text
365 |
366 | __KeyToKey__
367 | KeyCode::COMMA, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
368 | KeyCode::CURSOR_UP, ModifierFlag::COMMAND_R
369 |
370 | __KeyToKey__
371 | KeyCode::DOT, ModifierFlag::OPTION_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
372 | KeyCode::CURSOR_DOWN, ModifierFlag::COMMAND_R
373 |
374 |
375 |
376 | {{emacs_ignore_app}}
377 | __KeyToKey__
378 | KeyCode::B, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
379 | KeyCode::CURSOR_LEFT
380 |
381 | __KeyToKey__
382 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
383 | KeyCode::CURSOR_RIGHT
384 |
385 | __KeyToKey__
386 | KeyCode::B, ModifierFlag::OPTION_L, ModifierFlag::NONE,
387 | KeyCode::CURSOR_LEFT, ModifierFlag::OPTION_R
388 |
389 | __KeyToKey__
390 | KeyCode::F, ModifierFlag::OPTION_L, ModifierFlag::NONE,
391 | KeyCode::CURSOR_RIGHT, ModifierFlag::OPTION_R
392 |
393 | __KeyToKey__
394 | KeyCode::A, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
395 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R
396 |
397 | __KeyToKey__
398 | KeyCode::E, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
399 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R
400 |
401 | __KeyToKey__
402 | KeyCode::H, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
403 | KeyCode::DELETE
404 |
405 | __KeyToKey__
406 | KeyCode::D, ModifierFlag::OPTION_L, ModifierFlag::NONE,
407 | KeyCode::FORWARD_DELETE, ModifierFlag::OPTION_R
408 |
409 | __KeyToKey__
410 | KeyCode::U, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
411 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R, KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R, ModifierFlag::SHIFT_R, KeyCode::DELETE, KeyCode::DELETE, Option::NOREPEAT
412 |
413 |
414 |
415 | TERMINAL
416 | __KeyToKey__
417 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
418 | KeyCode::RETURN, ModifierFlag::COMMAND_R
419 |
420 |
421 |
422 |
423 | Skim,
424 | Kindle
425 |
426 | __KeyToKey__
427 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
428 | KeyCode::F, ModifierFlag::COMMAND_R, ModifierFlag::SHIFT_R, KeyCode::MINUS, ModifierFlag::COMMAND_R, ModifierFlag::SHIFT_R
429 |
430 |
431 |
432 | VIRTUALMACHINE
433 | __KeyToKey__
434 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
435 | KeyCode::F, ModifierFlag::COMMAND_L
436 |
437 |
438 |
439 |
440 | VIRTUALMACHINE,
441 | X11
442 |
443 | __KeyToKey__
444 | KeyCode::R, ModifierFlag::OPTION_L, ModifierFlag::NONE,
445 | KeyCode::R, ModifierFlag::COMMAND_R
446 |
447 | __KeyToKey__
448 | KeyCode::E, ModifierFlag::OPTION_L, ModifierFlag::NONE,
449 | KeyCode::E, ModifierFlag::COMMAND_R
450 |
451 | __KeyToKey__
452 | KeyCode::D, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
453 | KeyCode::D, ModifierFlag::COMMAND_R
454 |
455 | __KeyToKey__
456 | KeyCode::H, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
457 | KeyCode::DELETE
458 |
459 | __KeyToKey__
460 | KeyCode::D, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
461 | KeyCode::FORWARD_DELETE
462 |
463 | __KeyToKey__
464 | KeyCode::U, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
465 | KeyCode::END, KeyCode::HOME, ModifierFlag::SHIFT_L, KeyCode::DELETE, KeyCode::DELETE, Option::NOREPEAT
466 |
467 | __KeyToKey__
468 | KeyCode::DELETE, ModifierFlag::CONTROL_L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
469 | KeyCode::DELETE, ModifierFlag::CONTROL_R
470 |
471 | __KeyToKey__
472 | KeyCode::D, ModifierFlag::CONTROL_L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
473 | KeyCode::FORWARD_DELETE, ModifierFlag::CONTROL_R
474 |
475 | __KeyToKey__
476 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
477 | KeyCode::CURSOR_RIGHT, ModifierFlag::CONTROL_R
478 |
479 | __KeyToKey__
480 | KeyCode::B, ModifierFlag::CONTROL_L, ModifierFlag::OPTION_L, ModifierFlag::NONE,
481 | KeyCode::CURSOR_LEFT, ModifierFlag::CONTROL_R
482 |
483 | __KeyToKey__
484 | KeyCode::Q, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
485 | KeyCode::F4, ModifierFlag::OPTION_R
486 |
487 | __KeyToKey__
488 | KeyCode::R, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
489 | KeyCode::R, ModifierFlag::CONTROL_R
490 |
491 | __KeyToKey__
492 | KeyCode::L, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
493 | KeyCode::L, ModifierFlag::CONTROL_R
494 |
495 | __KeyToKey__
496 | KeyCode::C, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
497 | KeyCode::C, ModifierFlag::CONTROL_R
498 |
499 | __KeyToKey__
500 | KeyCode::V, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
501 | KeyCode::V, ModifierFlag::CONTROL_R
502 |
503 | __KeyToKey__
504 | KeyCode::X, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
505 | KeyCode::X, ModifierFlag::CONTROL_R
506 |
507 | __KeyToKey__
508 | KeyCode::Z, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
509 | KeyCode::Z, ModifierFlag::CONTROL_R
510 |
511 | __KeyToKey__
512 | KeyCode::A, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
513 | KeyCode::A, ModifierFlag::CONTROL_R
514 |
515 | __KeyToKey__
516 | KeyCode::F, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
517 | KeyCode::F, ModifierFlag::CONTROL_R
518 |
519 | __KeyToKey__
520 | KeyCode::S, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
521 | KeyCode::S, ModifierFlag::CONTROL_R
522 |
523 | __KeyToKey__
524 | KeyCode::W, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
525 | KeyCode::W, ModifierFlag::CONTROL_R
526 |
527 | __KeyToKey__
528 | KeyCode::T, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
529 | KeyCode::T, ModifierFlag::CONTROL_R
530 |
531 | __KeyToKey__
532 | KeyCode::A, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
533 | KeyCode::HOME
534 |
535 | __KeyToKey__
536 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
537 | KeyCode::HOME
538 |
539 | __KeyToKey__
540 | KeyCode::E, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
541 | KeyCode::END
542 |
543 | __KeyToKey__
544 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_L, ModifierFlag::NONE,
545 | KeyCode::END
546 |
547 | __KeyToKey__
548 | KeyCode::P, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
549 | KeyCode::CURSOR_UP
550 |
551 | __KeyToKey__
552 | KeyCode::N, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
553 | KeyCode::CURSOR_DOWN
554 |
555 | __KeyToKey__
556 | KeyCode::F, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
557 | KeyCode::CURSOR_RIGHT
558 |
559 | __KeyToKey__
560 | KeyCode::B, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
561 | KeyCode::CURSOR_LEFT
562 |
563 |
564 |
565 | Bilibili
566 | __KeyToKey__
567 | KeyCode::TAB, ModifierFlag::CONTROL_L, ModifierFlag::NONE,
568 | KeyCode::CURSOR_RIGHT, ModifierFlag::COMMAND_R, ModifierFlag::OPTION_R
569 |
570 | __KeyToKey__
571 | KeyCode::TAB, ModifierFlag::CONTROL_L, ModifierFlag::SHIFT_L, ModifierFlag::NONE,
572 | KeyCode::CURSOR_LEFT, ModifierFlag::COMMAND_R, ModifierFlag::OPTION_R
573 |
574 |
575 |
576 |
577 |
578 |
--------------------------------------------------------------------------------