Merge pull request #8029 from keymanapp/chore/merge-master20230116-epic-ldml

chore: merge master into feature-ldml 🙀
This commit is contained in:
Steven R. Loomis 2023-01-17 09:16:08 -06:00 • committed by GitHub
commit ed164805ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 431 additions and 76 deletions

View file

@ -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)

View file

@ -1 +1 @@
17.0.24
17.0.30

View file

@ -7,6 +7,7 @@
"ms-python.python",
"mads-hartmann.bash-ide-vscode",
"timonwong.shellcheck",
"maelvalais.autoconf"
"maelvalais.autoconf",
"webfreak.debug",
]
}

View file

@ -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"

View file

@ -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"

113
docs/websites/README.md Normal file
View file

@ -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
```

View file

@ -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

View file

@ -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:

View file

@ -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):

View file

@ -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):

View file

@ -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():

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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
;;
*)

View file

@ -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

View file

@ -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
}

View file

@ -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 ' +

View file

@ -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
;;
*)

View file

@ -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
}

View file

@ -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
}

View file

@ -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')