summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorglasseyes <dglassey@gmail.com>2018-11-27 14:18:36 +0700
committerglasseyes <dglassey@gmail.com>2018-11-27 14:18:36 +0700
commit9f70f8cfc86a0a5343c8eac7ca24a1b5253f91cc (patch)
tree018b6d740c8e6384d434582334022c84f5b63c55
New upstream version 10.99.33
-rw-r--r--MANIFEST.in3
-rw-r--r--PKG-INFO11
-rw-r--r--README.md91
-rw-r--r--keyman_config.egg-info/PKG-INFO11
-rw-r--r--keyman_config.egg-info/SOURCES.txt40
-rw-r--r--keyman_config.egg-info/dependency_links.txt1
-rw-r--r--keyman_config.egg-info/requires.txt6
-rw-r--r--keyman_config.egg-info/top_level.txt1
-rw-r--r--keyman_config/__init__.py1
-rw-r--r--keyman_config/accelerators.py13
-rw-r--r--keyman_config/check_mime_type.py20
-rwxr-xr-xkeyman_config/convertico.py45
-rwxr-xr-xkeyman_config/downloadkeyboard.py97
-rwxr-xr-xkeyman_config/get_kmp.py157
-rw-r--r--keyman_config/icons/cross20.pngbin0 -> 687 bytes
-rw-r--r--keyman_config/icons/defaultpackage.gifbin0 -> 8372 bytes
-rw-r--r--keyman_config/icons/expand20.pngbin0 -> 551 bytes
-rw-r--r--keyman_config/icons/help20.pngbin0 -> 681 bytes
-rw-r--r--keyman_config/icons/icon_kmp.pngbin0 -> 775 bytes
-rwxr-xr-xkeyman_config/install_kmp.py331
-rwxr-xr-xkeyman_config/install_window.py337
-rw-r--r--keyman_config/keyboard_details.py164
-rwxr-xr-xkeyman_config/kmpmetadata.py485
-rwxr-xr-xkeyman_config/kvk2ldml.py351
-rwxr-xr-xkeyman_config/list_installed_kmp.py150
-rwxr-xr-xkeyman_config/uninstall_kmp.py111
-rw-r--r--keyman_config/version.py7
-rwxr-xr-xkeyman_config/view_installed.py272
-rw-r--r--keyman_config/welcome.py64
-rwxr-xr-xkm-config30
-rwxr-xr-xkm-kvk2ldml58
-rw-r--r--km-kvk2ldml.bash-completion17
-rwxr-xr-xkm-package-get31
-rw-r--r--km-package-get.bash-completion32
-rwxr-xr-xkm-package-install145
-rw-r--r--km-package-install.bash-completion38
-rwxr-xr-xkm-package-list-installed59
-rw-r--r--km-package-list-installed.bash-completion17
-rwxr-xr-xkm-package-uninstall29
-rw-r--r--km-package-uninstall.bash-completion36
-rw-r--r--setup.cfg5
-rw-r--r--setup.py33
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
new file mode 100644
index 0000000..183b531
--- /dev/null
+++ b/keyman_config/icons/cross20.png
Binary files differ
diff --git a/keyman_config/icons/defaultpackage.gif b/keyman_config/icons/defaultpackage.gif
new file mode 100644
index 0000000..d1fe67c
--- /dev/null
+++ b/keyman_config/icons/defaultpackage.gif
Binary files differ
diff --git a/keyman_config/icons/expand20.png b/keyman_config/icons/expand20.png
new file mode 100644
index 0000000..1cbaffa
--- /dev/null
+++ b/keyman_config/icons/expand20.png
Binary files differ
diff --git a/keyman_config/icons/help20.png b/keyman_config/icons/help20.png
new file mode 100644
index 0000000..f7ed0c1
--- /dev/null
+++ b/keyman_config/icons/help20.png
Binary files differ
diff --git a/keyman_config/icons/icon_kmp.png b/keyman_config/icons/icon_kmp.png
new file mode 100644
index 0000000..f5ca91b
--- /dev/null
+++ b/keyman_config/icons/icon_kmp.png
Binary files differ
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,
+)