diff options
author | glasseyes <dglassey@gmail.com> | 2018-11-27 14:18:36 +0700 |
---|---|---|
committer | glasseyes <dglassey@gmail.com> | 2018-11-27 14:18:36 +0700 |
commit | 9f70f8cfc86a0a5343c8eac7ca24a1b5253f91cc (patch) | |
tree | 018b6d740c8e6384d434582334022c84f5b63c55 |
New upstream version 10.99.33
42 files changed, 3299 insertions, 0 deletions
diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0910ba5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include keyman_config/icons * +include README.md +include *.bash-completion diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..ed8cecf --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 1.0 +Name: keyman_config +Version: 10.99.33 +Summary: Keyman for Linux configuration +Home-page: http://www.keyman.com/ +Author: Daniel Glassey +Author-email: wdg@debian.org +License: MIT +Description: UNKNOWN +Keywords: keyman,keyman-config,keyboard +Platform: UNKNOWN diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b1e45a --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Linux KMP installer + +## Preparing to run + +If you are running from the repo or installing keyman-config manually rather than from a package +then you will need to + +`sudo apt install python3-lxml python3-magic python3-numpy python3-pil python3-requests +python3-requests-cache python3 python3-gi gir1.2-webkit-3.0 dconf-cli python3-setuptools` + +You will also need kmflcomp either from a package or built and installed locally. + +run the script `./createkeymandirs.sh` to create the directories for these programs to +install the packages to + +### Installing manually from the repo + +`make && sudo make install` will install locally to /usr/local +`python3 setup.py --help install` will give you more install options +You will need `sudo apt install python3-pip` to `make uninstall` + +## Things to run + +### km-config + +`./km-config` + +This shows a list of installed kmps. +For each one it has buttons to `show the welcome page`, `show more information` and `uninstall`. + +##### Buttons + +* `Refresh` - useful if you install or uninstall on the commandline while running km-config. +* `Download keyboard...` - runs `DownloadKmpWindow` (see below) +* `Install keyboard...` - opens a file choose dialog to choose a kmp file to install and bring up the `InstallKmpWindow` for more details and to confirm installing. + +##### Download window + +This uses the keyman.com website to install kmps. + +The website doesn't know about linux yet (until after 10.0 release) so +pretending to be a mac for now. + +Search for a language or keyboard in the search box +Select a keyboard from the list +In 'Downloads for your device' there will be a 'Install keyboard' button for the keyboard for macOS +Click it to download the keyboard and bring up the `InstallKmpWindow` for more details and to confirm installing. + +Secondary-click gives you a menu including 'Back' to go back a page. + + +### km-package-install + +Command line installer for kmp + +`km-package-install -k <keyboard id>` +or +`km-package-install -f <kmp file>` + +### km-package-uninstall + +Command line uninstaller for kmp + +`km-package-uninstall <keyboard id>` + +### km-package-list-installed + +`km-package-list-installed` shows name, version, id, description of each installed keyboard + +`km-package-list-installed -s` shows name, version, id of each installed keyboard + +### km-package-get + +Download Keyman keyboard package to ~/Downloads + +`km-package-get <keyboard id>` + +### km-kvk2ldml + +Convert a Keyman kvk on-screen keyboard file to an LDML file. Optionally print +the details of the kvk file [-p] optionally with all keys [-k]. + +`km-kvk2ldml [-p] [-k] [-o LDMLFILE] <kvk file>` + +## Building the Debian package + +You will need the build dependencies as well as the runtime dependencies above + +`sudo apt install dh-python python3-all debhelper help2man` + +Run `make deb`. This will build the Debian package in the make_deb directory. diff --git a/keyman_config.egg-info/PKG-INFO b/keyman_config.egg-info/PKG-INFO new file mode 100644 index 0000000..fb604f5 --- /dev/null +++ b/keyman_config.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 1.0 +Name: keyman-config +Version: 10.99.33 +Summary: Keyman for Linux configuration +Home-page: http://www.keyman.com/ +Author: Daniel Glassey +Author-email: wdg@debian.org +License: MIT +Description: UNKNOWN +Keywords: keyman,keyman-config,keyboard +Platform: UNKNOWN diff --git a/keyman_config.egg-info/SOURCES.txt b/keyman_config.egg-info/SOURCES.txt new file mode 100644 index 0000000..5a0961b --- /dev/null +++ b/keyman_config.egg-info/SOURCES.txt @@ -0,0 +1,40 @@ +MANIFEST.in +README.md +km-config +km-kvk2ldml +km-kvk2ldml.bash-completion +km-package-get +km-package-get.bash-completion +km-package-install +km-package-install.bash-completion +km-package-list-installed +km-package-list-installed.bash-completion +km-package-uninstall +km-package-uninstall.bash-completion +setup.py +keyman_config/__init__.py +keyman_config/accelerators.py +keyman_config/check_mime_type.py +keyman_config/convertico.py +keyman_config/downloadkeyboard.py +keyman_config/get_kmp.py +keyman_config/install_kmp.py +keyman_config/install_window.py +keyman_config/keyboard_details.py +keyman_config/kmpmetadata.py +keyman_config/kvk2ldml.py +keyman_config/list_installed_kmp.py +keyman_config/uninstall_kmp.py +keyman_config/version.py +keyman_config/view_installed.py +keyman_config/welcome.py +keyman_config.egg-info/PKG-INFO +keyman_config.egg-info/SOURCES.txt +keyman_config.egg-info/dependency_links.txt +keyman_config.egg-info/requires.txt +keyman_config.egg-info/top_level.txt +keyman_config/icons/cross20.png +keyman_config/icons/defaultpackage.gif +keyman_config/icons/expand20.png +keyman_config/icons/help20.png +keyman_config/icons/icon_kmp.png
\ No newline at end of file diff --git a/keyman_config.egg-info/dependency_links.txt b/keyman_config.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/keyman_config.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/keyman_config.egg-info/requires.txt b/keyman_config.egg-info/requires.txt new file mode 100644 index 0000000..29d78a3 --- /dev/null +++ b/keyman_config.egg-info/requires.txt @@ -0,0 +1,6 @@ +lxml +numpy +Pillow +requests +requests-cache +python-magic diff --git a/keyman_config.egg-info/top_level.txt b/keyman_config.egg-info/top_level.txt new file mode 100644 index 0000000..796df5a --- /dev/null +++ b/keyman_config.egg-info/top_level.txt @@ -0,0 +1 @@ +keyman_config diff --git a/keyman_config/__init__.py b/keyman_config/__init__.py new file mode 100644 index 0000000..58f3ace --- /dev/null +++ b/keyman_config/__init__.py @@ -0,0 +1 @@ +from .version import __version__ diff --git a/keyman_config/accelerators.py b/keyman_config/accelerators.py new file mode 100644 index 0000000..4b68e8b --- /dev/null +++ b/keyman_config/accelerators.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +def bind_accelerator(accelerators, widget, accelerator, signal='clicked'): + key, mod = Gtk.accelerator_parse(accelerator) + widget.add_accelerator(signal, accelerators, key, mod, Gtk.AccelFlags.VISIBLE) + +def init_accel(win): + win.accelerators = Gtk.AccelGroup() + win.add_accel_group(win.accelerators) diff --git a/keyman_config/check_mime_type.py b/keyman_config/check_mime_type.py new file mode 100644 index 0000000..b9d9cc7 --- /dev/null +++ b/keyman_config/check_mime_type.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 + +import logging +import subprocess +import webbrowser +import urllib.parse + + +def check_mime_type(webview, frame, request, mimetype, policy_decision): + """Handle downloads and PDF files.""" + if mimetype == 'application/pdf': + logging.info("check_mime_type: Download and run %s", request.get_uri()) + parse_url = urllib.parse.urlparse(request.get_uri()) + if parse_url.scheme == "file": + subprocess.call(['xdg-open', parse_url.path]) + else: + webbrowser.open(request.get_uri()) + policy_decision.ignore() + return True + return False diff --git a/keyman_config/convertico.py b/keyman_config/convertico.py new file mode 100755 index 0000000..3251cc5 --- /dev/null +++ b/keyman_config/convertico.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 + +import logging +import numpy as np +import os +import sys +from PIL import Image + + +def changeblacktowhite(im): + data = np.array(im) # "data" is a height x width x 4 numpy array + red, green, blue, alpha = data.T # Temporarily unpack the bands for readability + + # Replace black with white... (leaves alpha values alone...) + white_areas = (red == 0) & (blue == 0) & (green == 0) + data[..., :-1][white_areas.T] = (255, 255, 255) # Transpose back needed + + im2 = Image.fromarray(data) + return im2 + +def checkandsaveico(icofile): + im = Image.open(icofile) + im = im.convert('RGBA') + im2 = im + num, colour = max(im.getcolors(im.size[0]*im.size[1])) + logging.debug("checkandsaveico maxcolour: num {0}: colour {1}".format(num, colour)) + if num > 160 and colour == (0, 0, 0, 0): + logging.info("checkandsaveico:" + icofile + " mostly black so changing black to white") + im2 = changeblacktowhite(im) + im2.save(icofile + ".bmp") + im3 = Image.open(icofile + ".bmp") + im4 = im3.resize((64, 64), Image.ANTIALIAS) + im4.save(icofile + ".png") + os.remove(icofile + ".bmp") + + +def main(argv): + if len(sys.argv) != 2: + logging.error("convertico.py <ico file>") + sys.exit(2) + logging.basicConfig(level=logging.INFO) + checkandsaveico(sys.argv[1]) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/keyman_config/downloadkeyboard.py b/keyman_config/downloadkeyboard.py new file mode 100755 index 0000000..47de49a --- /dev/null +++ b/keyman_config/downloadkeyboard.py @@ -0,0 +1,97 @@ +#!/usr/bin/python3 + +import logging +import os.path +import urllib.parse +import pathlib +import subprocess +import webbrowser +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('WebKit', '3.0') +from gi.repository import Gtk, WebKit +from keyman_config.get_kmp import get_download_folder, download_kmp_file +from keyman_config.install_window import InstallKmpWindow +from keyman_config.check_mime_type import check_mime_type +from keyman_config.accelerators import bind_accelerator, init_accel + +class DownloadKmpWindow(Gtk.Window): + + def __init__(self, view=None): + self.accelerators = None + Gtk.Window.__init__(self, title="Download Keyman keyboards") + self.endonclose = False + self.viewwindow = view + init_accel(self) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + s = Gtk.ScrolledWindow() + # TODO update (or remove) user_agent once website supports Linux kmp packages + user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" + webview = WebKit.WebView() + settings = WebKit.WebSettings() + settings.set_property('user-agent', user_agent) + webview.set_settings(settings) + webview.connect("navigation-policy-decision-requested", self.check) + webview.connect("mime-type-policy-decision-requested", check_mime_type) + # TODO update website URI once website supports Linux kmp packages + webview.load_uri("https://keyman.com/keyboards?embed=macos&version=10") + s.add(webview) + vbox.pack_start(s, True, True, 0) + + bbox = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.HORIZONTAL) + #bbox.set_layout(Gtk.ButtonBoxStyle.END) + + button = Gtk.Button.new_with_mnemonic("_Close") + button.connect("clicked", self.on_close_clicked) + bbox.pack_end(button, False, False, 12) + bind_accelerator(self.accelerators, button, '<Control>w') + vbox.pack_start(bbox, False, False, 12) + + self.add(vbox) + + def process_kmp(self, url, downloadfile): + logging.info("Downloading kmp file to %s", downloadfile) + if download_kmp_file(url, downloadfile): + logging.info("File downloaded") + w = InstallKmpWindow(downloadfile, online=True, viewkmp=self.viewwindow, downloadwindow=self) + if w.checkcontinue: + w.show_all() + return True + return False + + def check(self, view, frame, req, nav, policy): + uri = req.get_uri() + parsed = urllib.parse.urlparse(uri) + if parsed.scheme == "keyman": + if parsed.path == "download": + qs = urllib.parse.parse_qs(parsed.query) + downloadfile = os.path.join(get_download_folder(), qs['filename'][0]) + if self.process_kmp(qs['url'][0], downloadfile): + policy.ignore() + return True + elif parsed.path == "link": + qs = urllib.parse.parse_qs(parsed.query) + webbrowser.open(qs['url'][0]) + return True + return False + + def on_close_clicked(self, button): + logging.debug("Closing download window") + if self.endonclose: + Gtk.main_quit() + else: + self.close() + + def connectdestroy(self): + self.connect("destroy", Gtk.main_quit) + self.endonclose = True + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + w = DownloadKmpWindow() + w.connectdestroy() + w.resize(800, 450) + w.show_all() + Gtk.main() diff --git a/keyman_config/get_kmp.py b/keyman_config/get_kmp.py new file mode 100755 index 0000000..3d831a3 --- /dev/null +++ b/keyman_config/get_kmp.py @@ -0,0 +1,157 @@ +#!/usr/bin/python3 + +import sys +import datetime +import time +import json +import logging +import requests +import requests_cache +import os +from pathlib import Path + + +def get_keyboard_data(keyboardid, weekCache=False): + """ + Get Keyboard data from web api. + + Args: + keyboardid (str): Keyboard ID + weekCache (bool) : cache data for 1 week, default is 1 day + Returns: + dict: Keyboard data + """ + logging.info("Getting data for keyboard %s", keyboardid) + api_url = "https://api.keyman.com/keyboard/" + keyboardid + logging.debug("At URL %s", api_url) + home = str(Path.home()) + cache_dir = keyman_cache_dir() + current_dir = os.getcwd() + if weekCache: + expire_after = datetime.timedelta(days=7) + else: + expire_after = datetime.timedelta(days=1) + os.chdir(cache_dir) + requests_cache.install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) + now = time.ctime(int(time.time())) + response = requests.get(api_url) + logging.debug("Time: {0} / Used Cache: {1}".format(now, response.from_cache)) + os.chdir(current_dir) + requests_cache.core.uninstall_cache() + if response.status_code == 200: + return response.json() + else: + return None + +def get_download_folder(): + """ + Folder where downloaded files will be saved. + + Returns: + str: path where downloaded files will be saved + """ + return keyman_cache_dir() + +def keyman_cache_dir(): + """ + User keyman cache folder + It will be created if it doesn't already exist + + Returns: + str: path of user keyman cache folder + """ + home = os.path.expanduser("~") + cachebase = os.environ.get("XDG_CACHE_HOME", os.path.join(home, ".cache")) + km_cache=os.path.join(cachebase, "keyman") + if not os.path.isdir(km_cache): + os.mkdir(km_cache) + return km_cache + +def user_keyman_dir(): + home = os.path.expanduser("~") + datahome = os.environ.get("XDG_DATA_HOME", os.path.join(home, ".local", "share")) + return os.path.join(datahome, "keyman") + +def user_keyman_font_dir(): + home = os.path.expanduser("~") + datahome = os.environ.get("XDG_DATA_HOME", os.path.join(home, ".local", "share")) + return os.path.join(datahome, "fonts", "keyman") + + +def user_keyboard_dir(keyboardid): + return os.path.join(user_keyman_dir(), keyboardid) + + +def get_kmp_file(kbdata, cache=False): + """ + Get info from keyboard data to download kmp then download it. + + Args: + kbdata (dict): Keyboard data + cache (bool): Whether to cache the kmp file web request + Returns: + str: path where kmp file has been downloaded + """ + if 'packageFilename' not in kbdata: + logging.info("get_kmp.py: Keyboard does not have a kmp file available") + return None + + kmp_url = "https://downloads.keyman.com/keyboards/" + kbdata['id'] + "/" + kbdata['version'] + "/" + kbdata['packageFilename'] + downloadfile = os.path.join(get_download_folder(), kbdata['packageFilename']) + return download_kmp_file(kmp_url, downloadfile, cache) + +def download_kmp_file(url, kmpfile, cache=False): + """ + Download kmp file. + + Args: + url (str): URL to download the kmp file from. + kmpfile (str): Where to save the kmp file. + currently it does no checks on this location + assumes that is in users keyman cache dir + cache(bool): Whether to cache the kmp file web request for a week + Returns: + str: path where kmp file has been downloaded + """ + logging.info("Download URL: %s", url) + downloadfile = None + + if cache: + cache_dir = keyman_cache_dir() + current_dir = os.getcwd() + expire_after = datetime.timedelta(days=7) + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) + os.chdir(cache_dir) + requests_cache.install_cache(cache_name='keyman_kmp_cache', backend='sqlite', expire_after=expire_after) + now = time.ctime(int(time.time())) + + response = requests.get(url) #, stream=True) + + if cache: + logging.debug("Time: {0} / Used Cache: {1}".format(now, response.from_cache)) + os.chdir(current_dir) + requests_cache.core.uninstall_cache() + + if response.status_code == 200: + with open(kmpfile, 'wb') as f: + f.write(response.content) + downloadfile = kmpfile + return downloadfile + +def get_kmp(keyboardid): + """ + Download a kmp file given a keyboard id. + + Args: + keyboardid (str): Keyboard ID + Returns: + str: path where kmp file has been downloaded + """ + kbdata = get_keyboard_data(keyboardid) + if (kbdata): + return get_kmp_file(kbdata) + else: + logging.warning("get_kmp.py: Could not download information about keyboard.") + return None + return
\ No newline at end of file diff --git a/keyman_config/icons/cross20.png b/keyman_config/icons/cross20.png Binary files differnew file mode 100644 index 0000000..183b531 --- /dev/null +++ b/keyman_config/icons/cross20.png diff --git a/keyman_config/icons/defaultpackage.gif b/keyman_config/icons/defaultpackage.gif Binary files differnew file mode 100644 index 0000000..d1fe67c --- /dev/null +++ b/keyman_config/icons/defaultpackage.gif diff --git a/keyman_config/icons/expand20.png b/keyman_config/icons/expand20.png Binary files differnew file mode 100644 index 0000000..1cbaffa --- /dev/null +++ b/keyman_config/icons/expand20.png diff --git a/keyman_config/icons/help20.png b/keyman_config/icons/help20.png Binary files differnew file mode 100644 index 0000000..f7ed0c1 --- /dev/null +++ b/keyman_config/icons/help20.png diff --git a/keyman_config/icons/icon_kmp.png b/keyman_config/icons/icon_kmp.png Binary files differnew file mode 100644 index 0000000..f5ca91b --- /dev/null +++ b/keyman_config/icons/icon_kmp.png diff --git a/keyman_config/install_kmp.py b/keyman_config/install_kmp.py new file mode 100755 index 0000000..2fe6389 --- /dev/null +++ b/keyman_config/install_kmp.py @@ -0,0 +1,331 @@ +#!/usr/bin/python3 + +import argparse +import json +import logging +import os.path +import subprocess +import sys +import tempfile +import zipfile +from os import listdir, makedirs +from shutil import copy2, rmtree +from ast import literal_eval +from enum import Enum + +import requests + +from keyman_config.get_kmp import get_keyboard_data, get_kmp, user_keyboard_dir, user_keyman_dir, user_keyman_font_dir +from keyman_config.kmpmetadata import parseinfdata, parsemetadata, KMFileTypes +from keyman_config.uninstall_kmp import uninstall_kmp +from keyman_config.convertico import checkandsaveico +from keyman_config.kvk2ldml import convert_kvk_to_ldml, output_ldml + +#TODO userdir install +# special processing for kmn if needed +#TODO optionally standardise throughout on variable names +# packageID for kmps and keyboardID for keyboards +# see https://docs.google.com/document/d/1sj7W6pCiN-_iRss5iRdib1aHaSTmYoLIueQSKJeNy8Q/edit#heading=h.mq0rc28mf031 + +class InstallStatus(Enum): + Continue = 0 + Warning = 1 + Abort = 2 + +class InstallError(Exception): + """Exception raised for errors in KMP installation. + + Attributes: + status -- InstallStatus for what to do when the error occurrs + message -- explanation of the error + """ + + def __init__(self, status, message): + self.status = status + self.message = message + +def list_files(directory, extension): + return (f for f in listdir(directory) if f.endswith('.' + extension)) + +def extract_kmp(kmpfile, directory): + with zipfile.ZipFile(kmpfile,"r") as zip_ref: + zip_ref.extractall(directory) + +def get_metadata(tmpdirname): + """ + Get metadata from kmp.json if it exists. + If it does not exist then will return get_infdata + + Args: + inputfile (str): path to kmp file + tmpdirname(str): temp directory to extract kmp + + Returns: + list[5]: info, system, options, keyboards, files + see kmpmetadata.parsemetadata for details + """ + kmpjson = os.path.join(tmpdirname, "kmp.json") + if os.path.isfile(kmpjson): + return parsemetadata(kmpjson, False) + else: + return get_infdata(tmpdirname) + +def get_infdata(tmpdirname): + """ + Get metadata from kmp.inf if it exists. + + Args: + inputfile (str): path to kmp file + tmpdirname(str): temp directory to extract kmp + + Returns: + list[5]: info, system, options, keyboards, files + see kmpmetadata.parseinfdata for details + """ + kmpinf = os.path.join(tmpdirname, "kmp.inf") + if os.path.isfile(kmpinf): + info, system, options, keyboards, files = parseinfdata(kmpinf, False) + return info, system, options, keyboards, files + else: + return None, None, None, None, None + +def download_source(kbid, kbdir, sourcePath): + # just get latest version of kmn unless there turns out to be a way to get the version of a file at a date + base_url = "https://raw.github.com/keymanapp/keyboards/master/" + sourcePath + kmn_url = base_url + "/source/" + kbid + ".kmn" + response = requests.get(kmn_url) + if response.status_code == 200: + kmn_file = os.path.join(kbdir, kbid + ".kmn") + with open(kmn_file, 'wb') as f: + f.write(response.content) + logging.info("Installing %s.kmn as keyman file", kbid) + logging.info("Compiling kmn file") + subprocess.run(["kmflcomp", kmn_file], stdout=subprocess.PIPE, stderr= subprocess.STDOUT) + kmfl_file = os.path.join(kbdir, kbid + ".kmfl") + if not os.path.isfile(kmfl_file): + message = "Could not compile %s to %s so not installing keyboard." % (kmn_file, kmfl_file) + os.remove(kmn_file) + rmtree(kbdir) + raise InstallError(InstallStatus.Abort, message) + else: + message = "install_kmp.py: warning: no kmn source file for %s so not installing keyboard." % (kbid) + rmtree(kbdir) + raise InstallError(InstallStatus.Abort, message) + icodownloadfile = os.path.join(kbdir, kbid + ".ico") + if not os.path.isfile(icodownloadfile): + ico_url = base_url + "/source/" + kbid + ".ico" + response = requests.get(ico_url) + if response.status_code == 200: + with open(icodownloadfile, 'wb') as f: + f.write(response.content) + logging.info("Installing %s.ico as keyman file", kbid) + checkandsaveico(icodownloadfile) + else: + logging.warning("install_kmp.py: warning: no ico source file for %s", kbid) + +def process_keyboard_data(kbid, kbdir): + kbdata = get_keyboard_data(kbid) + if kbdata: + if not os.path.isdir(kbdir): + os.makedirs(kbdir) + if 'sourcePath' in kbdata: + download_source(kbid, kbdir, kbdata['sourcePath']) + + with open(os.path.join(kbdir, kbid + '.json'), 'w') as outfile: + json.dump(kbdata, outfile) + logging.info("Installing api data file %s.json as keyman file", kbid) + else: + message = "install_kmp.py: error: cannot download keyboard data so not installing." + rmtree(kbdir) + raise InstallError(InstallStatus.Abort, message) + +def check_keyman_dir(basedir, error_message): + # check if keyman subdir exists + keyman_dir = os.path.join(basedir, "keyman") + if os.path.isdir(keyman_dir): + # Check for write access of keyman dir to be able to create subdir + if not os.access(keyman_dir, os.X_OK | os.W_OK): + raise InstallError(InstallStatus.Abort, error_message) + else: + # Check for write access of basedir and create keyman subdir if we can + if not os.access(basedir, os.X_OK | os.W_OK): + raise InstallError(InstallStatus.Abort, error_message) + os.mkdir(keyman_dir) + +def install_kmp_shared(inputfile, online=False): + """ + Install a kmp file to /usr/local/share/keyman + + Args: + inputfile (str): path to kmp file + online(bool, default=False): whether to attempt to get a source kmn and ico for the keyboard + """ + do_install_to_ibus = False + check_keyman_dir('/usr/local/share', "You do not have permissions to install the keyboard files to the shared area /usr/local/share/keyman") + check_keyman_dir('/usr/local/share/doc', "You do not have permissions to install the documentation to the shared documentation area /usr/local/share/doc/keyman") + check_keyman_dir('/usr/local/share/fonts', "You do not have permissions to install the font files to the shared font area /usr/local/share/fonts") + + kmpid, ext = os.path.splitext(os.path.basename(inputfile)) + kmpdir = os.path.join('/usr/local/share/keyman', kmpid) + kmpdocdir = os.path.join('/usr/local/share/doc/keyman', kmpid) + kmpfontdir = os.path.join('/usr/local/share/fonts/keyman', kmpid) + if not os.path.isdir(kmpdir): + os.makedirs(kmpdir) + extract_kmp(inputfile, kmpdir) + info, system, options, keyboards, files = get_metadata(kmpdir) + + if keyboards: + logging.info("Installing %s", info['name']['description']) + if online: + process_keyboard_data(kmpid, kmpdir) + if len(keyboards) > 1: + for kb in keyboards: + if kb['id'] != kmpid: + process_keyboard_data(kb['id'], kmpdir) + do_install_to_ibus = True + + for f in files: + fpath = os.path.join(kmpdir, f['name']) + ftype = f['type'] + if ftype == KMFileTypes.KM_DOC or ftype == KMFileTypes.KM_IMAGE: + #Special handling of doc and images to hard link them into doc dir + logging.info("Installing %s as documentation", f['name']) + if not os.path.isdir(kmpdocdir): + os.makedirs(kmpdocdir) + os.link(fpath, os.path.join(kmpdocdir, f['name'])) + elif ftype == KMFileTypes.KM_FONT: + #Special handling of font to hard link it into font dir + logging.info("Installing %s as font", f['name']) + if not os.path.isdir(kmpfontdir): + os.makedirs(kmpfontdir) + os.link(fpath, os.path.join(kmpfontdir, f['name'])) + elif ftype == KMFileTypes.KM_SOURCE: + #TODO for the moment just leave it for ibus-kmfl to ignore if it doesn't load + logging.info("Installing %s as keyman file", f['name']) + elif ftype == KMFileTypes.KM_OSK: + # Special handling to convert kvk into LDML + logging.info("Converting %s to LDML and installing both as as keyman file", f['name']) + ldml = convert_kvk_to_ldml(fpath) + name, ext = os.path.splitext(f['name']) + ldmlfile = os.path.join(kmpdir, name+".ldml") + output_ldml(ldmlfile, ldml) + # Special handling of icon to convert to PNG + elif ftype == KMFileTypes.KM_ICON: + logging.info("Converting %s to PNG and installing both as keyman files", f['name']) + checkandsaveico(fpath) + if do_install_to_ibus: + install_to_ibus(kmn_file) + else: + logging.error("install_kmp.py: error: No kmp.json or kmp.inf found in %s", inputfile) + logging.info("Contents of %s:", inputfile) + for o in os.listdir(kmpdir): + logging.info(o) + rmtree(kmpdir) + message = "install_kmp.py: error: No kmp.json or kmp.inf found in %s" % (inputfile) + raise InstallError(InstallStatus.Abort, message) + +def install_kmp_user(inputfile, online=False): + do_install_to_ibus = False + kmpid, ext = os.path.splitext(os.path.basename(inputfile)) + kmpdir=user_keyboard_dir(kmpid) + if not os.path.isdir(kmpdir): + os.makedirs(kmpdir) + + extract_kmp(inputfile, kmpdir) + info, system, options, keyboards, files = get_metadata(kmpdir) + + if keyboards: + logging.info("Installing %s", info['name']['description']) + if online: + process_keyboard_data(kmpid, kmpdir) + if len(keyboards) > 1: + for kb in keyboards: + if kb['id'] != kmpid: + process_keyboard_data(kb['id'], kmpdir) + do_install_to_ibus = True + + for f in files: + fpath = os.path.join(kmpdir, f['name']) + ftype = f['type'] + if ftype == KMFileTypes.KM_FONT: + #Special handling of font to hard link it into font dir + fontsdir = os.path.join(user_keyman_font_dir(), kmpid) + if not os.path.isdir(fontsdir): + os.makedirs(fontsdir) + os.link(fpath, os.path.join(fontsdir, f['name'])) + logging.info("Installing %s as font", f['name']) + elif ftype == KMFileTypes.KM_OSK: + # Special handling to convert kvk into LDML + logging.info("Converting %s to LDML and installing both as as keyman file", f['name']) + ldml = convert_kvk_to_ldml(fpath) + name, ext = os.path.splitext(f['name']) + ldmlfile = os.path.join(kmpdir, name+".ldml") + output_ldml(ldmlfile, ldml) + elif ftype == KMFileTypes.KM_ICON: + # Special handling of icon to convert to PNG + logging.info("Converting %s to PNG and installing both as keyman files", f['name']) + checkandsaveico(fpath) + elif ftype == KMFileTypes.KM_SOURCE: + #TODO for the moment just leave it for ibus-kmfl to ignore if it doesn't load + pass + if do_install_to_ibus: + kmn_file = os.path.join(kmpdir, kmpid + ".kmn") + install_to_ibus(kmn_file) + else: + logging.error("install_kmp.py: error: No kmp.json or kmp.inf found in %s", inputfile) + logging.info("Contents of %s:", inputfile) + for o in os.listdir(kmpdir): + logging.info(o) + rmtree(kmpdir) + message = "install_kmp.py: error: No kmp.json or kmp.inf found in %s" % (inputfile) + raise InstallError(InstallStatus.Abort, message) + + + +def install_to_ibus(kmn_file): + if sys.version_info.major == 3 and sys.version_info.minor < 6: + dconfreadresult = subprocess.run(["dconf", "read", "/desktop/ibus/general/preload-engines"], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT) + dconfread = dconfreadresult.stdout.decode("utf-8", "strict") + else: + dconfreadresult = subprocess.run(["dconf", "read", "/desktop/ibus/general/preload-engines"], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT, encoding="UTF8") + dconfread = dconfreadresult.stdout + if (dconfreadresult.returncode == 0) and dconfread: + preload_engines = literal_eval(dconfread) + preload_engines.append(kmn_file) + logging.info("Installing %s into IBus", kmn_file) + if sys.version_info.major == 3 and sys.version_info.minor < 6: + dconfwriteresult = subprocess.run(["dconf", "write", "/desktop/ibus/general/preload-engines", str(preload_engines)], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT) + else: + dconfwriteresult = subprocess.run(["dconf", "write", "/desktop/ibus/general/preload-engines", str(preload_engines)], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT, encoding="UTF8") + if (dconfwriteresult.returncode == 0): + # restart IBus to be sure the keyboard is installed + ibusrestartresult = subprocess.run(["ibus", "restart"]) + if (ibusrestartresult.returncode != 0): + message = "install_kmp.py: error %d: Could not restart IBus." % (ibusrestartresult.returncode) + raise InstallError(InstallStatus.Continue, message) + else: + message = "install_kmp.py: error %d: Could not install the keyboad to IBus." % (dconfwriteresult.returncode) + raise InstallError(InstallStatus.Continue, message) + else: + message = "install_kmp.py: error %d: Could not read dconf preload-engines entry so cannot install to IBus" % (dconfreadresult.returncode) + raise InstallError(InstallStatus.Continue, message) + + + +def install_kmp(inputfile, online=False, sharedarea=False): + """ + Install a kmp file + + Args: + inputfile (str): path to kmp file + online(bool, default=False): whether to attempt to get a source kmn and ico for the keyboard + sharedarea(bool, default=False): whether install kmp to shared area or user directory + """ + if sharedarea: + install_kmp_shared(inputfile, online) + else: + install_kmp_user(inputfile, online) diff --git a/keyman_config/install_window.py b/keyman_config/install_window.py new file mode 100755 index 0000000..c8958e8 --- /dev/null +++ b/keyman_config/install_window.py @@ -0,0 +1,337 @@ +#!/usr/bin/python3 + +# Install confirmation with details window + +import logging +import os.path +import pathlib +import subprocess +import sys +import webbrowser +import tempfile +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('WebKit', '3.0') +from gi.repository import Gtk, WebKit +from distutils.version import StrictVersion +from keyman_config.install_kmp import install_kmp, extract_kmp, get_metadata, InstallError, InstallStatus +from keyman_config.list_installed_kmp import get_kmp_version +from keyman_config.kmpmetadata import get_fonts +from keyman_config.welcome import WelcomeView +from keyman_config.uninstall_kmp import uninstall_kmp +from keyman_config.get_kmp import get_download_folder, user_keyboard_dir +from keyman_config.check_mime_type import check_mime_type +from keyman_config.accelerators import bind_accelerator, init_accel + +def find_keyman_image(image_file): + img_path = os.path.join("/usr/share/keyman/icons", image_file) + if not os.path.isfile(img_path): + img_path = os.path.join("/usr/local/share/keyman/icons/", image_file) + if not os.path.isfile(img_path): + img_path = os.path.join("keyman_config/icons/", image_file) + if not os.path.isfile(img_path): + img_path = image_file + if not os.path.isfile(img_path): + img_path = os.path.join("icons", image_file) + return img_path + +class InstallKmpWindow(Gtk.Window): + + def __init__(self, kmpfile, online=False, viewkmp=None, downloadwindow=None): + logging.debug("InstallKmpWindow: kmpfile: %s", kmpfile) + self.kmpfile = kmpfile + self.online = online + self.endonclose = False + self.viewwindow = viewkmp + self.download = downloadwindow + self.accelerators = None + keyboardid = os.path.basename(os.path.splitext(kmpfile)[0]) + installed_kmp_ver = get_kmp_version(keyboardid) + if installed_kmp_ver: + logging.info("installed kmp version %s", installed_kmp_ver) + + windowtitle = "Installing keyboard/package " + keyboardid + Gtk.Window.__init__(self, title=windowtitle) + init_accel(self) + + self.set_border_width(12) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + mainhbox = Gtk.Box() + + with tempfile.TemporaryDirectory() as tmpdirname: + extract_kmp(kmpfile, tmpdirname) + info, system, options, keyboards, files = get_metadata(tmpdirname) + self.kbname = keyboards[0]['name'] + self.checkcontinue = True + + if installed_kmp_ver: + if info['version']['description'] == installed_kmp_ver: + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, + Gtk.ButtonsType.YES_NO, "Keyboard is installed already") + dialog.format_secondary_text( + "The " + self.kbname + " keyboard is already installed at version " + installed_kmp_ver + + ". Do you want to uninstall then reinstall it?") + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.YES: + logging.debug("QUESTION dialog closed by clicking YES button") + uninstall_kmp(keyboardid) + elif response == Gtk.ResponseType.NO: + logging.debug("QUESTION dialog closed by clicking NO button") + self.checkcontinue = False + else: + try: + logging.info("package version %s", info['version']['description']) + logging.info("installed kmp version %s", installed_kmp_ver) + if StrictVersion(info['version']['description']) <= StrictVersion(installed_kmp_ver): + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, + Gtk.ButtonsType.YES_NO, "Keyboard is installed already") + dialog.format_secondary_text( + "The " + self.kbname + " keyboard is already installed with a newer version " + installed_kmp_ver + + ". Do you want to uninstall it and install the older version" + info['version']['description'] + "?") + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.YES: + logging.debug("QUESTION dialog closed by clicking YES button") + uninstall_kmp(keyboardid) + elif response == Gtk.ResponseType.NO: + logging.debug("QUESTION dialog closed by clicking NO button") + self.checkcontinue = False + except: + logging.warning("Exception uninstalling an old kmp, continuing") + pass + + image = Gtk.Image() + if options and "graphicFile" in options: + image.set_from_file(os.path.join(tmpdirname, options['graphicFile'])) + else: + img_default = find_keyman_image("defaultpackage.gif") + image.set_from_file(img_default) + + mainhbox.pack_start(image, False, False, 0) + + self.page1 = Gtk.Box() + self.page1.set_border_width(12) + + grid = Gtk.Grid() + self.page1.add(grid) + + label1 = Gtk.Label() + label1.set_text("Keyboard layouts: ") + label1.set_halign(Gtk.Align.END) + grid.add(label1) + prevlabel = label1 + label = Gtk.Label() + keyboardlayout = "" + for kb in keyboards: + if keyboardlayout != "": + keyboardlayout = keyboardlayout + "\n" + keyboardlayout = keyboardlayout + kb['name'] + label.set_text(keyboardlayout) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label1, Gtk.PositionType.RIGHT, 1, 1) + + fonts = get_fonts(files) + if fonts: + label2 = Gtk.Label() + # Fonts are optional + label2.set_text("Fonts: ") + label2.set_halign(Gtk.Align.END) + grid.attach_next_to(label2, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label2 + label = Gtk.Label() + fontlist = "" + for font in fonts: + if fontlist != "": + fontlist = fontlist + "\n" + if font['description'][:5] == "Font ": + fontdesc = font['description'][5:] + else: + fontdesc = font['description'] + fontlist = fontlist + fontdesc + label.set_text(fontlist) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label2, Gtk.PositionType.RIGHT, 1, 1) + + label3 = Gtk.Label() + label3.set_text("Package version: ") + label3.set_halign(Gtk.Align.END) + grid.attach_next_to(label3, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label3 + label = Gtk.Label() + label.set_text(info['version']['description']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label3, Gtk.PositionType.RIGHT, 1, 1) + + if info and 'author' in info: + label4 = Gtk.Label() + label4.set_text("Author: ") + label4.set_halign(Gtk.Align.END) + grid.attach_next_to(label4, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label4 + label = Gtk.Label() + if 'url' in info['author']: + label.set_markup("<a href=\"" + info['author']['url'] + "\" title=\"" + info['author']['url'] + "\">" + info['author']['description'] + "</a>") + else: + label.set_text(info['author']['description']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label4, Gtk.PositionType.RIGHT, 1, 1) + + + if info and 'website' in info: + label5 = Gtk.Label() + # Website is optional and may be a mailto for the author + label5.set_text("Website: ") + label5.set_halign(Gtk.Align.END) + grid.attach_next_to(label5, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label5 + label = Gtk.Label() + label.set_markup("<a href=\"" + info['website']['description'] + "\">" + info['website']['description'] + "</a>") + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label5, Gtk.PositionType.RIGHT, 1, 1) + + if info and 'copyright' in info: + label6 = Gtk.Label() + label6.set_text("Copyright: ") + label6.set_halign(Gtk.Align.END) + grid.attach_next_to(label6, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + label.set_text(info['copyright']['description']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label6, Gtk.PositionType.RIGHT, 1, 1) + + self.page2 = Gtk.Box() + s = Gtk.ScrolledWindow() + webview = WebKit.WebView() + webview.connect("navigation-policy-decision-requested", self.check) + webview.connect("mime-type-policy-decision-requested", check_mime_type) + + if options and "readmeFile" in options: + self.readme = options['readmeFile'] + else: + self.readme = "noreadme" + readme_file = os.path.join(tmpdirname, self.readme) + if os.path.isfile(readme_file): + readme_uri = pathlib.Path(readme_file).as_uri() + webview.load_uri(readme_uri) + s.add(webview) + self.page2.pack_start(s, True, True, 0) + + self.notebook = Gtk.Notebook() + self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) + mainhbox.pack_start(self.notebook, True, True, 0) + self.notebook.append_page( + self.page1, + Gtk.Label('Details')) + self.notebook.append_page( + self.page2, + Gtk.Label('README')) + else: + mainhbox.pack_start(self.page1, True, True, 0) + vbox.pack_start(mainhbox, True, True, 0) + + hbox = Gtk.Box(spacing=6) + vbox.pack_start(hbox, False, False, 0) + + button = Gtk.Button.new_with_mnemonic("_Install") + button.connect("clicked", self.on_install_clicked) + hbox.pack_start(button, False, False, 0) + + button = Gtk.Button.new_with_mnemonic("_Cancel") + button.connect("clicked", self.on_cancel_clicked) + hbox.pack_end(button, False, False, 0) + bind_accelerator(self.accelerators, button, '<Control>w') + + self.add(vbox) + self.resize(635, 270) + + def check(self, view, frame, req, nav, policy): + uri = req.get_uri() + if not self.readme in uri: + webbrowser.open(uri) + policy.ignore() + return True + return False + + def on_install_clicked(self, button): + logging.info("Installing keyboard") + try: + install_kmp(self.kmpfile, self.online) + if self.viewwindow: + self.viewwindow.refresh_installed_kmp() + if self.download: + self.download.close() + keyboardid = os.path.basename(os.path.splitext(self.kmpfile)[0]) + welcome_file = os.path.join(user_keyboard_dir(keyboardid), "welcome.htm") + if os.path.isfile(welcome_file): + uri_path = pathlib.Path(welcome_file).as_uri() + logging.debug(uri_path) + w = WelcomeView(uri_path, self.kbname) + w.resize(800, 600) + w.show_all() + else: + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, + Gtk.ButtonsType.OK, "Keyboard " + self.kbname + " installed") + dialog.run() + dialog.destroy() + except InstallError as e: + if e.status == InstallStatus.Abort: + message = "Keyboard " + self.kbname + " could not be installed.\n\nError Message:\n%s" % (e.message) + logging.error(message) + message_type = Gtk.MessageType.ERROR + else: + message = "Keyboard " + self.kbname + " could not be installed fully.\n\nWarning Message:\n%s" % (e.message) + logging.warning(message) + message_type = Gtk.MessageType.WARNING + dialog = Gtk.MessageDialog(self, 0, message_type, + Gtk.ButtonsType.OK, message) + dialog.run() + dialog.destroy() + if not self.endonclose: + self.close() + + def on_cancel_clicked(self, button): + logging.info("Cancel install keyboard") + if self.endonclose: + Gtk.main_quit() + else: + self.close() + + def connectdestroy(self): + self.connect("destroy", Gtk.main_quit) + self.endonclose = True + + +def main(argv): + if len(sys.argv) != 2: + logging.error("install_window.py <kmpfile>") + sys.exit(2) + + name, ext = os.path.splitext(sys.argv[1]) + if ext != ".kmp": + logging.error("install_window.py Input file", sys.argv[1], "is not a kmp file.") + logging.error("install_window.py <kmpfile>") + sys.exit(2) + + if not os.path.isfile(sys.argv[1]): + logging.error("install_window.py Keyman kmp file", sys.argv[1], "not found.") + logging.error("install_window.py <kmpfile>") + sys.exit(2) + + w = InstallKmpWindow(sys.argv[1]) + w.connectdestroy() + w.resize(800, 450) + if w.checkcontinue: + w.show_all() + Gtk.main() + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/keyman_config/keyboard_details.py b/keyman_config/keyboard_details.py new file mode 100644 index 0000000..876f372 --- /dev/null +++ b/keyman_config/keyboard_details.py @@ -0,0 +1,164 @@ +#!/usr/bin/python3 + +# Keyboard details window + +import gi +import os.path +gi.require_version('Gtk', '3.0') +gi.require_version('WebKit', '3.0') +from gi.repository import Gtk, WebKit + +# basics: keyboard name, package version, description +# other things: filename (of kmx), , +# OSK availability, documentation availability, package copyright +# also: supported languages, fonts +# from kmx?: keyboard version, encoding, layout type + +# there is data in kmp.inf/kmp.json +# there is possibly data in kbid.json (downloaded from api) + +class KeyboardDetailsView(Gtk.Window): + # TODO Display all the information that is available + # especially what is displayed for Keyman on Windows + # TODO clean up file once have what we want + def __init__(self, kmp): + if "keyboard" in kmp["name"].lower(): + wintitle = kmp["name"] + else: + wintitle = kmp["name"] + " keyboard" + Gtk.Window.__init__(self, title=wintitle) + + box = Gtk.Box(spacing=10) + self.add(box) + grid = Gtk.Grid() + #grid.set_column_homogeneous(True) + + box.add(grid) + #self.add(grid) + + # kbdatapath = os.path.join("/usr/local/share/keyman", kmp["id"], kmp["id"] + ".json") + + # show the icon somewhere + + label1 = Gtk.Label() + # where is the info about the kmx file? + label1.set_text("Filename: ") + label1.set_halign(Gtk.Align.END) + grid.add(label1) + prevlabel = label1 + # label = Gtk.Label() + # label.set_text(info['version']['description']) + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label1, Gtk.PositionType.RIGHT, 1, 1) + + label2 = Gtk.Label() + # stored in kmx + label2.set_text("Keyboard version: ") + label2.set_halign(Gtk.Align.END) + grid.attach_next_to(label2, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label2 + # label = Gtk.Label() + # label.set_text(info['version']['description']) + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label2, Gtk.PositionType.RIGHT, 1, 1) + + label3 = Gtk.Label() + label3.set_text("Package: ") + label3.set_halign(Gtk.Align.END) + grid.attach_next_to(label3, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label3 + label = Gtk.Label() + label.set_text(kmp['name']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label3, Gtk.PositionType.RIGHT, 1, 1) + + label4 = Gtk.Label() + label4.set_text("Package version: ") + label4.set_halign(Gtk.Align.END) + grid.attach_next_to(label4, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label4 + label = Gtk.Label() + label.set_text(kmp['version']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label4, Gtk.PositionType.RIGHT, 1, 1) + + label5 = Gtk.Label() + # stored in kmx + label5.set_text("Encodings: ") + label5.set_halign(Gtk.Align.END) + grid.attach_next_to(label5, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label5 + label = Gtk.Label() + label.set_text("Unicode") # assumed for now! + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label5, Gtk.PositionType.RIGHT, 1, 1) + + label6 = Gtk.Label() + # stored in kmx + label6.set_text("Layout Type: ") + label6.set_halign(Gtk.Align.END) + grid.attach_next_to(label6, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label6 + # label = Gtk.Label() + # label.set_text(info['version']['description']) + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label6, Gtk.PositionType.RIGHT, 1, 1) + + label7 = Gtk.Label() + label7.set_text("On Screen Keyboard: ") + label7.set_halign(Gtk.Align.END) + grid.attach_next_to(label7, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label7 + # label = Gtk.Label() + # label.set_text(info['version']['description']) + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label7, Gtk.PositionType.RIGHT, 1, 1) + + label8 = Gtk.Label() + label8.set_text("Documentation: ") + label8.set_halign(Gtk.Align.END) + grid.attach_next_to(label8, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label8 + #TODO need to know which area keyboard is installed in to show this + # label = Gtk.Label() + # welcome_file = os.path.join("/usr/local/share/doc/keyman", kmp["id"], "welcome.htm") + # if os.path.isfile(welcome_file): + # label.set_text("Installed") + # else: + # label.set_text("Not installed") + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label8, Gtk.PositionType.RIGHT, 1, 1) + + label9 = Gtk.Label() + # stored in kmx + label9.set_text("Message: ") + label9.set_halign(Gtk.Align.END) + grid.attach_next_to(label9, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label9 + label = Gtk.Label() + label.set_line_wrap(True) + label.set_text("This keyboard is distributed under the MIT license (MIT) as described somewhere") + #label.set_text(kmp["description"]) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label9, Gtk.PositionType.RIGHT, 1, 1) + + label10 = Gtk.Label() + # where is the copyright in the data, or get it from kmx? + label10.set_text("Copyright: ") + label10.set_halign(Gtk.Align.END) + grid.attach_next_to(label10, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + prevlabel = label10 + # label = Gtk.Label() + # label.set_text(kmp['copyright']) + # label.set_halign(Gtk.Align.START) + # label.set_selectable(True) + # grid.attach_next_to(label, label10, Gtk.PositionType.RIGHT, 1, 1) diff --git a/keyman_config/kmpmetadata.py b/keyman_config/kmpmetadata.py new file mode 100755 index 0000000..c742142 --- /dev/null +++ b/keyman_config/kmpmetadata.py @@ -0,0 +1,485 @@ +#!/usr/bin/python3 + +import json +import configparser +import logging +import sys +import os.path +import magic +from enum import Enum + +class KMFileTypes(Enum): + KM_ICON = 1 + KM_SOURCE = 2 + KM_OSK_SOURCE = 3 + KM_KMX = 4 + KM_OSK = 5 + KM_TOUCH = 6 + KM_FONT = 7 + KM_DOC = 8 + KM_META = 9 + KM_IMAGE = 10 + KM_TECKIT = 11 + KM_CC = 12 + KM_XML = 13 + KM_UNKNOWN = 99 + + +def print_info(info): + try: + print("---- Info ----") + print("Name: ", info['name']['description']) + print("Copyright: ", info['copyright']['description']) + if 'version' in info: + print("Version: ", info['version']['description']) + if 'author' in info: + print("Author: ", info['author']['description']) + if 'url' in info['author']: + print("Author URL: ", info['author']['url']) + if 'website' in info: + print("Website description: ", info['website']['description']) + if 'url' in info['website']: + print("Website URL: ", info['website']['url']) + except Exception as e: + print(type(e)) # the exception instance + print(e.args) # arguments stored in .args + print(e) # __str__ allows args to be printed directly, pass + pass + +def print_system(system): + try: + print("---- System ----") + if 'fileVersion' in system: + print("File Version: ", system['fileVersion']) + if 'keymanDeveloperVersion' in system: + print("Keyman Developer Version: ", system['keymanDeveloperVersion']) + except Exception as e: + print(type(e)) # the exception instance + print(e.args) # arguments stored in .args + print(e) # __str__ allows args to be printed directly, pass + pass + +def print_options(options): + try: + print("---- Options ----") + if 'readmeFile' in options: + print("Readme File: ", options['readmeFile']) + if 'graphicFile' in options: + print("Graphic File: ", options['graphicFile']) + except Exception as e: + print(type(e)) # the exception instance + print(e.args) # arguments stored in .args + print(e) # __str__ allows args to be printed directly, pass + pass + +def print_keyboards(keyboards): + try: + print("---- Keyboards ----") + for kb in keyboards: + print("Keyboard Name: ", kb['name']) + print("Keyboard Id: ", kb['id']) + if 'version' in kb: + print("Keyboard Version: ", kb['version']) + if 'oskFont' in kb: + print("Keyboard On screen keyboard Font: ", kb['oskFont']) + if 'oskFont' in kb: + print("Keyboard Display Font: ", kb['displayFont']) + print("Languages") + for lang in kb['languages']: + print(" Name: ", lang['name'], "Id: ", lang['id']) + except Exception as e: + print(type(e)) # the exception instance + print(e.args) # arguments stored in .args + print(e) # __str__ allows args to be printed directly, pass + pass + +def determine_filetype(kblist, filename): + """ + Determine file type of a filename in a kmp from the extension + + Args: + kblist (list): list of keyboard ids + filename (str): File name + + Returns: + KMFileTypes: Enum of file type + KM_ICON: Keyboard icon + KM_SOURCE: Keyboard source + KM_OSK_SOURCE: Keyboard on-screen keyboard source + KM_KMX: Compiled keyboard + KM_OSK: Compiled on screen keyboard + KM_TOUCH: JS touch keyboard + KM_FONT: Font + KM_DOC: Documentation + KM_META: Metadata + KM_IMAGE: Image + KM_TECKIT: Files to use with teckit + KM_CC: Consistent changes tables + KM_XML: unspecified xml files + KM_UNKNOWN: Unknown + """ + name, ext = os.path.splitext(filename) + if ext.lower() == ".ico": + return KMFileTypes.KM_ICON + elif ext.lower() == ".kmn": + return KMFileTypes.KM_SOURCE + elif ext.lower() == ".kvks": + return KMFileTypes.KM_OSK_SOURCE + elif ext.lower() == ".kmx": + return KMFileTypes.KM_KMX + elif ext.lower() == ".kvk": + return KMFileTypes.KM_OSK + elif ext.lower() == ".ttf" or ext.lower() == ".otf": + return KMFileTypes.KM_FONT + elif ext.lower() == ".js": + if kblist is None: + return KMFileTypes.KM_UNKNOWN + if name in kblist: + return KMFileTypes.KM_TOUCH + else: + if name == "keyrenderer": # currently 2018-09-21 this is the own known non touch js file + return KMFileTypes.KM_DOC + else: + return KMFileTypes.KM_UNKNOWN + elif ext.lower() == ".txt" or ext.lower() == ".pdf" or ext.lower() == ".htm" \ + or ext.lower() == ".html" or ext.lower() == ".doc" or ext.lower() == ".docx" \ + or ext.lower() == ".css" or ext.lower() == ".chm" or ext.lower() == "" \ + or ext.lower() == ".md" or ext.lower() == ".odt" or ext.lower() == ".rtf" \ + or ext.lower() == ".dot" or ext.lower() == ".mht" or ext.lower() == ".woff" \ + or ext.lower() == ".php": + return KMFileTypes.KM_DOC + elif ext.lower() == ".inf" or ext.lower() == ".json": + return KMFileTypes.KM_META + elif ext.lower() == ".png" or ext.lower() == ".jpeg" \ + or ext.lower() == ".jpg" or ext.lower() == ".gif" \ + or ext.lower() == ".bmp": + return KMFileTypes.KM_IMAGE + elif ext.lower() == ".tec" or ext.lower() == ".map": + return KMFileTypes.KM_TECKIT + elif ext.lower() == ".cct": + return KMFileTypes.KM_CC + elif ext.lower() == ".xml": + return KMFileTypes.KM_XML + else: + return KMFileTypes.KM_UNKNOWN + +def print_files(files, extracted_dir): + try: + print("---- Files ----") + for kbfile in files: + print("* File name: ", kbfile['name']) + print(" Description: ", kbfile['description']) + print(" Type: ", kbfile['type']) + file = os.path.join(extracted_dir, kbfile['name']) + if os.path.isfile(file): + print(" File", file, "exists") + ms = magic.open(magic.MAGIC_NONE) + ms.load() + ftype = ms.file(file) + print (" Type: ", ftype) + ms.close() + else: + print(" File", file, "does not exist") + except Exception as e: + print(type(e)) # the exception instance + print(e.args) # arguments stored in .args + print(e) # __str__ allows args to be printed directly, pass + pass + +def get_fonts(files): + fonts = [] + for kbfile in files: + if kbfile['type'] == KMFileTypes.KM_FONT: + fonts.append(kbfile) + return fonts + +def parseinfdata(inffile, verbose=False): + """ + Parse the metadata in a kmp.inf file. + + Args: + jsonfile (str): Path to kmp.inf + verbose (bool, default False): verbose output + + Returns: + list[5]: info, system, options, keyboards, files + info (dict): + name (dict): + description (str): KMP name + copyright (dict): + description (str): KMP copyright + version (dict): + description (str): KMP version + author (dict): + description (str): KMP author + url (str): contact url for the author + system (dict): System info + fileVersion (str): Keyman file format version + keymanDeveloperVersion (str): Keyman Developer version that compiled keyboard + options (dict): Keyboard options + readmeFile (str) : README for the keyboard + keyboards (list): Keyboards in the kmp + name (str): Keyboard name + id (str): Keyboard ID + version (str): Keyboard version + oskFont (str, optional): Recommended on screen keyboard font + displayFont (str, optional): Recommended display font + languages (list): Languages the keyboard is used for + name (str): Language name + id (str): Language ID + files (list): Files in the kmp + name (str): File name + description (str): File description + type (KMFileTypes): Keyman file type + """ + info = system = keyboards = files = options = nonexistent = None + extracted_dir = os.path.dirname(inffile) + + if os.path.isfile(inffile): + config = configparser.ConfigParser() + config.optionxform = str + logging.debug("parseinfdata: reading file:%s dir:%s", inffile, extracted_dir) + + with open(inffile, 'r', encoding='latin_1') as f: + config.read_file(f) + + info = None + for section in config.sections(): + if section == 'Info': + if not info: + info = {} + for item in config.items('Info'): + if item[0] == 'Name' or item[0] == 'name': + info['name'] = { 'description' : item[1].split("\"")[1] } + elif item[0] == 'Copyright' or item[0] == 'copyright': + info['copyright'] = { 'description' : item[1].split("\"")[1] } + elif item[0] == 'Version': + info['version'] = { 'description' : item[1].split("\"")[1] } + elif item[0] == 'Author': + info['author'] = { 'description' : item[1].split("\"")[1], 'url' : item[1].split("\"")[3] } + elif item[0] == "WebSite": + info['website'] = { 'description' : item[1].split("\"")[1], 'url' : item[1].split("\"")[3] } + else: + logging.warning("Unknown item in Info: %s", item[0]) + if section == 'PackageInfo': + if not info: + info = {} + info['version'] = { 'description' : "1.0" } + for item in config.items('PackageInfo'): + if item[0] == 'Name' or item[0] == 'name': + if item[1].split("\"")[1]: + info['name'] = { 'description' : item[1].split("\"")[1] } + else: + info['name'] = { 'description' : item[1].split("\"")[2] } + elif item[0] == 'Copyright' or item[0] == 'copyright': + if item[1].split("\"")[1]: + info['copyright'] = { 'description' : item[1].split("\"")[1] } + else: + info['copyright'] = { 'description' : item[1].split("\"")[2] } + elif item[0] == 'Version': + info['version'] = { 'description' : item[1].split("\"")[1] } + elif item[0] == 'Author': + info['author'] = { 'description' : item[1].split("\"")[1], 'url' : item[1].split("\"")[3] } + elif item[0] == "WebSite": + if item[1].split("\"")[1]: + info['website'] = { 'description' : item[1].split("\"")[1] } + else: + info['website'] = { 'description' : item[1].split("\"")[2] } + else: + logging.warning("Unknown item in Info: %s", item[0]) + elif section == 'Package': + system = {} + if not options: + options = {} + for item in config.items('Package'): + if item[0] == 'Version': + system['fileVersion'] = item[1] + elif item[0] == 'ReadMeFile': + options['readmeFile'] = item[1] + elif item[0] == 'GraphicFile': + options['graphicFile'] = item[1] + elif item[0] == 'DisableKeepFonts': + options['disableKeepFonts'] = item[1] + elif item[0] == 'BothVersionsIncluded': + options['bothVersionsIncluded'] = item[1] + elif item[0] == 'ExecuteProgram': + pass + else: + print("Unknown item in Package:", item[0]) + system['keymanDeveloperVersion'] = "" + elif "Keyboard" in section: + keyboards = [] + keyboard = {} + languages = [] + for item in config.items(section): + if item[0] == 'Name': + keyboard['name'] = item[1] + elif item[0] == 'ID': + keyboard['id'] = item[1] + elif item[0] == 'Version': + keyboard['version'] = item[1] + elif item[0] == 'OSKFont': + keyboard['oskFont'] = item[1] + elif item[0] == 'DisplayFont': + keyboard['displayFont'] = item[1] + elif item[0] == 'RTL': + keyboard['RTL'] = item[1] + elif "Language" in item[0]: + # only split on first ',' + langid, langname = item[1].split(",", 1) + languages.append({ 'name' : langname, 'id' : langid }) + else: + logging.warning("Unknown item in keyboard: %s", item[0]) + keyboard['languages'] = languages + keyboards.append(keyboard) + elif section == "Files": + files = [] + for item in config.items(section): + splititem = item[1].split("\"") + kbfile = { 'name' : splititem[3], 'description' : splititem[1], 'type' : determine_filetype(None, splititem[3]) } + files.append(kbfile) + elif section == "InstallFiles": + files = [] + for item in config.items(section): + kbfile = { 'name' : item[0], 'description' : item[1], 'type' : determine_filetype(None, item[0]) } + files.append(kbfile) + elif section == 'Install': + if not options: + options = {} + for item in config.items('Install'): + if item[0] == 'ReadmeFile': + options['readmeFile'] = item[1] + kblist = [] + + if not info: + info = {} + if not 'version' in info: + info['version'] = { 'description' : "1.0" } + # inf file may not have keyboards in legacy kmps so generate it if needed + if files and not keyboards: + id = "unknown" + keyboards = [] + for kbfile in files: + if kbfile['type'] == KMFileTypes.KM_KMX: + id = os.path.basename(os.path.splitext(kbfile['name'])[0]) + keyboards = [ { 'name' : id, + 'id' : id, + 'version' : info['version']['description'] } ] + + kblist = [] + if files: + for k in keyboards: + kblist.append(k['id']) + for kbfile in files: + if kbfile['type'] == KMFileTypes.KM_UNKNOWN: + kbfile['type'] = determine_filetype(kblist, kbfile['name']) + logging.debug("finished parsing %s", inffile) + + if verbose: + print_info(info) + print_system(system) + print_options(options) + print_keyboards(keyboards) + print_files(files, extracted_dir) + + return info, system, options, keyboards, files + +def parsemetadata(jsonfile, verbose=False): + """ + Parse the metadata in a kmp.json file. + + Args: + jsonfile (str): Path to kmp.json + verbose (bool, default False): verbose output + + Returns: + list[5]: info, system, options, keyboards, files + info (dict): + name (dict): + description (str): KMP name + copyright (dict): + description (str): KMP copyright + version (dict): + description (str): KMP version + author (dict): + description (str): KMP author + url (str): contact url for the author + system (dict): System info + fileVersion (str): Keyman file format version + keymanDeveloperVersion (str): Keyman Developer version that compiled keyboard + options (dict): Keyboard options + readmeFile (str) : README for the keyboard + keyboards (list): Keyboards in the kmp + name (str): Keyboard name + id (str): Keyboard ID + version (str): Keyboard version + oskFont (str, optional): Recommended on screen keyboard font + displayFont (str, optional): Recommended display font + languages (list): Languages the keyboard is used for + name (str): Language name + id (str): Language ID + files (list): Files in the kmp + name (str): File name + description (str): File description + """ + info = system = keyboards = files = options = nonexistent = None + extracted_dir = os.path.dirname(jsonfile) + + logging.debug("parsemetadata: reading file:%s dir:%s", jsonfile, extracted_dir) + + if os.path.isfile(jsonfile): + with open(jsonfile, "r") as read_file: + data = json.load(read_file) + for x in data: + if x == 'info': + info = data[x] + elif x == 'system': + system = data[x] + elif x == 'keyboards': + keyboards = data[x] + elif x == 'files': + files = data[x] + elif x == 'options': + options = data[x] + elif x == 'nonexistent': + nonexistent = data[x] + kblist = [] + for k in keyboards: + kblist.append(k['id']) + for kbfile in files: + kbfile['type'] = determine_filetype(kblist, kbfile['name']) + if nonexistent != None: + logging.warning("This should not happen") + if verbose: + print_info(info) + print_system(system) + if options: + print_options(options) + print_keyboards(keyboards) + print_files(files, extracted_dir) + return info, system, options, keyboards, files + +def main(argv): + if len(sys.argv) != 2: + logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>") + sys.exit(2) + inputfile = sys.argv[1] + + if not os.path.isfile(inputfile): + logging.error("kmpmetadata.py Input file ", inputfile, " not found.") + logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>") + sys.exit(2) + + name, ext = os.path.splitext(inputfile) + if ext == ".json": + parsemetadata(inputfile, True) + elif ext == ".inf": + parseinfdata(inputfile, True) + else: + logging.error("kmpmetadata.py Input file must be json or inf.") + logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>") + sys.exit(2) + +if __name__ == "__main__": + main(sys.argv[1:]) + diff --git a/keyman_config/kvk2ldml.py b/keyman_config/kvk2ldml.py new file mode 100755 index 0000000..d9be84b --- /dev/null +++ b/keyman_config/kvk2ldml.py @@ -0,0 +1,351 @@ +#!/usr/bin/python3 + +import argparse +import logging +import os.path +import struct +import sys +from lxml import etree + +# .kvk file format +# KVK files are variable length files with variable sized structures. + +# Magic 4 bytes 'KVKF' +# Version 4 bytes 0x600 +# Flags 1 byte bitmask: [102key?, DisplayUnderlying?, UseUnderlying?, AltGr?] +kvkk102key = b'\x01' +kvkkDisplayUnderlying = b'\x02' +kvkkUseUnderlying = b'\x04' +kvkkAltGr = b'\x08' +# AssociatedKeyboard NSTRING +# AnsiFont NFONT +# UnicodeFont NFONT + +# KeyCount: DWORD +# Keys: NKEY[KeyCount] + +class KVKData: + magic = "" + version = None + flags = 0 + key102 = False + DisplayUnderlying = False + UseUnderlying = False + AltGr = False + AssociatedKeyboard = "" + AnsiFont = None + UnicodeFont = None + KeyCount = 0 + Keys = [] + + +# NSTRING = (Length: Word; Chars: WChar[Length]) +# NFONT = (Name: NSTRING; Size: DWORD; Color: DWORD (RGBQuad)) + +class NFont: + name = "" + size = 0 + red = 0 + green = 0 + blue = 0 + resv = 0 + +# NKEY = ( +# Flags: BYTE; // 1:kvkkBitmap, 2:kvkkUnicode +kvkkBitmap = b'\x01' +kvkkUnicode = b'\x02' +# Shift: WORD; // See KVKS_* below +# VKey: WORD; +# Text: NSTRING; +# Bitmap: NBITMAP +# ) +# NBITMAP = (BitmapSize: DWORD; Bitmap: BYTE[BitmapSize]) + +class NKey: + number = 0 + flags = 0 + hasBitmap = False + hasUnicode = False + shiftflags = 0 + Normal = False + Shift = False + Ctrl = False + Alt = False + LCtrl = False + RCtrl = False + LAlt = False + RAlt = False + VKey = 0 + text = "" + bitmap = None + +# // Note that these differ from the KMX modifier bitmasks +# KVKS_NORMAL = 0; +# KVKS_SHIFT = 1; +# KVKS_CTRL = 2; +# KVKS_ALT = 4; +# KVKS_LCTRL = 8; +# KVKS_RCTRL = 16; +# KVKS_LALT = 32; +# KVKS_RALT = 64; +KVKS_NORMAL = b'\x00' +KVKS_SHIFT = b'\x01' +KVKS_CTRL = b'\x02' +KVKS_ALT = b'\x04' +KVKS_LCTRL = b'\x08' +KVKS_RCTRL = b'\x10' +KVKS_LALT = b'\x20' +KVKS_RALT= b'\x40' + + +# from web/source/kmwosk.ts +VKey_to_Iso = { + 90 : "B01", # Z + 88 : "B02", # X + 67 : "B03", # C + 86 : "B04", # V + 66 : "B05", # B + 78 : "B06", # N + 77 : "B07", # M + 188 : "B08", # , + 190 : "B09", # . + 191 : "B10", # / + 65 : "C01", # A + 83 : "C02", # S + 68 : "C03", # D + 70 : "C04", # F + 71 : "C05", # G + 72 : "C06", # H + 74 : "C07", # J + 75 : "C08", # K + 76 : "C09", # L + 186 : "C10", # ; + 222 : "C11", # ' + 81 : "D01", # Q + 87 : "D02", # W + 69 : "D03", # E + 82 : "D04", # R + 84 : "D05", # T + 89 : "D06", # Y + 85 : "D07", # U + 73 : "D08", # I + 79 : "D09", # O + 80 : "D10", # P + 219 : "D11", # [ + 221: "D12", # ] + 49 : "E01", # 1 + 50 : "E02", # 2 + 51 : "E03", # 3 + 52 : "E04", # 4 + 53 : "E05", # 5 + 54 : "E06", # 6 + 55 : "E07", # 7 + 56 : "E08", # 8 + 57 : "E09", # 9 + 48 : "E10", # 0 + 189 : "E11", # - + 187 : "E12", # = + 192 : "E00", # ` + 220 : "B00", # \ + 226 : "C12", # extra key on european keyboards + 32 : "A03" # space +} + + +def bytecheck(value, check): + if bytes([value & check[0]]) == check: + return True + else: + return False + +def get_nkey(file, fileContent, offset): + nkey = NKey() + data = struct.unpack_from("<B2H", fileContent, offset) + + nkey.flags = data[0] + nkey.hasBitmap = bytecheck(data[0], kvkkBitmap) + nkey.hasUnicode = bytecheck(data[0], kvkkUnicode) + + nkey.shiftflags = data[1] + if data[1] == 0: + nkey.Normal = True + nkey.Shift = bytecheck(data[1], KVKS_SHIFT) + nkey.Ctrl = bytecheck(data[1], KVKS_CTRL) + nkey.Alt = bytecheck(data[1], KVKS_ALT) + nkey.LCtrl = bytecheck(data[1], KVKS_LCTRL) + nkey.RCtrl = bytecheck(data[1], KVKS_RCTRL) + nkey.LAlt = bytecheck(data[1], KVKS_LALT) + nkey.RAlt = bytecheck(data[1], KVKS_RALT) + + nkey.VKey = data[2] + + nkey.text, newoffset = get_nstring(file, fileContent, offset + struct.calcsize("<B2H")) + nkey.bitmap, newoffset = get_nbitmap(file, fileContent, newoffset) + + return nkey, newoffset + + +def get_nfont(file, fileContent, offset): + nfont = NFont() + nfont.name, curoffset = get_nstring(file, fileContent, offset) + data = struct.unpack_from("<L4B", fileContent, curoffset) + nfont.resv = data[4] + nfont.blue = data[1] + nfont.green = data[2] + nfont.red = data[3] + nfont.size = data[0] + return nfont, curoffset + struct.calcsize("<L4B") + +def get_nstring(file, fileContent, offset): + stringlength = struct.unpack_from("<H", fileContent, offset) + file.seek(offset+2) + if stringlength[0] > 256: + logging.error("error: suspiciously long string. ABORT.") + sys.exit(5) + if stringlength[0]: + #don't read the null string terminator + stringdata = file.read((stringlength[0]-1)*2) + else: + stringdata = file.read(0) + return stringdata.decode('utf-16'), offset + 2 + (2 * stringlength[0]) + +def get_nbitmap(file, fileContent, offset): + bitmap = None + bitmaplength = struct.unpack_from("<I", fileContent, offset) + file.seek(offset + struct.calcsize("<I")) + bitmap = file.read(bitmaplength[0]) + return bitmap, offset + struct.calcsize("<I") + bitmaplength[0] + +def print_kvk(kvkData, allkeys=False): + print("keyboard:", kvkData.AssociatedKeyboard) + print("version", kvkData.version) + print("flags:", kvkData.flags) + if kvkData.key102: + print(" keyboard has 102 keys?") + if kvkData.DisplayUnderlying: + print(" keyboard displays underlying?") + if kvkData.UseUnderlying: + print(" keyboard uses underlying?") + if kvkData.AltGr: + print(" keyboard uses AltGr?") + + print("AnsiFont:", kvkData.AnsiFont.name) + print(" size:", kvkData.AnsiFont.size) + print(" colour: r:%d g:%d b:%d a:%d" % (kvkData.AnsiFont.red, kvkData.AnsiFont.green, kvkData.AnsiFont.blue, kvkData.AnsiFont.resv)) + print("UnicodeFont:", kvkData.UnicodeFont.name) + print(" size:", kvkData.UnicodeFont.size) + print(" colour: r:%d g:%d b:%d a:%d" % (kvkData.UnicodeFont.red, kvkData.UnicodeFont.green, kvkData.UnicodeFont.blue, kvkData.UnicodeFont.resv)) + print("numkeys:", kvkData.KeyCount) + if allkeys: + for key in kvkData.Keys: + print("number:", key.number) + if key.hasBitmap: + print(" key has bitmap") + if key.hasUnicode: + print(" key has unicode text") + if key.Normal: + print(" normal key") + if key.Shift: + print(" shift key") + if key.Ctrl: + print(" ctrl key") + if key.Alt: + print(" alt key") + if key.LCtrl: + print(" left ctrl key") + if key.RCtrl: + print(" right ctrl key") + if key.LAlt: + print(" left alt key") + if key.RAlt: + print(" right alt key") + print(" vkey:", key.VKey) + print(" text:", key.text) + +def plus_join(modifier, name): + if modifier: + modifier = modifier + "+" + modifier = modifier + name + return modifier + +def get_modifer(key): + plus = "+" + modifier = "" + if key.Normal: + return "None" + if key.Shift: + modifier = plus_join(modifier, "shift") + if key.Ctrl: + modifier = plus_join(modifier, "ctrl") + if key.Alt: + modifier = plus_join(modifier, "alt") + if key.LCtrl: + modifier = plus_join(modifier, "ctrlL") + if key.RCtrl: + modifier = plus_join(modifier, "ctrlR") + if key.LAlt: + modifier = plus_join(modifier, "altL") + if key.RAlt: + modifier = plus_join(modifier, "altR") + return modifier + +def convert_ldml(kvkData): + keymaps = {} + + for key in kvkData.Keys: + modifier = get_modifer(key) + if modifier in keymaps: + keymaps[modifier] = keymaps[modifier] + (key,) + else: + keymaps[modifier] = (key,) + + ldml = etree.Element("keyboard", locale = "zzz-keyman") + etree.SubElement(ldml, "version", platform = "10") + names = etree.SubElement(ldml, "names") + names.append( etree.Element("name", value = "ZZZ") ) + + for modifier in keymaps: + if modifier == "None": + keymap = etree.SubElement(ldml, "keyMap") + else: + keymap = etree.SubElement(ldml, "keyMap", modifiers = modifier) + for key in keymaps[modifier]: + if key.VKey in VKey_to_Iso: + iso_key = VKey_to_Iso[key.VKey] + keymap.append( etree.Element("map", iso = iso_key, to = key.text) ) + else: + logging.warning("Unknown vkey: %s", key.VKey) + return ldml + +def output_ldml(ldmlfile, ldml): + etree.ElementTree(ldml).write(ldmlfile, pretty_print=True) + +def parse_kvk_file(kvkfile): + kvkData = KVKData() + with open(kvkfile, mode='rb') as file: # b is important -> binary + fileContent = file.read() + + kvkstart = struct.unpack_from("<4s4cc", fileContent, 0) + kvkData.version = (kvkstart[1], kvkstart[2], kvkstart[3], kvkstart[4]) + kvkData.flags = kvkstart[5] + kvkData.key102 = bytecheck(kvkData.flags[0], kvkk102key) + kvkData.DisplayUnderlying = bytecheck(kvkData.flags[0], kvkkDisplayUnderlying) + kvkData.UseUnderlying = bytecheck(kvkData.flags[0], kvkkUseUnderlying) + kvkData.AltGr = bytecheck(kvkData.flags[0], kvkkAltGr) + + kvkData.AssociatedKeyboard, newoffset = get_nstring(file, fileContent, struct.calcsize("<4s4cc")) + kvkData.AnsiFont, newoffset = get_nfont(file, fileContent, newoffset) + kvkData.UnicodeFont, newoffset = get_nfont(file, fileContent, newoffset) + numkeys = struct.unpack_from("I", fileContent, newoffset) + kvkData.KeyCount = numkeys[0] + newoffset = newoffset + struct.calcsize("I") + + for num in range(numkeys[0]): + nkey, newoffset = get_nkey(file, fileContent, newoffset) + nkey.number = num + kvkData.Keys.append(nkey) + return kvkData + +def convert_kvk_to_ldml(kvkfile): + kvkData = parse_kvk_file(kvkfile) + return convert_ldml(kvkData)
\ No newline at end of file diff --git a/keyman_config/list_installed_kmp.py b/keyman_config/list_installed_kmp.py new file mode 100755 index 0000000..ae01759 --- /dev/null +++ b/keyman_config/list_installed_kmp.py @@ -0,0 +1,150 @@ +#!/usr/bin/python3 + +import argparse +import logging +import os +import json +import gi +from gi.repository import GObject +from keyman_config.kmpmetadata import parsemetadata, parseinfdata +from keyman_config.get_kmp import user_keyman_dir + +class InstallArea(GObject.GEnum): + IA_OS = 1 + IA_SHARED = 2 + IA_USER = 3 + IA_UNKNOWN = 99 + +def get_installed_kmp(area): + """ + Get list of installed keyboards in an install area. + + Args: + area (InstallArea): install area to check + InstallArea.IA_USER: ~/.local/share/keyman and ~/.kmfl + InstallArea.IA_SHARED: /usr/local/share/keyman + InstallArea.IA_OS: /usr/share/keyman + Returns: + list: Installed keyboards + dict: Keyboard + id (str): Keyboard ID + name (str): Keyboard name + kmpname (str): Keyboard name in local + version (str): Keyboard version + kmpversion (str): + path (str): base path where keyboard is installed + description (str): Keyboard description + """ + check_paths = [] + if area == InstallArea.IA_USER: + home = os.path.expanduser("~") + check_paths = [ user_keyman_dir(), os.path.join(home, ".kmfl") ] + elif area == InstallArea.IA_SHARED: + check_paths = [ "/usr/local/share/keyman" ] + elif area == InstallArea.IA_OS: + check_paths = [ "/usr/share/keyman" ] + + return get_installed_kmp_paths(check_paths) + + +def get_installed_kmp_paths(check_paths): + """ + Get list of installed keyboards. + + Args: + check_paths (list): list of paths to check + + Returns: + list: Installed keyboards + dict: Keyboard + id (str): Keyboard ID + name (str): Keyboard name + kmpname (str): Keyboard name in local + version (str): Keyboard version + kmpversion (str): + path (str): base path where keyboard is installed + description (str): Keyboard description + """ + installed_keyboards = {} + for keymanpath in check_paths: + if os.path.isdir(keymanpath): + for o in os.listdir(keymanpath): + if os.path.isdir(os.path.join(keymanpath,o)) and o != "icons": + name = md_name = version = md_version = description = kbdata = None + metadata = parsemetadata(os.path.join(keymanpath, o, "kmp.json")) + if not metadata[0]: + metadata = parseinfdata(os.path.join(keymanpath, o, "kmp.inf")) + kbjson = os.path.join(keymanpath, o, o + ".json") + if os.path.isfile(kbjson): + with open(kbjson, "r") as read_file: + kbdata = json.load(read_file) + if kbdata: + if 'description' in kbdata: + description = kbdata['description'] + version = kbdata['version'] + name = kbdata['name'] + if metadata[0]: + info = metadata[0] + md_version = info['version']['description'] + md_name = info['name']['description'] + if not name: + version = md_version + name = md_name + + installed_keyboards[o] = { "id" : o, "name" : name, "kmpname" : md_name, "version" : version, "kmpversion" : md_version, "path" : keymanpath, "description" : description} + return installed_keyboards + + +def get_kmp_version(keyboardid): + """ + Get version of the kmp for a keyboard ID. + This return the highest version if installed in more than one area + + Args: + keyboardid (dict): Keyboard ID + Returns: + str: kmp version if keyboard ID is installed + None: if not found + """ + version = None + user_kmp = get_installed_kmp(InstallArea.IA_USER) + shared_kmp = get_installed_kmp(InstallArea.IA_SHARED) + os_kmp = get_installed_kmp(InstallArea.IA_OS) + + if keyboardid in os_kmp: + version = os_kmp[keyboardid]['version'] + + if keyboardid in shared_kmp: + shared_version = shared_kmp[keyboardid]['version'] + if version: + if version < shared_version: + version = shared_version + else: + version = shared_version + + if keyboardid in user_kmp: + user_version = user_kmp[keyboardid]['version'] + if version: + if version < user_version: + version = user_version + else: + version = user_version + + return version + +def get_kmp_version_user(keyboardid): + """ + Get version of the kmp for a keyboard ID. + This only checks the user area. + + Args: + keyboardid (dict): Keyboard ID + Returns: + str: kmp version if keyboard ID is installed + None: if not found + """ + user_kmp = get_installed_kmp_user() + if keyboardid in user_kmp: + return user_kmp[keyboardid]['version'] + else: + return None diff --git a/keyman_config/uninstall_kmp.py b/keyman_config/uninstall_kmp.py new file mode 100755 index 0000000..312142b --- /dev/null +++ b/keyman_config/uninstall_kmp.py @@ -0,0 +1,111 @@ +#!/usr/bin/python3 + +import ast +import logging +import subprocess +import sys +import os.path +from shutil import rmtree +from keyman_config.get_kmp import user_keyboard_dir, user_keyman_font_dir + +def uninstall_from_ibus(kmnfile): + if sys.version_info.major == 3 and sys.version_info.minor < 6: + result = subprocess.run(["dconf", "read", "/desktop/ibus/general/preload-engines"], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT) + logging.debug(result.stdout.decode("utf-8", "strict")) + dconfread = result.stdout.decode("utf-8", "strict") + else: + result = subprocess.run(["dconf", "read", "/desktop/ibus/general/preload-engines"], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT, encoding="UTF8") + dconfread = result.stdout + if (result.returncode == 0) and dconfread: + preload_engines = ast.literal_eval(dconfread) + if kmnfile not in preload_engines: + logging.info("%s is not installed in IBus", kmnfile) + return + preload_engines.remove(kmnfile) + logging.info("Uninstalling %s from IBus", kmnfile) + if sys.version_info.major == 3 and sys.version_info.minor < 6: + result2 = subprocess.run(["dconf", "write", "/desktop/ibus/general/preload-engines", str(preload_engines)], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT) + else: + result2 = subprocess.run(["dconf", "write", "/desktop/ibus/general/preload-engines", str(preload_engines)], + stdout=subprocess.PIPE, stderr= subprocess.STDOUT, encoding="UTF8") + +def uninstall_kmp_shared(keyboardid): + """ + Uninstall a kmp from /usr/local/share/keyman + + Args: + keyboardid (str): Keyboard ID + """ + kbdir = os.path.join('/usr/local/share/keyman', keyboardid) + if not os.path.isdir(kbdir): + logging.error("Keyboard directory for %s does not exist. Aborting", keyboardid) + exit(3) + + kbdocdir = os.path.join('/usr/local/share/doc/keyman', keyboardid) + kbfontdir = os.path.join('/usr/local/share/fonts/keyman', keyboardid) + + logging.info("Uninstalling shared keyboard: %s", keyboardid) + if not os.access(kbdir, os.X_OK | os.W_OK): # Check for write access of keyman dir + logging.error("You do not have permissions to uninstall the keyboard files. You need to run this with `sudo`") + exit(3) + if os.path.isdir(kbdocdir): + if not os.access(kbdocdir, os.X_OK | os.W_OK): # Check for write access of keyman doc dir + logging.error("You do not have permissions to uninstall the documentation. You need to run this with `sudo`") + exit(3) + rmtree(kbdocdir) + logging.info("Removed documentation directory: %s", kbdocdir) + else: + logging.info("No documentation directory") + if os.path.isdir(kbfontdir): + if not os.access(kbfontdir, os.X_OK | os.W_OK): # Check for write access of keyman fonts + logging.error("You do not have permissions to uninstall the font files. You need to run this with `sudo`") + exit(3) + rmtree(kbfontdir) + logging.info("Removed font directory: %s", kbfontdir) + else: + logging.info("No font directory") + kmnfile = os.path.join(kbdir, keyboardid+".kmn") + uninstall_from_ibus(kmnfile) + rmtree(kbdir) + logging.info("Removed keyman directory: %s", kbdir) + logging.info("Finished uninstalling shared keyboard: %s", keyboardid) + +def uninstall_kmp_user(keyboardid): + """ + Uninstall a kmp from ~/.local/share/keyman + + Args: + keyboardid (str): Keyboard ID + """ + kbdir=user_keyboard_dir(keyboardid) + if not os.path.isdir(kbdir): + logging.error("Keyboard directory for %s does not exist. Aborting", keyboardid) + exit(3) + logging.info("Uninstalling local keyboard: %s", keyboardid) + kmnfile = os.path.join(kbdir, keyboardid+".kmn") + uninstall_from_ibus(kmnfile) + rmtree(kbdir) + logging.info("Removed user keyman directory: %s", kbdir) + fontdir=os.path.join(user_keyman_font_dir(), keyboardid) + if os.path.isdir(fontdir): + rmtree(fontdir) + logging.info("Removed user keyman font directory: %s", fontdir) + logging.info("Finished uninstalling local keyboard: %s", keyboardid) + + + +def uninstall_kmp(keyboardid, sharedarea=False): + """ + Uninstall a kmp + + Args: + keyboardid (str): Keyboard ID + sharedarea (str): whether to uninstall from shared /usr/local or ~/.local + """ + if sharedarea: + uninstall_kmp_shared(keyboardid) + else: + uninstall_kmp_user(keyboardid) diff --git a/keyman_config/version.py b/keyman_config/version.py new file mode 100644 index 0000000..933b8f5 --- /dev/null +++ b/keyman_config/version.py @@ -0,0 +1,7 @@ +#!/usr/bin/python3 + +# Store the version here so: +# 1) we don't load dependencies by storing it in __init__.py +# 2) we can import it in setup.py for the same reason +# 3) we can import it into your module module +__version__ = "10.99.33"
\ No newline at end of file diff --git a/keyman_config/view_installed.py b/keyman_config/view_installed.py new file mode 100755 index 0000000..45813cd --- /dev/null +++ b/keyman_config/view_installed.py @@ -0,0 +1,272 @@ +#!/usr/bin/python3 + +import logging +import os.path +import pathlib +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, Gdk, GdkPixbuf, GObject +from keyman_config.list_installed_kmp import get_installed_kmp, InstallArea +from keyman_config.welcome import WelcomeView +from keyman_config.keyboard_details import KeyboardDetailsView +from keyman_config.downloadkeyboard import DownloadKmpWindow +from keyman_config.install_window import InstallKmpWindow, find_keyman_image +from keyman_config.uninstall_kmp import uninstall_kmp +from keyman_config.accelerators import bind_accelerator, init_accel +from keyman_config.get_kmp import user_keyboard_dir + +class ViewInstalledWindowBase(Gtk.Window): + def __init__(self): + self.accelerators = None + Gtk.Window.__init__(self, title="Keyman Configuration") + init_accel(self) + + def refresh_installed_kmp(self): + pass + + def on_close_clicked(self, button): + logging.debug("Close application clicked") + Gtk.main_quit() + + def on_refresh_clicked(self, button): + logging.debug("Refresh application clicked") + self.refresh_installed_kmp() + + def on_download_clicked(self, button): + logging.debug("Download clicked") + w = DownloadKmpWindow(self) + w.resize(800, 450) + w.show_all() + + def on_installfile_clicked(self, button): + logging.debug("Install from file clicked") + dlg = Gtk.FileChooserDialog("Choose a kmp file..", self, Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) + dlg.resize(640, 480) + filter_text = Gtk.FileFilter() + filter_text.set_name("KMP files") + filter_text.add_pattern("*.kmp") + dlg.add_filter(filter_text) + response = dlg.run() + if response == Gtk.ResponseType.OK: + kmpfile = dlg.get_filename() + w = InstallKmpWindow(kmpfile, viewkmp=self) + w.resize(800, 450) + if w.checkcontinue: + w.show_all() + else: + w.destroy() + dlg.destroy() + +class ViewInstalledWindow(ViewInstalledWindowBase): + def __init__(self): + ViewInstalledWindowBase.__init__(self) + +# window is split left/right hbox +# right is ButtonBox +# possibly 2 ButtonBox in a vbox +# top one with _Remove, _About, ?_Welcome? or ?Read_Me? +# bottom one with _Download, _Install, Re_fresh, _Close +# left is GtkTreeView - does it need to be inside anything else apart from the hbox? +# with liststore which defines columns +# GdkPixbuf icon +# gchararray name +# gchararray version +# gchararray packageID (hidden) +# enum? area (user, shared, system) (icon or hidden?) +# gchararray welcomefile (hidden) (or just use area and packageID?) +# changing selected item in treeview changes what buttons are activated +# on selected_item_changed signal set the data that the buttons will use in their callbacks +# see https://developer.gnome.org/gtk3/stable/TreeWidget.html#TreeWidget + + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + s = Gtk.ScrolledWindow() + hbox.pack_start(s, True, True, 0) + + self.store = Gtk.ListStore(GdkPixbuf.Pixbuf, #icon + str, # name + str, # version + str, # packageID + int, # enum InstallArea (KmpArea is GObject version) + str) # path to welcome file if it exists or None + + # add installed keyboards to the the store e.g. + # treeiter = store.append([GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/local/share/keyman/libtralo/libtralo.ico.png", 16, 16), \ + # "LIBTRALO", "1.6.1", \ + # "libtralo", KmpArea.SHARED, True]) + + self.refresh_installed_kmp() + + self.tree = Gtk.TreeView(self.store) + + renderer = Gtk.CellRendererPixbuf() + column = Gtk.TreeViewColumn("Icon", renderer, pixbuf=0) + self.tree.append_column(column) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Name", renderer, text=1) + self.tree.append_column(column) + column = Gtk.TreeViewColumn("Version", renderer, text=2) + self.tree.append_column(column) + + select = self.tree.get_selection() + select.connect("changed", self.on_tree_selection_changed) + + s.add(self.tree) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + bbox_top = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.VERTICAL) + bbox_top.set_layout(Gtk.ButtonBoxStyle.START) + + self.uninstall_button = Gtk.Button.new_with_mnemonic("_Uninstall") + self.uninstall_button.set_tooltip_text("Uninstall keyboard package") + self.uninstall_button.connect("clicked", self.on_uninstall_clicked) + bbox_top.add(self.uninstall_button) + + self.about_button = Gtk.Button.new_with_mnemonic("_About") + self.about_button.set_tooltip_text("About keyboard package") + self.about_button.connect("clicked", self.on_about_clicked) + bbox_top.add(self.about_button) + + self.help_button = Gtk.Button.new_with_mnemonic("_Help") + self.help_button.set_tooltip_text("Help for keyboard package") + self.help_button.connect("clicked", self.on_help_clicked) + bbox_top.add(self.help_button) + + vbox.pack_start(bbox_top, False, False, 12) + + + bbox_bottom = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.VERTICAL) + bbox_bottom.set_layout(Gtk.ButtonBoxStyle.END) + + button = Gtk.Button.new_with_mnemonic("_Refresh") + button.set_tooltip_text("Refresh keyboard package list") + button.connect("clicked", self.on_refresh_clicked) + bbox_bottom.add(button) + + button = Gtk.Button.new_with_mnemonic("_Download") + button.set_tooltip_text("Download and install a keyboard package from the Keyman website") + button.connect("clicked", self.on_download_clicked) + bbox_bottom.add(button) + + button = Gtk.Button.new_with_mnemonic("_Install") + button.set_tooltip_text("Install a keyboard package from a file") + button.connect("clicked", self.on_installfile_clicked) + bbox_bottom.add(button) + + button = Gtk.Button.new_with_mnemonic("_Close") + button.set_tooltip_text("Close window") + button.connect("clicked", self.on_close_clicked) + bind_accelerator(self.accelerators, button, '<Control>q') + bind_accelerator(self.accelerators, button, '<Control>w') + bbox_bottom.add(button) + + vbox.pack_end(bbox_bottom, False, False, 12) + + hbox.pack_start(vbox, False, False, 12) + self.add(hbox) + + def addlistitems(self, installed_kmp, store, install_area): + for kmp in sorted(installed_kmp): + kmpdata = installed_kmp[kmp] + + if install_area == InstallArea.IA_USER: + welcome_file = os.path.join(user_keyboard_dir(kmpdata['id']), "welcome.htm") + icofile = os.path.join(user_keyboard_dir(kmpdata['id']), kmpdata['id'] + ".ico.png") + elif install_area == InstallArea.IA_SHARED: + welcome_file = os.path.join("/usr/local/share/keyman", kmpdata['id'], "welcome.htm") + icofile = os.path.join("/usr/local/share/keyman", kmpdata['id'], kmpdata['id'] + ".ico.png") + else: + welcome_file = os.path.join("/usr/share/keyman", kmpdata['id'], "welcome.htm") + icofile = os.path.join("/usr/share/keyman", kmpdata['id'], kmpdata['id'] + ".ico.png") + if not os.path.isfile(icofile): + icofile = find_keyman_image("icon_kmp.png") + + if not os.path.isfile(welcome_file): + welcome_file = None + + treeiter = store.append([GdkPixbuf.Pixbuf.new_from_file_at_size(icofile, 16, 16), \ + kmpdata['name'], \ + kmpdata['version'], \ + kmpdata['id'], \ + install_area, \ + welcome_file]) + + def refresh_installed_kmp(self): + logging.debug("Refreshing listview") + self.store.clear() + user_kmp = get_installed_kmp(InstallArea.IA_USER) + self.addlistitems(user_kmp, self.store, InstallArea.IA_USER) + shared_kmp = get_installed_kmp(InstallArea.IA_SHARED) + self.addlistitems(shared_kmp, self.store, InstallArea.IA_SHARED) + os_kmp = get_installed_kmp(InstallArea.IA_OS) + self.addlistitems(os_kmp, self.store, InstallArea.IA_OS) + + + def on_tree_selection_changed(self, selection): + model, treeiter = selection.get_selected() + if treeiter is not None: + self.uninstall_button.set_tooltip_text("Uninstall keyboard package " + model[treeiter][1]) + self.help_button.set_tooltip_text("Help for keyboard package " + model[treeiter][1]) + self.about_button.set_tooltip_text("About keyboard package " + model[treeiter][1]) + logging.debug("You selected", model[treeiter][1], "version", model[treeiter][2]) + if model[treeiter][4] == InstallArea.IA_USER: + logging.debug("Enabling uninstall button for", model[treeiter][3], "in", model[treeiter][4]) + self.uninstall_button.set_sensitive(True) + else: + self.uninstall_button.set_sensitive(False) + logging.debug("Disabling uninstall button for", model[treeiter][3], "in", model[treeiter][4]) + if model[treeiter][5]: + self.help_button.set_sensitive(True) + else: + self.help_button.set_sensitive(False) + + def on_help_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None: + logging.info("Open welcome.htm for" + model[treeiter][1] + "if available") + welcome_file = model[treeiter][5] + if welcome_file and os.path.isfile(welcome_file): + uri_path = pathlib.Path(welcome_file).as_uri() + logging.info("opening" + uri_path) + w = WelcomeView(uri_path, model[treeiter][3]) + w.resize(800, 600) + w.show_all() + else: + logging.info("welcome.htm not available") + + def on_uninstall_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None: + logging.info("Uninstall keyboard " + model[treeiter][3] + "?") + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, + Gtk.ButtonsType.YES_NO, "Uninstall keyboard?") + dialog.format_secondary_text( + "Are you sure that you want to uninstall the " + model[treeiter][1] + " keyboard") + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.YES: + logging.info("Uninstalling keyboard" + model[treeiter][1]) + # can only uninstall with the gui from user area + uninstall_kmp(model[treeiter][3]) + logging.info("need to refresh window after uninstalling a keyboard") + self.refresh_installed_kmp() + elif response == Gtk.ResponseType.NO: + logging.info("Not uninstalling keyboard " + model[treeiter][1]) + + def on_about_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None: + logging.info("Show keyboard details of " + model[treeiter][1]) + kmp = { "name" : model[treeiter][1], "version" : model[treeiter][2]} + w = KeyboardDetailsView(kmp) + w.resize(800, 450) + w.show_all() + +if __name__ == '__main__': + w = ViewInstalledWindow() + w.connect("destroy", Gtk.main_quit) + w.resize(576, 324) + w.show_all() + Gtk.main() diff --git a/keyman_config/welcome.py b/keyman_config/welcome.py new file mode 100644 index 0000000..18e4158 --- /dev/null +++ b/keyman_config/welcome.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +import gi +import logging +import subprocess +import webbrowser +import urllib.parse + +import webbrowser +gi.require_version('Gtk', '3.0') +gi.require_version('WebKit', '3.0') +from gi.repository import Gtk, WebKit +from keyman_config.check_mime_type import check_mime_type +from keyman_config.accelerators import bind_accelerator, init_accel + +class WelcomeView(Gtk.Window): + + def __init__(self, welcomeurl, keyboardname): + self.accelerators = None + kbtitle = keyboardname + " installed" + self.welcomeurl = welcomeurl + Gtk.Window.__init__(self, title=kbtitle) + init_accel(self) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + + s = Gtk.ScrolledWindow() + self.webview = WebKit.WebView() + self.webview.connect("navigation-policy-decision-requested", self.check) + self.webview.connect("mime-type-policy-decision-requested", check_mime_type) + self.webview.load_uri(welcomeurl) + s.add(self.webview) + vbox.pack_start(s, True, True, 0) + + hbox = Gtk.Box(spacing=12) + vbox.pack_start(hbox, False, False, 6) + + button = Gtk.Button.new_with_mnemonic("Open in _Web browser") + button.connect("clicked", self.on_openweb_clicked) + button.set_tooltip_text("Open in the default web browser to do things like printing") + hbox.pack_start(button, False, False, 12) + + button = Gtk.Button.new_with_mnemonic("_OK") + button.connect("clicked", self.on_ok_clicked) + hbox.pack_end(button, False, False, 12) + bind_accelerator(self.accelerators, button, '<Control>w') + + self.add(vbox) + + def check(self, view, frame, req, nav, policy): + uri = req.get_uri() + if not "welcome.htm" in uri: + webbrowser.open(uri) + policy.ignore() + return True + return False + + def on_openweb_clicked(self, button): + logging.info("\"Open in Web browser\" button was clicked") + webbrowser.open(self.welcomeurl) + + def on_ok_clicked(self, button): + logging.info("Closing welcome window") + self.close() diff --git a/km-config b/km-config new file mode 100755 index 0000000..a8a98d0 --- /dev/null +++ b/km-config @@ -0,0 +1,30 @@ +#!/usr/bin/python3 + +import argparse +import logging +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +from keyman_config import __version__ + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Keyman keyboards installation and information') + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + from keyman_config.view_installed import ViewInstalledWindow + w = ViewInstalledWindow() + w.resize(576, 324) + w.connect("destroy", Gtk.main_quit) + w.show_all() + Gtk.main() diff --git a/km-kvk2ldml b/km-kvk2ldml new file mode 100755 index 0000000..1df6be6 --- /dev/null +++ b/km-kvk2ldml @@ -0,0 +1,58 @@ +#!/usr/bin/python3 + +import argparse +import logging +import os.path +import sys + +from keyman_config import __version__ + +def main(): + parser = argparse.ArgumentParser(description='Convert a Keyman kvk on-screen keyboard file to an LDML file. Optionally print the details of the kvk file.') + parser.add_argument('-p', "--print", help='print kvk details', action="store_true") + parser.add_argument('-k', "--keys", help='if printing also print all keys', action="store_true") + parser.add_argument('kvkfile', help='kvk file') + parser.add_argument('-o', '--output', metavar='LDMLFILE', help='output LDML file location') + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml + + name, ext = os.path.splitext(args.kvkfile) + # Check if input file extension is kvk + if ext != ".kvk": + logging.error("km-kvk2ldml: error, input file %s is not a kvk file.", args.kvkfile) + logging.error("km-kvk2ldml [-h] [-k] [-p] [-o <ldml file>] <kvk file>") + sys.exit(2) + + # Check if input kvk file exists + if not os.path.isfile(args.kvkfile): + logging.error("km-kvk2ldml: error, input file %s does not exist.", args.kvkfile) + logging.error("km-kvk2ldml [-h] [-k] [-p] [-o <ldml file>] <kvk file>") + sys.exit(2) + + kvkData = parse_kvk_file(args.kvkfile) + + if args.print: + print_kvk(kvkData, args.keys) + + if args.output: + outputfile = args.output + else: + outputfile = name + ".ldml" + + with open(outputfile, 'wb') as ldmlfile: + ldml = convert_ldml(kvkData) + output_ldml(ldmlfile, ldml) + +if __name__ == "__main__": + main() diff --git a/km-kvk2ldml.bash-completion b/km-kvk2ldml.bash-completion new file mode 100644 index 0000000..ab40c3e --- /dev/null +++ b/km-kvk2ldml.bash-completion @@ -0,0 +1,17 @@ +#/usr/bin/env bash + +_km-kvk2ldml_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -p --print -k --keys -o --output -v --verbose -vv --veryverbose --version" + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} + +complete -F _km-kvk2ldml_completions km-kvk2ldml diff --git a/km-package-get b/km-package-get new file mode 100755 index 0000000..9d0ce71 --- /dev/null +++ b/km-package-get @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +import argparse +import logging +import os +import sys + +from keyman_config import __version__ + +def main(): + parser = argparse.ArgumentParser(description='Download Keyman keyboard package to ~/.cache/keyman') + parser.add_argument('id', help='Keyman keyboard id') + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + from keyman_config.get_kmp import get_kmp, keyman_cache_dir + get_kmp(args.id) + if os.path.exists(os.path.join(keyman_cache_dir(), 'kmpdirlist')): + os.remove(os.path.join(keyman_cache_dir(), 'kmpdirlist')) + +if __name__ == "__main__": + main() diff --git a/km-package-get.bash-completion b/km-package-get.bash-completion new file mode 100644 index 0000000..48b96b9 --- /dev/null +++ b/km-package-get.bash-completion @@ -0,0 +1,32 @@ +#/usr/bin/env bash + +_km-package-get_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -v --verbose -vv --veryverbose --version" + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + words="" + if [[ ! -e ~/.cache/keyman/kmpdirlist ]] ; then + if [[ -e ./km-package-install ]]; then + python3 -c "from imp import load_source;load_source('km_package_install', './km-package-install');from km_package_install import list_keyboards;list_keyboards()" + else + python3 -c "from imp import load_source;load_source('km_package_install', '/usr/bin/km-package-install');from km_package_install import list_keyboards;list_keyboards()" + fi + fi + + if [[ -r ~/.cache/keyman/kmpdirlist ]] ; then + for file in `cat ~/.cache/keyman/kmpdirlist`; do words="${words} ${file}"; done + COMPREPLY=($(compgen -W "${words}" -- ${cur})) + return 0 + fi +} + +complete -F _km-package-get_completions km-package-get diff --git a/km-package-install b/km-package-install new file mode 100755 index 0000000..d423a02 --- /dev/null +++ b/km-package-install @@ -0,0 +1,145 @@ +#!/usr/bin/python3 + +import argparse +import logging +import sys +import os +from keyman_config import __version__ + +import datetime +import time + +def get_keyboard_dir_page(kb_url): + import requests + import requests_cache + from keyman_config.get_kmp import keyman_cache_dir + + logging.info("Getting keyboard list") + logging.debug("At URL %s", kb_url) + cache_dir = keyman_cache_dir() + current_dir = os.getcwd() + expire_after = datetime.timedelta(days=7) + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) + os.chdir(cache_dir) + requests_cache.install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) + now = time.ctime(int(time.time())) + try: + response = requests.get(kb_url) + logging.debug("Time: {0} / Used Cache: {1}".format(now, response.from_cache)) + os.chdir(current_dir) + requests_cache.core.uninstall_cache() + if response.status_code == 200: + return response.text + else: + return None + except: + return None + + +def get_dir_list(): + from bs4 import BeautifulSoup + + url = "https://downloads.keyman.com/keyboards/" + page = get_keyboard_dir_page(url) + soup = BeautifulSoup(page, 'html.parser') + return [url + node.get('href') for node in soup.find_all('a')] + +def write_kmpdirlist(kmpdirfile): + with open(os.path.join(kmpdirfile), 'wt') as kmpdirlist: + for file in get_dir_list(): + #logging.debug(file) + kb = os.path.basename(os.path.dirname(file)) + if kb != "keyboards": + print(kb, file=kmpdirlist) + +def list_keyboards(): + from keyman_config.get_kmp import keyman_cache_dir + kmpdirfile = os.path.join(keyman_cache_dir(), 'kmpdirlist') + if not os.path.exists(kmpdirfile): + write_kmpdirlist(kmpdirfile) + else: + logging.debug("kmpdirlist already exists") + if os.path.getsize(kmpdirfile) == 0: + write_kmpdirlist(kmpdirfile) + +def main(): + parser = argparse.ArgumentParser(description='Install a Keyman keyboard, either a local .kmp file or specify a keyboard id to download and install') + parser.add_argument('-s', '--shared', action='store_true', help='Install to shared area /usr/local') + parser.add_argument('-f', '--file', metavar='<kmpfile>', help='Keyman kmp file') + parser.add_argument('-k', '--keyboardid', metavar='<keyboardid>', help='Keyman keyboard id') + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + if args.keyboardid and args.file: + logging.error("km-package-install: error: too many arguments: either install a local kmp file or specify a keyboard id to download and install.") + sys.exit(2) + + from keyman_config.install_kmp import install_kmp, InstallError, InstallStatus + from keyman_config.list_installed_kmp import get_kmp_version + from keyman_config.get_kmp import get_keyboard_data, get_kmp, keyman_cache_dir + + if os.path.exists(os.path.join(keyman_cache_dir(), 'kmpdirlist')): + os.remove(os.path.join(keyman_cache_dir(), 'kmpdirlist')) + + + def try_install_kmp(inputfile, arg, online=False, sharedarea=False): + try: + install_kmp(inputfile, online, sharedarea) + except InstallError as e: + if e.status == InstallStatus.Abort: + logging.error("km-package-install: error: Failed to install %s", arg) + logging.error(e.message) + sys.exit(3) + else: + logging.warning(e.message) + + if args.file: + name, ext = os.path.splitext(args.file) + if ext != ".kmp": + logging.error("km-package-install: Input file %s is not a kmp file.", args.file) + logging.error("km-package-install -f <kmpfile>") + sys.exit(2) + + if not os.path.isfile(args.file): + logging.error("km-package-install: Keyman kmp file %s not found.", args.file) + logging.error("km-package-install -f <kmpfile>") + sys.exit(2) + try_install_kmp(args.file, "file " + args.file, False, args.shared) + elif args.keyboardid: + installed_kmp_ver = get_kmp_version(args.keyboardid) + kbdata = get_keyboard_data(args.keyboardid) + if not kbdata: + logging.error("km-package-install: error: Could not download keyboard data for %s", args.keyboardid) + sys.exit(3) + if installed_kmp_ver: + if kbdata['version'] == installed_kmp_ver: + logging.error("km-package-install: The %s version of the %s keyboard is already installed.", installed_kmp_ver, args.keyboardid) + sys.exit(1) + elif float(kbdata['version']) > float(installed_kmp_ver): + logging.error("km-package-install: A newer version of %s keyboard is available. Uninstalling old version %s then downloading and installing new version %s.", args.keyboardid, installed_kmp_ver, kbdata['version']) + uninstall_kmp(args.keyboardid, args.shared) + + kmpfile = get_kmp(args.keyboardid) + if kmpfile: + try_install_kmp(kmpfile, "keyboard " + args.keyboardid, True, args.shared) + else: + logging.error("km-package-install: error: Could not download keyboard package %s", args.keyboardid) + sys.exit(2) + else: + logging.error("km-package-install: error: no arguments: either install a local kmp file or specify a keyboard id to download and install.") + sys.exit(2) + + + +if __name__ == "__main__": + main() diff --git a/km-package-install.bash-completion b/km-package-install.bash-completion new file mode 100644 index 0000000..5032030 --- /dev/null +++ b/km-package-install.bash-completion @@ -0,0 +1,38 @@ +#/usr/bin/env bash + +_km-package-install_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -v --verbose -vv --veryverbose --version -k --keyboardid -f --file -s --shared" + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + case "${prev}" in + "-k"|"--keyboardid") + words="" + if [[ ! -s ~/.cache/keyman/kmpdirlist ]] ; then + if [[ -e ./km-package-install ]]; then + python3 -c "from imp import load_source;load_source('km_package_install', './km-package-install');from km_package_install import list_keyboards;list_keyboards()" + else + python3 -c "from imp import load_source;load_source('km_package_install', '/usr/bin/km-package-install');from km_package_install import list_keyboards;list_keyboards()" + fi + fi + + if [[ -r ~/.cache/keyman/kmpdirlist ]] ; then + for file in `cat ~/.cache/keyman/kmpdirlist`; do words="${words} ${file}"; done + COMPREPLY=($(compgen -W "${words}" -- ${cur})) + return 0 + fi + ;; + *) + ;; + esac +} + +complete -F _km-package-install_completions km-package-install diff --git a/km-package-list-installed b/km-package-list-installed new file mode 100755 index 0000000..7d68b96 --- /dev/null +++ b/km-package-list-installed @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + +import argparse +import logging +import os +from keyman_config import __version__ + +def main(): + parser = argparse.ArgumentParser(description='Show installed Keyman keyboards with name, version, id.') + parser.add_argument('-l', "--long", help='long format also shows description', action="store_true") + parser.add_argument('-s', "--shared", help='show those installed in shared areas', action="store_true") + parser.add_argument('-o', "--os", help='show those installed by the OS', action="store_true") + parser.add_argument('-u', "--user", help='show those installed in user areas', action="store_true") + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + all = not args.user and not args.shared and not args.os + from keyman_config.list_installed_kmp import get_installed_kmp, InstallArea + if args.user or all: + installed_kmp = get_installed_kmp(InstallArea.IA_USER) + print("--- Installed user keyboards ---") + print_keyboards(installed_kmp, args.long) + if args.shared or all: + installed_kmp = get_installed_kmp(InstallArea.IA_SHARED) + print("--- Installed shared keyboards ---") + print_keyboards(installed_kmp, args.long) + if args.os or all: + installed_kmp = get_installed_kmp(InstallArea.IA_OS) + print("--- Installed OS keyboards ---") + print_keyboards(installed_kmp, args.long) + + +def print_keyboards(installed_kmp, verbose): + for kmp in sorted(installed_kmp): + print(installed_kmp[kmp]['name'] + ", version:", installed_kmp[kmp]['version'] + ", id:", kmp) + if verbose: + if installed_kmp[kmp]['version'] != installed_kmp[kmp]['kmpversion']: + print("Version mismatch. Installed keyboard is %s. Website says it is %s." % (installed_kmp[kmp]['kmpversion'], installed_kmp[kmp]['version'])) + if installed_kmp[kmp]['name'] != installed_kmp[kmp]['kmpname']: + print("Name mismatch. Installed keyboard is %s. Website says it is %s." % (installed_kmp[kmp]['name'], installed_kmp[kmp]['kmpname'])) + + if installed_kmp[kmp]['description']: + print(installed_kmp[kmp]['description']) + else: + print("No description") + print(os.linesep) + + +if __name__ == "__main__": + main() diff --git a/km-package-list-installed.bash-completion b/km-package-list-installed.bash-completion new file mode 100644 index 0000000..e90bd29 --- /dev/null +++ b/km-package-list-installed.bash-completion @@ -0,0 +1,17 @@ +#/usr/bin/env bash + +_km-package-list-installed_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -l --long -v --verbose -vv --veryverbose --version -u --user -o --os -s --shared" + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} + +complete -F _km-package-list-installed_completions km-package-list-installed diff --git a/km-package-uninstall b/km-package-uninstall new file mode 100755 index 0000000..f500fe3 --- /dev/null +++ b/km-package-uninstall @@ -0,0 +1,29 @@ +#!/usr/bin/python3 + +import argparse +import logging +import sys + +from keyman_config import __version__ + +def main(): + parser = argparse.ArgumentParser(description='Uninstall Keyman keyboard package.') + parser.add_argument('id', help='Keyman keyboard id') + parser.add_argument('-s', '--shared', action='store_true', help='Uninstall from shared area /usr/local') + parser.add_argument('--version', action='version', version='%(prog)s version '+__version__) + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + + args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + from keyman_config.uninstall_kmp import uninstall_kmp + uninstall_kmp(args.id, args.shared) + +if __name__ == "__main__": + main() diff --git a/km-package-uninstall.bash-completion b/km-package-uninstall.bash-completion new file mode 100644 index 0000000..ff07a31 --- /dev/null +++ b/km-package-uninstall.bash-completion @@ -0,0 +1,36 @@ +#/usr/bin/env bash + +_km-package-uninstall_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -s --shared -v --verbose -vv --veryverbose --version" + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + if [[ "${#COMP_WORDS[@]}" != "2" ]]; then + if [[ ${prev} != -* ]]; then + return 0 + fi + fi + + words="" + shared="" + case "${prev}" in + "-s"|"--shared") + for file in `ls -d /usr/local/share/keyman/*/`; do kbid="`basename ${file}`"; shared="${shared} ${kbid}"; done + COMPREPLY=($(compgen -W "${shared}" -- ${cur})) + ;; + *) + for file in `ls -d ~/.local/share/keyman/*/`; do kbid="`basename ${file}`"; words="${words} ${kbid}"; done + COMPREPLY=($(compgen -W "${words}" -- ${cur})) + ;; + esac +} + +complete -F _km-package-uninstall_completions km-package-uninstall diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..72f9d44 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_svn_revision = 0 +tag_date = 0 +tag_build = + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c86818f --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 + +from setuptools import setup, find_packages + +exec(open('keyman_config/version.py').read()) + +setup( + name="keyman_config", + version=__version__, + packages=find_packages(), + scripts=['km-config', 'km-package-get', + 'km-package-install', 'km-kvk2ldml', + 'km-package-uninstall', + 'km-package-list-installed', ], + + install_requires=[ + 'lxml', 'numpy', 'Pillow', 'requests', 'requests-cache', + 'python-magic', + ], + +# metadata to display on PyPI + author="Daniel Glassey", + author_email="wdg@debian.org", + description="Keyman for Linux configuration", + license="MIT", + keywords="keyman, keyman-config, keyboard", + url="http://www.keyman.com/", # project home page, if any + project_urls={ + "Bug Tracker": "https://github.com/keymanapp/issues", + "Source Code": "https://github.com/keymanapp/keyman/linux/tree/master/linux/keyman-config", + }, + # include_package_data=True, +) |