├── 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 | [![Travis Build Status](https://travis-ci.org/loggerhead/Easy-Karabiner.svg?branch=master)](https://travis-ci.org/loggerhead/Easy-Karabiner) 4 | [![Code Health](https://landscape.io/github/loggerhead/Easy-Karabiner/master/landscape.svg?branch=master)](https://landscape.io/github/loggerhead/Easy-Karabiner/master) 5 | [![Coverage Status](https://coveralls.io/repos/github/loggerhead/Easy-Karabiner/badge.svg)](https://coveralls.io/github/loggerhead/Easy-Karabiner) 6 | [![PyPI version](https://img.shields.io/pypi/v/easy_karabiner.svg)](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 | --------------------------------------------------------------------------------