PK!nrr CHANGELOG.md# Version 0.2 ## Features * Add X11, Windows, OS X, SVG target support * Add CLDR convertor (both directions) ## Changes * Android env var becomes `ANDROID_HOME` * Keyboard layout mode format is no longer a list. This: > default: [ > A B C, > D E F > ] Becomes: > default: | > A B C > D E F # Version 0.1 * Initial release, supporting iOS and Android. PK!'*(*(LICENSEApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non- exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no- charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK!3Fkbdgen/__init__.py__version__ = "1.0.1" PK!qy##kbdgen/__main__.pyimport sys def main(): if sys.version_info.major < 3: print("kbdgen only supports Python 3.") sys.exit(1) try: from .cli import run_cli sys.exit(run_cli()) except KeyboardInterrupt: sys.exit(255) if __name__ == "__main__": main() PK!^177kbdgen/base.pyimport logging import os import os.path import re import sys import itertools import unicodedata from collections import OrderedDict, namedtuple from . import orderedyaml, log class KbdgenException(Exception): pass class UserException(Exception): pass log.monkey_patch_trace_logging() def get_logger(path): return logging.getLogger(os.path.basename(os.path.splitext(path)[0])) log.enable_pretty_logging( fmt="%(color)s[%(levelname)1.1s %(module)s:%(lineno)d]%(end_color)s" + " %(message)s" ) logger = logging.getLogger() Action = namedtuple("Action", ["row", "position", "width"]) ProjectLocaleData = namedtuple("ProjectLocaleData", ["name", "description"]) VALID_ID_RE = re.compile(r"^[a-z][0-9a-z-_]+$") ISO_KEYS = ( "E00", "E01", "E02", "E03", "E04", "E05", "E06", "E07", "E08", "E09", "E10", "E11", "E12", "D01", "D02", "D03", "D04", "D05", "D06", "D07", "D08", "D09", "D10", "D11", "D12", "C01", "C02", "C03", "C04", "C05", "C06", # TODO fix the D13 special case. "C07", "C08", "C09", "C10", "C11", "D13", # C12 -> D13 "B00", "B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B09", "B10", ) MODE_LIST_ERROR = """\ '%s' must be defined as a string using block string format, not a list. For example, if your keyboard.yaml looks like: ``` modes: default: [ q w e r t y u i o p å, a s d f g h j k l ö æ, z x c v b n m ï ] ``` Convert that to: ``` modes: default: | q w e r t y u i o p å a s d f g h j k l ö æ z x c v b n m ï ``` """ def parse_layout(data, length_check=True): if isinstance(data, dict): o = OrderedDict() for key in ISO_KEYS: v = data.get(key, None) o[key] = str(v) if v is not None else None return o elif isinstance(data, str): data = re.sub(r"[\r\n\s]+", " ", data.strip()).split(" ") if length_check and len(data) != len(ISO_KEYS): raise Exception(len(data)) o = OrderedDict(zip(ISO_KEYS, data)) # Remove nulls for k in ISO_KEYS: if o[k] == r"\u{0}": o[k] = None return o def parse_touch_layout(data): return [re.split(r"\s+", x.strip()) for x in data.strip().split("\n")] class Project: def __init__(self, tree): self._tree = tree def relpath(self, end): return os.path.abspath(os.path.join(self.path, end)) @property def path(self): return self._tree["_path"] @property def locales(self): return self._tree["locales"] @property def author(self): return self._tree["author"] @property def email(self): return self._tree["email"] @property def layouts(self): return self._tree["layouts"] @property def targets(self): return self._tree["targets"] @property def internal_name(self): return self._tree["internalName"] @property def app_strings(self): return self._tree["appStrings"] @property def version(self): return str(self._tree["version"]) @property def build(self): return str(self._tree["build"]) @property def copyright(self): return self._tree.get("copyright", "") @property def organisation(self): return self._tree.get("organisation", "") def locale(self, tag): val = self.locales.get(tag, None) if val is None: return None return ProjectLocaleData(val["name"], val["description"]) @property def names(self): x = {} for tag, o in self.locales.items(): x[tag] = o["name"] return x @property def descriptions(self): x = {} for tag, o in self.locales.items(): x[tag] = o["description"] return x def first_locale(self): tag = next(iter(self.locales.keys())) return self.locale(tag) def target(self, target): return self._tree["targets"].get(target, {}) or {} def icon(self, target, size=None): val = self.target(target).get("icon", None) if val is None: return None if isinstance(val, str): return self.relpath(val) if size is None: # Find largest m = -1 for k in val: if k > m: m = k return self.relpath(val[m]) else: lrg = -1 m = sys.maxsize for k in val: if k > lrg: lrg = k if k >= size and k < m: m = k if m == sys.maxsize: return self.relpath(val[lrg]) return self.relpath(val[m]) class Keyboard: def __init__(self, tree): self._tree = tree @property def internal_name(self): return self._tree["internalName"] @property def native_display_name(self): return self.display_names[self.locale] @property def display_names(self): return self._tree["displayNames"] @property def locale(self): return self._tree["locale"] @property def special(self): return self._tree.get("special", {}) @property def decimal(self): return self._tree.get("decimal", None) @property def dead_keys(self): return self._tree.get("deadKeys", {}) @property def derive(self): return self._tree.get("derive", {}) @property def transforms(self): return self._tree.get("transforms", {}) @property def modifiers(self): return self._tree["modifiers"] @property def modes(self): return self._tree["modes"] @property def strings(self): return self._tree.get("strings", {}) @property def styles(self): return self._tree["styles"] def target(self, target): return self._tree.get("targets", {}).get(target, {}) or {} def get_actions(self, style): return self.styles[style]["actions"] def get_action(self, style, key): return self.styles[style]["actions"].get(key, None) @property def longpress(self): return self._tree["longpress"] def get_longpress(self, key): return self._tree["longpress"].get(key, None) @property def supported_targets(self): return self._tree.get("supportedTargets", None) def supported_target(self, target): targets = self.supported_targets if targets is None: return True return target in targets class Parser: def __init__(self): pass def _overrides(self, project, cfg_pairs): def resolve_path(path, v): chunks = path.split(".") last = chunks.pop() node = project for chunk in chunks: if node.get(chunk, None) is None: node[chunk] = OrderedDict() node = node[chunk] node[last] = v for path, v in cfg_pairs: resolve_path(path, v) def _parse_cfg_pairs(self, str_list): try: return [x.split("=", 1) for x in str_list] except Exception: raise Exception("Error: invalid key-value pair provided.") def _parse_global(self, cfg_file=None): if cfg_file is None: cfg_file = open( os.path.join(os.path.dirname(__file__), "global.yaml"), encoding="utf-8" ) return orderedyaml.load(cfg_file) @classmethod def _parse_keyboard_descriptor(cls, tree): for key in ["locale", "displayNames", "internalName", "modes"]: if key not in tree: raise Exception("%s key missing from file." % key) # TODO move this to android and ios generators # if 'mobile-default' not in tree['modes']: # raise Exception("No default mode supplied in file.") if "modifiers" not in tree or tree.get("modifiers", None) is None: tree["modifiers"] = [] if "longpress" not in tree or tree.get("longpress", None) is None: tree["longpress"] = OrderedDict() for mode in list(tree["modes"].keys()): if isinstance(tree["modes"][mode], list): raise Exception(MODE_LIST_ERROR % mode) try: # Soft layouts are special cased. if mode in ["mobile-default", "mobile-shift"]: tree["modes"][mode] = parse_touch_layout(tree["modes"][mode]) else: tree["modes"][mode] = parse_layout(tree["modes"][mode]) except Exception as e: raise Exception( ("'%s' is the wrong length. " + "Got %s, expected %s.") % (mode, str(e), len(ISO_KEYS)) ) for longpress, strings in tree["longpress"].items(): tree["longpress"][longpress] = re.split(r"\s+", strings.strip()) for style, styles in tree.get("styles", {}).items(): for action, info in styles["actions"].items(): styles["actions"][action] = Action(info[0], info[1], info[2]) return Keyboard(tree) def _parse_project(self, tree): for key in ["locales", "author", "email", "layouts", "targets"]: if key not in tree: raise Exception("%s key missing from file." % key) tree_path = tree["_path"] layouts = OrderedDict() known_ids = set() for layout in tree["layouts"]: try: fn = "%s.yaml" % layout with open(os.path.join(tree_path, fn), encoding="utf-8") as f: try: data = unicodedata.normalize("NFC", f.read()) kbdtree = orderedyaml.loads(data) rl = self._parse_keyboard_descriptor(kbdtree) dt = rl.derive.get("transforms", False) if dt is not False: derive_transforms(rl, True if dt == "all" else False) if rl.internal_name is None: raise UserException( "'%s' has no internalName field" % f.name ) if not VALID_ID_RE.match(rl.internal_name): raise UserException( ( "Internal name '%s' in file '%s' not valid. Must " + "begin with a-z, and after contain only a-z, " + "0-9, dashes (-) and underscores (_)." ) % (rl.internal_name, fn) ) if rl.internal_name in known_ids: raise UserException( "A duplicate internal name was found '%s' in file '%s'" % (rl.internal_name, fn) ) known_ids.add(rl.internal_name) layouts[rl.internal_name] = rl except Exception as e: logger.error("There was an error for file '%s.yaml':" % layout) raise e except FileNotFoundError: logger.error("Layout '%s' listed in project, but not found." % layout) return None tree["layouts"] = layouts return Project(tree) def parse(self, f, cfg_pairs=None, cfg_file=None): tree = self._parse_global(cfg_file) # Compose all decomposed unicode codepoints data = unicodedata.normalize("NFC", f.read()) tree.update(orderedyaml.loads(data)) tree["_path"] = os.path.dirname(os.path.abspath(f.name)) project = self._parse_project(tree) if project is None: return None if cfg_pairs is not None: logger.trace("cfg_pairs: %r", cfg_pairs) self._overrides(project._tree, self._parse_cfg_pairs(cfg_pairs)) return project def decompose(ch): x = unicodedata.normalize("NFKD", ch).replace(" ", "") if x == ch: try: c = "COMBINING %s" % unicodedata.name(ch).replace("MODIFIER LETTER ", "") return unicodedata.lookup(c) except Exception: pass return x def derive_transforms(layout, allow_glyphbombs=False): if layout._tree.get("transforms", None) is None: layout._tree["transforms"] = {} dead_keys = sorted(set(itertools.chain.from_iterable(layout.dead_keys.values()))) logger.trace("Dead keys: %r" % dead_keys) # Get all letter category input chars def char_filter(ch): if ch is None: return False if len(ch) != 1: return False return unicodedata.category(ch).startswith("L") input_chars = sorted( set( filter( char_filter, set( itertools.chain.from_iterable( (x.values() for x in layout.modes.values()) ) ), ) ) ) logger.trace("Input chars: %r" % input_chars) # Generate inputtable transforms for d in dead_keys: if layout.transforms.get(d, None) is None: layout.transforms[d] = {" ": d} dc = decompose(d) for ch in input_chars: composed = "%s%s" % (ch, dc) normalised = unicodedata.normalize("NFKC", composed) # Check if when composed the codepoint is not the same as decomposed if not allow_glyphbombs and composed == normalised: logger.trace("Skipping %s%s" % (d, ch)) continue logger.trace("Adding transform: %s%s -> %s" % (d, ch, normalised)) layout.transforms[d][ch] = normalised PK!3kbdgen/boolmap.pyimport array class BoolMap: def __init__(self, bytedata=None, default=False): self._default = bool(default) self._data = array.array("B") if bytedata is not None: self._data.frombytes(bytedata) def __getitem__(self, k): if not isinstance(k, int): raise KeyError(k) pos = k // 8 off = k % 8 if pos >= len(self._data): return self._default return bool(self._data[pos] & 1 << off) def __setitem__(self, k, v): if not isinstance(k, int): raise KeyError(k) if not isinstance(v, bool): raise ValueError(v) pos = k // 8 off = k % 8 if pos >= len(self._data): self._data.fromlist( [0 if self._default is False else 0xFF] * (pos - len(self._data) + 1) ) chunk = self._data[pos] if v: self._data[pos] = chunk | 1 << off else: self._data[pos] = ~(~chunk & 0xFF | (1 << off)) & 0xFF return v def __iter__(self): for v in self._data: for i in range(8): yield bool(v & (1 << i)) def to_bytes(self): return self._data.tobytes() def parse_range_data(data): range_data = [x.split("-", 1) for x in data.split(",")] new_ranges = [] for chunk in range_data: ln = len(chunk) if 0 > ln > 2: raise Exception() if len(chunk) == 1: chunk.append(chunk[0]) new_ranges.append(range(int(chunk[0], 16), int(chunk[1], 16) + 1)) return new_ranges def apply_ranges_to_boolmap(data): boolmap = BoolMap() for iterator in data: for i in iterator: boolmap[i] = True return boolmap if __name__ == "__main__": import sys with open(sys.argv[1]) as f: data = f.read() ranges = parse_range_data(data) bm = apply_ranges_to_boolmap(ranges) with open(sys.argv[1] + ".bin", "wb") as f: f.write(bm.to_bytes()) PK!LPN9N9kbdgen/cldr.pyimport datetime import itertools import os.path import re import unicodedata import lxml.etree from lxml.etree import SubElement from io import StringIO from collections import OrderedDict, namedtuple from .base import get_logger logger = get_logger(__file__) CP_REGEX = re.compile(r"\\u{(.+?)}") ENTITY_REGEX = re.compile(r"&#(\d+);") BAD_UNICODE_CATS = ("C", "Z", "M") def cldr_sub(value, repl, ignore_space=False): def r(x): c = unicodedata.category(x)[0] if (ignore_space and x == " ") or c not in BAD_UNICODE_CATS: return x else: return repl(x, c) return "".join([r(x) for x in value]) def decode_u(v, newlines=True): def chk(x): vv = chr(int(x.group(1), 16)) if not newlines and vv in ("\n", "\r"): return x.group(0) return vv if v is None: v = "" return CP_REGEX.sub(chk, str(v)) def encode_u(v): return cldr_sub(v, lambda x, c: r"\u{%X}" % ord(x)) def key_cmp(x): ch, n = parse_cell(x[0]) return -int(ch, 16) * 16 + n def process_value(*args): pv = lambda v: encode_u(decode_u(v)) # noqa: E731 return tuple(pv(i) for i in args) if len(args) > 1 else pv(args[0]) def cell_range(ch, from_, to_): for i in range(from_, to_ + 1): yield "%s%02d" % (ch, i) def parse_cell(x): a = x[0] b = int(x[1:]) return a, b def is_relevant_cell(x): ch, n = parse_cell(x) if ch == "E": return 0 <= n <= 12 if ch == "D": return 1 <= n <= 12 if ch == "C": return 1 <= n <= 12 if ch == "B": return 0 <= n <= 10 return False class UnknownNgramException(Exception): pass def split_for_set(s, string): o = [] i = len(string) while len(string) > 0: in_set = string[:i] in s if in_set: o.append(string[:i]) string = string[i:] i = len(string) elif i == 1: raise UnknownNgramException("'%s' not found in set." % string) else: i -= 1 return tuple(o) def keyboard_range(): return itertools.chain( cell_range("E", 0, 12), cell_range("D", 1, 12), cell_range("C", 1, 12), cell_range("B", 0, 10), ) def is_full_layout(o): """Strictly not accurate, as D13 is considered C12 for convenience.""" chain = keyboard_range() for v in chain: if v not in o: return False return True def filtered(v): if v == '"': return "'\"'" if v == "\\": # TODO check if this is necessary in practice return r'"\\"' if v in r" |-?:,[]{}#&*!>'%@`~=": return '"%s"' % v return encode_u(v) def to_xml(yaml_tree): tree = lxml.etree.fromstring( """""" % yaml_tree["internalName"] ) # TODO generate both these validly SubElement(tree, "version") SubElement(tree, "generation") names = SubElement(tree, "names") SubElement(names, "name", value=yaml_tree["displayNames"][yaml_tree["locale"]]) for mode, key_map in yaml_tree["modes"].items(): if not mode.startswith("iso-"): continue mod = mode[4:] if mod == "default": node = SubElement(tree, "keyMap") else: node = SubElement(tree, "keyMap", modifiers=mod) deadkey_set = { x for x in itertools.chain.from_iterable(yaml_tree["deadKeys"].values()) } if isinstance(key_map, dict): for k, v in sorted(key_map.items(), key=key_cmp): key_node = SubElement(node, "map", iso=str(k), to=str(v)) # TODO make this more optimal, chaining all lists and only # assigning when it makes sense to do so if v in deadkey_set and v not in yaml_tree["deadKeys"].get(mode, {}): key_node.attrib["transform"] = "no" else: chain = keyboard_range() for iso, to in zip(chain, re.split(r"[\s\n]+", key_map)): # Ignore nulls if to == r"\u{0}": continue key_node = SubElement(node, "map", iso=iso, to=to) if to in deadkey_set and to not in yaml_tree["deadKeys"].get(mode, {}): key_node.attrib["transform"] = "no" # Space special case! space = yaml_tree.get("special", {}).get("space", {}).get(mode, " ") SubElement(node, "map", iso="A03", to=space) transforms = SubElement(tree, "transforms", type="simple") for base, o in yaml_tree["transforms"].items(): for tr_from, tr_to in o.items(): n = SubElement(transforms, "transform") n.attrib["from"] = "%s%s" % (base, tr_from) n.attrib["to"] = tr_to out = lxml.etree.tostring( tree, xml_declaration=True, encoding="utf-8", pretty_print=True ).decode() return ENTITY_REGEX.sub(lambda x: "\\u{%s}" % hex(int(x.group(1)))[2:].upper(), out) class CLDRKeyboard: @classmethod def from_file(cls, f): return cls(f.read(), filename=os.path.basename(f.name)) def __init__(self, data, filename=None): self._filename = filename self._modes = OrderedDict() # Actual transforms themselves self._transforms = OrderedDict() # Key set self._key_set = set() # Known deadkey glyphs self._deadkey_set = set() # Deadkey layouts self._deadkeys = OrderedDict() self._comments = {} self._space = OrderedDict() tree = lxml.etree.fromstring(data) self._internal_name = tree.attrib["locale"] self._locale = self._internal_name.split("-")[0] self._name = tree.xpath("names/name")[0].attrib["value"] self._generate_keyset(tree) self._parse_transforms(tree) self._parse_keymaps(tree) def _generate_keyset(self, tree): self._key_set = {decode_u(n.attrib["to"]) for n in tree.xpath("keyMap/map")} self._key_set.add(" ") def _parse_keymaps(self, tree): is_osx = self._internal_name.endswith("osx") for keymap in tree.xpath("keyMap"): mode = CLDRMode(keymap.attrib.get("modifiers", "default")) new_mode = mode.kbdgen[0] self._comments[new_mode] = mode.cldr o = {} for key in keymap.xpath("map"): iso_key = key.attrib["iso"] # Special case for layout differences. if iso_key == "D13": iso_key = "C12" elif not is_relevant_cell(iso_key): if iso_key == "A03" and key.attrib["to"] != " ": self._space[new_mode] = process_value(key.attrib["to"]) continue o[iso_key] = process_value(key.attrib["to"]) if ( o[iso_key] in self._deadkey_set and key.attrib.get("transform", None) != "no" ): self._deadkeys.setdefault(new_mode, set()).add(o[iso_key]) # OS X definitions are in a pseudo-ANSI format that inverts # the E00 and B00 keys to prioritise B00 to the E00 position if # an ISO language keyboard layout is used on an ANSI keyboard. if is_osx and "B00" in o and "E00" in o: tmp = o["E00"] o["E00"] = o["B00"] o["B00"] = tmp if "B00" not in o and "E00" in o: o["B00"] = o["E00"] logger.warning( ( "B00 has been duplicated from E00 in '%s'; " + "ANSI keyboard definition?" ) % new_mode ) # Force ANSI-style keys into the ISO world. if "D13" in o: o["C12"] = o["D13"] del o["D13"] self._modes[new_mode] = OrderedDict(sorted(o.items(), key=key_cmp)) def _parse_transforms(self, tree): o = OrderedDict() def o_add(ng, v): last = o for k in ng[:-1]: last.setdefault(k, OrderedDict()) last = last[k] last[ng[-1]] = v for transform in tree.xpath("transforms[@type='simple']/transform"): ngrams = split_for_set(self._key_set, decode_u(transform.attrib["from"])) self._deadkey_set.add(ngrams[0]) o_add(ngrams, decode_u(transform.attrib["to"])) self._transforms = o def keys(self, mode): return self._modes[mode] def _as_yaml_transforms(self, x): x.write("\ntransforms:\n") def mm(n, it): for k, v in it.items(): if isinstance(v, str): x.write("%s%s: %s\n" % (" " * n, filtered(k), filtered(v))) elif isinstance(v, dict): x.write("%s%s:\n" % (" " * n, filtered(k))) mm(n + 2, v) mm(2, self._transforms) def as_yaml(self): x = StringIO() x.write( "### Generated from %s on %s.\n\n" % ( self._filename or "string data", datetime.datetime.utcnow().strftime("%Y-%m-%d at %H:%M"), ) ) x.write("internalName: %s\n\n" % self._internal_name) x.write("displayNames:\n %s: %s\n\n" % (self._locale, self._name)) x.write("locale: %s\n\n" % self._locale) x.write("modes:\n") for mode, o in self._modes.items(): if mode in self._comments: x.write(" # %s\n" % self._comments[mode]) x.write(" %s: |" % mode) cur = None for iso_key in keyboard_range(): if cur != iso_key[0]: x.write("\n ") cur = iso_key[0] x.write(" ") x.write(o.get(iso_key, r"\u{0}")) x.write("\n") if len(self._deadkeys) > 0: x.write("\ndeadKeys:\n") for mode, keys in self._deadkeys.items(): output = list(sorted(keys)) x.write((" %s: %r\n" % (mode, output)).replace("\\", r"\\")) if len(self._transforms) > 0: self._as_yaml_transforms(x) if len(self._space) > 0: x.write("\nspecial:\n space:\n") for mode, v in self._space.items(): x.write(" %s: %s\n" % (mode, filtered(v))) return x.getvalue() Left = "left" Right = "right" Both = "both" Shift = "shift" Alt = "alt" Caps = "caps" Ctrl = "ctrl" OSXCommand = "cmd" TOKENS = { "shift": Shift, "alt": Alt, "opt": Alt, "caps": Caps, "ctrl": Ctrl, "cmd": OSXCommand, } ModeToken = namedtuple("CLDRMode", ["name", "direction", "required"]) class CLDRMode: def __init__(self, data): self._raw = data self._kbdgen = None def _parse_tokens(self, tokens): def pt(tok): is_required = not tok.endswith("?") if not is_required: tok = tok[:-1] is_l = tok.endswith("L") is_r = tok.endswith("R") if is_l or is_r: tok = tok[:-1] known_token = TOKENS.get(tok, None) if known_token is None: return ModeToken(known_token, None, None) direction = Both if is_l: direction = Left if is_r: direction = Right return ModeToken(known_token, direction, is_required) return tuple(pt(x) for x in tokens) def _init_kbdgen(self): out = [] phrases = self._raw.split(" ") for ph in phrases: # Special case: "default" if ph == "default": if "iso-default" not in out: out.append("iso-default") continue tokens = self._parse_tokens(ph.split("+")) clean = tuple(tok[0] for tok in tokens if tok.required is True) prefix = "iso" if OSXCommand in clean: prefix = "osx" def mm(x): if x == "cmd": return 0 if x == "caps": return 5 if x == "ctrl": return 10 if x == "alt": return 20 if x == "shift": return 40 return 99 v = "%s-%s" % (prefix, "+".join(sorted(clean, key=mm))) if v not in out: out.append(v) # Sorted to bring shortest priority to the front # eg ('iso-caps+alt', 'iso-alt', 'osx-cmd+alt') from cs is wrong. return tuple(sorted(out, key=lambda x: x.count("+"))) @property def kbdgen(self): if self._kbdgen is None: self._kbdgen = self._init_kbdgen() return self._kbdgen @property def cldr(self): return self._raw def cldr2kbdgen_main(): import argparse import sys p = argparse.ArgumentParser(prog="cldr2kbdgen") p.add_argument( "--osx", action="store_true", help="Force detection of XML file as an OS X keyboard.", ) p.add_argument("cldr_xml", type=argparse.FileType("rb"), default=sys.stdin) p.add_argument("kbdgen_yaml", type=argparse.FileType("w"), default=sys.stdout) args = p.parse_args() args.kbdgen_yaml.write(CLDRKeyboard.from_file(args.cldr_xml).as_yaml()) def kbdgen2cldr_main(): import argparse import sys from . import orderedyaml p = argparse.ArgumentParser(prog="kbdgen2cldr") # p.add_argument('--osx', action='store_true', # help="Force detection of XML file as an OS X keyboard.") p.add_argument("kbdgen_yaml", type=argparse.FileType("r"), default=sys.stdout) p.add_argument("cldr_xml", type=argparse.FileType("w"), default=sys.stdin) args = p.parse_args() parsed = orderedyaml.load(args.kbdgen_yaml) args.cldr_xml.write(to_xml(parsed)) if __name__ == "__main__": import sys if len(sys.argv) < 2: print("cldr2kbdgen or kbdgen2cldr required as first param.") sys.exit(1) app = sys.argv.pop(1) if app == "cldr2kbdgen": cldr2kbdgen_main() elif app == "kbdgen2cldr": kbdgen2cldr_main() else: print("cldr2kbdgen or kbdgen2cldr required as first param.") sys.exit(1) PK!X$ kbdgen/cli.pyimport argparse import yaml import sys from . import __version__, gen from .base import KbdgenException, Parser, logger, UserException def parse_args(): def logging_type(string): n = { "critical": 50, "error": 40, "warning": 30, "info": 20, "debug": 10, "trace": 5, }.get(string, None) if n is None: raise argparse.ArgumentTypeError("Invalid logging level.") return n p = argparse.ArgumentParser(prog="kbdgen") p.add_argument("--version", action="version", version="%(prog)s " + __version__) p.add_argument("--logging", type=logging_type, default=20, help="Logging level") p.add_argument( "-K", "--key", nargs="*", dest="cfg_pairs", help="Key-value overrides (eg -K target.thing.foo=42)", ) p.add_argument( "-D", "--dry-run", action="store_true", help="Don't build, just do sanity checks.", ) p.add_argument( "-R", "--release", action="store_true", help="Compile in 'release' mode (where necessary).", ) p.add_argument( "-G", "--global", type=argparse.FileType("r"), help="Override the global.yaml file", ) p.add_argument("-r", "--repo", help="Git repo to generate output from") p.add_argument( "-b", "--branch", default="master", help="Git branch (default: master)" ) p.add_argument( "-t", "--target", required=True, choices=gen.generators.keys(), help="Target output.", ) p.add_argument( "-o", "--output", default=".", help="Output directory (default: current working directory)", ) p.add_argument( "project", help="Keyboard generation project (yaml)", type=argparse.FileType("r", encoding="utf-8"), default=sys.stdin, ) p.add_argument( "-f", "--flag", nargs="*", dest="flags", help="Generator-specific flags (for debugging)", default=[], ) p.add_argument("--github-username", help="GitHub username for source getting") p.add_argument("--github-token", help="GitHub token for source getting") p.add_argument("-c", "--command", help="Command to run for a given generators") p.add_argument("--ci", action="store_true", help="Continuous integration build") return p.parse_args() def run_cli(): args = parse_args() logger.setLevel(args.logging) try: project = Parser().parse(args.project, args.cfg_pairs) if project is None: raise Exception("Project parser returned empty project.") except yaml.scanner.ScannerError as e: logger.critical( "Error parsing project:\n%s %s" % (str(e.problem).strip(), str(e.problem_mark).strip()) ) return 1 except Exception as e: if logger.getEffectiveLevel() < 10: raise e logger.critical(e) # Short-circuit for user-caused exceptions if isinstance(e, UserException): return 1 logger.critical( "You should not be seeing this error. Please report this as a bug." ) logger.critical("URL: https://github.com/divvun/kbdgen/issues/") return 1 generator = gen.generators.get(args.target, None) if generator is None: print("Error: '%s' is not a valid target." % args.target, file=sys.stderr) print("Valid targets: %s" % ", ".join(gen.generators.keys()), file=sys.stderr) return 1 x = generator(project, dict(args._get_kwargs())) try: x.generate(x.output_dir) except KbdgenException as e: logger.error(e) PK!zkbdgen/filecache/__init__.pyimport sys import os import hashlib import requests from requests.auth import HTTPBasicAuth import tempfile import shutil from urllib.parse import urlparse from pathlib import Path from ..base import get_logger from .downloader import stream_download logger = get_logger(__file__) if sys.platform.startswith("win"): default_cache_dir = Path(os.getenv("LOCALAPPDATA")) / "kbdgen" / "cache" elif sys.platform.startswith("darwin"): default_cache_dir = Path(os.getenv("HOME")) / "Library" / "Caches" / "kbdgen" else: default_cache_dir = Path(os.getenv("HOME")) / ".cache" / "kbdgen" class FileCache: def __init__(self, cache_dir=default_cache_dir): self.cache_dir = Path(cache_dir) self.ensure_cache_exists() def ensure_cache_exists(self): if not self.cache_dir.exists(): os.makedirs(str(self.cache_dir), exist_ok=True) def is_cached_valid(self, filename: str, sha256sum: str) -> bool: candidate = self.cache_dir / filename if not candidate.exists(): return False if sha256sum is None: return True m = hashlib.sha256() with candidate.open("rb") as f: m.update(f.read()) new_sum = m.hexdigest() logger.debug("SHA256: %s", new_sum) return new_sum == sha256sum def save_directory_tree(self, id: str, basepath: str, tree: str): logger.debug( "Inject directory tree - id: %s, base: %s, tree: %s" % (id, basepath, tree) ) target = self.cache_dir / id / os.path.relpath(tree, basepath) logger.debug("src: %s, dst: %s" % (tree, target)) target.mkdir(parents=True, exist_ok=True) shutil.rmtree(target, ignore_errors=True) shutil.copytree(tree, target) def inject_directory_tree(self, id: str, tree: str, base_target: str) -> bool: logger.debug( "Inject directory tree: id: %s, tree: %s, base_target: %s" % (id, tree, base_target) ) tree_path = os.path.relpath(tree, base_target) src = self.cache_dir / id / tree_path target = Path(base_target) / Path(tree_path) logger.debug("src: %s, target: %s" % (src, target)) # TODO: this does not check if the directory has even a single file in it... if not src.exists(): return False os.makedirs(str(target), exist_ok=True) shutil.rmtree(str(target), ignore_errors=True) logger.debug("Copying '%s' to '%s'" % (src, target)) shutil.copytree(str(src), str(target)) return True def download(self, raw_url: str, sha256sum: str) -> str: url = urlparse(raw_url) filename = Path(url.path).name candidate = str(self.cache_dir / filename) if self.is_cached_valid(filename, sha256sum): return candidate logger.info("Downloading '%s'…" % filename) stream_download(raw_url, filename, candidate) if not self.is_cached_valid(filename, sha256sum): raise Exception("Cached file '%s' has failed integrity checks." % filename) return candidate def download_latest_from_github( self, repo: str, branch: str = "master", username: str = None, password: str = None, ) -> str: url = "https://api.github.com/repos/{repo}/commits/{branch}".format( repo=repo, branch=branch ) if username is not None and password is not None: repo_meta = requests.get(url, auth=HTTPBasicAuth(username, password)).json() else: repo_meta = requests.get(url).json() sha = repo_meta.get("sha", None) if sha is None: raise Exception("No sha found in response: %r" % sha) filename = "%s-%s.tgz" % (repo.replace("/", "-"), sha) candidate = str(self.cache_dir / filename) if self.is_cached_valid(filename, None): return candidate download_url = "https://api.github.com/repos/{repo}/tarball/{branch}".format( repo=repo, branch=branch ) logger.debug("Download URL: %s" % download_url) with tempfile.TemporaryDirectory() as tmpdir: fp = os.path.join(tmpdir, filename) stream_download(download_url, filename, fp) shutil.move(fp, candidate) return candidate PK!4!( ( kbdgen/filecache/downloader.pyimport humanize import requests import sys import shutil clr_line = "%c[2K\r" % 27 blocks = " ▏▎▍▌▋▊▉█" def stream_download(url: str, fn: str, output_file: str): r = requests.get(url, stream=True) with open(output_file, "wb") as f: i = 0 block_size = 1024 content_len = None for block in r.iter_content(block_size): if not block: continue f.write(block) if content_len is None: content_len = r.headers.get("content-length", None) if content_len is not None: max_size_raw = int(content_len) i = min(max_size_raw, i + block_size) write_download_progress(fn, i, max_size_raw) print() def generate_prog_bar(width: int, cur_sz: int, max_sz: int): if width < 1: return "" units = width t = cur_sz / max_sz * units if t == 0: bars = "▏" else: bars = "█" * int(t) et = t - int(t) if et > 0: extra = int(len(blocks) * et) bars += blocks[extra] return ("{:<%d}" % units).format(bars) def truncate_middle(text: str, sz: int) -> str: if len(text) <= sz: return text split = sz // 2 left = split right = split if sz % 2 == 0: left -= 1 return "%s…%s" % (text[:left], text[-right:]) def write_download_progress(fn: str, cur_sz: int, max_sz: int): if cur_sz < 1000: c = "%s B" % cur_sz else: c = humanize.naturalsize(min(cur_sz, max_sz)) m = humanize.naturalsize(max_sz) pc = "%.0f" % min(cur_sz / max_sz * 100.0, 100.0) w = min(shutil.get_terminal_size().columns, 80) max_fn_len = w // 3 fn = truncate_middle(fn, max_fn_len) pc_len = 4 space_punc_len = 7 msg_len = len(fn) + max(len(c) + len(m), 18) + pc_len + space_punc_len prog = generate_prog_bar(w - msg_len, cur_sz, max_sz) msg = "{clr}{fn}: {pc:>3}% {prog}▏ {frac:<18}".format( clr=clr_line, fn=fn, prog=prog, frac="{cur}/{max}".format(cur=c, max=m), pc=pc ) sys.stdout.write(msg) sys.stdout.flush() def test_download_bar(): import time import random fsize = 2717221829 i = 0 while i < fsize: write_download_progress("a-ridiculous-name-that-was-truncated.tgz", i, fsize) time.sleep(random.random() / 500) i += random.randint(0, 250000) i = fsize write_download_progress("a-ridiculous-name-that-was-truncated.tgz", i, fsize) print() if __name__ == "__main__": test_download_bar() PK! r{{kbdgen/gen/__init__.pyfrom collections import OrderedDict from .ios import AppleiOSGenerator from .android import AndroidGenerator from .win import WindowsGenerator from .osx import OSXGenerator from .x11 import XKBGenerator from .svgkbd import SVGGenerator from .json import JSONGenerator from .errormodel import ErrorModelGenerator generators = OrderedDict( ( ("win", WindowsGenerator), ("osx", OSXGenerator), ("x11", XKBGenerator), ("svg", SVGGenerator), ("android", AndroidGenerator), ("ios", AppleiOSGenerator), ("json", JSONGenerator), ("errormodel", ErrorModelGenerator), ) ) PK!R˦uukbdgen/gen/android.pyimport copy import os.path import shutil import sys import glob import subprocess from collections import defaultdict from pathlib import Path from lxml import etree from lxml.etree import Element, SubElement import tarfile import tempfile from .base import Generator, run_process from ..filecache import FileCache from ..base import get_logger from .. import boolmap logger = get_logger(__file__) ANDROID_GLYPHS = {} for api in (21, 23): with open( os.path.join( os.path.dirname(__file__), "bin", "android-glyphs-api%s.bin" % api ), "rb", ) as f: ANDROID_GLYPHS[api] = boolmap.BoolMap(f.read()) class AndroidGenerator(Generator): REPO = "giella-ime" ANDROID_NS = "http://schemas.android.com/apk/res/android" NS = "http://schemas.android.com/apk/res-auto" def _element(self, *args, **kwargs): o = {} for k, v in kwargs.items(): if k in ["keySpec", "additionalMoreKeys", "keyHintLabel"] and v in [ "#", "@", ]: v = "\\" + v o["{%s}%s" % (self.NS, k)] = v return Element(*args, **o) def _android_subelement(self, *args, **kwargs): o = {} for k, v in kwargs.items(): if k == "keySpec" and v in ["#", "@"]: v = "\\" + v o["{%s}%s" % (self.ANDROID_NS, k)] = v return SubElement(*args, **o) def _subelement(self, *args, **kwargs): o = {} for k, v in kwargs.items(): if k == "keySpec" and v in ["#", "@"]: v = "\\" + v o["{%s}%s" % (self.NS, k)] = v return SubElement(*args, **o) def _tostring(self, tree): return etree.tostring( tree, pretty_print=True, xml_declaration=True, encoding="utf-8" ).decode() @property def _version(self): return self._project.target("android").get("version", self._project.version) @property def _build(self): return self._project.target("android").get("build", self._project.build) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cache = FileCache() def generate(self, base="."): if not self.sanity_check(): logger.error("Sanity checks failed; aborting.") return if self.dry_run: logger.info("Dry run completed.") return deps_dir = os.path.join(base, "deps") self.repo_dir = os.path.join(deps_dir, self.REPO) os.makedirs(deps_dir, exist_ok=True) tree_id = self.get_source_tree(base) self.native_locale_workaround(base) dsn = self._project.target("android").get("sentryDsn", None) if dsn is not None: self.add_sentry_dsn(dsn, base) styles = [("phone", "xml"), ("tablet", "xml-sw600dp")] files = [] layouts = defaultdict(list) logger.info("Updating XML strings…") for name, kbd in self.supported_layouts.items(): files += [ ( "app/src/main/res/xml/keyboard_layout_set_%s.xml" % name.lower(), self.kbd_layout_set(kbd), ), ("app/src/main/res/xml/kbd_%s.xml" % name.lower(), self.keyboard(kbd)), ] for style, prefix in styles: self.gen_key_width(kbd, style) files.append( ( "app/src/main/res/%s/rows_%s.xml" % (prefix, name.lower()), self.rows(kbd, style), ) ) for row in self.rowkeys(kbd, style): row = ("app/src/main/res/%s/%s" % (prefix, row[0]), row[1]) files.append(row) layouts[kbd.target("android").get("minimumSdk", None)].append(kbd) self.update_strings_xml(kbd, base) self.update_method_xmls(layouts, base) self.create_gradle_properties(base, self.is_release) self.save_files(files, base) # Add zhfst files if found self.add_zhfst_files(base) # self.update_dict_authority(base) self.update_localisation(base) self.generate_icons(base) self.build(base, tree_id, self.is_release) def native_locale_workaround(self, base): for name, kbd in self.supported_layouts.items(): if len(kbd.locale) <= 2: continue # locale = 'zz_%s' % kbd.locale # kbd.display_names[locale] = kbd.display_names[kbd.locale] # kbd._tree['locale'] = locale self.update_locale_exception(kbd, base) def sanity_check(self): if super().sanity_check() is False: return False sane = True if os.environ.get("JAVA_HOME", None) and not shutil.which("java"): logger.error("`java` not found on path and JAVA_HOME not set.") sane = False if os.environ.get("ANDROID_HOME", None) is None: logger.error("ANDROID_HOME must be provided and point to the Android SDK directory.") sane = False if os.environ.get("NDK_HOME", None) is None: logger.error("NDK_HOME must be provided and point to the Android NDK directory.") sane = False if self.is_release: key_store_path = self.environ_or_target("ANDROID_KEYSTORE", "keyStore") if key_store_path is None: logger.error( "A keystore must be provided with target property `keyStore` " + "or environment variable `ANDROID_KEYSTORE` for release builds." ) sane = False key_alias = self.environ_or_target("ANDROID_KEYALIAS", "keyAlias") if key_alias is None: logger.error( "A key alias must be provided with target property `keyAlias` " + "or environment variable `ANDROID_KEYALIAS` for release builds." ) sane = False store_pw = os.environ.get("STORE_PW", None) key_pw = os.environ.get("KEY_PW", None) if store_pw is None or key_pw is None: logger.error("STORE_PW and KEY_PW must be set for a release build.") sane = False pid = self._project.target("android").get("packageId") if pid is None: sane = False logger.error("No package ID provided for Android target.") for name, kbd in self.supported_layouts.items(): for dn_locale in kbd.display_names: if dn_locale in ["zz", kbd.locale]: continue for mode, rows in kbd.modes.items(): for n, row in enumerate(rows): if len(row) > 12: logger.warning( ( "[%s] row %s has %s keys. It is " + "recommended to have 12 keys or less per " + "row." ) % (name, n + 1, len(row)) ) for api_v in [21, 23]: if not self.detect_unavailable_glyphs(kbd, api_v): sane = False return sane def _update_dict_auth_xml(self, auth, base): path = os.path.join( base, "deps", self.REPO, "app/src/main/res/values/dictionary-pack.xml" ) with open(path) as f: tree = etree.parse(f) nodes = tree.xpath("string[@name='authority']") if len(nodes) == 0: logger.error("No authority string found in XML!") return nodes[0].text = auth with open(path, "w") as f: f.write(self._tostring(tree)) def _update_dict_auth_java(self, auth, base): # ಠ_ಠ target = "com.android.inputmethod.dictionarypack.aosp" # (╯°□°)╯︵ ┻━┻ src_path = ( "app/src/main/java/com/android/inputmethod/" + "dictionarypack/DictionaryPackConstants.java" ) path = os.path.join(base, "deps", self.REPO, src_path) # (┛◉Д◉)┛彡┻━┻ with open(path) as f: o = f.read().replace(target, auth) with open(path, "w") as f: f.write(o) def update_dict_authority(self, base): auth = "%s.dictionarypack" % self._project.target("android")["packageId"] logger.info("Updating dict authority string to '%s'…" % auth) self._update_dict_auth_xml(auth, base) self._update_dict_auth_java(auth, base) def add_zhfst_files(self, build_dir): nm = "app/src/main/assets/dicts" dict_path = os.path.join(build_dir, "deps", self.REPO, nm) if os.path.exists(dict_path): shutil.rmtree(dict_path) os.makedirs(dict_path, exist_ok=True) files = glob.glob(os.path.join(self._project.path, "*.zhfst")) if len(files) == 0: logger.warning("No ZHFST files found.") return path = os.path.join( build_dir, "deps", self.REPO, "app/src/main/res", "xml", "spellchecker.xml" ) with open(path) as f: tree = etree.parse(f) root = tree.getroot() # Empty the file for child in root: root.remove(child) for fn in files: bfn = os.path.basename(fn) logger.info("Adding '%s' to '%s'…" % (bfn, nm)) shutil.copyfile(fn, os.path.join(dict_path, bfn)) lang, _ = os.path.splitext(os.path.basename(fn)) if len(lang) > 2: lang = "zz_%s" % lang.upper() self._android_subelement( root, "subtype", label="@string/subtype_generic", subtypeLocale=lang ) with open(path, "w") as f: f.write(self._tostring(tree)) def _update_locale(self, d, values): fn = os.path.join(d, "strings-appname.xml") node = None if os.path.exists(fn): with open(fn) as f: tree = etree.parse(f) nodes = tree.xpath("string[@name='english_ime_name']") if len(nodes) > 0: node = nodes[0] else: tree = etree.XML("") if node is None: node = SubElement(tree, "string", name="english_ime_name") node.text = values["name"].replace("'", r"\'") with open(fn, "w") as f: f.write(self._tostring(tree)) def update_localisation(self, base): res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/res") logger.info("Updating localisation values…") self._update_locale( os.path.join(res_dir, "values"), self._project.locales["en"] ) for locale, values in self._project.locales.items(): d = os.path.join(res_dir, "values-%s" % locale) if os.path.isdir(d): self._update_locale(d, values) def generate_icons(self, base): icon = self._project.icon("android") if icon is None: logger.warning("no icon supplied!") return res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/res") cmd_tmpl = "convert -resize %dx%d %s %s" for suffix, dimen in ( ("mdpi", 48), ("hdpi", 72), ("xhdpi", 96), ("xxhdpi", 144), ("xxxhdpi", 192), ): mipmap_dir = "drawable-%s" % suffix cmd = cmd_tmpl % ( dimen, dimen, icon, os.path.join(res_dir, mipmap_dir, "ic_launcher_keyboard.png"), ) logger.info("Creating '%s' at size %dx%d" % (mipmap_dir, dimen, dimen)) process = subprocess.Popen( cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out, err = process.communicate() if process.returncode != 0: logger.error(err.decode()) logger.error( "Application ended with error code %s." % process.returncode ) # TODO throw exception instead. sys.exit(process.returncode) def _gradle(self, *args): # HACK: let's be honest it's all hacks with open(os.path.join(self.repo_dir, "local.properties"), "a") as f: f.write("sdk.dir=%s\n" % os.environ["ANDROID_HOME"]) cmd = ["./gradlew"] + list(args) + ["-Dorg.gradle.jvmargs=-Xmx4096M"] return run_process(cmd, cwd=self.repo_dir, show_output=True) == 0 def build(self, base, tree_id, release_mode=True): targets = [ ("armv7-linux-androideabi", "armeabi-v7a"), ("aarch64-linux-android", "arm64-v8a"), ] res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/jniLibs") cwd = os.path.join(self.repo_dir, "..", "hfst-ospell-rs") if not self.cache.inject_directory_tree(tree_id, res_dir, self.repo_dir): logger.info("Building native components…") for (target, jni_name) in targets: logger.info("Building %s architecture…" % target) run_process( [ "cargo", "ndk", "--android-platform", "21", "--target", target, "--", "build", "--release", ], cwd=cwd, show_output=True, ) jni_dir = os.path.join(res_dir, jni_name) Path(jni_dir).mkdir(parents=True, exist_ok=True) shutil.copyfile( os.path.join(cwd, "target", target, "release/libhfstospell.so"), os.path.join(jni_dir, "libhfstospell.so"), ) self.cache.save_directory_tree(tree_id, self.repo_dir, res_dir) else: logger.info("Native components copied from cache.") logger.info("Generating .apk…") if not self._gradle("assembleRelease" if release_mode else "assembleDebug"): return 1 if not release_mode: suffix = "debug" else: suffix = "release" path = os.path.join(base, "deps", self.REPO, "app/build/outputs/apk", suffix) fn = "app-%s.apk" % suffix out_fn = os.path.join( base, "%s-%s_%s.apk" % (self._project.internal_name, self._version, suffix) ) logger.info("Copying '%s' -> '%s'…" % (fn, out_fn)) os.makedirs(base, exist_ok=True) shutil.copy(os.path.join(path, fn), out_fn) def _str_xml(self, val_dir, name, subtype): os.makedirs(val_dir, exist_ok=True) fn = os.path.join(val_dir, "strings.xml") if not os.path.exists(fn): root = etree.XML("") else: with open(fn) as f: root = etree.parse(f).getroot() SubElement(root, "string", name="subtype_%s" % subtype).text = name with open(fn, "w") as f: f.write(self._tostring(root)) def update_locale_exception(self, kbd, base): res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/res") fn = os.path.join(res_dir, "values", "donottranslate.xml") logger.info("Adding '%s' to '%s'…" % (kbd.locale, fn)) with open(fn) as f: tree = etree.parse(f) # Add to exception keys node = tree.xpath("string-array[@name='subtype_locale_exception_keys']")[0] SubElement(node, "item").text = kbd.locale node = tree.xpath( "string-array[@name='subtype_locale_displayed_in_root_locale']" )[0] SubElement(node, "item").text = kbd.locale SubElement( tree.getroot(), "string", name="subtype_in_root_locale_%s" % kbd.locale ).text = kbd.display_names[kbd.locale] with open(fn, "w") as f: f.write(self._tostring(tree.getroot())) def add_sentry_dsn(self, dsn, base): res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/res") fn = os.path.join(res_dir, "values", "donottranslate.xml") logger.info("Adding Sentry DSN to '%s'…" % fn) with open(fn) as f: tree = etree.parse(f) # Add to exception keys node = tree.xpath("string[@name='sentry_dsn']")[0] node.text = dsn with open(fn, "w") as f: f.write(self._tostring(tree.getroot())) def update_strings_xml(self, kbd, base): # TODO sanity check for non-existence directories # TODO run this only once preferably res_dir = os.path.join(base, "deps", self.REPO, "app/src/main/res") for locale, name in kbd.display_names.items(): if len(locale) > 2: continue if locale == "en": val_dir = os.path.join(res_dir, "values") else: val_dir = os.path.join(res_dir, "values-%s" % locale) self._str_xml(val_dir, name, kbd.internal_name.lower()) def gen_method_xml(self, kbds, tree): root = tree.getroot() for kbd in kbds: self._android_subelement( root, "subtype", icon="@drawable/ic_ime_switcher_dark", label="@string/subtype_%s" % kbd.internal_name.lower(), imeSubtypeLocale=kbd.locale, imeSubtypeMode="keyboard", imeSubtypeExtraValue="KeyboardLayoutSet=%s,AsciiCapable,EmojiCapable" % kbd.internal_name.lower(), ) return self._tostring(tree) def update_method_xmls(self, layouts, base): # None because no API version specified (nor needed) base_layouts = layouts[None] del layouts[None] logger.info("Updating method definitions…") path = os.path.join(base, "deps", self.REPO, "app/src/main/res", "%s") fn = os.path.join(path, "method.xml") with open(fn % "xml") as f: tree = etree.parse(f) root = tree.getroot() # Empty the method.xml file for child in root: root.remove(child) with open(fn % "xml", "w") as f: f.write(self.gen_method_xml(base_layouts, tree)) for kl, vl in reversed(sorted(layouts.items())): for kr, vr in layouts.items(): if kl >= kr: continue layouts[kr] = vl + vr for api_ver, kbds in layouts.items(): xmlv = "xml-v%s" % api_ver os.makedirs(path % xmlv, exist_ok=True) with open(fn % xmlv, "w") as f: f.write(self.gen_method_xml(kbds, copy.deepcopy(tree))) def save_files(self, files, base): fn = os.path.join(base, "deps", self.REPO) logger.info("Embedding generated keyboard XML files…") for k, v in files: with open(os.path.join(fn, k), "w") as f: f.write(v) def _unfurl_tarball(self, tarball, target_dir): with tempfile.TemporaryDirectory() as tmpdir: tarfile.open(tarball, "r:gz").extractall(str(tmpdir)) target = [x for x in Path(tmpdir).iterdir() if x.is_dir()][0] os.makedirs(str(target_dir.parent), exist_ok=True) shutil.move(target, target_dir) def get_source_tree(self, base, repo="divvun/giella-ime", branch="master"): """ Downloads the IME source from Github as a tarball, then extracts to deps dir. """ logger.info("Getting source files from %s %s branch…" % (repo, branch)) deps_dir = Path(os.path.join(base, "deps")) shutil.rmtree(str(deps_dir), ignore_errors=True) tarball = self.cache.download_latest_from_github( repo, branch, username=self._args.get("github_username", None), password=self._args.get("github_token", None), ) hfst_ospell_tbl = self.cache.download_latest_from_github( "bbqsrc/hfst-ospell-rs", "master", username=self._args.get("github_username", None), password=self._args.get("github_token", None), ) self._unfurl_tarball(tarball, deps_dir / self.REPO) shutil.rmtree(str(deps_dir / "../hfst-ospell-rs"), ignore_errors=True) self._unfurl_tarball(hfst_ospell_tbl, deps_dir / "hfst-ospell-rs") return hfst_ospell_tbl.split("/")[-1].split(".")[0] def environ_or_target(self, env_key, target_key): return os.environ.get( env_key, self._project.target("android").get(target_key, None) ) def create_gradle_properties(self, base, release_mode=False): key_store_path = self.environ_or_target("ANDROID_KEYSTORE", "keyStore") or "" key_store = self._project.relpath(key_store_path) logger.debug("Key store: %s" % key_store) key_alias = self.environ_or_target("ANDROID_KEYALIAS", "keyAlias") or "" tmpl = """\ ext.app = [ storeFile: "{store_file}", keyAlias: "{key_alias}", storePassword: "{store_pw}", keyPassword: "{key_pw}", packageName: "{pkg_name}", versionCode: {build}, versionName: "{version}", playEmail: "{play_email}", playCredentials: "{play_creds}" ] """ data = tmpl.format( store_file=os.path.abspath(key_store).replace('"', '\\"'), key_alias=key_alias.replace('"', '\\"'), version=self._version, build=self._build, pkg_name=self._project.target("android")["packageId"].replace('"', '\\"'), play_email=os.environ.get("PLAY_STORE_ACCOUNT", "").replace('"', '\\"'), play_creds=os.environ.get("PLAY_STORE_P12", "").replace('"', '\\"'), store_pw=os.environ.get("STORE_PW", "").replace('"', '\\"'), key_pw=os.environ.get("KEY_PW", "").replace('"', '\\"'), ).replace("$", "\\$") fn = os.path.join(base, "deps", self.REPO, "app/local.gradle") with open(fn, "w") as f: f.write(data) def kbd_layout_set(self, kbd): out = Element("KeyboardLayoutSet", nsmap={"latin": self.NS}) kbd_str = "@xml/kbd_%s" % kbd.internal_name.lower() self._subelement( out, "Element", elementName="alphabet", elementKeyboard=kbd_str, enableProximityCharsCorrection="true", ) for name, kbd_str in ( ("alphabetAutomaticShifted", kbd_str), ("alphabetManualShifted", kbd_str), ("alphabetShiftLocked", kbd_str), ("alphabetShiftLockShifted", kbd_str), ("symbols", "@xml/kbd_symbols"), ("symbolsShifted", "@xml/kbd_symbols_shift"), ("phone", "@xml/kbd_phone"), ("phoneSymbols", "@xml/kbd_phone_symbols"), ("number", "@xml/kbd_number"), ): self._subelement(out, "Element", elementName=name, elementKeyboard=kbd_str) return self._tostring(out) def row_has_special_keys(self, kbd, n, style): for key, action in kbd.get_actions(style).items(): if action.row == n: return True return False def rows(self, kbd, style): out = Element("merge", nsmap={"latin": self.NS}) self._subelement(out, "include", keyboardLayout="@xml/key_styles_common") for n, values in enumerate(kbd.modes["mobile-default"]): n += 1 row = self._subelement(out, "Row") include = self._subelement( row, "include", keyboardLayout="@xml/rowkeys_%s%s" % (kbd.internal_name.lower(), n), ) if not self.row_has_special_keys(kbd, n, style): self._attrib(include, keyWidth="%.2f%%p" % (100 / len(values))) else: self._attrib(include, keyWidth="%.2f%%p" % self.key_width) # All the fun buttons! self._subelement(out, "include", keyboardLayout="@xml/row_qwerty4") return self._tostring(out) def gen_key_width(self, kbd, style): m = 0 for row in kbd.modes["mobile-default"]: r = len(row) if r > m: m = r vals = {"phone": 95, "tablet": 90} self.key_width = vals[style] / m def keyboard(self, kbd, **kwargs): out = Element("Keyboard", nsmap={"latin": self.NS}) self._attrib(out, **kwargs) self._subelement( out, "include", keyboardLayout="@xml/rows_%s" % kbd.internal_name.lower() ) return self._tostring(out) def rowkeys(self, kbd, style): # TODO check that lengths of both modes are the same for n in range(1, len(kbd.modes["mobile-default"]) + 1): merge = Element("merge", nsmap={"latin": self.NS}) switch = self._subelement(merge, "switch") case = self._subelement( switch, "case", keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|" + "alphabetShiftLockShifted", ) self.add_rows(kbd, n, kbd.modes["mobile-shift"][n - 1], style, case) default = self._subelement(switch, "default") self.add_rows(kbd, n, kbd.modes["mobile-default"][n - 1], style, default) yield ( "rowkeys_%s%s.xml" % (kbd.internal_name.lower(), n), self._tostring(merge), ) def _attrib(self, node, **kwargs): for k, v in kwargs.items(): node.attrib["{%s}%s" % (self.NS, k)] = v def add_button_type(self, key, action, row, tree, is_start): node = self._element("Key") width = action.width if width == "fill": if is_start: width = "%.2f%%" % ((100 - (self.key_width * len(row))) / 2) else: width = "fillRight" elif width.endswith("%"): width += "p" if key == "backspace": self._attrib(node, keyStyle="deleteKeyStyle") if key == "enter": self._attrib(node, keyStyle="enterKeyStyle") if key == "shift": self._attrib(node, keyStyle="shiftKeyStyle") self._attrib(node, keyWidth=width) tree.append(node) def add_special_buttons(self, kbd, n, style, row, tree, is_start): side = "left" if is_start else "right" for key, action in kbd.get_actions(style).items(): if action.row == n and action.position in [side, "both"]: self.add_button_type(key, action, row, tree, is_start) def add_rows(self, kbd, n, values, style, out): i = 1 show_number_hints = kbd.target("android").get("showNumberHints", True) self.add_special_buttons(kbd, n, style, values, out, True) for key in values: more_keys = kbd.get_longpress(key) node = self._subelement(out, "Key", keySpec=key) # If top row, and between 0 and 9 keys, show numeric hint is_numeric = n == 1 and i > 0 and i <= 10 show_glyph_hint = more_keys is not None if show_glyph_hint: self._attrib( node, keyHintLabel=more_keys[0], moreKeys=",".join(more_keys) ) if is_numeric: # Handle 0 being last on a keyboard case if i == 10: i = 0 self._attrib(node, additionalMoreKeys=str(i)) if show_number_hints: self._attrib(node, keyHintLabel=str(i)) if i > 0: i += 1 self.add_special_buttons(kbd, n, style, values, out, False) def detect_unavailable_glyphs(self, layout, api_ver): if layout.target("android").get("minimumSdk", 0) > api_ver: return True glyphs = ANDROID_GLYPHS.get(api_ver, None) has_error = False if glyphs is None: logger.warning( ( "no glyphs file found for API %s! Can't detect " + "missing characters from Android font!" ) % api_ver ) return for mode_name, vals in layout.modes.items(): for v in vals: for c in v: if len(c) > 1: logger.debug("%s is several glyphs?" % c) continue if glyphs[ord(c)] is False: logger.error( ( "[%s] Key '%s' (codepoint: U+%04X) " "is not supported by API %s! Set minimumSdk " "to suppress this error." ) % (layout.internal_name, c, ord(c), api_ver) ) has_error = True for vals in layout.longpress.values(): for v in vals: for c in v: if glyphs[ord(c)] is False: logger.debug( ( "[%s] Long press key '%s' (codepoint: U+%04X) " + "is not supported by API %s!" ) % (layout.internal_name, c, ord(c), api_ver) ) return not has_error PK!kbdgen/gen/base.pyimport itertools import logging import os import os.path import subprocess import sys from functools import lru_cache from collections import OrderedDict from ..base import ISO_KEYS, KbdgenException logger = logging.getLogger() class MissingApplicationException(KbdgenException): pass class GenerationError(KbdgenException): pass def bind_iso_keys(other): return OrderedDict(((k, v) for k, v in zip(ISO_KEYS, other))) class Generator: def __init__(self, project, args=None): self._project = project self._args = args or {} @property def repo(self): return self._args.get("repo", None) @property def branch(self): return self._args.get("branch", "master") @property def is_release(self): return self._args.get("release", False) @property def dry_run(self): return self._args.get("dry_run", False) @property def output_dir(self): return self._args.get("output", ".") @property @lru_cache(maxsize=1) def supported_layouts(self): t = self._args["target"] o = OrderedDict() for k, v in self._project.layouts.items(): if v.supported_target(t): o[k] = v return o def sanity_check(self) -> bool: if len(self.supported_layouts) == 0: logger.error("This project defines no supported layouts for this target.") return False else: logger.debug("Supported layouts: %s" % ", ".join(self.supported_layouts)) return True class PhysicalGenerator(Generator): def validate_layout(self, layout): # TODO finish cls-based validate_layout mode_keys = set(layout.modes.keys()) deadkey_keys = set(layout.dead_keys.keys()) undefined_modes = deadkey_keys - mode_keys if len(undefined_modes) > 0: raise Exception( "Dead key modes are defined for undefined modes: %r" % (list(undefined_modes),) ) for mode, keys in layout.dead_keys.items(): dead_keys = set(keys) layer_keys = set(layout.modes[mode].values()) matched_keys = dead_keys & layer_keys if matched_keys != dead_keys: raise Exception( "Specified dead keys missing from mode %r: %r" % (mode, list(dead_keys - matched_keys)) ) class TouchGenerator(Generator): def validate_layout(self): pass MSG_LAYOUT_MISSING = "Layout '%s' is missing a required mode: '%s'." def mode_iter(keyboard, key, required=False): mode = keyboard.modes.get(key, None) if mode is None: if required: raise GenerationError(MSG_LAYOUT_MISSING % (keyboard.internal_name, key)) return itertools.repeat(None) return mode.values() def mode_dict(keyboard, key, required=False, space=False): mode = keyboard.modes.get(key, None) if mode is None: if required: raise GenerationError(MSG_LAYOUT_MISSING % (keyboard.internal_name, key)) return OrderedDict(zip(ISO_KEYS, itertools.repeat(None))) if space: sp = keyboard.special.get("space", {}).get(key, " ") mode["A03"] = sp return mode def git_update(dst, branch, clean, cwd=".", logger=print): msg = "Updating repository '%s'…" % dst logger(msg) cmd = """git reset --hard && git fetch --all && git checkout %s && %s git pull && git submodule init && git submodule sync && git submodule update""" % ( branch, "git clean -fdx &&" if clean else "", ) cmd = cmd.replace("\n", " ") cwd = os.path.join(cwd, dst) # TODO error checking process = subprocess.Popen(cmd, cwd=cwd, shell=True) process.wait() def git_clone(src, dst, branch, clean, cwd=".", logger=print): msg = "Cloning repository '%s' to '%s'…" % (src, dst) logger(msg) cmd = ["git", "clone", src, dst] # TODO error checking process = subprocess.Popen(cmd, cwd=cwd) process.wait() # Silence logger for update. git_update(dst, branch, cwd, logger=lambda x: None) def iterable_set(iterable): return {i for i in itertools.chain.from_iterable(iterable)} def filepath(fp, *args): return os.path.join(os.path.dirname(fp), *args) class DictWalker: def on_branch(self, base, branch): return base, branch def on_leaf(self, base, branch, leaf): return base, branch, leaf def __init__(self, dict_): self._dict = dict_ def __iter__(self): def walk(dict_, buf): for k, v in dict_.items(): if isinstance(v, dict): c = yield self.on_branch(tuple(buf), k) if c is False: continue nbuf = buf[:] nbuf.append(k) for vv in walk(v, nbuf): yield vv elif isinstance(v, (int, str)): yield self.on_leaf(tuple(buf), k, v) else: raise TypeError(v) for v in walk(self._dict, []): yield v def __call__(self): # Run iterator to death for _ in self: pass def run_process(cmd, cwd=None, show_output=False, return_process=False, shell=False): try: process = subprocess.Popen( cmd, shell=shell, cwd=str(cwd) if cwd is not None else None, stderr=None if show_output else subprocess.PIPE, stdout=None if show_output else subprocess.PIPE, ) except Exception as e: logger.error("Process failed to launch with the following error message:") logger.error(e) sys.exit(1) if return_process: return process if show_output: process.wait() return process.returncode else: out, err = process.communicate() if process.returncode != 0: x = err.decode() if x.strip() == "": x = out.decode() logger.error(x) logger.error("Application ended with error code %s." % (process.returncode)) sys.exit(process.returncode) return out, err PK!?B>>'kbdgen/gen/bin/android-glyphs-api21.biny===`=`@}?%;_??7pD?0@QM======?x???????0   ?u|xc2? `0@,$0`p!? ~ @&????~~~0/_<?1d{_?{ @?PK!pTU>>'kbdgen/gen/bin/android-glyphs-api23.binOymӇ9^?;9===`=`@}?/_%;_??====== ??????????0` ??????~~~0??_??~1?PK!RCRCkbdgen/gen/bin/keyboard-iso.svg E00 E01 E02 E03 E04 E05 E06 E07 E08 E09 E10 E11 E12 E13 D00 D01 D02 D03 D04 D05 D06 D07 D08 D09 D10 D11 D12 C00 C01 C02 C03 C04 C05 C06 C07 C08 C09 C10 C11 C12 C13 B99 B00 B01 B02 B03 B04 B05 B06 B07 B08 B09 B10 B11 A99 Ctrl A00 A01 A02 Alt A03 A08 AltGr A09 A10 A11 A12 Ctrl PK!Xrrkbdgen/gen/bin/keysym.tsv/*********************************************************** Copyright 1987, 1994, 1998 The Open Group Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of The Open Group shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from The Open Group. Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts All Rights Reserved Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Digital not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ******************************************************************/ space 0020 exclam 0021 quotedbl 0022 numbersign 0023 dollar 0024 percent 0025 ampersand 0026 apostrophe 0027 parenleft 0028 parenright 0029 asterisk 002A plus 002B comma 002C minus 002D period 002E slash 002F 0 0030 1 0031 2 0032 3 0033 4 0034 5 0035 6 0036 7 0037 8 0038 9 0039 colon 003A semicolon 003B less 003C equal 003D greater 003E question 003F at 0040 A 0041 B 0042 C 0043 D 0044 E 0045 F 0046 G 0047 H 0048 I 0049 J 004A K 004B L 004C M 004D N 004E O 004F P 0050 Q 0051 R 0052 S 0053 T 0054 U 0055 V 0056 W 0057 X 0058 Y 0059 Z 005A bracketleft 005B backslash 005C bracketright 005D asciicircum 005E underscore 005F grave 0060 a 0061 b 0062 c 0063 d 0064 e 0065 f 0066 g 0067 h 0068 i 0069 j 006A k 006B l 006C m 006D n 006E o 006F p 0070 q 0071 r 0072 s 0073 t 0074 u 0075 v 0076 w 0077 x 0078 y 0079 z 007A braceleft 007B bar 007C braceright 007D asciitilde 007E nobreakspace 00A0 exclamdown 00A1 cent 00A2 sterling 00A3 currency 00A4 yen 00A5 brokenbar 00A6 section 00A7 diaeresis 00A8 copyright 00A9 ordfeminine 00AA guillemotleft 00AB notsign 00AC hyphen 00AD registered 00AE macron 00AF degree 00B0 plusminus 00B1 twosuperior 00B2 threesuperior 00B3 acute 00B4 mu 00B5 paragraph 00B6 periodcentered 00B7 cedilla 00B8 onesuperior 00B9 masculine 00BA guillemotright 00BB onequarter 00BC onehalf 00BD threequarters 00BE questiondown 00BF Agrave 00C0 Aacute 00C1 Acircumflex 00C2 Atilde 00C3 Adiaeresis 00C4 Aring 00C5 AE 00C6 Ccedilla 00C7 Egrave 00C8 Eacute 00C9 Ecircumflex 00CA Ediaeresis 00CB Igrave 00CC Iacute 00CD Icircumflex 00CE Idiaeresis 00CF ETH 00D0 Ntilde 00D1 Ograve 00D2 Oacute 00D3 Ocircumflex 00D4 Otilde 00D5 Odiaeresis 00D6 multiply 00D7 Oslash 00D8 Ooblique 00D8 Ugrave 00D9 Uacute 00DA Ucircumflex 00DB Udiaeresis 00DC Yacute 00DD THORN 00DE ssharp 00DF agrave 00E0 aacute 00E1 acircumflex 00E2 atilde 00E3 adiaeresis 00E4 aring 00E5 ae 00E6 ccedilla 00E7 egrave 00E8 eacute 00E9 ecircumflex 00EA ediaeresis 00EB igrave 00EC iacute 00ED icircumflex 00EE idiaeresis 00EF eth 00F0 ntilde 00F1 ograve 00F2 oacute 00F3 ocircumflex 00F4 otilde 00F5 odiaeresis 00F6 division 00F7 oslash 00F8 ooblique 00F8 ugrave 00F9 uacute 00FA ucircumflex 00FB udiaeresis 00FC yacute 00FD thorn 00FE ydiaeresis 00FF Aogonek 0104 breve 02D8 Lstroke 0141 Lcaron 013D Sacute 015A Scaron 0160 Scedilla 015E Tcaron 0164 Zacute 0179 Zcaron 017D Zabovedot 017B aogonek 0105 ogonek 02DB lstroke 0142 lcaron 013E sacute 015B caron 02C7 scaron 0161 scedilla 015F tcaron 0165 zacute 017A doubleacute 02DD zcaron 017E zabovedot 017C Racute 0154 Abreve 0102 Lacute 0139 Cacute 0106 Ccaron 010C Eogonek 0118 Ecaron 011A Dcaron 010E Dstroke 0110 Nacute 0143 Ncaron 0147 Odoubleacute 0150 Rcaron 0158 Uring 016E Udoubleacute 0170 Tcedilla 0162 racute 0155 abreve 0103 lacute 013A cacute 0107 ccaron 010D eogonek 0119 ecaron 011B dcaron 010F dstroke 0111 nacute 0144 ncaron 0148 odoubleacute 0151 rcaron 0159 uring 016F udoubleacute 0171 tcedilla 0163 abovedot 02D9 Hstroke 0126 Hcircumflex 0124 Iabovedot 0130 Gbreve 011E Jcircumflex 0134 hstroke 0127 hcircumflex 0125 idotless 0131 gbreve 011F jcircumflex 0135 Cabovedot 010A Ccircumflex 0108 Gabovedot 0120 Gcircumflex 011C Ubreve 016C Scircumflex 015C cabovedot 010B ccircumflex 0109 gabovedot 0121 gcircumflex 011D ubreve 016D scircumflex 015D kra 0138 Rcedilla 0156 Itilde 0128 Lcedilla 013B Emacron 0112 Gcedilla 0122 Tslash 0166 rcedilla 0157 itilde 0129 lcedilla 013C emacron 0113 gcedilla 0123 tslash 0167 ENG 014A eng 014B Amacron 0100 Iogonek 012E Eabovedot 0116 Imacron 012A Ncedilla 0145 Omacron 014C Kcedilla 0136 Uogonek 0172 Utilde 0168 Umacron 016A amacron 0101 iogonek 012F eabovedot 0117 imacron 012B ncedilla 0146 omacron 014D kcedilla 0137 uogonek 0173 utilde 0169 umacron 016B Wcircumflex 0174 wcircumflex 0175 Ycircumflex 0176 ycircumflex 0177 Babovedot 1E02 babovedot 1E03 Dabovedot 1E0A dabovedot 1E0B Fabovedot 1E1E fabovedot 1E1F Mabovedot 1E40 mabovedot 1E41 Pabovedot 1E56 pabovedot 1E57 Sabovedot 1E60 sabovedot 1E61 Tabovedot 1E6A tabovedot 1E6B Wgrave 1E80 wgrave 1E81 Wacute 1E82 wacute 1E83 Wdiaeresis 1E84 wdiaeresis 1E85 Ygrave 1EF2 ygrave 1EF3 OE 0152 oe 0153 Ydiaeresis 0178 overline 203E kana_fullstop 3002 kana_openingbracket 300C kana_closingbracket 300D kana_comma 3001 kana_conjunctive 30FB kana_WO 30F2 kana_a 30A1 kana_i 30A3 kana_u 30A5 kana_e 30A7 kana_o 30A9 kana_ya 30E3 kana_yu 30E5 kana_yo 30E7 kana_tsu 30C3 prolongedsound 30FC kana_A 30A2 kana_I 30A4 kana_U 30A6 kana_E 30A8 kana_O 30AA kana_KA 30AB kana_KI 30AD kana_KU 30AF kana_KE 30B1 kana_KO 30B3 kana_SA 30B5 kana_SHI 30B7 kana_SU 30B9 kana_SE 30BB kana_SO 30BD kana_TA 30BF kana_CHI 30C1 kana_TSU 30C4 kana_TE 30C6 kana_TO 30C8 kana_NA 30CA kana_NI 30CB kana_NU 30CC kana_NE 30CD kana_NO 30CE kana_HA 30CF kana_HI 30D2 kana_FU 30D5 kana_HE 30D8 kana_HO 30DB kana_MA 30DE kana_MI 30DF kana_MU 30E0 kana_ME 30E1 kana_MO 30E2 kana_YA 30E4 kana_YU 30E6 kana_YO 30E8 kana_RA 30E9 kana_RI 30EA kana_RU 30EB kana_RE 30EC kana_RO 30ED kana_WA 30EF kana_N 30F3 voicedsound 309B semivoicedsound 309C Farsi_0 06F0 Farsi_1 06F1 Farsi_2 06F2 Farsi_3 06F3 Farsi_4 06F4 Farsi_5 06F5 Farsi_6 06F6 Farsi_7 06F7 Farsi_8 06F8 Farsi_9 06F9 Arabic_percent 066A Arabic_superscript_alef 0670 Arabic_tteh 0679 Arabic_peh 067E Arabic_tcheh 0686 Arabic_ddal 0688 Arabic_rreh 0691 Arabic_comma 060C Arabic_fullstop 06D4 Arabic_0 0660 Arabic_1 0661 Arabic_2 0662 Arabic_3 0663 Arabic_4 0664 Arabic_5 0665 Arabic_6 0666 Arabic_7 0667 Arabic_8 0668 Arabic_9 0669 Arabic_semicolon 061B Arabic_question_mark 061F Arabic_hamza 0621 Arabic_maddaonalef 0622 Arabic_hamzaonalef 0623 Arabic_hamzaonwaw 0624 Arabic_hamzaunderalef 0625 Arabic_hamzaonyeh 0626 Arabic_alef 0627 Arabic_beh 0628 Arabic_tehmarbuta 0629 Arabic_teh 062A Arabic_theh 062B Arabic_jeem 062C Arabic_hah 062D Arabic_khah 062E Arabic_dal 062F Arabic_thal 0630 Arabic_ra 0631 Arabic_zain 0632 Arabic_seen 0633 Arabic_sheen 0634 Arabic_sad 0635 Arabic_dad 0636 Arabic_tah 0637 Arabic_zah 0638 Arabic_ain 0639 Arabic_ghain 063A Arabic_tatweel 0640 Arabic_feh 0641 Arabic_qaf 0642 Arabic_kaf 0643 Arabic_lam 0644 Arabic_meem 0645 Arabic_noon 0646 Arabic_ha 0647 Arabic_waw 0648 Arabic_alefmaksura 0649 Arabic_yeh 064A Arabic_fathatan 064B Arabic_dammatan 064C Arabic_kasratan 064D Arabic_fatha 064E Arabic_damma 064F Arabic_kasra 0650 Arabic_shadda 0651 Arabic_sukun 0652 Arabic_madda_above 0653 Arabic_hamza_above 0654 Arabic_hamza_below 0655 Arabic_jeh 0698 Arabic_veh 06A4 Arabic_keheh 06A9 Arabic_gaf 06AF Arabic_noon_ghunna 06BA Arabic_heh_doachashmee 06BE Farsi_yeh 06CC Arabic_farsi_yeh 06CC Arabic_yeh_baree 06D2 Arabic_heh_goal 06C1 Cyrillic_GHE_bar 0492 Cyrillic_ghe_bar 0493 Cyrillic_ZHE_descender 0496 Cyrillic_zhe_descender 0497 Cyrillic_KA_descender 049A Cyrillic_ka_descender 049B Cyrillic_KA_vertstroke 049C Cyrillic_ka_vertstroke 049D Cyrillic_EN_descender 04A2 Cyrillic_en_descender 04A3 Cyrillic_U_straight 04AE Cyrillic_u_straight 04AF Cyrillic_U_straight_bar 04B0 Cyrillic_u_straight_bar 04B1 Cyrillic_HA_descender 04B2 Cyrillic_ha_descender 04B3 Cyrillic_CHE_descender 04B6 Cyrillic_che_descender 04B7 Cyrillic_CHE_vertstroke 04B8 Cyrillic_che_vertstroke 04B9 Cyrillic_SHHA 04BA Cyrillic_shha 04BB Cyrillic_SCHWA 04D8 Cyrillic_schwa 04D9 Cyrillic_I_macron 04E2 Cyrillic_i_macron 04E3 Cyrillic_O_bar 04E8 Cyrillic_o_bar 04E9 Cyrillic_U_macron 04EE Cyrillic_u_macron 04EF Serbian_dje 0452 Macedonia_gje 0453 Cyrillic_io 0451 Ukrainian_ie 0454 Macedonia_dse 0455 Ukrainian_i 0456 Ukrainian_yi 0457 Cyrillic_je 0458 Cyrillic_lje 0459 Cyrillic_nje 045A Serbian_tshe 045B Macedonia_kje 045C Ukrainian_ghe_with_upturn 0491 Byelorussian_shortu 045E Cyrillic_dzhe 045F numerosign 2116 Serbian_DJE 0402 Macedonia_GJE 0403 Cyrillic_IO 0401 Ukrainian_IE 0404 Macedonia_DSE 0405 Ukrainian_I 0406 Ukrainian_YI 0407 Cyrillic_JE 0408 Cyrillic_LJE 0409 Cyrillic_NJE 040A Serbian_TSHE 040B Macedonia_KJE 040C Ukrainian_GHE_WITH_UPTURN 0490 Byelorussian_SHORTU 040E Cyrillic_DZHE 040F Cyrillic_yu 044E Cyrillic_a 0430 Cyrillic_be 0431 Cyrillic_tse 0446 Cyrillic_de 0434 Cyrillic_ie 0435 Cyrillic_ef 0444 Cyrillic_ghe 0433 Cyrillic_ha 0445 Cyrillic_i 0438 Cyrillic_shorti 0439 Cyrillic_ka 043A Cyrillic_el 043B Cyrillic_em 043C Cyrillic_en 043D Cyrillic_o 043E Cyrillic_pe 043F Cyrillic_ya 044F Cyrillic_er 0440 Cyrillic_es 0441 Cyrillic_te 0442 Cyrillic_u 0443 Cyrillic_zhe 0436 Cyrillic_ve 0432 Cyrillic_softsign 044C Cyrillic_yeru 044B Cyrillic_ze 0437 Cyrillic_sha 0448 Cyrillic_e 044D Cyrillic_shcha 0449 Cyrillic_che 0447 Cyrillic_hardsign 044A Cyrillic_YU 042E Cyrillic_A 0410 Cyrillic_BE 0411 Cyrillic_TSE 0426 Cyrillic_DE 0414 Cyrillic_IE 0415 Cyrillic_EF 0424 Cyrillic_GHE 0413 Cyrillic_HA 0425 Cyrillic_I 0418 Cyrillic_SHORTI 0419 Cyrillic_KA 041A Cyrillic_EL 041B Cyrillic_EM 041C Cyrillic_EN 041D Cyrillic_O 041E Cyrillic_PE 041F Cyrillic_YA 042F Cyrillic_ER 0420 Cyrillic_ES 0421 Cyrillic_TE 0422 Cyrillic_U 0423 Cyrillic_ZHE 0416 Cyrillic_VE 0412 Cyrillic_SOFTSIGN 042C Cyrillic_YERU 042B Cyrillic_ZE 0417 Cyrillic_SHA 0428 Cyrillic_E 042D Cyrillic_SHCHA 0429 Cyrillic_CHE 0427 Cyrillic_HARDSIGN 042A Greek_ALPHAaccent 0386 Greek_EPSILONaccent 0388 Greek_ETAaccent 0389 Greek_IOTAaccent 038A Greek_IOTAdieresis 03AA Greek_OMICRONaccent 038C Greek_UPSILONaccent 038E Greek_UPSILONdieresis 03AB Greek_OMEGAaccent 038F Greek_accentdieresis 0385 Greek_horizbar 2015 Greek_alphaaccent 03AC Greek_epsilonaccent 03AD Greek_etaaccent 03AE Greek_iotaaccent 03AF Greek_iotadieresis 03CA Greek_iotaaccentdieresis 0390 Greek_omicronaccent 03CC Greek_upsilonaccent 03CD Greek_upsilondieresis 03CB Greek_upsilonaccentdieresis 03B0 Greek_omegaaccent 03CE Greek_ALPHA 0391 Greek_BETA 0392 Greek_GAMMA 0393 Greek_DELTA 0394 Greek_EPSILON 0395 Greek_ZETA 0396 Greek_ETA 0397 Greek_THETA 0398 Greek_IOTA 0399 Greek_KAPPA 039A Greek_LAMDA 039B Greek_LAMBDA 039B Greek_MU 039C Greek_NU 039D Greek_XI 039E Greek_OMICRON 039F Greek_PI 03A0 Greek_RHO 03A1 Greek_SIGMA 03A3 Greek_TAU 03A4 Greek_UPSILON 03A5 Greek_PHI 03A6 Greek_CHI 03A7 Greek_PSI 03A8 Greek_OMEGA 03A9 Greek_alpha 03B1 Greek_beta 03B2 Greek_gamma 03B3 Greek_delta 03B4 Greek_epsilon 03B5 Greek_zeta 03B6 Greek_eta 03B7 Greek_theta 03B8 Greek_iota 03B9 Greek_kappa 03BA Greek_lamda 03BB Greek_lambda 03BB Greek_mu 03BC Greek_nu 03BD Greek_xi 03BE Greek_omicron 03BF Greek_pi 03C0 Greek_rho 03C1 Greek_sigma 03C3 Greek_finalsmallsigma 03C2 Greek_tau 03C4 Greek_upsilon 03C5 Greek_phi 03C6 Greek_chi 03C7 Greek_psi 03C8 Greek_omega 03C9 leftradical 23B7 topleftradical 250C horizconnector 2500 topintegral 2320 botintegral 2321 vertconnector 2502 topleftsqbracket 23A1 botleftsqbracket 23A3 toprightsqbracket 23A4 botrightsqbracket 23A6 topleftparens 239B botleftparens 239D toprightparens 239E botrightparens 23A0 leftmiddlecurlybrace 23A8 rightmiddlecurlybrace 23AC lessthanequal 2264 notequal 2260 greaterthanequal 2265 integral 222B therefore 2234 variation 221D infinity 221E nabla 2207 approximate 223C similarequal 2243 ifonlyif 21D4 implies 21D2 identical 2261 radical 221A includedin 2282 includes 2283 intersection 2229 union 222A logicaland 2227 logicalor 2228 partialderivative 2202 function 0192 leftarrow 2190 uparrow 2191 rightarrow 2192 downarrow 2193 soliddiamond 25C6 checkerboard 2592 ht 2409 ff 240C cr 240D lf 240A nl 2424 vt 240B lowrightcorner 2518 uprightcorner 2510 upleftcorner 250C lowleftcorner 2514 crossinglines 253C horizlinescan1 23BA horizlinescan3 23BB horizlinescan5 2500 horizlinescan7 23BC horizlinescan9 23BD leftt 251C rightt 2524 bott 2534 topt 252C vertbar 2502 emspace 2003 enspace 2002 em3space 2004 em4space 2005 digitspace 2007 punctspace 2008 thinspace 2009 hairspace 200A emdash 2014 endash 2013 signifblank 2423 ellipsis 2026 doubbaselinedot 2025 onethird 2153 twothirds 2154 onefifth 2155 twofifths 2156 threefifths 2157 fourfifths 2158 onesixth 2159 fivesixths 215A careof 2105 figdash 2012 leftanglebracket 27E8 decimalpoint 002E rightanglebracket 27E9 oneeighth 215B threeeighths 215C fiveeighths 215D seveneighths 215E trademark 2122 signaturemark 2613 leftopentriangle 25C1 rightopentriangle 25B7 emopencircle 25CB emopenrectangle 25AF leftsinglequotemark 2018 rightsinglequotemark 2019 leftdoublequotemark 201C rightdoublequotemark 201D prescription 211E permille 2030 minutes 2032 seconds 2033 latincross 271D filledrectbullet 25AC filledlefttribullet 25C0 filledrighttribullet 25B6 emfilledcircle 25CF emfilledrect 25AE enopencircbullet 25E6 enopensquarebullet 25AB openrectbullet 25AD opentribulletup 25B3 opentribulletdown 25BD openstar 2606 enfilledcircbullet 2022 enfilledsqbullet 25AA filledtribulletup 25B2 filledtribulletdown 25BC leftpointer 261C rightpointer 261E club 2663 diamond 2666 heart 2665 maltesecross 2720 dagger 2020 doubledagger 2021 checkmark 2713 ballotcross 2717 musicalsharp 266F musicalflat 266D malesymbol 2642 femalesymbol 2640 telephone 260E telephonerecorder 2315 phonographcopyright 2117 caret 2038 singlelowquotemark 201A doublelowquotemark 201E leftcaret 003C rightcaret 003E downcaret 2228 upcaret 2227 overbar 00AF downtack 22A4 upshoe 2229 downstile 230A underbar 005F jot 2218 quad 2395 uptack 22A5 circle 25CB upstile 2308 downshoe 222A rightshoe 2283 leftshoe 2282 lefttack 22A3 righttack 22A2 hebrew_doublelowline 2017 hebrew_aleph 05D0 hebrew_bet 05D1 hebrew_gimel 05D2 hebrew_dalet 05D3 hebrew_he 05D4 hebrew_waw 05D5 hebrew_zain 05D6 hebrew_chet 05D7 hebrew_tet 05D8 hebrew_yod 05D9 hebrew_finalkaph 05DA hebrew_kaph 05DB hebrew_lamed 05DC hebrew_finalmem 05DD hebrew_mem 05DE hebrew_finalnun 05DF hebrew_nun 05E0 hebrew_samech 05E1 hebrew_ayin 05E2 hebrew_finalpe 05E3 hebrew_pe 05E4 hebrew_finalzade 05E5 hebrew_zade 05E6 hebrew_qoph 05E7 hebrew_resh 05E8 hebrew_shin 05E9 hebrew_taw 05EA Thai_kokai 0E01 Thai_khokhai 0E02 Thai_khokhuat 0E03 Thai_khokhwai 0E04 Thai_khokhon 0E05 Thai_khorakhang 0E06 Thai_ngongu 0E07 Thai_chochan 0E08 Thai_choching 0E09 Thai_chochang 0E0A Thai_soso 0E0B Thai_chochoe 0E0C Thai_yoying 0E0D Thai_dochada 0E0E Thai_topatak 0E0F Thai_thothan 0E10 Thai_thonangmontho 0E11 Thai_thophuthao 0E12 Thai_nonen 0E13 Thai_dodek 0E14 Thai_totao 0E15 Thai_thothung 0E16 Thai_thothahan 0E17 Thai_thothong 0E18 Thai_nonu 0E19 Thai_bobaimai 0E1A Thai_popla 0E1B Thai_phophung 0E1C Thai_fofa 0E1D Thai_phophan 0E1E Thai_fofan 0E1F Thai_phosamphao 0E20 Thai_moma 0E21 Thai_yoyak 0E22 Thai_rorua 0E23 Thai_ru 0E24 Thai_loling 0E25 Thai_lu 0E26 Thai_wowaen 0E27 Thai_sosala 0E28 Thai_sorusi 0E29 Thai_sosua 0E2A Thai_hohip 0E2B Thai_lochula 0E2C Thai_oang 0E2D Thai_honokhuk 0E2E Thai_paiyannoi 0E2F Thai_saraa 0E30 Thai_maihanakat 0E31 Thai_saraaa 0E32 Thai_saraam 0E33 Thai_sarai 0E34 Thai_saraii 0E35 Thai_saraue 0E36 Thai_sarauee 0E37 Thai_sarau 0E38 Thai_sarauu 0E39 Thai_phinthu 0E3A Thai_baht 0E3F Thai_sarae 0E40 Thai_saraae 0E41 Thai_sarao 0E42 Thai_saraaimaimuan 0E43 Thai_saraaimaimalai 0E44 Thai_lakkhangyao 0E45 Thai_maiyamok 0E46 Thai_maitaikhu 0E47 Thai_maiek 0E48 Thai_maitho 0E49 Thai_maitri 0E4A Thai_maichattawa 0E4B Thai_thanthakhat 0E4C Thai_nikhahit 0E4D Thai_leksun 0E50 Thai_leknung 0E51 Thai_leksong 0E52 Thai_leksam 0E53 Thai_leksi 0E54 Thai_lekha 0E55 Thai_lekhok 0E56 Thai_lekchet 0E57 Thai_lekpaet 0E58 Thai_lekkao 0E59 Korean_Won 20A9 Armenian_ligature_ew 0587 Armenian_full_stop 0589 Armenian_verjaket 0589 Armenian_separation_mark 055D Armenian_but 055D Armenian_hyphen 058A Armenian_yentamna 058A Armenian_exclam 055C Armenian_amanak 055C Armenian_accent 055B Armenian_shesht 055B Armenian_question 055E Armenian_paruyk 055E Armenian_AYB 0531 Armenian_ayb 0561 Armenian_BEN 0532 Armenian_ben 0562 Armenian_GIM 0533 Armenian_gim 0563 Armenian_DA 0534 Armenian_da 0564 Armenian_YECH 0535 Armenian_yech 0565 Armenian_ZA 0536 Armenian_za 0566 Armenian_E 0537 Armenian_e 0567 Armenian_AT 0538 Armenian_at 0568 Armenian_TO 0539 Armenian_to 0569 Armenian_ZHE 053A Armenian_zhe 056A Armenian_INI 053B Armenian_ini 056B Armenian_LYUN 053C Armenian_lyun 056C Armenian_KHE 053D Armenian_khe 056D Armenian_TSA 053E Armenian_tsa 056E Armenian_KEN 053F Armenian_ken 056F Armenian_HO 0540 Armenian_ho 0570 Armenian_DZA 0541 Armenian_dza 0571 Armenian_GHAT 0542 Armenian_ghat 0572 Armenian_TCHE 0543 Armenian_tche 0573 Armenian_MEN 0544 Armenian_men 0574 Armenian_HI 0545 Armenian_hi 0575 Armenian_NU 0546 Armenian_nu 0576 Armenian_SHA 0547 Armenian_sha 0577 Armenian_VO 0548 Armenian_vo 0578 Armenian_CHA 0549 Armenian_cha 0579 Armenian_PE 054A Armenian_pe 057A Armenian_JE 054B Armenian_je 057B Armenian_RA 054C Armenian_ra 057C Armenian_SE 054D Armenian_se 057D Armenian_VEV 054E Armenian_vev 057E Armenian_TYUN 054F Armenian_tyun 057F Armenian_RE 0550 Armenian_re 0580 Armenian_TSO 0551 Armenian_tso 0581 Armenian_VYUN 0552 Armenian_vyun 0582 Armenian_PYUR 0553 Armenian_pyur 0583 Armenian_KE 0554 Armenian_ke 0584 Armenian_O 0555 Armenian_o 0585 Armenian_FE 0556 Armenian_fe 0586 Armenian_apostrophe 055A Georgian_an 10D0 Georgian_ban 10D1 Georgian_gan 10D2 Georgian_don 10D3 Georgian_en 10D4 Georgian_vin 10D5 Georgian_zen 10D6 Georgian_tan 10D7 Georgian_in 10D8 Georgian_kan 10D9 Georgian_las 10DA Georgian_man 10DB Georgian_nar 10DC Georgian_on 10DD Georgian_par 10DE Georgian_zhar 10DF Georgian_rae 10E0 Georgian_san 10E1 Georgian_tar 10E2 Georgian_un 10E3 Georgian_phar 10E4 Georgian_khar 10E5 Georgian_ghan 10E6 Georgian_qar 10E7 Georgian_shin 10E8 Georgian_chin 10E9 Georgian_can 10EA Georgian_jil 10EB Georgian_cil 10EC Georgian_char 10ED Georgian_xan 10EE Georgian_jhan 10EF Georgian_hae 10F0 Georgian_he 10F1 Georgian_hie 10F2 Georgian_we 10F3 Georgian_har 10F4 Georgian_hoe 10F5 Georgian_fi 10F6 Xabovedot 1E8A Ibreve 012C Zstroke 01B5 Gcaron 01E6 Ocaron 01D2 Obarred 019F xabovedot 1E8B ibreve 012D zstroke 01B6 gcaron 01E7 ocaron 01D2 obarred 0275 SCHWA 018F schwa 0259 EZH 01B7 ezh 0292 Lbelowdot 1E36 lbelowdot 1E37 Abelowdot 1EA0 abelowdot 1EA1 Ahook 1EA2 ahook 1EA3 Acircumflexacute 1EA4 acircumflexacute 1EA5 Acircumflexgrave 1EA6 acircumflexgrave 1EA7 Acircumflexhook 1EA8 acircumflexhook 1EA9 Acircumflextilde 1EAA acircumflextilde 1EAB Acircumflexbelowdot 1EAC acircumflexbelowdot 1EAD Abreveacute 1EAE abreveacute 1EAF Abrevegrave 1EB0 abrevegrave 1EB1 Abrevehook 1EB2 abrevehook 1EB3 Abrevetilde 1EB4 abrevetilde 1EB5 Abrevebelowdot 1EB6 abrevebelowdot 1EB7 Ebelowdot 1EB8 ebelowdot 1EB9 Ehook 1EBA ehook 1EBB Etilde 1EBC etilde 1EBD Ecircumflexacute 1EBE ecircumflexacute 1EBF Ecircumflexgrave 1EC0 ecircumflexgrave 1EC1 Ecircumflexhook 1EC2 ecircumflexhook 1EC3 Ecircumflextilde 1EC4 ecircumflextilde 1EC5 Ecircumflexbelowdot 1EC6 ecircumflexbelowdot 1EC7 Ihook 1EC8 ihook 1EC9 Ibelowdot 1ECA ibelowdot 1ECB Obelowdot 1ECC obelowdot 1ECD Ohook 1ECE ohook 1ECF Ocircumflexacute 1ED0 ocircumflexacute 1ED1 Ocircumflexgrave 1ED2 ocircumflexgrave 1ED3 Ocircumflexhook 1ED4 ocircumflexhook 1ED5 Ocircumflextilde 1ED6 ocircumflextilde 1ED7 Ocircumflexbelowdot 1ED8 ocircumflexbelowdot 1ED9 Ohornacute 1EDA ohornacute 1EDB Ohorngrave 1EDC ohorngrave 1EDD Ohornhook 1EDE ohornhook 1EDF Ohorntilde 1EE0 ohorntilde 1EE1 Ohornbelowdot 1EE2 ohornbelowdot 1EE3 Ubelowdot 1EE4 ubelowdot 1EE5 Uhook 1EE6 uhook 1EE7 Uhornacute 1EE8 uhornacute 1EE9 Uhorngrave 1EEA uhorngrave 1EEB Uhornhook 1EEC uhornhook 1EED Uhorntilde 1EEE uhorntilde 1EEF Uhornbelowdot 1EF0 uhornbelowdot 1EF1 Ybelowdot 1EF4 ybelowdot 1EF5 Yhook 1EF6 yhook 1EF7 Ytilde 1EF8 ytilde 1EF9 Ohorn 01A0 ohorn 01A1 Uhorn 01AF uhorn 01B0 EcuSign 20A0 ColonSign 20A1 CruzeiroSign 20A2 FFrancSign 20A3 LiraSign 20A4 MillSign 20A5 NairaSign 20A6 PesetaSign 20A7 RupeeSign 20A8 WonSign 20A9 NewSheqelSign 20AA DongSign 20AB EuroSign 20AC zerosuperior 2070 foursuperior 2074 fivesuperior 2075 sixsuperior 2076 sevensuperior 2077 eightsuperior 2078 ninesuperior 2079 zerosubscript 2080 onesubscript 2081 twosubscript 2082 threesubscript 2083 foursubscript 2084 fivesubscript 2085 sixsubscript 2086 sevensubscript 2087 eightsubscript 2088 ninesubscript 2089 partdifferential 2202 emptyset 2205 elementof 2208 notelementof 2209 containsas 220B squareroot 221A cuberoot 221B fourthroot 221C dintegral 222C tintegral 222D because 2235 approxeq 2245 notapproxeq 2247 notidentical 2262 stricteq 2263 braille_blank 2800 braille_dots_1 2801 braille_dots_2 2802 braille_dots_12 2803 braille_dots_3 2804 braille_dots_13 2805 braille_dots_23 2806 braille_dots_123 2807 braille_dots_4 2808 braille_dots_14 2809 braille_dots_24 280a braille_dots_124 280b braille_dots_34 280c braille_dots_134 280d braille_dots_234 280e braille_dots_1234 280f braille_dots_5 2810 braille_dots_15 2811 braille_dots_25 2812 braille_dots_125 2813 braille_dots_35 2814 braille_dots_135 2815 braille_dots_235 2816 braille_dots_1235 2817 braille_dots_45 2818 braille_dots_145 2819 braille_dots_245 281a braille_dots_1245 281b braille_dots_345 281c braille_dots_1345 281d braille_dots_2345 281e braille_dots_12345 281f braille_dots_6 2820 braille_dots_16 2821 braille_dots_26 2822 braille_dots_126 2823 braille_dots_36 2824 braille_dots_136 2825 braille_dots_236 2826 braille_dots_1236 2827 braille_dots_46 2828 braille_dots_146 2829 braille_dots_246 282a braille_dots_1246 282b braille_dots_346 282c braille_dots_1346 282d braille_dots_2346 282e braille_dots_12346 282f braille_dots_56 2830 braille_dots_156 2831 braille_dots_256 2832 braille_dots_1256 2833 braille_dots_356 2834 braille_dots_1356 2835 braille_dots_2356 2836 braille_dots_12356 2837 braille_dots_456 2838 braille_dots_1456 2839 braille_dots_2456 283a braille_dots_12456 283b braille_dots_3456 283c braille_dots_13456 283d braille_dots_23456 283e braille_dots_123456 283f braille_dots_7 2840 braille_dots_17 2841 braille_dots_27 2842 braille_dots_127 2843 braille_dots_37 2844 braille_dots_137 2845 braille_dots_237 2846 braille_dots_1237 2847 braille_dots_47 2848 braille_dots_147 2849 braille_dots_247 284a braille_dots_1247 284b braille_dots_347 284c braille_dots_1347 284d braille_dots_2347 284e braille_dots_12347 284f braille_dots_57 2850 braille_dots_157 2851 braille_dots_257 2852 braille_dots_1257 2853 braille_dots_357 2854 braille_dots_1357 2855 braille_dots_2357 2856 braille_dots_12357 2857 braille_dots_457 2858 braille_dots_1457 2859 braille_dots_2457 285a braille_dots_12457 285b braille_dots_3457 285c braille_dots_13457 285d braille_dots_23457 285e braille_dots_123457 285f braille_dots_67 2860 braille_dots_167 2861 braille_dots_267 2862 braille_dots_1267 2863 braille_dots_367 2864 braille_dots_1367 2865 braille_dots_2367 2866 braille_dots_12367 2867 braille_dots_467 2868 braille_dots_1467 2869 braille_dots_2467 286a braille_dots_12467 286b braille_dots_3467 286c braille_dots_13467 286d braille_dots_23467 286e braille_dots_123467 286f braille_dots_567 2870 braille_dots_1567 2871 braille_dots_2567 2872 braille_dots_12567 2873 braille_dots_3567 2874 braille_dots_13567 2875 braille_dots_23567 2876 braille_dots_123567 2877 braille_dots_4567 2878 braille_dots_14567 2879 braille_dots_24567 287a braille_dots_124567 287b braille_dots_34567 287c braille_dots_134567 287d braille_dots_234567 287e braille_dots_1234567 287f braille_dots_8 2880 braille_dots_18 2881 braille_dots_28 2882 braille_dots_128 2883 braille_dots_38 2884 braille_dots_138 2885 braille_dots_238 2886 braille_dots_1238 2887 braille_dots_48 2888 braille_dots_148 2889 braille_dots_248 288a braille_dots_1248 288b braille_dots_348 288c braille_dots_1348 288d braille_dots_2348 288e braille_dots_12348 288f braille_dots_58 2890 braille_dots_158 2891 braille_dots_258 2892 braille_dots_1258 2893 braille_dots_358 2894 braille_dots_1358 2895 braille_dots_2358 2896 braille_dots_12358 2897 braille_dots_458 2898 braille_dots_1458 2899 braille_dots_2458 289a braille_dots_12458 289b braille_dots_3458 289c braille_dots_13458 289d braille_dots_23458 289e braille_dots_123458 289f braille_dots_68 28a0 braille_dots_168 28a1 braille_dots_268 28a2 braille_dots_1268 28a3 braille_dots_368 28a4 braille_dots_1368 28a5 braille_dots_2368 28a6 braille_dots_12368 28a7 braille_dots_468 28a8 braille_dots_1468 28a9 braille_dots_2468 28aa braille_dots_12468 28ab braille_dots_3468 28ac braille_dots_13468 28ad braille_dots_23468 28ae braille_dots_123468 28af braille_dots_568 28b0 braille_dots_1568 28b1 braille_dots_2568 28b2 braille_dots_12568 28b3 braille_dots_3568 28b4 braille_dots_13568 28b5 braille_dots_23568 28b6 braille_dots_123568 28b7 braille_dots_4568 28b8 braille_dots_14568 28b9 braille_dots_24568 28ba braille_dots_124568 28bb braille_dots_34568 28bc braille_dots_134568 28bd braille_dots_234568 28be braille_dots_1234568 28bf braille_dots_78 28c0 braille_dots_178 28c1 braille_dots_278 28c2 braille_dots_1278 28c3 braille_dots_378 28c4 braille_dots_1378 28c5 braille_dots_2378 28c6 braille_dots_12378 28c7 braille_dots_478 28c8 braille_dots_1478 28c9 braille_dots_2478 28ca braille_dots_12478 28cb braille_dots_3478 28cc braille_dots_13478 28cd braille_dots_23478 28ce braille_dots_123478 28cf braille_dots_578 28d0 braille_dots_1578 28d1 braille_dots_2578 28d2 braille_dots_12578 28d3 braille_dots_3578 28d4 braille_dots_13578 28d5 braille_dots_23578 28d6 braille_dots_123578 28d7 braille_dots_4578 28d8 braille_dots_14578 28d9 braille_dots_24578 28da braille_dots_124578 28db braille_dots_34578 28dc braille_dots_134578 28dd braille_dots_234578 28de braille_dots_1234578 28df braille_dots_678 28e0 braille_dots_1678 28e1 braille_dots_2678 28e2 braille_dots_12678 28e3 braille_dots_3678 28e4 braille_dots_13678 28e5 braille_dots_23678 28e6 braille_dots_123678 28e7 braille_dots_4678 28e8 braille_dots_14678 28e9 braille_dots_24678 28ea braille_dots_124678 28eb braille_dots_34678 28ec braille_dots_134678 28ed braille_dots_234678 28ee braille_dots_1234678 28ef braille_dots_5678 28f0 braille_dots_15678 28f1 braille_dots_25678 28f2 braille_dots_125678 28f3 braille_dots_35678 28f4 braille_dots_135678 28f5 braille_dots_235678 28f6 braille_dots_1235678 28f7 braille_dots_45678 28f8 braille_dots_145678 28f9 braille_dots_245678 28fa braille_dots_1245678 28fb braille_dots_345678 28fc braille_dots_1345678 28fd braille_dots_2345678 28fe braille_dots_12345678 28ff Sinh_ng 0D82 Sinh_h2 0D83 Sinh_a 0D85 Sinh_aa 0D86 Sinh_ae 0D87 Sinh_aee 0D88 Sinh_i 0D89 Sinh_ii 0D8A Sinh_u 0D8B Sinh_uu 0D8C Sinh_ri 0D8D Sinh_rii 0D8E Sinh_lu 0D8F Sinh_luu 0D90 Sinh_e 0D91 Sinh_ee 0D92 Sinh_ai 0D93 Sinh_o 0D94 Sinh_oo 0D95 Sinh_au 0D96 Sinh_ka 0D9A Sinh_kha 0D9B Sinh_ga 0D9C Sinh_gha 0D9D Sinh_ng2 0D9E Sinh_nga 0D9F Sinh_ca 0DA0 Sinh_cha 0DA1 Sinh_ja 0DA2 Sinh_jha 0DA3 Sinh_nya 0DA4 Sinh_jnya 0DA5 Sinh_nja 0DA6 Sinh_tta 0DA7 Sinh_ttha 0DA8 Sinh_dda 0DA9 Sinh_ddha 0DAA Sinh_nna 0DAB Sinh_ndda 0DAC Sinh_tha 0DAD Sinh_thha 0DAE Sinh_dha 0DAF Sinh_dhha 0DB0 Sinh_na 0DB1 Sinh_ndha 0DB3 Sinh_pa 0DB4 Sinh_pha 0DB5 Sinh_ba 0DB6 Sinh_bha 0DB7 Sinh_ma 0DB8 Sinh_mba 0DB9 Sinh_ya 0DBA Sinh_ra 0DBB Sinh_la 0DBD Sinh_va 0DC0 Sinh_sha 0DC1 Sinh_ssha 0DC2 Sinh_sa 0DC3 Sinh_ha 0DC4 Sinh_lla 0DC5 Sinh_fa 0DC6 Sinh_al 0DCA Sinh_aa2 0DCF Sinh_ae2 0DD0 Sinh_aee2 0DD1 Sinh_i2 0DD2 Sinh_ii2 0DD3 Sinh_u2 0DD4 Sinh_uu2 0DD6 Sinh_ru2 0DD8 Sinh_e2 0DD9 Sinh_ee2 0DDA Sinh_ai2 0DDB Sinh_o2 0DDC Sinh_oo2 0DDD Sinh_au2 0DDE Sinh_lu2 0DDF Sinh_ruu2 0DF2 Sinh_luu2 0DF3 Sinh_kunddaliya 0DF4 PK!Gs kbdgen/gen/errormodel.pyimport os.path from io import StringIO from collections import namedtuple from math import sqrt from .base import Generator from ..base import get_logger logger = get_logger(__file__) Key = namedtuple("Key", ["x", "y", "dist"]) def generate_error_map(layout): pass def find_key_distance(a, b): (a_x, a_y) = a (b_x, b_y) = b return sqrt((b_x - a_x) ** 2 + (b_y - a_y) ** 2) def generate_distances(coords): o = {} for (a, a_xy) in coords.items(): dists = {} for (b, b_xy) in coords.items(): dists[b] = find_key_distance(a_xy, b_xy) o[a] = Key(a_xy[0], a_xy[1], dists) return o def calculate_row_offsets(rows): longest = -1 for row in rows: if len(row) > longest: longest = len(row) offsets = [] for row in rows: offsets.append((longest - len(row)) / 2) return offsets def generate_coordinates(rows, offsets): o = {} for (y, row) in enumerate(rows): x_offset = offsets[y] for (x, key) in enumerate(row): o[key] = (x_offset + x, y) return o def convert_phy_mode(mode): if isinstance(mode, list): return mode rows = [] cur = [] logger.warn(mode) p = "E" # TODO: this is naive and must fill gaps for k, v in mode.items(): if not k.startswith(p): p = k[0] rows.append(cur) cur = [] cur.append(v) if len(cur) > 0: rows.append(cur) return rows def generate_xfst_macro(coords): alphas = " | ".join(coords.keys()) regexes = [] for (a, coord) in coords.items(): regexes.append("[%s::0] @> [%s::0]" % (a, a)) for (b, dist) in coord.dist.items(): regexes.append("[%s::0] @> [%s::%s]" % (a, b, dist)) data = """\ set print-weight ON set precision 6 define Alphabet [%% %s] ; regex %s || Alphabet [Alphabet*] __ [Alphabet*] Alphabet ; """ % ( alphas, " ,\n ".join(regexes), ) return data def generate_att(coords): # find end point of all items c = 1 pairs = [] out = StringIO() for (a, coord) in coords.items(): for (b, dist) in coord.dist.items(): pairs.append([a, b, dist]) pairs.sort() for (a, b, dist) in pairs: first = c c += 1 out.write("0\t{0}\t{1}\t{1}\t0.0\n".format(first, a)) out.write("{0}\t@TERMINUS@\t{1}\t{1}\t{2}\n".format(first, b, "%.6f" % dist)) out.write("{0} 0.0\n".format(c)) v = out.getvalue().replace("@TERMINUS@", str(c)) return v class ErrorModelGenerator(Generator): def generate(self, base="."): out_dir = os.path.abspath(base) os.makedirs(out_dir, exist_ok=True) for name, layout in self._project.layouts.items(): logger.info(name) logger.info("---") for (mode_name, mode) in layout.modes.items(): if not mode_name.startswith("mobile"): continue mode = convert_phy_mode(mode) offsets = calculate_row_offsets(mode) coords = generate_coordinates(mode, offsets) map_ = generate_distances(coords) print(generate_att(map_)) return PK!SfVfVkbdgen/gen/ios.pyimport plistlib import sys import re import shutil import glob import multiprocessing import tarfile import tempfile import os import json import subprocess from lxml import etree from collections import OrderedDict from pathlib import Path from ..base import get_logger from ..filecache import FileCache from .base import Generator, run_process from .osxutil import Pbxproj logger = get_logger(__file__) VERSION_RE = re.compile(r"Xcode (\d+)\.(\d+)") class AppleiOSGenerator(Generator): @property def _version(self): return self._project.target("ios").get("version", self._project.version) @property def _build(self): return self._project.target("ios").get("build", self._project.build) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cache = FileCache() def _unfurl_tarball(self, tarball, target_dir): with tempfile.TemporaryDirectory() as tmpdir: tarfile.open(tarball, "r:gz").extractall(str(tmpdir)) target = [x for x in Path(tmpdir).iterdir() if x.is_dir()][0] os.makedirs(str(target_dir.parent), exist_ok=True) shutil.move(target, target_dir) def get_source_tree(self, base, repo="divvun/giellakbd-ios", branch="master"): """ Downloads the IME source from Github as a tarball, then extracts to deps dir. """ logger.info("Getting source files…") deps_dir = Path(os.path.join(base, "ios-build")) shutil.rmtree(str(deps_dir), ignore_errors=True) tarball = self.cache.download_latest_from_github( repo, branch, username=self._args.get("github_username", None), password=self._args.get("github_token", None), ) hfst_ospell_tbl = self.cache.download_latest_from_github( "bbqsrc/hfst-ospell-rs", "master", username=self._args.get("github_username", None), password=self._args.get("github_token", None), ) self._unfurl_tarball(tarball, deps_dir) shutil.rmtree(str(deps_dir / "Dependencies/hfst-ospell-rs"), ignore_errors=True) self._unfurl_tarball(hfst_ospell_tbl, deps_dir / "Dependencies/hfst-ospell-rs") @property def pkg_id(self): return self._project.target("ios")["packageId"].replace("_", "-") def command_ids(self): return ",".join([self.pkg_id] + self.all_bundle_ids()) def process_command(self, command): if command == "ids": print(self.command_ids()) return def generate(self, base="."): command = self._args.get("command", None) if command is not None: self.process_command(command) return if not self.ensure_xcode_version(): return if not self.ensure_cocoapods(): return if not self.sanity_check(): return if self.dry_run: logger.info("Dry run completed.") return self.get_source_tree(base, branch=self.branch) deps_dir = os.path.join(base, "ios-build") path = os.path.join(deps_dir, "GiellaKeyboard.xcodeproj", "project.pbxproj") if not os.path.isfile(path): logger.error("No Xcode project found. Did you use the correct repository?") return pbxproj = Pbxproj(path) layouts = [] for name, layout in self.supported_layouts.items(): layouts.append(self.generate_json_layout(name, layout)) fn = os.path.join(deps_dir, "Keyboard", "KeyboardDefinitions.json") with open(fn, "w") as f: json.dump(layouts, f, indent=2) plist_path = os.path.join(deps_dir, "HostingApp", "Info.plist") # Hosting app plist with open(plist_path, "rb") as f: plist = plistlib.load(f, dict_type=OrderedDict) with open(plist_path, "wb") as f: self.update_plist(plist, f) kbd_plist_path = os.path.join(deps_dir, "Keyboard", "Info.plist") dev_team = self._project.target("ios").get("teamId", None) with open(kbd_plist_path, "rb") as f: kbd_plist = plistlib.load(f, dict_type=OrderedDict) for n, layout in enumerate(self.supported_layouts.values()): name = layout.internal_name os.makedirs(os.path.join(deps_dir, "Keyboard", name), exist_ok=True) plist_gpath = os.path.join("Keyboard", name, "Info.plist") ref = pbxproj.create_plist_file("Info.plist") pbxproj.add_path(["Keyboard", name]) pbxproj.add_ref_to_group(ref, ["Keyboard", name]) new_plist_path = os.path.join(deps_dir, plist_gpath) with open(new_plist_path, "wb") as f: self.update_kbd_plist(kbd_plist, f, layout, n) # pbx_target, appex_ref = pbxproj.duplicate_target("Keyboard", name, plist_gpath) id_ = "%s.%s" % (self.pkg_id, name.replace("_", "-")) pbxproj.set_target_package_id(name, id_) if dev_team is not None: pbxproj.set_target_build_setting(name, "DEVELOPMENT_TEAM", dev_team) pbxproj.add_appex_to_target_embedded_binaries( "%s.appex" % name, "HostingApp" ) pbxproj.remove_target("Keyboard") # Set package ids properly pbxproj.set_target_package_id("HostingApp", self.pkg_id) if dev_team is not None: pbxproj.set_target_build_setting("HostingApp", "DEVELOPMENT_TEAM", dev_team) # Create locale strings self.create_locales(pbxproj, deps_dir) # Update pbxproj with locales with open(path, "w") as f: self.update_pbxproj(pbxproj, f) # Generate icons for hosting app self.gen_hosting_app_icons(deps_dir) # Add correct ids for entitlements self.update_app_group_entitlements(deps_dir) # Install CocoaPods deps self.run_cocoapods(deps_dir) # Add ZHFST files self.add_zhfst_files(deps_dir) if self.is_release: self.build_release(base, deps_dir, path, pbxproj) else: # self.build_debug(base, deps_dir) logger.info("You may now open '%s/GiellaKeyboard.xcworkspace'." % deps_dir) def run_cocoapods(self, deps_dir): logger.info("Updating CocoaPods repository (this may take quite some time)…") run_process(["pod", "repo", "update"]) logger.info("Installing CocoaPods dependencies…") run_process(["pod", "install"], cwd=deps_dir) def _update_app_group_entitlements(self, group_id, subpath, deps_dir): plist_path = os.path.join(deps_dir, subpath) with open(plist_path, "rb") as f: plist = plistlib.load(f, dict_type=OrderedDict) plist["com.apple.security.application-groups"] = [group_id] with open(plist_path, "wb") as f: plistlib.dump(plist, f) def update_app_group_entitlements(self, deps_dir): group_id = "group.%s" % self.pkg_id logger.info("Setting app group to '%s'…" % group_id) self._update_app_group_entitlements( group_id, "HostingApp/HostingApp.entitlements", deps_dir ) self._update_app_group_entitlements( group_id, "Keyboard/Keyboard.entitlements", deps_dir ) def ensure_cocoapods(self): if shutil.which("pod") is None: logger.error( "'pod' could not be found on your PATH. Please " + "ensure CocoaPods is installed (`gem install cocoapods`)." ) return False return True def ensure_xcode_version(self): if shutil.which("xcodebuild") is None: logger.error( "'xcodebuild' could not be found on your PATH. Please " + "ensure Xcode and its associated command line tools are installed." ) return False process = subprocess.Popen( ["xcodebuild", "-version"], stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out, err = process.communicate() if process.returncode != 0: logger.error(err.decode().strip()) logger.error("Application ended with error code %s." % process.returncode) sys.exit(process.returncode) v = VERSION_RE.match(out.decode().split("\n")[0].strip()) major = int(v.groups()[0]) minor = int(v.groups()[1]) logger.debug("Xcode version: %s.%s" % (major, minor)) if major >= 10: return True logger.error("Your version of Xcode is too old. You need 10.0 or later.") return False def embed_provisioning_profiles(self, pbxproj_path, pbxproj, deps_dir): logger.info("Embedding provisioning profiles…") o = {} for item in self.all_bundle_ids() + [self.pkg_id]: name = ( item.split(".")[-1].replace("-", "_") if item != self.pkg_id else "HostingApp" ) profile = self.load_provisioning_profile(item, deps_dir) logger.debug( "Profile: %s %s -> %s" % (profile["UUID"], profile["Name"], name) ) pbxproj.set_target_build_setting( name, "PROVISIONING_PROFILE", profile["UUID"] ) pbxproj.set_target_build_setting( name, "PROVISIONING_PROFILE_SPECIFIER", profile["Name"] ) o[item] = profile["UUID"] with open(pbxproj_path, "w") as f: f.write(str(pbxproj)) return o def load_provisioning_profile(self, item, deps_dir): cmd = "security cms -D -i %s.mobileprovision" % item out, err = run_process(cmd.split(" "), cwd=deps_dir) return plistlib.loads(out) def build_debug(self, base_dir, deps_dir): cpu_count = multiprocessing.cpu_count() cmd = ( "xcodebuild -configuration Debug -scheme HostingApp " + "-allowProvisioningUpdates -jobs %s " % cpu_count + 'CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO' ) process = subprocess.Popen(cmd, cwd=os.path.join(deps_dir), shell=True) process.wait() if process.returncode != 0: logger.error("Application ended with error code %s." % process.returncode) sys.exit(process.returncode) def build_release(self, base_dir, deps_dir, pbxproj_path, pbxproj): build_dir = deps_dir # TODO check signing ID exists in advance (in sanity checks) xcarchive = os.path.abspath( os.path.join(build_dir, "%s.xcarchive" % self._project.internal_name) ) plist = os.path.abspath(os.path.join(build_dir, "opts.plist")) ipa = os.path.abspath(os.path.join(build_dir, "ipa")) if os.path.exists(xcarchive): shutil.rmtree(xcarchive) if os.path.exists(ipa): os.remove(ipa) code_sign_id = self._project.target("ios").get("codeSignId", None) if code_sign_id is None: raise Exception("codeSignId cannot be null") team_id = self._project.target("ios").get("teamId", None) if team_id is None: raise Exception("teamId cannot be null") plist_obj = { "teamID": team_id, "method": "app-store", "provisioningProfiles": {}, } logger.info("Setting up keychain…") returncode = run_process( "fastlane travis", cwd=deps_dir, shell=True, show_output=True ) if returncode != 0: # oN eRrOr GoTo NeXt logger.warn("Application ended with error code %s." % returncode) logger.info("Downloading signing certificates…") cmd = "fastlane match appstore --app_identifier=%s" % self.command_ids() logger.debug(cmd) returncode = run_process(cmd, cwd=deps_dir, shell=True, show_output=True) if returncode != 0: logger.error("Application ended with error code %s." % returncode) sys.exit(returncode) logger.info("Downloading provisioning profiles…") for item in self.all_bundle_ids() + [self.pkg_id]: cmd = "fastlane sigh -a %s -b %s -z -q %s.mobileprovision" % ( item, team_id, item, ) logger.debug(cmd) returncode = run_process(cmd, cwd=deps_dir, shell=True, show_output=True) if returncode != 0: logger.error("Application ended with error code %s." % returncode) sys.exit(returncode) plist_obj["provisioningProfiles"] = self.embed_provisioning_profiles( pbxproj_path, pbxproj, deps_dir ) with open(plist, "wb") as f: plistlib.dump(plist_obj, f) cmd1 = ( 'xcodebuild archive -archivePath "%s" ' % xcarchive + "-workspace GiellaKeyboard.xcworkspace -configuration Release " + "-scheme HostingApp " + "-jobs %s " % multiprocessing.cpu_count() + "-quiet " + 'CODE_SIGN_IDENTITY="%s" ' % code_sign_id + "DEVELOPMENT_TEAM=%s" % team_id ) cmd2 = ( "xcodebuild -exportArchive " + '-archivePath "%s" -exportPath "%s" ' % (xcarchive, ipa) + '-exportOptionsPlist "%s" ' % plist ) for cmd, msg in ( (cmd1, "Building .xcarchive…"), (cmd2, "Building .ipa and signing…"), ): logger.info(msg) logger.debug(cmd) returncode = run_process(cmd, cwd=deps_dir, shell=True, show_output=True) if returncode != 0: logger.error("Application ended with error code %s." % returncode) sys.exit(returncode) # if os.path.exists(xcarchive): # shutil.rmtree(xcarchive) logger.info("Done! -> %s" % ipa) def _tostring(self, tree): return etree.tostring( tree, pretty_print=True, xml_declaration=True, encoding="utf-8" ).decode() def add_zhfst_files(self, build_dir): nm = "dicts.bundle" path = os.path.join(build_dir, nm) if os.path.exists(path): shutil.rmtree(path) os.makedirs(path, exist_ok=True) files = glob.glob(os.path.join(self._project.path, "*.zhfst")) if len(files) == 0: logger.warning("No ZHFST files found.") return for fn in files: bfn = os.path.basename(fn) logger.info("Adding '%s' to '%s'…" % (bfn, nm)) shutil.copyfile(fn, os.path.join(path, bfn)) def gen_hosting_app_icons(self, build_dir): if self._project.icon("ios") is None: logger.warning("no icon supplied!") return path = os.path.join( build_dir, "HostingApp", "Images.xcassets", "AppIcon.appiconset" ) with open(os.path.join(path, "Contents.json")) as f: contents = json.load(f, object_pairs_hook=OrderedDict) cmd_tmpl = "convert -resize {h}x{w} -background white -alpha remove -gravity center -extent {h}x{w} {src} {out}" work_items = [] for obj in contents["images"]: scale = float(obj["scale"][:-1]) h, w = obj["size"].split("x") h = float(h) * scale w = float(w) * scale icon = self._project.icon("ios", w) fn = "%s-%s@%s.png" % (obj["idiom"], obj["size"], obj["scale"]) obj["filename"] = fn cmd = cmd_tmpl.format(h=h, w=w, src=icon, out=os.path.join(path, fn)) msg = "Creating '%s' from '%s'…" % (fn, icon) work_items.append((msg, run_process(cmd.split(" "), return_process=True))) for (msg, process) in work_items: logger.info(msg) process.wait() with open(os.path.join(path, "Contents.json"), "w") as f: json.dump(contents, f) def get_translatables_from_storyboard(self, xml_fn): with open(xml_fn) as f: tree = etree.parse(f) o = [] for key, node, attr_node in [ (n.attrib["value"], n.getparent().getparent(), n) for n in tree.xpath("//*[@keyPath='translate']") ]: if "placeholder" in node.attrib: o.append(("%s.placeholder" % node.attrib["id"], key)) if "text" in node.attrib or node.find("string[@key='text']") is not None: o.append(("%s.text" % node.attrib["id"], key)) state_node = node.find("state") if state_node is not None: o.append( ("%s.%sTitle" % (node.attrib["id"], state_node.attrib["key"]), key) ) attr_node.getparent().remove(attr_node) o.sort() with open(xml_fn, "w") as f: f.write(self._tostring(tree)) return o def write_l10n_str(self, f, key, value): f.write( ('"%s" = %s;\n' % (key, json.dumps(value, ensure_ascii=False))).encode( "utf-8" ) ) def create_locales(self, pbxproj, gen_dir): about_dir = self._project.target("ios").get("aboutDir", None) about_locales = [] # If aboutDir is set, get the supported locales if about_dir is not None: about_dir = self._project.relpath(about_dir) about_locales = [ os.path.splitext(x)[0] for x in os.listdir(about_dir) if x.endswith(".txt") ] for locale, attrs in self._project.locales.items(): lproj_dir = locale if locale != "en" else "Base" lproj = os.path.join(gen_dir, "HostingApp", "%s.lproj" % lproj_dir) os.makedirs(lproj, exist_ok=True) with open(os.path.join(lproj, "InfoPlist.strings"), "ab") as f: self.write_l10n_str(f, "CFBundleName", attrs["name"]) self.write_l10n_str(f, "CFBundleDisplayName", attrs["name"]) # Add About.txt to the lproj if exists if locale in about_locales: about_file = os.path.join(about_dir, "%s.txt" % locale) shutil.copyfile(about_file, os.path.join(lproj, "About.txt")) if lproj_dir != "Base": file_ref = pbxproj.create_text_file(locale, "About.txt") pbxproj.add_file_ref_to_variant_group(file_ref, "About.txt") def get_layout_locales(self, layout): locales = set(layout.display_names.keys()) locales.remove("en") locales.add("Base") locales.add(layout.locale) return locales def get_project_locales(self): locales = set(self._project.locales.keys()) locales.remove("en") locales.add("Base") return locales def get_all_locales(self): o = self.get_project_locales() for layout in self.supported_layouts.values(): o |= self.get_layout_locales(layout) return sorted(list(o)) def update_pbxproj(self, pbxproj, f): pbxproj.root["knownRegions"] = self.get_all_locales() ref = pbxproj.add_plist_strings_to_build_phase( self.get_project_locales(), "HostingApp" ) pbxproj.add_ref_to_group(ref, ["HostingApp", "Supporting Files"]) # for name, layout in self.supported_layouts.items(): # ref = pbxproj.add_plist_strings_to_build_phase( # self.get_layout_locales(layout), name) # pbxproj.add_ref_to_group(ref, ["Generated", name]) f.write(str(pbxproj)) def all_bundle_ids(self): out = [] for n, layout in enumerate(self.supported_layouts.values()): bundle_id = "%s.%s" % (self.pkg_id, layout.internal_name.replace("_", "-")) out.append(bundle_id) return out def update_kbd_plist(self, plist, f, layout, n): pkg_id = self.pkg_id plist["CFBundleName"] = layout.native_display_name plist["CFBundleDisplayName"] = layout.native_display_name plist["CFBundleShortVersionString"] = str(self._version) plist["CFBundleVersion"] = str(self._build) plist["LSApplicationQueriesSchemes"][0] = pkg_id plist["NSExtension"]["NSExtensionAttributes"]["PrimaryLanguage"] = layout.locale plist["DivvunKeyboardIndex"] = n plistlib.dump(plist, f) def update_plist(self, plist, f): pkg_id = self.pkg_id dsn = self._project.target("ios").get("sentryDsn", None) if dsn is not None: plist["SentryDSN"] = dsn plist["CFBundleName"] = self._project.target("ios")["bundleName"] plist["CFBundleDisplayName"] = self._project.target("ios")["bundleName"] plist["CFBundleShortVersionString"] = str(self._version) plist["CFBundleVersion"] = str(self._build) plist["CFBundleURLTypes"][0]["CFBundleURLSchemes"][0] = pkg_id plist["LSApplicationQueriesSchemes"][0] = pkg_id plistlib.dump(plist, f) def generate_json_layout(self, name, layout): local_name = layout.display_names.get(layout.locale, None) if local_name is None: raise Exception( ("Keyboard '%s' requires localisation " + "into its own locale.") % layout.internal_name ) out = OrderedDict() out["name"] = local_name out["internalName"] = name out["return"] = layout.strings.get("return", "return") out["space"] = layout.strings.get("space", "space") out["longPress"] = layout.longpress out["normal"] = layout.modes["mobile-default"] out["shifted"] = layout.modes["mobile-shift"] return out PK!Ώppkbdgen/gen/json.pyimport os.path from collections import OrderedDict from .base import Generator from ..base import get_logger import json logger = get_logger(__file__) class JSONGenerator(Generator): def generate(self, base="."): out_dir = os.path.abspath(base) os.makedirs(out_dir, exist_ok=True) fn = os.path.join(out_dir, "%s.json" % self._project.internal_name) layouts = OrderedDict() for name, layout in self.supported_layouts.items(): layouts[layout.internal_name] = layout._tree with open(fn, "w") as f: json.dump({"layouts": layouts}, f, indent=2) PK!njE%GGkbdgen/gen/osx.pyimport os.path import shutil import itertools import tempfile import sys import re from lxml import etree import binascii from lxml.etree import SubElement from collections import defaultdict, OrderedDict from textwrap import indent, dedent from ..base import get_logger from .base import PhysicalGenerator, run_process, DictWalker from .osxutil import OSXKeyLayout, OSX_HARDCODED, OSX_KEYMAP logger = get_logger(__file__) INVERTED_ID_RE = re.compile(r"[^A-Za-z0-9]") class OSXGenerator(PhysicalGenerator): @property def disable_transforms(self): return "disable-transforms" in self._args["flags"] def sanity_check(self): if super().sanity_check() is False: return False fail = False ids = [] for layout in self.supported_layouts.values(): id_ = self._layout_name(layout) if id_ in ids: logger.error("A duplicate internal name was detected: '%s'." % id_) else: ids.append(id_) if fail: logger.error( "macOS keyboard internal names are converted to only contain " + "A-Z, a-z, and 0-9. Please ensure your internal names are " + "still unique after this process." ) return not fail def generate(self, base="."): if not self.sanity_check(): return self.build_dir = os.path.abspath(base) # Flag used for debugging, not for general use and undocumented if self.disable_transforms: logger.critical( "Dead keys and transforms will not be generated (disable-transforms)" ) o = OrderedDict() for name, layout in self.supported_layouts.items(): try: self.validate_layout(layout) except Exception as e: logger.error( "[%s] Error while validating layout:\n%s" % (layout.internal_name, e) ) continue logger.info("Generating '%s'…" % name) o[name] = self.generate_xml(layout) if self.dry_run: logger.info("Dry run completed.") return logger.info("Creating bundle…") bundle_path = self.create_bundle(self.build_dir) res_path = os.path.join(bundle_path, "Contents", "Resources") translations = defaultdict(dict) for name, data in o.items(): layout = self.supported_layouts[name] fn = self._layout_name(layout) for locale, lname in layout.display_names.items(): translations[locale][fn] = lname logger.debug("%s.keylayout -> bundle" % fn) with open(os.path.join(res_path, "%s.keylayout" % fn), "w") as f: f.write(data) self.write_icon(res_path, layout) self.write_localisations(res_path, translations) logger.info("Creating installer…") pkg_path = self.create_installer(bundle_path) if self.is_release: logger.info("Signing installer…") self.sign_installer(pkg_path) else: logger.info("Installer generated at '%s'." % pkg_path) def generate_iconset(self, icon, output_fn): cmd_tmpl = ( "convert -resize {d}x{d} -background transparent " + "-gravity center -extent {d}x{d}" ) files = ( ("icon_16x16", 16), ("icon_16x16@2x", 32), ("icon_32x32", 32), ("icon_32x32@2x", 64), ) iconset = tempfile.TemporaryDirectory(suffix=".iconset") for name, dimen in files: fn = "%s.png" % name cmd = cmd_tmpl.format(d=dimen).split(" ") + [ icon, os.path.join(iconset.name, fn), ] logger.info("Creating '%s.png' at size %dx%d" % (name, dimen, dimen)) run_process(cmd) cmd = ["iconutil", "--convert", "icns", "--output", output_fn, iconset.name] run_process(cmd) iconset.cleanup() def write_icon(self, res_path, layout): icon = layout.target("osx").get("icon", None) # Get base icon if icon is None: icon = self._project.target("osx").get("icon", None) if icon is None: logger.warning("no icon for layout '%s'." % layout.internal_name) return iconpath = self._project.relpath(icon) fn = os.path.join(res_path, "%s.icns" % self._layout_name(layout)) self.generate_iconset(iconpath, fn) def write_localisations(self, res_path, translations): for locale, o in translations.items(): path = os.path.join(res_path, "%s.lproj" % locale) os.makedirs(path) with open(os.path.join(path, "InfoPlist.strings"), "w") as f: for name, lname in o.items(): f.write('"%s" = "%s";\n' % (name, lname)) def _layout_name(self, layout): return INVERTED_ID_RE.sub("", layout.internal_name) def create_bundle(self, path): # Bundle ID must contain be in format *.keyboardlayout. # Failure to do so and the bundle will not be detected as a keyboard bundle bundle_id = "%s.keyboardlayout.%s" % ( self._project.target("osx")["packageId"], self._project.internal_name, ) bundle_path = os.path.join(path, "%s.bundle" % bundle_id) if os.path.exists(bundle_path): shutil.rmtree(bundle_path) bundle_name = self._project.target("osx").get("bundleName", None) if bundle_name is None: raise Exception( "Target 'osx' has no 'bundleName' property. " "Please fix your project YAML file." ) os.makedirs(os.path.join(bundle_path, "Contents", "Resources"), exist_ok=True) target_tmpl = indent( dedent( """\ KLInfo_%s TISInputSourceID %s.%s TISIntendedLanguage %s """ ), " ", ) targets = [] for name, layout in self.supported_layouts.items(): layout_name = self._layout_name(layout) targets.append( target_tmpl % (layout_name, bundle_id, layout_name, layout.locale) ) with open(os.path.join(bundle_path, "Contents", "Info.plist"), "w") as f: f.write( dedent( """\ CFBundleIdentifier %s CFBundleName %s CFBundleVersion %s CFBundleShortVersionString %s %s """ # noqa: E501 ) % ( bundle_id, bundle_name, self._project.target("osx").get("build", "1"), self._project.target("osx").get("version", "0.0.0"), "\n".join(targets), ) ) return bundle_path def generate_distribution_xml(self, component_fn, working_dir): dist_fn = os.path.join(working_dir.name, "distribution.xml") bundle_name = self._project.target("osx").get("bundleName", None) # Root "bundle id" is used as a unique key only in the pkg xml bundle_id = self._project.target("osx")["packageId"] root = etree.fromstring("""""") SubElement(root, "title").text = bundle_name SubElement(root, "options", customize="never", rootVolumeOnly="true") choices_outline = SubElement(root, "choices-outline") line = SubElement(choices_outline, "line", choice="default") SubElement(line, "line", choice=bundle_id) SubElement(root, "choice", id="default") choice = SubElement(root, "choice", id=bundle_id, visible="false") SubElement(choice, "pkg-ref", id=bundle_id) SubElement( root, "pkg-ref", id=bundle_id, version="0", auth="root", onConclusion="RequireRestart", ).text = os.path.basename(component_fn) target = self._project.target("osx") bg = target.get("background", None) if bg is not None: SubElement(root, "background", file=bg, alignment="bottomleft") for key in ("license", "welcome", "readme", "conclusion"): fn = target.get(key, None) if fn is not None: SubElement(root, key, file=fn) with open(dist_fn, "wb") as f: f.write( etree.tostring( root, xml_declaration=True, encoding="utf-8", pretty_print=True ) ) return dist_fn def create_component_pkg(self, bundle, version, working_dir): pkg_name = "%s.pkg" % self._project.target("osx")["packageId"] pkg_path = os.path.join(working_dir.name, pkg_name) cmd = [ "pkgbuild", "--component", os.path.join(working_dir.name, bundle), "--ownership", "recommended", "--install-location", "/Library/Keyboard Layouts", "--version", version, pkg_path, ] out, err = run_process(cmd, self.build_dir) return pkg_path def create_installer(self, bundle): working_dir = tempfile.TemporaryDirectory() version = self._project.target("osx").get("version", None) if version is None: logger.warn("No version for installer specified; defaulting to '0.0.0'.") version = "0.0.0" component_pkg_path = self.create_component_pkg(bundle, version, working_dir) resources = self._project.target("osx").get("resources", None) if resources is not None: resources = self._project.relpath(resources) dist_xml_path = self.generate_distribution_xml(component_pkg_path, working_dir) bundle_name = self._project.target("osx")["bundleName"].replace(" ", "_") pkg_name = "%s_%s.unsigned.pkg" % (bundle_name, version) cmd = [ "productbuild", "--distribution", dist_xml_path, "--version", version, "--package-path", working_dir.name, ] if resources is not None: cmd += ["--resources", resources] cmd += [pkg_name] run_process(cmd, self.build_dir) working_dir.cleanup() return os.path.join(self.build_dir, pkg_name) def sign_installer(self, pkg_path): version = self._project.target("osx").get("version", None) if version is None: logger.critical( "A version must be defined for a signed package. Add a version " + "property to targets.osx in your project.yaml." ) sys.exit(1) signed_path = "%s %s.pkg" % (self._project.target("osx")["bundleName"], version) sign_id = self._project.target("osx").get("codeSignId", None) if sign_id is None: logger.error("No signing identify found; skipping.") return cmd = ["productsign", "--sign", sign_id, pkg_path, signed_path] run_process(cmd, self.build_dir) cmd = ["pkgutil", "--check-signature", signed_path] out, err = run_process(cmd, self.build_dir) logger.info(out.decode().strip()) logger.info( "Installer generated at '%s'." % os.path.join(self.build_dir, signed_path) ) def _layout_id(self, layout) -> str: return str( -min( max(binascii.crc_hqx(layout.internal_name.encode("utf-8"), 0) // 2, 1), 32768, ) ) def generate_xml(self, layout): name = self._layout_name(layout) out = OSXKeyLayout(name, self._layout_id(layout)) dead_keys = set(itertools.chain.from_iterable(layout.dead_keys.values())) action_keys = set() for x in DictWalker(layout.transforms): for i in x[0] + (x[1],): action_keys.add(str(i)) # Naively add all keys for mode_name in OSXKeyLayout.modes: logger.trace("BEGINNING MODE: %r" % mode_name) mode = layout.modes.get(mode_name, None) if mode is None: msg = "layout '%s' has no mode '%s'" % (layout.internal_name, mode_name) if mode_name.startswith("osx-"): logger.debug(msg) else: logger.warning(msg) continue # All keymaps must include a code 0 out.set_key(mode_name, "", "0") logger.trace( "Dead keys - mode:%r keys:%r" % (mode_name, layout.dead_keys.get(mode_name, [])) ) for iso, key in mode.items(): if key is None: key = "" key_id = OSX_KEYMAP[iso] if self.disable_transforms: out.set_key(mode_name, key, key_id) continue if key in layout.dead_keys.get(mode_name, []): logger.trace("Dead key found - mode:%r key:%r" % (mode_name, key)) if key in layout.transforms: logger.trace( "Set deadkey - mode:%r key:%r id:%r" % (mode_name, key, key_id) ) out.set_deadkey( mode_name, key, key_id, layout.transforms[key].get(" ", key) ) else: logger.warning( "Dead key '%s' not found in mode '%s'; " "build will continue, but please fix." % (key, mode_name) ) out.set_key(mode_name, key, key_id) else: out.set_key(mode_name, key, key_id) # Now cater for transforms too if key in action_keys and key not in dead_keys: logger.trace( "Transform - mode:%r key:%r id:%r" % (mode_name, key, key_id) ) out.set_transform_key(mode_name, key, key_id) # Space bar special case sp = layout.special.get("space", {}).get(mode_name, " ") out.set_key(mode_name, sp, "49") if not self.disable_transforms and len(layout.transforms) > 0: out.set_transform_key(mode_name, sp, "49") # Add hardcoded keyboard bits for key_id, key in OSX_HARDCODED.items(): out.set_key(mode_name, key, key_id) class TransformWalker(DictWalker): def on_branch(self, base, branch): logger.trace("BRANCH: %r" % branch) if branch not in dead_keys or not out.actions.has(branch): logger.error( "Transform %r not supported; is a deadkey missing?" % branch ) return False action_id = out.actions.get(branch) # "Key %s" % branch if base == (): action = out.action_cache.get(action_id, None) if action is not None: if len(action.xpath('when[@state="none"]')) > 0: return when_state = "none" next_state = out.states.get(branch) # "State %s" % branch else: when_state = out.states.get( "".join(base) ) # "State %s" % "".join(base) next_state = "%s%s" % (when_state, branch) logger.trace( "Branch: action:%r when:%r next:%r" % (action_id, when_state, next_state) ) try: out.add_transform(action_id, when_state, next=next_state) except Exception as e: logger.error( "[%s] Error while adding branch transform:\n%s\n%r" % ( layout.internal_name, e, (branch, action_id, when_state, next_state), ) ) def on_leaf(self, base, branch, leaf): if not out.actions.has(branch): logger.debug( "Leaf transform %r not supported. Is a deadkey missing?" % branch ) return action_id = out.actions.get(branch) # "Key %s" % branch when_state = out.states.get("".join(base)) # "State %s" % "".join(base) logger.trace( "Leaf: action:%r when:%r leaf:%r" % (action_id, when_state, leaf) ) try: out.add_transform(action_id, when_state, output=str(leaf)) except Exception as e: logger.error( "[%s] Error while adding leaf transform:\n%s\n%r" % (layout.internal_name, e, (action_id, when_state, leaf)) ) if not self.disable_transforms: TransformWalker(layout.transforms)() return bytes(out).decode("utf-8") PK!ӉDHgHgkbdgen/gen/osxutil.pyimport copy import json import uuid import pathlib import itertools import subprocess import re from collections import OrderedDict from lxml import etree from lxml.etree import Element, SubElement from ..base import parse_layout, get_logger from ..cldr import CP_REGEX logger = get_logger(__file__) OSX_KEYMAP = OrderedDict( ( ("C01", "0"), ("C02", "1"), ("C03", "2"), ("C04", "3"), ("C06", "4"), ("C05", "5"), ("B01", "6"), ("B02", "7"), ("B03", "8"), ("B04", "9"), ("B00", "50"), # E00 flipped! ("B05", "11"), ("D01", "12"), ("D02", "13"), ("D03", "14"), ("D04", "15"), ("D06", "16"), ("D05", "17"), ("E01", "18"), ("E02", "19"), ("E03", "20"), ("E04", "21"), ("E06", "22"), ("E05", "23"), ("E12", "24"), ("E09", "25"), ("E07", "26"), ("E11", "27"), ("E08", "28"), ("E10", "29"), ("D12", "30"), ("D09", "31"), ("D07", "32"), ("D11", "33"), ("D08", "34"), ("D10", "35"), # U WOT 36 - space yeah yeah ("C09", "37"), ("C07", "38"), ("C11", "39"), ("C08", "40"), ("C10", "41"), ("D13", "42"), ("B08", "43"), ("B10", "44"), ("B06", "45"), ("B07", "46"), ("B09", "47"), # U WOT 48 - backspace yeah yeah ("A03", "49"), ("E00", "10"), # B00 flipped! ("E13", "93"), ("B11", "94"), ) ) OSX_HARDCODED = OrderedDict( ( ("36", r"\u{D}"), ("48", r"\u{9}"), ("51", r"\u{8}"), ("53", r"\u{1B}"), ("64", r"\u{10}"), ("66", r"\u{1D}"), ("70", r"\u{1C}"), ("71", r"\u{1B}"), ("72", r"\u{1F}"), ("76", r"\u{3}"), ("77", r"\u{1E}"), ("79", r"\u{10}"), ("80", r"\u{10}"), ("96", r"\u{10}"), ("97", r"\u{10}"), ("98", r"\u{10}"), ("99", r"\u{10}"), ("100", r"\u{10}"), ("101", r"\u{10}"), ("103", r"\u{10}"), ("105", r"\u{10}"), ("106", r"\u{10}"), ("107", r"\u{10}"), ("109", r"\u{10}"), ("111", r"\u{10}"), ("113", r"\u{10}"), ("114", r"\u{5}"), ("115", r"\u{1}"), ("116", r"\u{B}"), ("117", r"\u{7F}"), ("118", r"\u{10}"), ("119", r"\u{4}"), ("120", r"\u{10}"), ("121", r"\u{C}"), ("122", r"\u{10}"), ("123", r"\u{1C}"), ("124", r"\u{1D}"), ("125", r"\u{1F}"), ("126", r"\u{1E}"), ) ) def plutil_get_json(path): cmd = "plutil -convert json -o -".split(" ") cmd.append(path) process = subprocess.Popen(cmd, stdout=subprocess.PIPE) json_str = process.communicate()[0].decode() return json.loads(json_str, object_pairs_hook=OrderedDict) def plutil_to_xml_str(json_obj): cmd = "plutil -convert xml1 -o - -".split(" ") process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) return process.communicate(json.dumps(json_obj).encode())[0].decode() class Pbxproj: @staticmethod def gen_key(): return uuid.uuid4().hex[8:].upper() def __init__(self, path): self._proj = plutil_get_json(path) def __str__(self): return plutil_to_xml_str(self._proj) @property def objects(self): return self._proj["objects"] @property def root(self): return self.objects[self._proj["rootObject"]] @property def main_group(self): return self.objects[self.root["mainGroup"]] def find_ref_for_name(self, name, isa=None): for ref, o in self.objects.items(): if o.get("name", None) == name and ( isa is None or o.get("isa", None) == isa ): return ref return None def find_resource_build_phase(self, target_name): targets = [self.objects[t] for t in self.root["targets"]] target = None for t in targets: if t["name"] == target_name: target = t break if target is None: return None for build_phase in target["buildPhases"]: phase = self.objects[build_phase] if phase["isa"] == "PBXResourcesBuildPhase": return phase return None def create_plist_string_variant(self, variants): o = { "isa": "PBXVariantGroup", "children": variants, "name": "InfoPlist.strings", "sourceTree": "", } return o def add_plist_strings(self, locales): plist_strs = [self.create_plist_string_file(l) for l in locales] variant = self.create_plist_string_variant(plist_strs) var_key = Pbxproj.gen_key() self.objects[var_key] = variant key = Pbxproj.gen_key() self.objects[key] = {"isa": "PBXBuildFile", "fileRef": var_key} return (var_key, key) def add_plist_strings_to_build_phase(self, locales, target_name): phase = self.find_resource_build_phase(target_name) (var_ref, ref) = self.add_plist_strings(locales) phase["files"].append(ref) return var_ref def find_variant_group(self, target): for o in self.objects.values(): if ( o.get("isa", None) == "PBXVariantGroup" and o.get("name", None) == target ): break else: raise Exception("No src found.") return o def set_target_build_setting(self, target, key, value): o = self.find_target(target) build_cfg_list = self.objects[o["buildConfigurationList"]] build_cfgs = [self.objects[x] for x in build_cfg_list["buildConfigurations"]] for cfg in build_cfgs: cfg["buildSettings"][key] = value def set_target_package_id(self, target, new_id): o = self.find_target(target) build_cfg_list = self.objects[o["buildConfigurationList"]] build_cfgs = [self.objects[x] for x in build_cfg_list["buildConfigurations"]] for cfg in build_cfgs: cfg["buildSettings"]["PRODUCT_BUNDLE_IDENTIFIER"] = new_id def add_file_ref_to_variant_group(self, file_ref, variant_name): variant = self.find_variant_group(variant_name) variant["children"].append(file_ref) return variant def add_plist_strings_to_variant_group(self, locales, variant_name, target_name): variant = self.find_variant_group(variant_name) o = [] for locale in locales: ref = self.create_plist_string_file(locale, target_name) variant["children"].append(ref) o.append(ref) return o def add_ref_to_group(self, ref, group_list): o = self.main_group n = False for g in group_list: for c in o["children"]: co = self.objects[c] if n: break if co.get("path", co.get("name", None)) == g: o = co n = True if n: n = False continue else: # Create new group ref = Pbxproj.gen_key() self.objects[ref] = { "isa": "PBXGroup", "children": [], "path": g, "sourceTree": "", } o["children"].append(ref) n = False o = self.objects[ref] continue o["children"].append(ref) return True def create_file_reference(self, file_type, locale, name, **kwargs): o = { "isa": "PBXFileReference", "lastKnownFileType": file_type, "name": locale, "path": "%s.lproj/%s" % (locale, name), "sourceTree": "", } o.update(kwargs) k = Pbxproj.gen_key() self.objects[k] = o return k def create_plist_file(self, plist_path): o = { "isa": "PBXFileReference", "lastKnownFileType": "text.plist.xml", "name": pathlib.Path(plist_path).name, "path": plist_path, "sourceTree": "", } k = Pbxproj.gen_key() self.objects[k] = o return k def create_plist_string_file(self, locale, name="InfoPlist.strings"): return self.create_file_reference("text.plist.strings", locale, name) def create_text_file(self, locale, name): return self.create_file_reference("text", locale, name) def add_path(self, path_list, target=None): if target is None: target = self.main_group for name in path_list: children = [self.objects[r] for r in target["children"]] for c in children: if c.get("path", None) == name: target = c break else: ref = Pbxproj.gen_key() o = { "children": [], "isa": "PBXGroup", "path": name, "sourceTree": "", } self.objects[ref] = o target["children"].append(ref) target = self.objects[ref] def clear_target_dependencies(self, target): for o in self.objects.values(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == target ): break else: raise Exception("No src found.") # HACK: unclear; leaves dangling nodes o["dependencies"] = [] def clear_target_embedded_binaries(self, target): for o in self.objects.values(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == target ): break else: raise Exception("No src found.") target_o = o for o in [self.objects[x] for x in target_o["buildPhases"]]: if ( o.get("isa", None) == "PBXCopyFilesBuildPhase" and o.get("name", None) == "Embed App Extensions" ): break else: raise Exception("No src found.") o["files"] = [] def create_container_item_proxy(self, container_portal, remote_ref, info): ref = Pbxproj.gen_key() self.objects[ref] = { "isa": "PBXContainerItemProxy", "containerPortal": container_portal, "proxyType": "1", "remoteGlobalIDString": remote_ref, "remoteInfo": info, } logger.debug(self.objects[ref]) return ref def create_target_dependency(self, proxy_ref, dep_ref): ref = Pbxproj.gen_key() self.objects[ref] = { "isa": "PBXTargetDependency", "targetProxy": proxy_ref, "target": dep_ref, } logger.debug(self.objects[ref]) return ref def add_dependency_to_target(self, target_ref, dep_ref): target = self.objects[target_ref] if target.get("dependencies", None) is None: target["dependencies"] = [] target["dependencies"].append(dep_ref) logger.debug(target) def add_appex_to_target_dependencies(self, appex, target): logger.debug("%s %s" % (appex, target)) # Find target appex_ref = self.find_ref_for_name(appex, isa="PBXNativeTarget") logger.debug("Appex ref: " + appex_ref) # Create container proxy proxy_ref = self.create_container_item_proxy( self._proj["rootObject"], appex_ref, appex ) logger.debug("Proxy ref: " + proxy_ref) # Create target dependency dep_ref = self.create_target_dependency(proxy_ref, appex_ref) logger.debug("Target dep ref: " + dep_ref) # Add to deps target_ref = self.find_ref_for_name(target, isa="PBXNativeTarget") logger.debug(target_ref) self.add_dependency_to_target(target_ref, dep_ref) def add_appex_to_target_embedded_binaries(self, appex, target): for appex_ref, o in self.objects.items(): if ( o.get("isa", None) == "PBXFileReference" and o.get("path", None) == appex ): break else: raise Exception("No appex src found.") for o in self.objects.values(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == target ): break else: raise Exception("No target src found.") target_o = o for o in [self.objects[x] for x in target_o["buildPhases"]]: if ( o.get("isa", None) == "PBXCopyFilesBuildPhase" and o.get("name", None) == "Embed App Extensions" ): break else: raise Exception("No src found.") ref = Pbxproj.gen_key() appex_o = { "isa": "PBXBuildFile", "fileRef": appex_ref, "settings": {"ATTRIBUTES": ["RemoveHeadersOnCopy"]}, } self.objects[ref] = appex_o o["files"].append(ref) def find_target(self, target): for o in self.objects.values(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == target ): return o else: raise Exception("No src found.") def add_source_ref_to_build_phase(self, ref, target): target_o = self.find_target(target) for o in [self.objects[x] for x in target_o["buildPhases"]]: if o.get("isa", None) == "PBXSourcesBuildPhase": break else: raise Exception("No src found.") nref = Pbxproj.gen_key() self.objects[nref] = {"isa": "PBXBuildFile", "fileRef": ref} o["files"].append(nref) def remove_target(self, target): for ref, o in self.objects.items(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == target ): break else: raise Exception("No src found.") prod_ref = o["productReference"] del self.objects[o["productReference"]] for nref, o in self.objects.items(): if ( o.get("isa", None) == "PBXBuildFile" and o.get("fileRef", None) == prod_ref ): break else: raise Exception("No src found.") for o in self.objects.values(): if o.get("isa", None) == "PBXGroup" and o.get("name", None) == "Products": break else: raise Exception("No src found.") o["children"].remove(prod_ref) self.root["targets"].remove(ref) def duplicate_target(self, src_name, dst_name, plist_path): for o in self.objects.values(): if ( o.get("isa", None) == "PBXNativeTarget" and o.get("name", None) == src_name ): break else: raise Exception("No src found.") base_clone = copy.deepcopy(o) base_ref = Pbxproj.gen_key() self.objects[base_ref] = base_clone new_phases = [] for phase in base_clone["buildPhases"]: ref = Pbxproj.gen_key() new_phases.append(ref) self.objects[ref] = copy.deepcopy(self.objects[phase]) base_clone["buildPhases"] = new_phases base_clone["name"] = dst_name conf_ref = Pbxproj.gen_key() conf_clone = copy.deepcopy(self.objects[base_clone["buildConfigurationList"]]) self.objects[conf_ref] = conf_clone base_clone["buildConfigurationList"] = conf_ref new_confs = [] for conf in conf_clone["buildConfigurations"]: ref = Pbxproj.gen_key() new_confs.append(ref) self.objects[ref] = copy.deepcopy(self.objects[conf]) self.objects[ref]["buildSettings"]["INFOPLIST_FILE"] = plist_path self.objects[ref]["buildSettings"]["PRODUCT_NAME"] = dst_name self.objects[ref]["buildSettings"]["CODE_SIGN_STYLE"] = "Manual" self.objects[ref]["buildSettings"]["ENABLE_BITCODE"] = "NO" conf_clone["buildConfigurations"] = new_confs appex_ref = Pbxproj.gen_key() appex_clone = copy.deepcopy(self.objects[base_clone["productReference"]]) self.objects[appex_ref] = appex_clone appex_clone["path"] = "%s.appex" % dst_name base_clone["productReference"] = appex_ref # PBXContainerItemProxy etc seem unaffected by leaving dependencies in # base_clone['dependencies'] = [] self.add_ref_to_group(appex_ref, ["Products"]) self.root["targets"].append(base_ref) return base_clone, appex_ref def generate_osx_mods(): conv = OrderedDict( ( ("cmd", "command"), ("caps", "caps"), ("alt", "anyOption"), ("shift", "anyShift"), ) ) def gen_conv(tpl): tplo = [] for t, v in conv.items(): if t not in tpl: v += "?" tplo.append(v) return tuple(tplo) m = ("caps", "alt", "shift") mods = (x for i in range(len(m)) for x in itertools.combinations(m, i)) o = OrderedDict() for mod in mods: mod = ("cmd",) + mod o["osx-%s" % "+".join(mod)] = (" ".join(gen_conv(mod)),) return o class OSXKeyLayout: doctype = ( '' ) modes = OrderedDict( ( ("iso-default", ("command?",)), ("iso-shift", ("anyShift caps? command?",)), ("iso-caps", ("caps",)), ("iso-caps+shift", ("caps anyShift",)), ("iso-alt", ("anyOption command?",)), ("iso-alt+shift", ("anyOption anyShift caps? command?",)), ("iso-caps+alt", ("caps anyOption command?",)), ("iso-caps+alt+shift", ("caps anyOption anyShift command?",)), ("iso-ctrl", ("anyShift? caps? anyOption? anyControl",)), ("osx-cmd", ("command",)), ("osx-cmd+shift", ("command anyShift",)), ) ) modes.update(generate_osx_mods()) # TODO unused required = ("iso-default", "iso-shift", "iso-caps") DEFAULT_CMD = parse_layout( r""" § 1 2 3 4 5 6 7 8 9 0 - = q w e r t y u i o p [ ] a s d f g h j k l ; ' \ ` z x c v b n m , . / """ ) DEFAULT_CMD_SHIFT = parse_layout( r""" ± ! @ # $ % ^ & * ( ) _ + Q W E R T Y U I O P { } A S D F G H J K L : " | ~ Z X C V B N M < > ? """ ) def __bytes__(self): """XML almost; still encode the control chars. Death to standards!""" # Convert v = CP_REGEX.sub(lambda x: "&#x%04X;" % int(x.group(1), 16), str(self)) v = re.sub( r"&(quot|amp|apos|lt|gt);", lambda x: { """: """, "&": "&", "'": "'", "<": "<", ">": ">", }[x.group(0)], v, ) return ('\n%s' % v).encode("utf-8") def __str__(self): root = copy.deepcopy(self.elements["root"]) actions = root.xpath("actions")[0] terminators = root.xpath("terminators")[0] if len(actions) == 0: root.remove(actions) if len(terminators) == 0: root.remove(terminators) return etree.tostring( root, encoding="unicode", doctype=self.doctype, pretty_print=True ) def __init__(self, name, id_): modifiers_ref = "modifiers" mapset_ref = "default" self.elements = {} root = Element("keyboard", group="126", id=id_, name=name) self.elements["root"] = root self.elements["layouts"] = SubElement(root, "layouts") SubElement( self.elements["layouts"], "layout", first="0", last="17", mapSet=mapset_ref, modifiers=modifiers_ref, ) self.elements["modifierMap"] = SubElement( root, "modifierMap", id=modifiers_ref, defaultIndex="0" ) self.elements["keyMapSet"] = SubElement(root, "keyMapSet", id=mapset_ref) self.elements["actions"] = SubElement(root, "actions") self.elements["terminators"] = SubElement(root, "terminators") self.key_cache = {} self.kmap_cache = {} self.action_cache = {} class KeyIncrementer: def __init__(self, prefix): self.prefix = prefix self.data = {} self.c = 0 def has(self, key): return key in self.data def get(self, key): if self.data.get(key, None) is None: self.data[key] = self.c self.c += 1 return "%s%03d" % (self.prefix, self.data[key]) self.states = KeyIncrementer("s") self.actions = KeyIncrementer("a") self._n = 0 def _add_modifier_map(self, mode): mm = self.elements["modifierMap"] kms = self.elements["keyMapSet"] node = SubElement(mm, "keyMapSelect", mapIndex=str(self._n)) mods = self.modes.get(mode, None) for mod in mods: SubElement(node, "modifier", keys=mod) self.kmap_cache[mode] = SubElement(kms, "keyMap", index=str(self._n)) self._n += 1 return self.kmap_cache[mode] def _get_kmap(self, mode): kmap = self.kmap_cache.get(mode, None) if kmap is not None: return kmap return self._add_modifier_map(mode) def _set_key(self, mode, key, key_id, action=None, output=None): if action is not None and output is not None: raise Exception("Cannot specify contradictory action and output.") key_key = "%s %s" % (mode, key_id) node = self.key_cache.get(key_key, None) if node is None: kmap_node = self._get_kmap(mode) node = SubElement(kmap_node, "key", code=key_id) self.key_cache[key_key] = node if action is not None: node.attrib["action"] = str(action) if node.attrib.get("output", None) is not None: del node.attrib["output"] elif output is not None: node.attrib["output"] = str(output) if node.attrib.get("action", None) is not None: del node.attrib["action"] def _set_default_action(self, key): action_id = self.actions.get(key) # "Key %s" % key action = self.action_cache.get(action_id, None) if action is None: action = SubElement(self.elements["actions"], "action", id=action_id) self.action_cache[action_id] = action def _set_terminator(self, action_id, output): termin = self.elements["terminators"].xpath( 'when[@state="%s"]' % action_id.replace('"', r""") ) if len(termin) == 0: el = SubElement(self.elements["terminators"], "when") el.set("state", action_id) el.set("output", output) def _set_default_transform(self, action_id, output): action = self.action_cache.get(action_id, None) # TODO create a generic create or get method for actions if action is None: logger.trace( "Create default action - action:%r output:%r" % (action_id, output) ) action = SubElement(self.elements["actions"], "action", id=action_id) self.action_cache[action_id] = action if len(action.xpath('when[@state="none"]')) == 0: logger.trace( "Create 'none' when - action:%r output:%r" % (action_id, output) ) el = SubElement(action, "when") el.set("state", "none") el.set("output", output) def set_key(self, mode, key, key_id): self._set_key(mode, key, key_id, output=key) def set_deadkey(self, mode, key, key_id, output): """output is the output when the deadkey is followed by an invalid""" logger.trace("%r %r %r %r" % (mode, key, key_id, output)) action_id = self.actions.get(key) # "Key %s" % key pressed_id = self.states.get(key) # "State %s" % key self._set_key(mode, key, key_id, action=action_id) # Create default action (set to pressed state) self._set_default_action(key) self._set_terminator(pressed_id, output) def set_transform_key(self, mode, key, key_id): action_id = self.actions.get(key) # "Key %s" % key self._set_key(mode, key, key_id, action=action_id) # Find action, add none state (move the output) self._set_default_transform(action_id, key) def add_transform(self, action_id, state, output=None, next=None): action = self.action_cache.get(action_id, None) if action is None: raise Exception("'%s' was not a found action_id." % action_id) if output is not None and next is not None: raise Exception("Output and next cannot be simultaneously defined.") if output is not None: el = SubElement(action, "when") el.set("state", state) el.set("output", output) elif next is not None: el = SubElement(action, "when") el.set("state", state) el.set("next", next) # logger.trace("%r" % el) PK!}C##kbdgen/gen/svgkbd.pyimport copy import os.path import itertools from lxml import etree from lxml.etree import Element, SubElement from textwrap import dedent from ..base import get_logger from .base import Generator, mode_dict, ISO_KEYS from ..cldr import decode_u logger = get_logger(__file__) class SVGGenerator(Generator): def generate(self, base="."): with open( os.path.join(os.path.dirname(__file__), "bin", "keyboard-iso.svg") ) as f: tree = etree.parse(f) root = tree.getroot() files = [] logger.info( "Several warnings may fire about XML incompatible strings. " + "Incompatible strings are currently just ignored." ) for name, layout in self.supported_layouts.items(): files.append( ( "%s.svg" % name, layout.display_names.get(layout.locale, layout.locale), self.generate_svg(layout, copy.deepcopy(root)), ) ) out_dir = os.path.abspath(base) os.makedirs(out_dir, exist_ok=True) for fn, _, data in files: with open(os.path.join(out_dir, fn), "wb") as f: f.write(data) # Get English name, or fallback to internal name kbd_name = self._project.locales.get("en", {}).get( "name", self._project.internal_name ) with open(os.path.join(out_dir, "layout.html"), "w") as f: f.write( dedent( """\ Layout(s) for the %s

Legend:

Mode Standard Dead
Default black green
AltGr/Option red orange
Caps Lock (Mode Switch) blue pink
Caps Lock + AltGr/Option: purple green
""" ) % kbd_name ) for fn, name, _ in sorted(files): f.write("

%s

\n" % name) f.write('
\n' % fn) # f.write('
\n' % fn) f.write( dedent( """\ """ ) ) def _make_key_group(self, primary, secondary, cls=None): if cls is None: cls = "" trans = { "\u00A0": "NBSP", "\u200B": "ZWSP", "\u200C": "ZWNJ", "\u200D": "ZWJ", "\u2060": "WJ", } primary = trans.get(primary, primary) secondary = trans.get(secondary, secondary) g = Element("g", **{"class": ("key-group " + cls).strip()}) p = SubElement( g, "text", **{"dy": "1em", "y": "32", "x": "32", "class": "key-text-primary"} ) try: p.text = primary except Exception as e: logger.warning("For char 0x%04x: %s" % (ord(primary), e)) s = SubElement( g, "text", **{"dy": "-.4em", "y": "32", "x": "32", "class": "key-text-secondary"} ) try: s.text = secondary except Exception as e: logger.warning("For char 0x%04x: %s" % (ord(secondary), e)) return (g, p, s) def generate_svg(self, layout, root): default = mode_dict(layout, "iso-default", required=True) shift = mode_dict(layout, "iso-shift") caps = mode_dict(layout, "iso-caps") caps_shift = mode_dict(layout, "iso-caps+shift") alts = mode_dict(layout, "iso-alt") alts_shift = mode_dict(layout, "iso-alt+shift") alt_caps = mode_dict(layout, "iso-caps+alt") alt_caps_shift = mode_dict(layout, "iso-caps+alt+shift") for k in itertools.chain(ISO_KEYS, ("A03",)): groups = [] dk = decode_u(default.get(k, "")) or None dk_dead = dk is not None and default[k] in layout.dead_keys.get( "iso-default", {} ) sk = decode_u(shift.get(k, "")) or None sk_dead = sk is not None and shift[k] in layout.dead_keys.get( "iso-shift", {} ) ack = decode_u(alt_caps.get(k, "")) or None ack_dead = ack is not None and alt_caps[k] in layout.dead_keys.get( "iso-caps+alt", {} ) acsk = decode_u(alt_caps_shift.get(k, "")) or None acsk_dead = acsk is not None and alt_caps_shift[k] in layout.dead_keys.get( "iso-caps+alt+shift", {} ) ak = decode_u(alts.get(k, "")) or None ak_dead = ak is not None and alts[k] in layout.dead_keys.get("iso-alt", {}) ask = decode_u(alts_shift.get(k, "")) or None ask_dead = ask is not None and alts_shift[k] in layout.dead_keys.get( "iso-alt+shift", {} ) ck = decode_u(caps.get(k, "")) or None ck_dead = ck is not None and caps[k] in layout.dead_keys.get("iso-caps", {}) csk = decode_u(caps_shift.get(k, "")) or None csk_dead = csk is not None and caps_shift[k] in layout.dead_keys.get( "iso-caps+shift", {} ) g = root.xpath("//*[contains(@class,'%s')]" % k.lower())[0] if True: # has_group1: group1, p1, s1 = self._make_key_group(dk, sk, "key-group-1") if dk_dead: p1.attrib["class"] += " key-dead" if sk_dead: s1.attrib["class"] += " key-dead" g.append(group1) groups.append(group1) if True: # has_group2: group2, p2, s2 = self._make_key_group(ak, ask, "key-group-2") if ak_dead: p2.attrib["class"] += " key-dead" if ask_dead: s2.attrib["class"] += " key-dead" g.append(group2) groups.append(group2) if True: # has_group3: group3, p3, s3 = self._make_key_group(ck, csk, "key-group-3") if ck_dead: p3.attrib["class"] += " key-dead" if csk_dead: s3.attrib["class"] += " key-dead" g.append(group3) groups.append(group3) if True: # has_group4: group4, p4, s4 = self._make_key_group(ack, acsk, "key-group-4") if ack_dead: p4.attrib["class"] += " key-dead" if acsk_dead: s4.attrib["class"] += " key-dead" g.append(group4) groups.append(group4) if len(groups) == 2: groups[0].attrib["transform"] = "translate(-14, 0)" groups[1].attrib["transform"] = "translate(14, 0)" if len(groups) == 3: groups[0].attrib["transform"] = "translate(-20, 0)" groups[2].attrib["transform"] = "translate(20, 0)" if len(groups) == 4: groups[0].attrib["transform"] = "translate(-24, 0)" groups[1].attrib["transform"] = "translate(-8, 0)" groups[2].attrib["transform"] = "translate(8, 0)" groups[3].attrib["transform"] = "translate(24, 0)" return etree.tostring( root, encoding="utf-8", xml_declaration=True, pretty_print=True ) PK!!kbdgen/gen/win.pyimport io import os import os.path import ntpath import lcid as lcidlib import unicodedata import shutil import concurrent.futures import uuid import re import sys import subprocess from collections import OrderedDict from distutils.dir_util import copy_tree from textwrap import dedent from ..base import get_logger from ..filecache import FileCache from .base import Generator, bind_iso_keys, run_process, mode_iter from ..cldr import decode_u logger = get_logger(__file__) KBDGEN_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "divvun.no") is_windows = sys.platform.startswith("win32") or sys.platform.startswith("cygwin") def guid(kbd_id): return uuid.uuid5(KBDGEN_NAMESPACE, kbd_id) # SC 53 is decimal, 39 is space WIN_VK_MAP = bind_iso_keys( ( "OEM_5", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "OEM_PLUS", "OEM_4", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "OEM_6", "OEM_1", "A", "S", "D", "F", "G", "H", "J", "K", "L", "OEM_3", "OEM_7", "OEM_2", "OEM_102", "Z", "X", "C", "V", "B", "N", "M", "OEM_COMMA", "OEM_PERIOD", "OEM_MINUS", ) ) WIN_KEYMAP = bind_iso_keys( ( "29", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "2b", "56", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", ) ) DEFAULT_KEYNAMES = """\ KEYNAME 01 Esc 0e Backspace 0f Tab 1c Enter 1d Ctrl 2a Shift 36 "Right Shift" 37 "Num *" 38 Alt 39 Space 3a "Caps Lock" 3b F1 3c F2 3d F3 3e F4 3f F5 40 F6 41 F7 42 F8 43 F9 44 F10 45 Pause 46 "Scroll Lock" 47 "Num 7" 48 "Num 8" 49 "Num 9" 4a "Num -" 4b "Num 4" 4c "Num 5" 4d "Num 6" 4e "Num +" 4f "Num 1" 50 "Num 2" 51 "Num 3" 52 "Num 0" 53 "Num Del" 54 "Sys Req" 57 F11 58 F12 7c F13 7d F14 7e F15 7f F16 80 F17 81 F18 82 F19 83 F20 84 F21 85 F22 86 F23 87 F24 KEYNAME_EXT 1c "Num Enter" 1d "Right Ctrl" 35 "Num /" 37 "Prnt Scrn" 38 "Right Alt" 45 "Num Lock" 46 Break 47 Home 48 Up 49 "Page Up" 4b Left 4d Right 4f End 50 Down 51 "Page Down" 52 Insert 53 Delete 54 <00> 56 Help 5b "Left Windows" 5c "Right Windows" 5d Application """ def win_filter(*args, force=False): def wf(v): """actual filter function""" if v is None: return "-1" v = str(v) if re.match(r"^\d{4}$", v): return v v = decode_u(v) if v == "\0": return "-1" # check for anything outsize A-Za-z range if not force and re.match("^[A-Za-z]$", v): return v return "%04x" % ord(v) return tuple(wf(i) for i in args) # Grapheme clusters are known as 'ligatures' in Microsoft jargon. # This naming is terrible so we're going to use glyphbomb instead. def win_glyphbomb(v): o = tuple("%04x" % ord(c) for c in decode_u(v)) if len(o) > 4: raise Exception( 'Glyphbombs ("grapheme clusters") cannot be longer than 4 codepoints.' ) return o inno_langs = {"en": "English", "fi": "Finnish", "nb": "Norwegian"} custom_msgs = {"Enable": {"en": "Enable %1", "fi": "Aktivoi %1", "nb": "Aktiver %1"}} class WindowsGenerator(Generator): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cache = FileCache() def get_or_download_kbdi(self): if os.environ.get("KBDI", None) is not None: kbdi = os.environ["KBDI"] logger.info("Using kbdi provided by KBDI environment variable: '%s'" % kbdi) return kbdi kbdi_sha256 = "79c7cc003c0bf66e73c18f3980cf3a0d58966fb974090a94aaf6d9a7cd45aeb4" kbdi_url = "https://github.com/bbqsrc/kbdi/releases/download/v0.4.3/kbdi.exe" return self.cache.download(kbdi_url, kbdi_sha256) def get_or_download_kbdi_legacy(self): if os.environ.get("KBDI_LEGACY", None) is not None: kbdi = os.environ["KBDI_LEGACY"] logger.info( "Using kbdi-legacy provided by KBDI_LEGACY environment variable: '%s'" % kbdi ) return kbdi kbdi_sha256 = "442303f689bb6c4ca668c28193d30b2cf27202265b5bc8adf0952473581337b2" kbdi_url = ( "https://github.com/bbqsrc/kbdi/releases/download/v0.4.3/kbdi-legacy.exe" ) return self.cache.download(kbdi_url, kbdi_sha256) def get_or_download_signcode(self): signcode_sha256 = ( "b347a3bfe9a0370366a24cb4e535c8f7cc113e8903fd2e13ebe09595090d8d54" ) signcode_url = "https://brendan.so/files/signcode.exe" return self.cache.download(signcode_url, signcode_sha256) def generate(self, base="."): outputs = OrderedDict() if not self.sanity_check(): return if self.is_release: try: kbdi = self.get_or_download_kbdi() kbdi_legacy = self.get_or_download_kbdi_legacy() except Exception as e: logger.critical(e) return for layout in self.supported_layouts.values(): outputs[self._klc_get_name(layout, False)] = self.generate_klc(layout) if self.dry_run: logger.info("Dry run completed.") return build_dir = os.path.abspath(base) os.makedirs(build_dir, exist_ok=True) executor = concurrent.futures.ThreadPoolExecutor(max_workers=4) try: futures = [] for name, data in outputs.items(): klc_path = os.path.join(build_dir, "%s.klc" % name) self.write_klc_file(klc_path, data) if self.is_release: futures.append( executor.submit( self.build_dll, name, "i386", klc_path, build_dir ) ) futures.append( executor.submit( self.build_dll, name, "amd64", klc_path, build_dir ) ) futures.append( executor.submit( self.build_dll, name, "wow64", klc_path, build_dir ) ) for future in futures: future.result() finally: executor.shutdown() if self.is_release: self.copy_nlp_files(build_dir) for os_ in [("Windows 7", kbdi_legacy), ("Windows 8/8.1/10", kbdi)]: shutil.copyfile(os_[1], os.path.join(build_dir, "kbdi.exe")) self.generate_inno_script(os_[0], build_dir) self.build_installer(os_[0], build_dir) def copy_nlp_files(self, build_dir): target = self._project.target("win") src_path = target.get("customLocales", None) if src_path is None: return src_path = self._project.relpath(src_path) nlp_path = os.path.join(build_dir, "nlp") copy_tree(src_path, nlp_path) def write_klc_file(self, filepath, data): logger.info("Writing '%s'…" % filepath) with open(filepath, "w", encoding="utf-16-le", newline="\r\n") as f: f.write("\ufeff") f.write(data) def get_inno_setup_dir(self): possibles = [os.environ.get("INNO_PATH", None)] if is_windows: possibles += [ "C:\\Program Files\\Inno Setup 5", "C:\\Program Files (x86)\\Inno Setup 5", ] for p in possibles: if p is None: continue if os.path.isdir(p): return p def get_msklc_dir(self): possibles = [os.environ.get("MSKLC_PATH", None)] if is_windows: possibles += [ "C:\\Program Files\\Microsoft Keyboard Layout Creator 1.4", "C:\\Program Files (x86)\\Microsoft Keyboard Layout Creator 1.4", ] for p in possibles: if p is None: continue if os.path.isdir(p): return p def get_mono_dir(self): possibles = [os.environ.get("MONO_PATH", None)] if is_windows: possibles += ["C:\\Program Files\\Mono", "C:\\Program Files (x86)\\Mono"] for p in possibles: if p is None: continue if os.path.isdir(p): return p def sanity_check(self): if super().sanity_check() is False: return False pfx = self._project.target("win").get("codeSignPfx", None) codesign_pw = os.environ.get("CODESIGN_PW", None) if pfx is not None and codesign_pw is None: logger.error( "Environment variable CODESIGN_PW must be set for a release build." ) return False elif pfx is None: logger.warn("No code signing PFX was provided; setup will not be signed.") if self._project.organisation == "": logger.warn("Property 'organisation' is undefined for this project.") if self._project.copyright == "": logger.warn("Property 'copyright' is undefined for this project.") if self._project.target("win").get("version", None) is None: logger.error( "Property 'targets.win.version' must be defined in the " + "project for this target." ) return False guid = self._project.target("win").get("uuid", None) if guid is None: logger.error( "Property 'targets.win.uuid' must be defined in the project " + "for this target." ) return False try: uuid.UUID(guid) except Exception: logger.error("Property 'targets.win.uuid' is not a valid UUID.") return False for layout in self.supported_layouts.values(): lcid = lcidlib.get(layout.locale) if lcid is None and layout.target("win").get("locale", None) is None: logger.error( dedent( """\ Layout '%s' specifies a locale not recognised by Windows. To solve this issue, insert the below into the relevant layout file with the ISO 639-3 code plus the written script of the language in BCP 47 format: targets: win: locale: xyz-Latn """ # noqa: E501 ) % layout.internal_name ) return False if lcid is None and layout.target("win").get("languageName", None) is None: logger.error( dedent( """\ Layout '%s' requires the display name for the language to be supplied. targets: win: languageName: Pig Latin """ ) ) fail = False ids = [] for layout in self.supported_layouts.values(): id_ = self._klc_get_name(layout) if id_ in ids: fail = True msg = ( "Duplicate id found for '%s': '%s'; " + "set targets.win.id to override." ) logger.error(msg, layout.internal_name, id_) else: ids.append(id_) if fail: return False if not self.is_release: return True if not is_windows: # Check for wine if not shutil.which("wine"): logger.error("`wine` must exist on your PATH to build keyboard DLLs.") return False # Check wine version out, err = subprocess.Popen( ["wine", "--version"], stdout=subprocess.PIPE ).communicate() v_chunks = [int(x) for x in out.decode().split("-").pop().split(".")] if v_chunks[0] < 2 or (v_chunks[0] == 2 and v_chunks[1] < 10): logger.warn( "Builds are not known to succeed with Wine versions less than " + "2.10; here be dragons." ) # Check for INNO_PATH if self.get_inno_setup_dir() is None: logger.error( "Inno Setup 5 must be installed or INNO_PATH environment variable must " + "point to the Inno Setup 5 directory." ) return False # Check for MSKLC_PATH if self.get_msklc_dir() is None: logger.error( "Microsoft Keyboard Layout Creator 1.4 must be installed or MSKLC_PATH " + "environment variable must point to the MSKLC directory." ) return False return True def _wine_path(self, thing): if is_windows: return ntpath.abspath(thing) else: return "Z:%s" % ntpath.abspath(thing) def _wine_cmd(self, *args): if is_windows: return args else: return ["wine"] + list(args) @property def _kbdutool(self): if is_windows: return "%s\\bin\\i386\\kbdutool.exe" % self.get_msklc_dir() else: return "%s/bin/i386/kbdutool.exe" % self.get_msklc_dir() def build_dll(self, name, arch, klc_path, build_dir): # x86, x64, wow64 flags = {"i386": "-x", "amd64": "-m", "wow64": "-o"} flag = flags[arch] out_path = os.path.join(build_dir, arch) os.makedirs(out_path, exist_ok=True) logger.info("Building '%s' for %s…" % (name, arch)) cmd = self._wine_cmd( self._kbdutool, "-n", flag, "-u", self._wine_path(klc_path) ) run_process(cmd, cwd=out_path) pfx = self._project.target("win").get("codeSignPfx", None) if pfx is None: logger.warn( "'%s' for %s was not code signed due to no codeSignPfx property." % (name, arch) ) return logger.info("Signing '%s' for %s…" % (name, arch)) pfx_path = self._wine_path(self._project.relpath(pfx)) logger.debug("PFX path: %s", pfx_path) cmd = self._wine_cmd( self._wine_path(self.get_or_download_signcode()), "-a", "sha1", "-t", "http://timestamp.verisign.com/scripts/timstamp.dll", "-pkcs12", pfx_path, "-$", "commercial", self._wine_path(os.path.join(out_path, "%s.dll" % name)), ) run_process(cmd, cwd=out_path) def _generate_inno_languages(self): out = [] target = self._project.target("win") license_format = target.get("licenseFormat", "txt") app_license_path = target.get("licensePath", None) license_locales = [] if app_license_path is not None: app_license_path = self._project.relpath(app_license_path) license_locales = [ os.path.splitext(x)[0] for x in os.listdir(app_license_path) if x.endswith(".%s" % license_format) ] en_license = self._wine_path( os.path.join(app_license_path, "en.%s" % license_format) ) readme_format = target.get("readmeFormat", "txt") app_readme_path = target.get("readmePath", None) readme_locales = [] if app_readme_path is not None: app_readme_path = self._project.relpath(app_readme_path) readme_locales = [ os.path.splitext(x)[0] for x in os.listdir(app_readme_path) if x.endswith(".%s" % readme_format) ] en_readme = self._wine_path( os.path.join(app_readme_path, "en.%s" % readme_format) ) for locale, attrs in self._project.locales.items(): if locale not in inno_langs: logger.info("'%s' not supported by setup script; skipping." % locale) continue buf = io.StringIO() if locale == "en": buf.write('Name: "en"; MessagesFile: "compiler:Default.isl"') else: buf.write( 'Name: "%s"; MessagesFile: "compiler:Languages\\%s.isl"' % (locale, inno_langs[locale]) ) p = None if locale in license_locales: p = self._wine_path( os.path.join(app_license_path, "%s.%s" % (locale, license_format)) ) elif app_license_path is not None: p = en_license if p: buf.write('; LicenseFile: "%s"' % p) q = None if locale in readme_locales: q = self._wine_path( os.path.join(app_readme_path, "%s.%s" % (locale, readme_format)) ) elif app_readme_path is not None: q = en_readme if q: buf.write('; InfoBeforeFile: "%s"' % q) out.append(buf.getvalue()) return "\n".join(out) def _generate_inno_custom_messages(self): """Writes out the localised name for the installer and Start Menu group""" buf = io.StringIO() for key in inno_langs.keys(): if key not in self._project.locales: continue loc = self._project.locale(key) or self._project.first_locale() buf.write("%s.AppName=%s\n" % (key, loc.name)) buf.write("%s.Enable=%s\n" % (key, custom_msgs["Enable"][key])) return buf.getvalue() def _installer_fn(self, os_, name, version): if os_ == "Windows 7": return "%s_%s.win7.exe" % (name, version) else: return "%s_%s.exe" % (name, version) def _generate_inno_setup(self, app_url, os_): o = self._generate_inno_os_config(os_).strip() + "\n" pfx = self._project.target("win").get("codeSignPfx", None) if pfx is None: return o pfx = self._project.relpath(pfx) app_name = self._project.first_locale().name o += ( "SignTool=signtool -a sha1 " + "-t http://timestamp.verisign.com/scripts/timstamp.dll " + "-pkcs12 $q%s$q -$ commercial " + "-n $q%s$q -i $q%s$q $f" ) % (self._wine_path(pfx), app_name, app_url) return o def _generate_inno_os_config(self, os_): if os_ == "Windows 7": return dedent( """ OnlyBelowVersion=0,6.3.9200 """ ) else: return "MinVersion=0,6.3.9200" def generate_inno_script(self, os_, build_dir): logger.info("Generating Inno Setup script for %s…" % os_) target = self._project.target("win") try: app_version = target["version"] app_publisher = self._project.organisation app_url = target.get("url", "") app_uuid = target["uuid"] if app_uuid.startswith("{"): app_uuid = app_uuid[1:] if app_uuid.endswith("}"): app_uuid = app_uuid[:-1] except KeyError as e: logger.error("Property %s is not defined at targets.win." % e) sys.exit(1) app_license_path = target.get("licensePath", None) if app_license_path is not None: app_license_path = self._project.relpath(app_license_path) app_readme_path = target.get("readmePath", None) if app_readme_path is not None: app_readme_path = self._project.relpath(app_readme_path) script = """\ #define MyAppVersion "%s" #define MyAppPublisher "%s" #define MyAppURL "%s" #define MyAppUUID "{{%s}" #define BuildDir "%s" [Setup] AppId={#MyAppUUID} AppName={cm:AppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={pf}\\{cm:AppName} DisableDirPage=no DefaultGroupName={cm:AppName} OutputBaseFilename=install Compression=lzma SolidCompression=yes ArchitecturesInstallIn64BitMode=x64 AlwaysRestart=yes AllowCancelDuringInstall=no UninstallRestartComputer=yes UninstallDisplayName={cm:AppName} %s [Languages] %s [CustomMessages] %s [Files] Source: "{#BuildDir}\\kbdi.exe"; DestDir: "{app}" Source: "{#BuildDir}\\i386\\*"; DestDir: "{sys}"; Check: not Is64BitInstallMode; Flags: restartreplace uninsrestartdelete Source: "{#BuildDir}\\amd64\\*"; DestDir: "{sys}"; Check: Is64BitInstallMode; Flags: restartreplace uninsrestartdelete Source: "{#BuildDir}\\wow64\\*"; DestDir: "{syswow64}"; Check: Is64BitInstallMode; Flags: restartreplace uninsrestartdelete """.strip() % ( # noqa: E501 app_version, app_publisher, app_url, app_uuid, self._wine_path(build_dir), self._generate_inno_setup(app_url, os_), self._generate_inno_languages(), self._generate_inno_custom_messages(), ) custom_locales = target.get("customLocales", None) if custom_locales is not None: custom_locales_path = self._project.relpath(custom_locales) locales = [ os.path.splitext(x)[0] for x in os.listdir(custom_locales_path) if x.endswith(".nlp") ] reg = [] for l in locales: o = ( dedent( """ Root: HKLM; Subkey: "SYSTEM\\CurrentControlSet\\Control\\Nls\\CustomLocale"; ValueType: string; ValueName: "{locale}"; ValueData: "{locale}"; Flags: uninsdeletevalue """ # noqa: E501 ) .strip() .replace("\n", " ") .format(locale=l) ) reg.append(o) script += """Source: "{#BuildDir}\\nlp\\*"; """ script += """DestDir: "{win}\\Globalization"; """ script += """Flags: restartreplace uninsrestartdelete\n""" script += "\n[Registry]\n" script += "\n".join(reg) script += "\n" # Add Run section run_scr = io.StringIO() run_scr.write("[Run]\n") uninst_scr = io.StringIO() uninst_scr.write("[UninstallRun]\n") icons_scr = io.StringIO() icons_scr.write("[Icons]\n") # Pre-install clean script run_scr.write( 'Filename: "{app}\\kbdi.exe"; Parameters: "clean"; ' "Flags: runhidden waituntilterminated\n" ) for layout in self.supported_layouts.values(): kbd_id = self._klc_get_name(layout) dll_name = "%s.dll" % kbd_id language_code = layout.target("win").get("locale", layout.locale) language_name = layout.target("win").get("languageName", None) if language_name is not None: logger.info( "Using language name '%s' for layout '%s'." % (language_name, layout.internal_name) ) else: logger.info( ( "Using Windows default language name for layout '%s'; this " + "can be overridden by providing a value for " + "targets.win.languageName." ) % layout.internal_name ) guid_str = "{%s}" % str(guid(kbd_id)).upper() # Install script run_scr.write('Filename: "{app}\\kbdi.exe"; Parameters: "keyboard_install') run_scr.write(' -t ""%s""' % language_code) # BCP 47 tag if language_name: run_scr.write(' -l ""%s""' % language_name) # Language display name run_scr.write(' -g ""{%s""' % guid_str) # Product code run_scr.write(" -d %s" % dll_name) # Layout DLL run_scr.write( ' -n ""%s""' % layout.native_display_name ) # Layout native display name run_scr.write(" -e") # Enable layout after installing it run_scr.write('"; Flags: runhidden waituntilterminated\n') # Enablement icon icons_scr.write( 'Name: "{group}\\{cm:Enable,%s}"; ' % layout.native_display_name ) icons_scr.write('Filename: "{app}\\kbdi.exe"; ') icons_scr.write( 'Parameters: "keyboard_enable -g ""{%s"" -t %s"; ' % (guid_str, language_code) ) icons_scr.write( "Flags: runminimized preventpinning excludefromshowinnewinstall\n" ) # Uninstall script uninst_scr.write( 'Filename: "{app}\\kbdi.exe"; Parameters: "keyboard_uninstall' ) uninst_scr.write( ' ""{%s"""; Flags: runhidden waituntilterminated\n' % guid_str ) script = "\n\n".join( (script, run_scr.getvalue(), uninst_scr.getvalue(), icons_scr.getvalue()) ) fn_os = "all" if os_ != "Windows 7" else "win7" with open( os.path.join(build_dir, "install.%s.iss" % fn_os), "w", encoding="utf-8-sig", newline="\r\n", ) as f: f.write(script) def build_installer(self, os_, build_dir): logger.info("Building installer for %s…" % os_) iscc = os.path.join(self.get_inno_setup_dir(), "ISCC.exe") output_path = self._wine_path(build_dir) fn_os = "all" if os_ != "Windows 7" else "win7" script_path = self._wine_path(os.path.join(build_dir, "install.%s.iss" % fn_os)) name = self._project.first_locale().name version = self._project.target("win")["version"] cmd = self._wine_cmd( iscc, "/O%s" % output_path, "/Ssigntool=%s $p" % self._wine_path(self.get_or_download_signcode()), script_path, ) logger.trace(cmd) run_process(cmd, cwd=build_dir) fn = self._installer_fn(os_, name.replace(" ", "_"), version) shutil.move(os.path.join(build_dir, "install.exe"), os.path.join(build_dir, fn)) logger.info("Installer generated at '%s'." % os.path.join(build_dir, fn)) def _klc_get_name(self, layout, show_errors=True): id_ = layout.target("win").get("id", None) if id_ is not None: if len(id_) != 5 and show_errors: logger.warning( "Keyboard id '%s' should be exactly 5 characters, got %d." % (id_, len(id_)) ) return "kbd" + id_ return "kbd" + re.sub(r"[^A-Za-z0-9-]", "", layout.internal_name)[:5] def _klc_write_headers(self, layout, buf): buf.write( 'KBD\t%s\t"%s"\n\n' % (self._klc_get_name(layout, False), layout.display_names[layout.locale]) ) copyright_ = self._project.copyright or r"¯\_(ツ)_/¯" organisation = self._project.organisation or r"¯\_(ツ)_/¯" locale = layout.target("win").get("locale", layout.locale) buf.write('COPYRIGHT\t"%s"\n\n' % copyright_) buf.write('COMPANY\t"%s"\n\n' % organisation) buf.write('LOCALENAME\t"%s"\n\n' % locale) lcid = lcidlib.get_hex8(locale) or lcidlib.get_hex8(layout.locale) or "00002000" buf.write('LOCALEID\t"%s"\n\n' % lcid) buf.write("VERSION\t1.0\n\n") # 0: default, 1: shift, 2: ctrl, 6: altGr/ctrl+alt, 7: shift+6 buf.write("SHIFTSTATE\n\n0\n1\n2\n6\n7\n\n") buf.write("LAYOUT ;\n\n") buf.write( "//SC\tVK_ \t\tCaps\tNormal\tShift\tCtrl\tAltGr\tAltShft\t-> Output\n" ) buf.write( "//--\t----\t\t----\t------\t-----\t----\t-----\t-------\t ------\n\n" ) def _klc_write_keys(self, layout, buf): col0 = mode_iter(layout, "iso-default", required=True) col1 = mode_iter(layout, "iso-shift") col2 = mode_iter(layout, "iso-ctrl") col6 = mode_iter(layout, "iso-alt") col7 = mode_iter(layout, "iso-alt+shift") alt_caps = mode_iter(layout, "iso-alt+caps") caps = mode_iter(layout, "iso-caps") caps_shift = mode_iter(layout, "iso-caps+shift") # Hold all the glyphbombs glyphbombs = [] for (sc, vk, c0, c1, c2, c6, c7, cap, scap, acap) in zip( WIN_KEYMAP.values(), WIN_VK_MAP.values(), col0, col1, col2, col6, col7, caps, caps_shift, alt_caps, ): cap_mode = 0 if cap is not None and c0 != cap and c1 != cap: cap_mode = "SGCap" elif cap is None: cap_mode += 1 if c0 != c1 else 0 cap_mode += 4 if c6 != c7 else 0 else: cap_mode += 1 if cap == c1 else 0 cap_mode += 4 if acap == c7 else 0 cap_mode = str(cap_mode) if len(vk) < 8: vk += "\t" buf.write("%s\t%s\t%s" % (sc, vk, cap_mode)) # n is the col number for glyphbombs. for n, mode, key in ( (0, "iso-default", c0), (1, "iso-shift", c1), (2, "iso-ctrl", c2), (3, "iso-alt", c6), (4, "iso-alt+shift", c7), ): filtered = decode_u(key or "") if key is not None and len(filtered) > 1: buf.write("\t%%") glyphbombs.append((filtered, (vk, str(n)) + win_glyphbomb(key))) else: buf.write("\t%s" % win_filter(key)) if key in layout.dead_keys.get(mode, []): buf.write("@") buf.write("\t// %s %s %s %s %s\n" % (c0, c1, c2, c6, c7)) if cap_mode == "SGCap": if cap is not None and len(win_glyphbomb(cap)) > 1: cap = None logger.error( "Caps key '%s' is a glyphbomb and cannot be used in Caps Mode." % cap ) if scap is not None and len(win_glyphbomb(scap)) > 1: scap = None msg = ( "Caps+Shift key '%s' is a glyphbomb and " + "cannot be used in Caps Mode." ) logger.error(msg % cap) buf.write( "-1\t-1\t\t0\t%s\t%s\t\t\t\t// %s %s\n" % (win_filter(cap, scap) + (cap, scap)) ) # Space, such special case oh my. buf.write("39\tSPACE\t\t0\t") if "space" not in layout.special: buf.write("0020\t0020\t0020\t-1\t-1\n") else: o = layout.special["space"] buf.write( "%s\t%s\t%s\t%s\t%s\n" % win_filter( o.get("iso-default", "0020"), o.get("iso-shift", "0020"), o.get("iso-ctrl", "0020"), o.get("iso-alt", None), o.get("iso-alt+shift", None), ) ) # Decimal key on keypad. decimal = layout.decimal or "." buf.write( "53\tDECIMAL\t\t0\t%s\t%s\t-1\t-1\t-1\n\n" % win_filter(decimal, decimal) ) # Glyphbombs! if len(glyphbombs) > 0: buf.write("LIGATURE\n\n") buf.write("//VK_\tMod#\tChr0\tChr1\tChr2\tChr3\n") buf.write("//----\t----\t----\t----\t----\t----\n\n") for original, col in glyphbombs: more_tabs = len(col) - 7 buf.write( "%s\t\t%s%s\t// %s\n" % (col[0], "\t".join(col[1:]), "\t" * more_tabs, original) ) buf.write("\n") # Deadkeys! for basekey, o in layout.transforms.items(): if len(basekey) != 1: logger.warning( ("Base key '%s' invalid for Windows " + "deadkeys; skipping.") % basekey ) continue buf.write("DEADKEY\t%s\n\n" % win_filter(basekey)) for key, output in o.items(): if key == " ": continue key = str(key) output = str(output) if len(key) != 1 or len(output) != 1: logger.warning( ("%s%s -> %s is invalid for Windows " + "deadkeys; skipping.") % (basekey, key, output) ) continue buf.write( "%s\t%s\t// %s -> %s\n" % (win_filter(key, output, force=True) + (key, output)) ) # Create fallback key from space, or the basekey. output = o.get(" ", basekey) buf.write("0020\t%s\t// -> %s\n\n" % (win_filter(output)[0], output)) def _klc_write_deadkey_names(self, layout, buf): buf.write("KEYNAME_DEAD\n\n") for basekey, o in layout.transforms.items(): if len(basekey) != 1: logger.warning( ("Base key '%s' invalid for Windows " + "deadkeys; skipping.") % basekey ) continue buf.write( '%s\t"%s"\n' % (win_filter(basekey)[0], unicodedata.name(basekey)) ) def _klc_write_footer(self, layout, buf): language_name = layout.target("win").get("languageName", "Undefined") lcid = lcidlib.get(layout.locale) or 0x0C00 layout_name = layout.native_display_name buf.write("\nDESCRIPTIONS\n\n") buf.write("%04x\t%s\n" % (lcid, layout_name)) buf.write("\nLANGUAGENAMES\n\n") buf.write("%04x\t%s\n" % (lcid, language_name)) buf.write("ENDKBD\n") def generate_klc(self, layout): buf = io.StringIO() self._klc_write_headers(layout, buf) self._klc_write_keys(layout, buf) buf.write(DEFAULT_KEYNAMES) self._klc_write_deadkey_names(layout, buf) self._klc_write_footer(layout, buf) return buf.getvalue() PK! ^ kbdgen/gen/x11.pyimport os from ..base import get_logger from .base import Generator, filepath, mode_iter, ISO_KEYS from ..cldr import CP_REGEX logger = get_logger(__file__) keysym_to_str = {} with open(filepath(__file__, "bin", "keysym.tsv")) as f: line = f.readline() while line: if line.startswith("*"): break line = f.readline() line = f.readline() while line: string, keysymstr = line.strip().split("\t") keysym = int(keysymstr, 16) keysym_to_str[keysym] = string line = f.readline() class XKBGenerator(Generator): def generate(self, base="."): if not self.sanity_check(): return self.build_dir = os.path.abspath(base) os.makedirs(self.build_dir, exist_ok=True) if self.dry_run: logger.info("Dry run completed.") return xkb_fn = os.path.join(self.build_dir, "%s.xkb" % (self._project.internal_name)) xcompose_fn = os.path.join( self.build_dir, "%s.xcompose" % (self._project.internal_name) ) # First char in Supplemental Private Use Area-A self.surrogate = 0xF0000 self.xkb = open(xkb_fn, "w") self.xcompose = open(xcompose_fn, "w") for name, layout in self.supported_layouts.items(): self.write_nonsense(name, layout) self.xkb.close() self.xcompose.close() def filter_xkb_keysyms(self, v): """actual filter function""" if v is None: return "" v = CP_REGEX.sub(lambda x: chr(int(x.group(1), 16)), v) if len(v) > 1: cps = " ".join(["U%04X" % ord(x) for x in v]) self.xcompose.write(" : %s # %s\n" % (self.surrogate, cps, v)) o = self.surrogate self.surrogate += 1 else: o = ord(v) return keysym_to_str.get(o, "U%04X" % o) def write_nonsense(self, name, layout): buf = self.xkb ligs = self.xcompose ligs.write("# %s\n" % name) buf.write("default partial alphanumeric_keys\n") buf.write('xkb_symbols "basic" {\n\n') buf.write(' include "latin"\n') buf.write(' name[Group1] = "%s";\n\n' % layout.display_names[layout.locale]) col0 = mode_iter(layout, "iso-default", required=True) col1 = mode_iter(layout, "iso-shift") col2 = mode_iter(layout, "iso-alt") col3 = mode_iter(layout, "iso-alt+shift") def xkb_filter(self, *args): out = [self.filter_xkb_keysyms(i) for i in args] while len(out) > 0 and out[-1] == "": out.pop() return tuple(out) for (iso, c0, c1, c2, c3) in zip(ISO_KEYS, col0, col1, col2, col3): cols = ", ".join("%10s" % x for x in xkb_filter(self, c0, c1, c2, c3)) buf.write(" key { [ %s ] };\n" % (iso, cols)) buf.write('\n include "level3(ralt_switch)"\n};\n\n') ligs.write("\n") PK!Z kbdgen/global.yamlappStrings: en: testText: "Type some text here to test" dismissKeyboard: "Dismiss Keyboard" openSettings: "Open Settings" instructionsTitle: "How to enable the keyboards:" instructions: | 1. In Settings, go to General > Keyboard > Keyboards > Add New Keyboard... 2. Under "Third-Party Keyboards", choose this keyboard. 3. A list of keyboard layouts will appear. Enable the keyboards you wish to use, and press "Done". You may now access these keyboards by pressing the globe icon next to the spacebar. imageSource: 'Background image: "Mountains outside Narvik" by Alexander Cahlenstein (CC BY)' nb: testText: "Skriv tekst her for å teste" dismissKeyboard: "Skjul tastatur" openSettings: "Åpne Innstillinger" instructionsTitle: "Hvordan du slår på tastaturene:" instructions: | 1. I Innstillinger, gå til Generelt > Tastatur > Tastaturer > Legg til nytt tastatur... 2. Under «Tredjepartstastaturer», velg denne tastaturappen. 3. Slå på de tastaturene du vil ha i lista som kommer opp, og trykk «Ferdig». Du kan nå bytte til disse tastaturene ved å trykke på globustasten til venstre for mellomromstasten. imageSource: 'Bakgrunnsbilde: «Mountains outside Narvik» av Alexander Cahlenstein (CC BY)' nn: testText: "Skriv tekst her for å testa" dismissKeyboard: "Gøym tastatur" openSettings: "Opna Innstillinger" instructionsTitle: "Korleis du slår på tastatura:" instructions: | 1. I Innstillinger, gå til Generelt > Tastatur > Tastaturer > Legg til nytt tastatur... 2. Under «Tredjepartstastaturer», vel denne tastaturappen. 3. Slå på dei tastatura du vil ha i lista som kjem opp, og trykk «Ferdig». Du kan nå byta til desse tastatura ved å trykkja på globustasten til venstre for mellomromstasten. imageSource: 'Bakgrunnsbilete: «Mountains outside Narvik» av Alexander Cahlenstein (CC BY)' se: testText: "Geahččál čállit dása" dismissKeyboard: "Čiehkat boallobeavddi" openSettings: "Raba heivehusaid" instructionsTitle: "Mo oažžu boallobevddiid doaibmat:" instructions: | 1. Innstillingeris, mana dán merkošii: Generelt > Tastatur > Tastaturer > Legg til nytt tastatur... 2. «Tredjepartstastaturer» vuolde válljet dán boallobeavdeprográmmaža. 3. Vállje daid boallobevddiid maid dárbbašat listtus mii ihtá ja deaddil «Ferdig». Dál sáhtát molsut boallobevddiid gaskkas deaddilettiin globusboalu maid gávnnat gaskaboalu gurutbealde. imageSource: 'Duogášgovva: «Mountains outside Narvik» maid Alexander Cahlenstein lea govven (CC BY)' sv: testText: "Skriv text här för att testa" dismissKeyboard: "Göm tangentbord" openSettings: "Öppna Inställningar" instructionsTitle: "Hur du slår på tangentborden:" instructions: | 1. I Inställningar, välj Allmänt > Tangentbord > Tangentbord > Lägg till nytt tangentbord... 2. Under «Tangentbord från tredje part», välj denna tangentbordsappen. 3. Slå på de tangentborden du vill använda i listan som dyker upp, och tryck «Klar». Du kan nu byta mellan tangentborden genom att trycka på globtangenten vänster om mellanslagstangenten. imageSource: 'Bakgrundsbild: «Mountains outside Narvik» av Alexander Cahlenstein (CC BY)' fi: testText: "Kirjoita tähän testataksesi" dismissKeyboard: "Kätke näppäimistö" openSettings: "Avaa asetukset" instructionsTitle: "Kuinka otat näppäimistön käyttöön:" instructions: | 1. Asetuksissa: Yleiset > Näppäimistö > Näppäimistöt > Lisää näppäimistö... 2. «Muiden valmistajien näppäimistöt» -tekstin alla valitse tämä näppäimistö. 3. Valitse listalta tarvitsemasi näppäimistöt ja paina «Valmis». Voit valita näppäimistön painamalla globusnappia välinäppäimen vasemmalla puolella. imageSource: 'Taustakuva: «Mountains outside Narvik» Alexander Cahlenstein:sta (CC BY)' PK!UW!> > kbdgen/log.py# LogFormatter taken straight from # https://github.com/tornadoweb/tornado/blob/dcd1ef81df68ba928e6bbeb1cf194f1ff694ec49/tornado/log.py # Other bits mangled to support Python 3 only and remove deps. # # Copyright 2012 Facebook # Copyright 2016 Brendan Molloy # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # Modified by Brendan Molloy (c) 2015 # """Logging support for kbdgen. kbdgen uses three logger streams: * ``tornado.access``: Per-request logging for kbdgen's HTTP servers (and potentially other servers in the future) * ``tornado.application``: Logging of errors from application code (i.e. uncaught exceptions from callbacks) * ``tornado.general``: General-purpose logging, including any errors or warnings from kbdgen itself. These streams may be configured independently using the standard library's `logging` module. For example, you may wish to send ``tornado.access`` logs to a separate file for analysis. """ from __future__ import absolute_import, division, print_function, with_statement import logging import logging.handlers import sys def to_unicode(value): """Converts a string argument to a unicode string. If the argument is already a unicode string or None, it is returned unchanged. Otherwise it must be a byte string and is decoded as utf8. """ if isinstance(value, (str, type(None))): return value if not isinstance(value, bytes): raise TypeError("Expected bytes, unicode, or None; got %r" % type(value)) return value.decode("utf-8") try: import curses except ImportError: curses = None # Logger objects for internal tornado use access_log = logging.getLogger("kbdgen.access") app_log = logging.getLogger("kbdgen.application") gen_log = logging.getLogger("kbdgen.general") def _stderr_supports_color(): color = False if curses and hasattr(sys.stderr, "isatty") and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: color = True except Exception: pass return color def _safe_unicode(s): try: return to_unicode(s) except UnicodeDecodeError: return repr(s) class LogFormatter(logging.Formatter): """Log formatter used in kbdgen. Key features of this formatter are: * Color support when logging to a terminal that supports it. * Timestamps on every log line. * Robust against str/bytes encoding problems. """ DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" # noqa DEFAULT_DATE_FORMAT = "%y%m%d %H:%M:%S" DEFAULT_COLORS = { logging.DEBUG: 4, # Blue logging.INFO: 2, # Green logging.WARNING: 3, # Yellow logging.ERROR: 1, # Red logging.CRITICAL: 1, # Red } def __init__( self, color=True, fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, colors=DEFAULT_COLORS, ): r""" :arg bool color: Enables color support. :arg string fmt: Log message format. It will be applied to the attributes dict of log records. The text between ``%(color)s`` and ``%(end_color)s`` will be colored depending on the level if color support is on. :arg dict colors: color mappings from logging level to terminal color code :arg string datefmt: Datetime format. Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``. .. versionchanged:: 3.2 Added ``fmt`` and ``datefmt`` arguments. """ logging.Formatter.__init__(self, datefmt=datefmt) self._fmt = fmt self._colors = {} if color and _stderr_supports_color(): # The curses module has some str/bytes confusion in # python3. Until version 3.2.3, most methods return # bytes, but only accept strings. In addition, we want to # output these strings with the logging module, which # works with unicode strings. The explicit calls to # unicode() below are harmless in python2 but will do the # right conversion in python 3. fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or "" if (3, 0) < sys.version_info < (3, 2, 3): fg_color = str(fg_color, "ascii") for levelno, code in colors.items(): self._colors[levelno] = str(curses.tparm(fg_color, code), "ascii") self._normal = str(curses.tigetstr("sgr0"), "ascii") else: self._normal = "" def format(self, record): try: message = record.getMessage() assert isinstance(message, str) # guaranteed by logging # Encoding notes: The logging module prefers to work with character # strings, but only enforces that log messages are instances of # basestring. In python 2, non-ascii bytestrings will make # their way through the logging framework until they blow up with # an unhelpful decoding error (with this formatter it happens # when we attach the prefix, but there are other opportunities for # exceptions further along in the framework). # # If a byte string makes it this far, convert it to unicode to # ensure it will make it out to the logs. Use repr() as a fallback # to ensure that all byte strings can be converted successfully, # but don't do it by default so we don't add extra quotes to ascii # bytestrings. This is a bit of a hacky place to do this, but # it's worth it since the encoding errors that would otherwise # result are so useless (and tornado is fond of using utf8-encoded # byte strings whereever possible). record.message = _safe_unicode(message) except Exception as e: record.message = "Bad message (%r): %r" % (e, record.__dict__) record.asctime = self.formatTime(record, self.datefmt) if record.levelno in self._colors: record.color = self._colors[record.levelno] record.end_color = self._normal else: record.color = record.end_color = "" formatted = self._fmt % record.__dict__ if record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: # exc_text contains multiple lines. We need to _safe_unicode # each line separately so that non-utf8 bytes don't cause # all the newlines to turn into '\n'. lines = [formatted.rstrip()] lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n")) formatted = "\n".join(lines) return formatted.replace("\n", "\n ") def enable_pretty_logging(options=None, logger=None, fmt=None): """Turns on formatted logging output as configured.""" if logger is None: logger = logging.getLogger() # Set up color if we are in a tty and curses is installed channel = logging.StreamHandler() channel.setFormatter(LogFormatter(fmt=fmt)) logger.addHandler(channel) def monkey_patch_trace_logging(): setattr(logging, "TRACE", 5) logging._levelToName[logging.TRACE] = "TRACE" logging._nameToLevel["TRACE"] = logging.TRACE def trace(self, msg, *args, **kwargs): if self.isEnabledFor(logging.TRACE): self._log(logging.TRACE, msg, args, **kwargs) logging.Logger.trace = trace LogFormatter.DEFAULT_COLORS[logging.TRACE] = 5 PK!!kbdgen/orderedyaml.pyimport io from collections import OrderedDict import yaml import yaml.constructor # Courtesy of https://gist.github.com/844388. Thanks! class OrderedDictYAMLLoader(yaml.Loader): """A YAML loader that loads mappings into ordered dictionaries.""" def __init__(self, *args, **kwargs): yaml.Loader.__init__(self, *args, **kwargs) self.add_constructor("tag:yaml.org,2002:map", type(self).construct_yaml_map) self.add_constructor("tag:yaml.org,2002:omap", type(self).construct_yaml_map) def construct_yaml_map(self, node): data = OrderedDict() yield data value = self.construct_mapping(node) data.update(value) def construct_mapping(self, node, deep=False): if isinstance(node, yaml.MappingNode): self.flatten_mapping(node) else: raise yaml.constructor.ConstructorError( None, None, "expected a mapping node, but found %s" % node.id, node.start_mark, ) mapping = OrderedDict() for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) try: hash(key) except TypeError as exc: raise yaml.constructor.ConstructorError( "while constructing a mapping", node.start_mark, "found unacceptable key (%s)" % exc, key_node.start_mark, ) value = self.construct_object(value_node, deep=deep) mapping[key] = value return mapping def load(f): return yaml.load(f, OrderedDictYAMLLoader) def loads(string): return yaml.load(io.StringIO(string), OrderedDictYAMLLoader) PK!H휑B'kbdgen-1.0.1.dist-info/entry_points.txtN+I/N.,()JI)2NJIOͳPz !+$<.T5`x+$9#C!pPK!'*(*(kbdgen-1.0.1.dist-info/LICENSEApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non- exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no- charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK!HnHTUkbdgen-1.0.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H^s kbdgen-1.0.1.dist-info/METADATAVmo6_q,:vb^u$n0Դx9KJRa;R/qlC؉xϽ?w{B /h2:1+Q` \fdH6B: ‚>kFX MbIVޗ.ʯ9L1jur2ԎRd+LX8ri٤+cS΢B{n,S7oOf\gR]rSR{szȏ'G,ΩB{MY0W!kdyːr*٠ M~05 ;#u2(^¥ˊjthgk?[ճ5^?\9OD /ޜc>Wǃ'Ae]vG_`,.|]6(1%dU`o[+ӱ]fUəў41]bgƇw `?__ɍkMTB'\<>Ů! ZВ=JN'Ǐ]hEG=}oMAn.X2| *A5iTlJAm\i0/LRbUh56: MҤEbcZ(~@Q~1dD T4!*oLrop*aK 6(A4=I+7(d%IiU05,B*D݄$ g,@B:V $?CaŸ{ܜ ?pWK7|.bn5bUUei3XքiW*@1]*]Y ]O_RZqHhEç Ġ隖zf=ih1jdFb Z-5^nڙ,9#ϱ >OA8p%fa@@껲 ǙH{#Z8qA+,1U*ͦE$}|ҴJB?.'{˚It@)W1ǰ qTc՜B!? J˪lC|3D<zARjṐݤ2/:TY /Bz邇B{J 8uZ˹H;0P>#ykon[6IJ.` !/oԑ8IʓVm;Y c),nߌSk{1 >=5Mơ©`A+ȷlǜ ŕαa2$ O4窨(op'y6Ƅ'@cu\{ 8`}7frY=r9y&,;/t`:m ]E; g DP_7EJ[WG,LhB WajKh fI愒BT XEL0| 0Fe_ՁI4iMǿR@H]/*[IH(<Y~*!f"atHU)/x'8DJske-a0|D~] OWgyQ )eƍt5R'J/3D< !+Vv.nsKFĤnp݉AȞ:ob!^I#vM؛C{Tr ᕡ38{ wgNpL}` \-w~>k$6//_jt_v- N=NP5w?XmbuG(֚-t/k7?ŀO{|.o8Tw@ i 7ݫ\z=S6F!\']Jz-^SgsR?)9a^X%>'akbdgen/gen/bin/android-glyphs-api21.binPK!pTU>>'kbdgen/gen/bin/android-glyphs-api23.binPK!RCRCkbdgen/gen/bin/keyboard-iso.svgPK!Xrr]#kbdgen/gen/bin/keysym.tsvPK!Gs nkbdgen/gen/errormodel.pyPK!SfVfVkbdgen/gen/ios.pyPK!Ώppkbdgen/gen/json.pyPK!njE%GGkbdgen/gen/osx.pyPK!ӉDHgHgCkbdgen/gen/osxutil.pyPK!}C##dkbdgen/gen/svgkbd.pyPK!!jkbdgen/gen/win.pyPK! ^ H[kbdgen/gen/x11.pyPK!Z 3gkbdgen/global.yamlPK!UW!> > wxkbdgen/log.pyPK!!kbdgen/orderedyaml.pyPK!H휑B'kbdgen-1.0.1.dist-info/entry_points.txtPK!'*(*(kbdgen-1.0.1.dist-info/LICENSEPK!HnHTUkbdgen-1.0.1.dist-info/WHEELPK!H^s kbdgen-1.0.1.dist-info/METADATAPK!H`  -kbdgen-1.0.1.dist-info/RECORDPK!!=