diff --git a/HISTORY.md b/HISTORY.md index eec093d169..b937f09aa6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,34 @@ # Keyman Version History +## 17.0.29 alpha 2023-01-16 + +* fix(linux): Fix crash (un-)installing shared keyboard (#8020) +* chore(linux): Don't report KeyboardInterrupt to Sentry (#8021) +* chore(linux): Update sample settings (#8018) +* feat(linux): Enhance tab completion in km-package-install (#8005) + +## 17.0.28 alpha 2023-01-12 + +* feat(linux): Display error messages in the UI (#8006) +* bug(linux): Empty keyboard after failed installation (#8008) + +## 17.0.27 alpha 2023-01-11 + +* chore(linux): Refactor completion script (#8002) + +## 17.0.26 alpha 2023-01-10 + +* feat(linux): Add Back button to "Download Keyman Keyboards" dialog (#7994) +* feat(linux): List fonts in the uninstall confirmation dialog (#7995) + +## 17.0.25 alpha 2023-01-09 + +* chore(linux): Remove unnecessary variable (#7988) + +## 17.0.24 alpha 2023-01-06 + +* chore(common/resources): Add Docker readme (#7980) + ## 17.0.23 alpha 2023-01-03 * fix(linux): add IBUS_HAS_PREFILTER ifdef to linux/ibus-keyman/tests (#7958) diff --git a/VERSION.md b/VERSION.md index 58fa7d50d5..7aa7e61a51 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.24 +17.0.30 diff --git a/docs/settings/linux/extensions.json b/docs/settings/linux/extensions.json index bd7f528324..81603ea639 100644 --- a/docs/settings/linux/extensions.json +++ b/docs/settings/linux/extensions.json @@ -7,6 +7,7 @@ "ms-python.python", "mads-hartmann.bash-ide-vscode", "timonwong.shellcheck", - "maelvalais.autoconf" + "maelvalais.autoconf", + "webfreak.debug", ] } diff --git a/docs/settings/linux/launch.json b/docs/settings/linux/launch.json index cbf10cb216..b407dacdb7 100644 --- a/docs/settings/linux/launch.json +++ b/docs/settings/linux/launch.json @@ -24,14 +24,17 @@ "valuesFormatting": "parseText" }, { + // Mount the Keyman root directory as a Shared Folder in the VM. // kill ibus-engine-keyman, if running // start: gdbserver 10.3.0.53:2345 /media/sf_Develop/keyman/keyman/linux/ibus-keyman/src/ibus-engine-keyman // then attach debugger in vscode + // Replace the IP address after `gdbserver` with the IP address of the host machine. + // Replace the IP address below with the address of the VM running gdbserver. "type": "gdb", "request": "attach", - "name": "Attach to gdbserver", - "executable": "./linux/ibus-keyman/src/ibus-engine-keyman", - "target": "10.3.0.53:2345", + "name": "Attach to gdbserver (ibus-keyman)", + "executable": "${workspaceFolder}/linux/ibus-keyman/src/ibus-engine-keyman", + "target": "10.3.0.52:2345", "remote": true, "cwd": "${workspaceFolder}/linux/ibus-keyman", "valuesFormatting": "parseText" diff --git a/docs/settings/linux/tasks.json b/docs/settings/linux/tasks.json index 76feb6bfff..32960cf6cc 100644 --- a/docs/settings/linux/tasks.json +++ b/docs/settings/linux/tasks.json @@ -145,9 +145,9 @@ "CPPFLAGS=\"-DG_MESSAGES_DEBUG -I${workspaceFolder}/core/build/arch/debug/include/ -I${workspaceFolder}/common/include/ -I${workspaceFolder}/core/include/\"", "CFLAGS=\"-g -O0\"", "CXXFLAGS=\"-g -O0\"", - "KEYMAN_PROC_LIBS=\"-L${workspaceFolder}/common/core/desktop/build/arch/debug/src -lkmnkbp0\"", - "KEYMAN_PROC_CFLAGS=\"-I${workspaceFolder}/common/core/desktop/build/arch/debug/include -I${workspaceFolder}/common/core/desktop/include\"", - "PKG_CONFIG_PATH=${workspaceFolder}/common/core/desktop/build/arch/debug/meson-private" + "KEYMAN_PROC_LIBS=\"-L${workspaceFolder}/core/build/arch/debug/src -lkmnkbp0\"", + "KEYMAN_PROC_CFLAGS=\"-I${workspaceFolder}/core/build/arch/debug/include -I${workspaceFolder}/core/include -I${workspaceFolder}/common/include\"", + "PKG_CONFIG_PATH=${workspaceFolder}/core/build/arch/debug/meson-private" ], "options": { "cwd": "${workspaceFolder}/linux/ibus-keyman" diff --git a/docs/websites/README.md b/docs/websites/README.md new file mode 100644 index 0000000000..caa3397286 --- /dev/null +++ b/docs/websites/README.md @@ -0,0 +1,113 @@ +# How to Set Up a Local Web Server for the Keyman Web Pages + +Currently, most of the Keyman websites are running via IIS. Refer to the Keyman [wiki](https://github.com/keymanapp/keyman/wiki/How-to-set-up-a-local-web-server-for-the-Keyman-web-pages) for setting that up on Windows 10. + +To make IIS redirects compatible with the Docker sites below, see +https://github.com/keymanapp/keyman.com/issues/337#issuecomment-1336339387 + +As the websites get migrated to Apache via Docker, follow the installation steps below: + +## Pre-requisite Installs +* [Docker Desktop](https://www.docker.com/products/docker-desktop/) + +On Windows, Docker will need either: +* Hyper-V or +* WSL 2.0 - Install with this guide: +https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers + * WSL will then need a Linux image (e.g. Ubuntu app) from the Microsoft Store + +#### Other Docker Notes +Docker tends to throttle Docker image downloads, so some developer offices may want to set up a proxy server. If the proxy server is set up, carefully edit the JSON file per in Docker Settings -> Docker Engine https://docs.docker.com/registry/recipes/mirror/#configure-the-docker-daemon and click 'Apply & Restart'. Note the example (lingnet) is for running inside the Linguistics Institute (Chiang Mai) + +``` + "registry-mirrors": ["https://docker.io.registry.lingnet/"], + "insecure-registries" : [ + "docker.io.registry.lingnet", + "registry.lingnet" + ] +``` + + +### These pre-reqs are still needed but will be removed in keymanapp/help.keyman.com#674 +* PHP 7.4 +* Composer + +## Builder BASH Script Actions + +#### Configure +Installs PHP dependencies in vendor/ folder. This step will be removed in keymanapp/help.keyman.com#674 +1. Run `./build.sh configure`. + +#### Stop the Docker container +1. Run `./build.sh stop` + +#### Build the Docker image +1. Run `./build.sh build`. + +#### Start the Docker container +1. Run `./build.sh start`. + +After this, you can access the website at the following ports: + +| Website | URL | +|------------|-----| +|help.keyman | http://localhost:8055 | + + +#### Remove the Docker container and image +1. Run `./build.sh clean`. + +#### Running tests +Checks for broken links +1. Run `./build.sh test` + +--------- + +## Kubernetes Deployment +For production, the websites are deployed with Kubernetes. + + +### How to run help.keyman.com locally with Docker Desktop's Kubernetes singlenode cluster + +For testing Kubernetes deployment, there are yaml files under the corresponding website's repo: `/resources/kubectl`, that cover local developer testing. + +### Pre-requisites +On the host machine, install [Docker](https://docs.docker.com/get-docker/), then enable Kubernetes in the settings. Ensure you have built a help-keyman-app Docker image, and either tag it `docker.dallas.languagetechnology.org/keyman/help-keyman-app` or modify the `app-php` containers `image:` value to match you local copy's name. + +### Deploying to a desktop cluster +To deploy the dev version to the cluster do the following: +1. Ensure your `kubectl` context is set to `docker-desktop`, though the Docker Desktop systray icon or by running: +```bash +$> kubectl config use-context docker-desktop +``` +2. Create a keyman namespace if it does not already exist: +```bash +$> kubectl create ns keyman +``` +3. Apply the configs for the resources and start the pod: +```bash +$> kubectl --namespace keyman apply \ + -f resources/kubectl/help-kubectl-dev.yaml \ + -f resources/kubectl/help-kubectl.yaml +``` +### Testing the site and `/api/deploy` webhook endpoint +The site can be reached on http://localhost:30080/ via web browser, and the deploy api is on http://localhost:30900/api/deploy, and can be activated like so: +```bash +$> curl -v --request POST \ + -H "Content-Type: application/json" \ + -H "X-Hub-Signature-256: sha256=49af8531106a369bfee369f91dadec597e8ea3992ec2802bbe655be0ece17f15" \ + --data '{"action":"push","ref":"refs/heads/staging"}' \ + http://localhost:30900/api/deploy +``` +This simulates enough of a GitHub webhook push event to pass validation on the responder. + +### Clean up after testing + +To remove the k8s pod and resources, and delete everything do: +```bash +$> kubectl --namespace=keyman delete {pod,cm,svc,secret,pvc}/help-keyman-com +``` +Or just delete the pod and keep the resources for further testing: +```bash +$> kubectl --namespace=keyman delete pod/help-keyman-com +``` diff --git a/linux/debian/control b/linux/debian/control index b42e7bc5c9..0a0446cb1a 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -86,6 +86,7 @@ Depends: python3-bs4, python3-gi, python3-sentry-sdk (>= 1.1) | python3-raven, + dbus-x11, ${misc:Depends}, ${python3:Depends}, Description: Keyman for Linux configuration diff --git a/linux/keyman-config/keyman_config/__init__.py b/linux/keyman-config/keyman_config/__init__.py index ee2326a86f..1b0b283900 100644 --- a/linux/keyman-config/keyman_config/__init__.py +++ b/linux/keyman-config/keyman_config/__init__.py @@ -37,6 +37,15 @@ def secure_lookup(data, key1, key2 = None): return None +def before_send(event, hint): + if 'exc_info' in hint: + exc_type, exc_value, tb = hint['exc_info'] + if isinstance(exc_value, KeyboardInterrupt): + # Ignore KeyboardInterrupt exception + return None + return event + + gettext.bindtextdomain('keyman-config', '/usr/share/locale') gettext.textdomain('keyman-config') @@ -77,6 +86,7 @@ else: environment=__environment__, release='release-' + __versionwithtag__, integrations=[sentry_logging], + before_send=before_send ) set_user({'id': hash(getpass.getuser())}) with configure_scope() as scope: diff --git a/linux/keyman-config/keyman_config/downloadkeyboard.py b/linux/keyman-config/keyman_config/downloadkeyboard.py index d79b1e3d44..a77008a07b 100755 --- a/linux/keyman-config/keyman_config/downloadkeyboard.py +++ b/linux/keyman-config/keyman_config/downloadkeyboard.py @@ -30,13 +30,26 @@ class DownloadKmpWindow(Gtk.Dialog): s = Gtk.ScrolledWindow() self.webview = WebKit2.WebView() self.webview.connect("decide-policy", self._keyman_policy) + self.webview.connect("load-changed", self._update_back_button) url = KeymanComUrl + "/go/linux/" + __releaseversion__ + "/download-keyboards" self.webview.load_uri(url) s.add(self.webview) self.get_content_area().pack_start(s, True, True, 0) - self.add_button(_("_Close"), Gtk.ResponseType.CLOSE) + hbox = Gtk.Box(spacing=6) + self.get_content_area().pack_start(hbox, False, False, 0) + + self.back_button = Gtk.Button.new_with_mnemonic(_("_Back")) + self.back_button.set_tooltip_text(_("Back to search")) + self.back_button.connect("clicked", self._on_back_clicked) + self.back_button.set_sensitive(False) + hbox.pack_start(self.back_button, False, False, 0) + + close_button = Gtk.Button.new_with_mnemonic(_("_Close")) + close_button.set_tooltip_text(_("Close dialog")) + close_button.connect("clicked", self._on_close_clicked) + hbox.pack_end(close_button, False, False, 0) if self.parentWindow is not None: self.getinfo = GetInfo(self.parentWindow.incomplete_kmp) @@ -44,6 +57,15 @@ class DownloadKmpWindow(Gtk.Dialog): self.resize(800, 450) self.show_all() + def _update_back_button(self, webview, load_event): + self.back_button.set_sensitive(webview.can_go_back()) + + def _on_back_clicked(self, button): + self.webview.go_back() + + def _on_close_clicked(self, button): + self.response(Gtk.ResponseType.CLOSE) + def _process_kmp(self, url, downloadfile: str): logging.info("Downloading kmp file to %s", downloadfile) if download_kmp_file(url, downloadfile): @@ -52,6 +74,12 @@ class DownloadKmpWindow(Gtk.Dialog): self.response(Gtk.ResponseType.OK) self.close() return True + logging.error(_("Downloading kmp file failed")) + dialog = Gtk.MessageDialog( + self, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, _("Downloading keyboard file failed")) + dialog.run() + dialog.destroy() return False def _keyman_policy(self, web_view, decision, decision_type): diff --git a/linux/keyman-config/keyman_config/handle_install.py b/linux/keyman-config/keyman_config/handle_install.py index a6fccc0179..4938f23d41 100644 --- a/linux/keyman-config/keyman_config/handle_install.py +++ b/linux/keyman-config/keyman_config/handle_install.py @@ -20,7 +20,6 @@ def download_and_install_package(url): """ parsedUrl = urlparse(url) bcp47 = _extract_bcp47(parsedUrl.query) - severity = logging.ERROR if parsedUrl.scheme == 'keyman': logging.info("downloading " + url) @@ -49,7 +48,7 @@ def download_and_install_package(url): return if packageFile and not _install_package(packageFile, bcp47): - logging.log(severity, "Can't find file " + url) + logging.error("Can't find file " + url) def _extract_bcp47(query): diff --git a/linux/keyman-config/keyman_config/install_kmp.py b/linux/keyman-config/keyman_config/install_kmp.py index bed23e3f41..eb2069c319 100755 --- a/linux/keyman-config/keyman_config/install_kmp.py +++ b/linux/keyman-config/keyman_config/install_kmp.py @@ -106,14 +106,14 @@ class InstallKmp(): self.kmpdocdir = get_keyman_doc_dir(area, self.packageID) self.kmpfontdir = get_keyman_font_dir(area, self.packageID) - if not self._safeMakeDirs(self.packageDir): - return - if not os.path.isfile(inputfile): message = _("File {kmpfile} doesn't exist").format(kmpfile=inputfile) logging.error("install_kmp.py: %s", message) raise InstallError(InstallStatus.Abort, message) + if not self._safeMakeDirs(self.packageDir): + return + extract_kmp(inputfile, self.packageDir) if is_fcitx_running(): diff --git a/linux/keyman-config/keyman_config/kmpmetadata.py b/linux/keyman-config/keyman_config/kmpmetadata.py index 83351d5172..4632531e75 100755 --- a/linux/keyman-config/keyman_config/kmpmetadata.py +++ b/linux/keyman-config/keyman_config/kmpmetadata.py @@ -506,7 +506,6 @@ def get_metadata(tmpdirname): If it does not exist then will return get_and_convert_infdata Args: - inputfile (str): path to kmp file tmpdirname(str): temp directory to extract kmp Returns: diff --git a/linux/keyman-config/keyman_config/uninstall_kmp.py b/linux/keyman-config/keyman_config/uninstall_kmp.py index fe8931dbe3..84c083694d 100755 --- a/linux/keyman-config/keyman_config/uninstall_kmp.py +++ b/linux/keyman-config/keyman_config/uninstall_kmp.py @@ -37,31 +37,32 @@ def uninstall_kmp_shared(packageID): """ kbdir = get_keyboard_dir(InstallLocation.Shared, packageID) if not os.path.isdir(kbdir): - logging.error("Keyboard directory for %s does not exist. Aborting", packageID) - exit(3) + msg = _("Keyboard directory for %s does not exist." % packageID) + logging.error(msg) + return msg kbdocdir = get_keyman_doc_dir(InstallLocation.Shared, packageID) kbfontdir = get_keyman_font_dir(InstallLocation.Shared, packageID) logging.info("Uninstalling shared keyboard: %s", packageID) 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) + msg = _("You do not have permissions to uninstall the keyboard files. You need to run this with `sudo`") + logging.error(msg) + return msg 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) + msg = _("You do not have permissions to uninstall the documentation. You need to run this with `sudo`") + logging.error(msg) + return msg delete_dir(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) + msg = _("You do not have permissions to uninstall the font files. You need to run this with `sudo`") + logging.error(msg) + return msg delete_dir(kbfontdir) logging.info("Removed font directory: %s", kbfontdir) else: @@ -80,6 +81,7 @@ def uninstall_kmp_shared(packageID): delete_dir(kbdir) logging.info("Removed keyman directory: %s", kbdir) logging.info("Finished uninstalling shared keyboard: %s", packageID) + return '' def uninstall_keyboards_from_ibus(keyboards, packageDir): @@ -130,8 +132,9 @@ def uninstall_kmp_user(packageID): """ kbdir = get_keyboard_dir(InstallLocation.User, packageID) if not os.path.isdir(kbdir): - logging.error("Keyboard directory for %s does not exist. Aborting", packageID) - exit(3) + msg = _("Keyboard directory for %s does not exist." % packageID) + logging.error(msg) + return msg logging.info("Uninstalling local keyboard: %s", packageID) info, system, options, keyboards, files = get_metadata(kbdir) if keyboards: @@ -150,6 +153,7 @@ def uninstall_kmp_user(packageID): delete_dir(fontdir) logging.info("Removed user keyman font directory: %s", fontdir) logging.info("Finished uninstalling local keyboard: %s", packageID) + return '' def uninstall_kmp(packageID, sharedarea=False): @@ -161,8 +165,9 @@ def uninstall_kmp(packageID, sharedarea=False): sharedarea (str): whether to uninstall from shared /usr/local or ~/.local """ if sharedarea: - uninstall_kmp_shared(packageID) + msg = uninstall_kmp_shared(packageID) else: - uninstall_kmp_user(packageID) + msg = uninstall_kmp_user(packageID) get_keyman_config_service().keyboard_list_changed() + return msg diff --git a/linux/keyman-config/keyman_config/view_installed.py b/linux/keyman-config/keyman_config/view_installed.py index 959c0196d9..671ca2d279 100755 --- a/linux/keyman-config/keyman_config/view_installed.py +++ b/linux/keyman-config/keyman_config/view_installed.py @@ -22,6 +22,7 @@ from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, get_keyman_dir) from keyman_config.install_window import InstallKmpWindow, find_keyman_image from keyman_config.keyboard_details import KeyboardDetailsView +from keyman_config.kmpmetadata import get_fonts, parsemetadata from keyman_config.list_installed_kmp import get_installed_kmp from keyman_config.options import OptionsView from keyman_config.uninstall_kmp import uninstall_kmp @@ -351,18 +352,40 @@ class ViewInstalledWindow(ViewInstalledWindowBase): 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 package?")) - dialog.format_secondary_text( - _("Are you sure that you want to uninstall the {keyboard} keyboard and its fonts?") - .format(keyboard=model[treeiter][1])) + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + _("Uninstall keyboard package?")) + msg = _("Are you sure that you want to uninstall the {keyboard} keyboard?").format( + keyboard=model[treeiter][1]) + kbdir = get_keyboard_dir(InstallLocation.User, model[treeiter][3]) + kmpjson = os.path.join(kbdir, "kmp.json") + if os.path.isfile(kmpjson): + info, system, options, keyboards, files = parsemetadata(kmpjson, False) + fonts = get_fonts(files) + if fonts: + # Fonts are optional + fontlist = "" + for font in fonts: + if 'description' in font: + if fontlist != "": + fontlist = fontlist + "\n" + if font['description'][:5] == "Font ": + fontdesc = font['description'][5:] + else: + fontdesc = font['description'] + fontlist = fontlist + fontdesc + msg += "\n\n" + _("The following fonts will also be uninstalled:\n") + fontlist + dialog.format_secondary_text(msg) 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]) + msg = uninstall_kmp(model[treeiter][3]) + if not msg == '': + md = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, _("Uninstalling keyboard failed.\n\nError message: ") + msg) + md.run() + md.destroy() logging.info("need to restart window after uninstalling a keyboard") self.restart() elif response == Gtk.ResponseType.NO: diff --git a/linux/keyman-config/km-config.bash-completion b/linux/keyman-config/km-config.bash-completion index bc28b19212..b1f46c0076 100644 --- a/linux/keyman-config/km-config.bash-completion +++ b/linux/keyman-config/km-config.bash-completion @@ -1,4 +1,4 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-config_completions() { @@ -9,14 +9,15 @@ _km-config_completions() opts="-h --help -v --verbose -vv --veryverbose --version -i --install" if [[ ${cur} == -* ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi # default IFS splits 'file://' into 'file', ':' and '//' - if [ "${prev}" == ":" -a "${COMP_WORDS[COMP_CWORD-2]}" == "file" ]; then + if [ "${prev}" == ":" ] && [ "${COMP_WORDS[COMP_CWORD-2]}" == "file" ]; then local flag=${COMP_WORDS[COMP_CWORD-3]} - if [ $flag == "-i" -o $flag == "--install" ]; then + if [ "$flag" == "-i" ] || [ "$flag" == "--install" ]; then prev=$flag fi fi @@ -25,7 +26,8 @@ _km-config_completions() "-i"|"--install") local IFS=$'\n' compopt -o filenames - COMPREPLY=( $(compgen -f -X "!"*.kmp -- $cur) $(compgen -d -- $cur) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -f -X "!"*.kmp -- "$cur") $(compgen -d -- "$cur") ) return 0 ;; *) diff --git a/linux/keyman-config/km-kvk2ldml.bash-completion b/linux/keyman-config/km-kvk2ldml.bash-completion index c89967666f..3eb2950c39 100644 --- a/linux/keyman-config/km-kvk2ldml.bash-completion +++ b/linux/keyman-config/km-kvk2ldml.bash-completion @@ -1,4 +1,4 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-kvk2ldml_completions() { @@ -9,7 +9,8 @@ _km-kvk2ldml_completions() opts="-h --help -p --print -k --keys -o --output -v --verbose -vv --veryverbose --version" if [[ ${cur} == -* ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi @@ -17,7 +18,8 @@ _km-kvk2ldml_completions() "-o"|"--output") local IFS=$'\n' compopt -o filenames - COMPREPLY=( $(compgen -f -- $cur) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -f -- "$cur") ) return 0 ;; *) @@ -26,7 +28,8 @@ _km-kvk2ldml_completions() local IFS=$'\n' compopt -o filenames - COMPREPLY=( $(compgen -f -X "!"*.kvk -- $cur) $(compgen -d -- $cur) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -f -X "!"*.kvk -- "$cur") $(compgen -d -- "$cur") ) } complete -F _km-kvk2ldml_completions km-kvk2ldml diff --git a/linux/keyman-config/km-package-get.bash-completion b/linux/keyman-config/km-package-get.bash-completion index 9a1d5ac3aa..9bacb1b8fe 100644 --- a/linux/keyman-config/km-package-get.bash-completion +++ b/linux/keyman-config/km-package-get.bash-completion @@ -1,17 +1,17 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-package-get_completions() { - local cur prev opts cache + local cur opts cache pkg_install_path COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" opts="-h --help -v --verbose -vv --veryverbose --version" cache=${XDG_CACHE_HOME:-~/.cache}/keyman/kmpdirlist if [[ ${cur} == -* ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi @@ -21,15 +21,17 @@ _km-package-get_completions() # Unfortunately with bash completion scripts it's not possible to factor out # common code. if [[ -e ./km-package-install ]]; then - python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', './km-package-install');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" + pkg_install_path='./km-package-install' else - python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', '/usr/bin/km-package-install');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" + pkg_install_path='/usr/bin/km-package-install' fi + python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', $pkg_install_path);spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" fi if [[ -r $cache ]] ; then - for file in `cat $cache`; do words="${words} ${file}"; done - COMPREPLY=($(compgen -W "${words}" -- ${cur})) + while read -r file; do words="${words} ${file}"; done < "$cache" + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -W "${words}" -- "${cur}")) return 0 fi } diff --git a/linux/keyman-config/km-package-install b/linux/keyman-config/km-package-install index dc2c3a355d..205a82b186 100755 --- a/linux/keyman-config/km-package-install +++ b/linux/keyman-config/km-package-install @@ -10,6 +10,8 @@ from pkg_resources import parse_version from zipfile import is_zipfile from keyman_config import KeymanApiUrl, __version__, secure_lookup +from keyman_config.install_kmp import extract_kmp +from keyman_config.kmpmetadata import get_metadata from keyman_config.uninstall_kmp import uninstall_kmp @@ -84,6 +86,33 @@ def list_keyboards(): write_kmpdirlist(kmpdirfile) +def _list_languages_for_keyboard_impl(packageId, packageDir): + from keyman_config.get_kmp import keyman_cache_dir + kmpfile = os.path.join(keyman_cache_dir(), packageId) + if not os.path.exists(kmpfile): + kmpfile = kmpfile + '.kmp' + if not os.path.exists(kmpfile): + return '' + extract_kmp(kmpfile, packageDir) + info, system, options, keyboards, files = get_metadata(packageDir) + if not keyboards: + return '' + firstKeyboard = keyboards[0] + if not secure_lookup(firstKeyboard, 'languages') or len(firstKeyboard['languages']) <= 0: + return '' + result = '' + for lang in firstKeyboard['languages']: + if result: + result += '\n' + result += lang['id'] + return result + + +def list_languages_for_keyboard(packageId, packageDir): + result = _list_languages_for_keyboard_impl(packageId, packageDir) + print(result) + + def main(): parser = argparse.ArgumentParser( description='Install a Keyman keyboard package, either a local .kmp file or specify a ' + diff --git a/linux/keyman-config/km-package-install.bash-completion b/linux/keyman-config/km-package-install.bash-completion index 768ea26bb2..1a424e8ea6 100644 --- a/linux/keyman-config/km-package-install.bash-completion +++ b/linux/keyman-config/km-package-install.bash-completion @@ -1,20 +1,29 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-package-install_completions() { - local cur prev opts cache + local cur prev opts cache pkg_install_path COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="-h --help -v --verbose -vv --veryverbose --version -p --package -f --file -s --shared" + opts="-h --help -v --verbose -vv --veryverbose --version -p --package -f --file -s --shared -l --bcp47" cache=${XDG_CACHE_HOME:-~/.cache}/keyman/kmpdirlist if [[ ${cur} == -* ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi + if [[ -e ./km-package-install ]]; then + pkg_install_path='./km-package-install' + get_kmp_path='keyman_config/get_kmp.py' + else + pkg_install_path='/usr/bin/km-package-install' + get_kmp_path='/usr/lib/python3/dist-packages/keyman_config/get_kmp.py' + fi + case "${prev}" in "-p"|"--package") words="" @@ -22,23 +31,58 @@ _km-package-install_completions() # NOTE: identical code in `km-package-get.bash-completion`. # Unfortunately with bash completion scripts it's not possible to factor out # common code. - if [[ -e ./km-package-install ]]; then - python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', './km-package-install');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" - else - python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', '/usr/bin/km-package-install');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" - fi + + python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', '$pkg_install_path');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_keyboards()" fi if [[ -r $cache ]] ; then - for file in `cat $cache`; do words="${words} ${file}"; done - COMPREPLY=($(compgen -W "${words}" -- ${cur})) + while read -r file; do words="${words} ${file}"; done < "$cache" + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -W "${words}" -- "${cur}")) return 0 fi ;; "-f"|"--file") local IFS=$'\n' compopt -o filenames - COMPREPLY=( $(compgen -f -X "!"*.kmp -- $cur) $(compgen -d -- $cur) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -f -X "!"*.kmp -- "$cur") $(compgen -d -- "$cur") ) + return 0 + ;; + "-l"|"--bcp47") + local packageId="" + local packageDir + packageDir=$(mktemp -d) + for ((i=0;i<$COMP_CWORD;i++)); do + case ${COMP_WORDS[$i]} in + "-p"|"--package") + packageId="${COMP_WORDS[$i+1]}" + # shellcheck disable=SC2086 # doesn't work with quotes + if [ ! -f ${XDG_CACHE_HOME:-~/.cache}/keyman/$packageId ] && [ ! -f ${XDG_CACHE_HOME:-~/.cache}/keyman/${packageId}.kmp ]; then + python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('get_kmp', '$get_kmp_path');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.get_kmp('$packageId')" + fi + break + ;; + "-f"|"--file") + package="${COMP_WORDS[$i+1]}" + packageId=$(basename "$package") + # shellcheck disable=SC2086 # doesn't work with quotes + cp "$package" ${XDG_CACHE_HOME:-~/.cache}/keyman/${packageId} + break + ;; + *) + ;; + esac + done + if [ -z "$packageId" ]; then + return 0 + fi + words="" + while read -r lang; do + words="$words $lang" + done < <(python3 -c "from importlib.machinery import SourceFileLoader;from importlib.util import module_from_spec, spec_from_loader;loader = SourceFileLoader('km_package_install', '$pkg_install_path');spec = spec_from_loader(loader.name, loader);mod = module_from_spec(spec);loader.exec_module(mod);mod.list_languages_for_keyboard('$packageId', '$packageDir')") + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -W "${words}" -- "${cur}")) return 0 ;; *) diff --git a/linux/keyman-config/km-package-list-installed.bash-completion b/linux/keyman-config/km-package-list-installed.bash-completion index e90bd29699..a3758b467e 100644 --- a/linux/keyman-config/km-package-list-installed.bash-completion +++ b/linux/keyman-config/km-package-list-installed.bash-completion @@ -1,15 +1,15 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-package-list-installed_completions() { - local cur prev opts + local cur 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}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi } diff --git a/linux/keyman-config/km-package-uninstall.bash-completion b/linux/keyman-config/km-package-uninstall.bash-completion index 451a2e3e80..191e709eab 100644 --- a/linux/keyman-config/km-package-uninstall.bash-completion +++ b/linux/keyman-config/km-package-uninstall.bash-completion @@ -1,4 +1,4 @@ -#/usr/bin/env bash +#!/usr/bin/env bash _km-package-uninstall_completions() { @@ -9,7 +9,8 @@ _km-package-uninstall_completions() opts="-h --help -s --shared -v --verbose -vv --veryverbose --version" if [[ ${cur} == -* ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi @@ -23,12 +24,15 @@ _km-package-uninstall_completions() shared="" case "${prev}" in "-s"|"--shared") - for file in $(ls -1 /usr/local/share/keyman); do kbid="`basename ${file}`"; shared="${shared} ${kbid}"; done - COMPREPLY=($(compgen -W "${shared}" -- ${cur})) + for file in /usr/local/share/keyman/*; do kbid="$(basename "${file}")"; shared="${shared} ${kbid}"; done + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -W "${shared}" -- "${cur}")) ;; *) - for file in $(ls -1 ${XDG_DATA_HOME:-~/.local/share}/keyman); do kbid="`basename ${file}`"; words="${words} ${kbid}"; done - COMPREPLY=($(compgen -W "${words}" -- ${cur})) + # shellcheck disable=SC2231 # doesn't work with quotes + for file in ${XDG_DATA_HOME:-~/.local/share}/keyman/*; do kbid="$(basename "${file}")"; words="${words} ${kbid}"; done + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -W "${words}" -- "${cur}")) ;; esac } diff --git a/linux/keyman-config/tests/test_package_install_completion.py b/linux/keyman-config/tests/test_package_install_completion.py new file mode 100644 index 0000000000..97238e3dc2 --- /dev/null +++ b/linux/keyman-config/tests/test_package_install_completion.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +import os +import tempfile +import unittest +from unittest.mock import patch, ANY +from importlib.machinery import SourceFileLoader +from importlib.util import module_from_spec, spec_from_loader + + +class PackageInstallCompletionTests(unittest.TestCase): + + def setUp(self): + patcher1 = patch('keyman_config.install_kmp.extract_kmp') + self.mockExtractKmp = patcher1.start() + self.addCleanup(patcher1.stop) + patcher2 = patch('keyman_config.kmpmetadata.get_metadata') + self.mockGetMetadata = patcher2.start() + self.addCleanup(patcher2.stop) + + loader = SourceFileLoader('km_package_install', os.path.join(os.path.dirname( + os.path.abspath(__file__)), '../km-package-install')) + spec = spec_from_loader(loader.name, loader) + if spec: + self.mod = module_from_spec(spec) + loader.exec_module(self.mod) + + self.tempDir = tempfile.TemporaryDirectory() + os.environ["XDG_CACHE_HOME"] = self.tempDir.name + self.cacheDir = os.path.join(self.tempDir.name, 'keyman') + os.makedirs(self.cacheDir) + + def _list_languages_for_keyboard_impl(self, packageId): + return self.mod._list_languages_for_keyboard_impl(packageId, 'someDir') + + def test_PackageCompletionNoLanguage(self): + open(os.path.join(self.cacheDir, 'foo'), 'w') + self.mockGetMetadata.return_value = (None, None, None, [{}], None) + result = self._list_languages_for_keyboard_impl('foo') + self.assertEqual(result, "") + + def test_PackageCompletionOneLanguage(self): + open(os.path.join(self.cacheDir, 'khmer_angkor'), 'w') + self.mockGetMetadata.return_value = ( + None, None, None, + [{'languages': [{'name': 'Central Khmer (Khmer, Cambodia)', 'id': 'km'}]}], + None) + result = self._list_languages_for_keyboard_impl('khmer_angkor') + self.assertEqual(result, "km") + + def test_PackageCompletionMultipleLanguages(self): + open(os.path.join(self.cacheDir, 'sil_euro_latin'), 'w') + self.mockGetMetadata.return_value = ( + None, None, None, + [{'languages': [ + {'name': 'English', 'id': 'en'}, + {'name': 'French', 'id': 'fr'}, + {'name': 'German', 'id': 'de'}]}], + None) + result = self._list_languages_for_keyboard_impl('sil_euro_latin') + self.assertEqual(result, 'en\nfr\nde')