From c6e733747556576ab6317fa7a8523b46dc427efb Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 9 Feb 2026 18:59:35 +0100 Subject: [PATCH 01/57] refactor(linux): make `get_ibus_version()` public --- linux/keyman-config/keyman_config/ibus_util.py | 4 ++-- linux/keyman-config/tests/ibus_util_tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/linux/keyman-config/keyman_config/ibus_util.py b/linux/keyman-config/keyman_config/ibus_util.py index 7eec49a373..3401b7d3b9 100644 --- a/linux/keyman-config/keyman_config/ibus_util.py +++ b/linux/keyman-config/keyman_config/ibus_util.py @@ -143,7 +143,7 @@ def verify_ibus_daemon(start): return retval -def _get_ibus_version(): +def get_ibus_version(): ibus_version = subprocess.run(('ibus', 'version'), check=False, stdout=subprocess.PIPE).stdout match = re.search(r'^IBus (.*)\n$', ibus_version.decode('utf-8')) @@ -156,7 +156,7 @@ def _get_ibus_version(): def _start_ibus_daemon(realuser): try: - if version.parse(_get_ibus_version()) >= version.parse('1.5.28'): + if version.parse(get_ibus_version()) >= version.parse('1.5.28'): # IBus ~1.5.28 added the `start` command, so we use that if possible # and let IBus deal with the necessary parameters args = ['ibus', 'start', '-d'] diff --git a/linux/keyman-config/tests/ibus_util_tests.py b/linux/keyman-config/tests/ibus_util_tests.py index e9b9261a20..57829f0502 100644 --- a/linux/keyman-config/tests/ibus_util_tests.py +++ b/linux/keyman-config/tests/ibus_util_tests.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import Mock, patch -from keyman_config.ibus_util import _get_ibus_version, get_ibus_bus, install_to_ibus, uninstall_from_ibus +from keyman_config.ibus_util import get_ibus_version, get_ibus_bus, install_to_ibus, uninstall_from_ibus @patch('keyman_config.ibus_util.IBus.Bus') @@ -100,7 +100,7 @@ class IbusUtilTests(unittest.TestCase): ) # Execute - ibus_version = _get_ibus_version() + ibus_version = get_ibus_version() # Verify self.assertEqual(ibus_version, '1.5.28') From cb1de97ae744b44c9a62e10cb5fec93625aa7390 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 9 Feb 2026 17:14:45 +0100 Subject: [PATCH 02/57] feat(linux): implement diagnostic report This change allows to collect data for a diagnostic report. This can be done either from the command line through the new `km-diag-report` tool, or in `km-config` from the new `Support` tab that allows to generate the report and then copy it to the clipboard or save as a file. The diagnostic report contains installed browsers and their versions and installation methods as well as the default browser. It checks for browsers installed through apt (Ubuntu/Debian), snap, and flatpak. The change also has an implementation for pacman (Arch Linux) and dnf/yum (Fedora), although these have not been tested. Fixes: #7784 --- linux/debian/keyman.bash-completion | 1 + linux/debian/keyman.links | 1 + .../keyman_config/diag_report.py | 516 +++++++++++++++++ .../keyman-config/keyman_config/ibus_util.py | 36 +- .../keyman_config/support_widget.py | 172 ++++++ .../keyman_config/view_installed.py | 2 + linux/keyman-config/km-diag-report | 39 ++ .../km-diag-report.bash-completion | 18 + linux/keyman-config/maninc/km-config.inc | 2 +- linux/keyman-config/setup.py | 3 +- .../keyman-config/tests/diag_report_tests.py | 517 ++++++++++++++++++ 11 files changed, 1296 insertions(+), 11 deletions(-) create mode 100644 linux/keyman-config/keyman_config/diag_report.py create mode 100644 linux/keyman-config/keyman_config/support_widget.py create mode 100755 linux/keyman-config/km-diag-report create mode 100644 linux/keyman-config/km-diag-report.bash-completion create mode 100644 linux/keyman-config/tests/diag_report_tests.py diff --git a/linux/debian/keyman.bash-completion b/linux/debian/keyman.bash-completion index 5f489e0087..ba3ffe590f 100644 --- a/linux/debian/keyman.bash-completion +++ b/linux/debian/keyman.bash-completion @@ -1,4 +1,5 @@ linux/keyman-config/km-config.bash-completion km-config +linux/keyman-config/km-diag-report.bash-completion km-diag-report linux/keyman-config/km-kvk2ldml.bash-completion km-kvk2ldml linux/keyman-config/km-package-get.bash-completion km-package-get linux/keyman-config/km-package-install.bash-completion km-package-install diff --git a/linux/debian/keyman.links b/linux/debian/keyman.links index 7ed08a12f6..01a312f17e 100644 --- a/linux/debian/keyman.links +++ b/linux/debian/keyman.links @@ -1,4 +1,5 @@ /usr/share/keyman-config/km-config /usr/bin/km-config +/usr/share/keyman-config/km-diag-report /usr/bin/km-diag-report /usr/share/keyman-config/km-kvk2ldml /usr/bin/km-kvk2ldml /usr/share/keyman-config/km-package-get /usr/bin/km-package-get /usr/share/keyman-config/km-package-install /usr/bin/km-package-install diff --git a/linux/keyman-config/keyman_config/diag_report.py b/linux/keyman-config/keyman_config/diag_report.py new file mode 100644 index 0000000000..a2ec76f403 --- /dev/null +++ b/linux/keyman-config/keyman_config/diag_report.py @@ -0,0 +1,516 @@ +#!/usr/bin/python3 +''' +Keyman is copyright (C) SIL Global. MIT License. + +Collects diagnostic information about the system for troubleshooting. +''' +import logging +import os +import platform +import shutil +import subprocess + +from keyman_config.get_kmp import InstallLocation +from keyman_config.ibus_util import get_ibus_version +from keyman_config.list_installed_kmp import get_installed_kmp +from keyman_config.version import ( + __version__, + __versionwithtag__, + __pkgversion__, + __tier__ +) + + +class DiagReport: + """ + Collects diagnostic information about the system for troubleshooting. + """ + + def _get_keyman_version(self): + """Get Keyman version information.""" + return { + 'version': __version__, + 'version_with_tag': __versionwithtag__, + 'package_version': __pkgversion__, + 'tier': __tier__ + } + + def _get_package_version(self): + """Get the dpkg package version of keyman.""" + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f', '${Version}', 'keyman'], + capture_output=True, check=False) + if result.returncode == 0: + return result.stdout.decode('utf-8').strip() + except FileNotFoundError: + logging.debug('dpkg-query not found') + except Exception as e: + logging.warning('Error getting package version: %s', e) + return None + + def _get_fcitx_version(self): # sourcery skip: use-next + """Get the fcitx5 version if available.""" + try: + result = subprocess.run( + ['fcitx5', '--version'], + capture_output=True, check=False) + if result.returncode == 0: + # Parse output like "fcitx5 version: 5.0.23" + output = result.stdout.decode('utf-8').strip() + for line in output.split('\n'): + if 'version' in line.lower(): + return line.split(':')[-1].strip() + return output + except FileNotFoundError: + logging.debug('fcitx5 not found') + except Exception as e: + logging.warning('Error getting fcitx version: %s', e) + return None + + def _get_os_info(self): + """Get OS distribution information.""" + os_info = { + 'platform': platform.platform(), + 'system': platform.system(), + 'release': platform.release(), + 'machine': platform.machine() + } + + # Try to get freedesktop os-release info + try: + self._get_freedesktop_os_release_info(os_info) + except (OSError, AttributeError): + # Fallback to lsb_release + try: + result = subprocess.run( + ['lsb_release', '-a'], + capture_output=True, check=False) + if result.returncode == 0: + output = result.stdout.decode('utf-8') + for line in output.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + key = key.strip().lower().replace(' ', '_') + os_info[key] = value.strip() + except FileNotFoundError: + logging.debug('lsb_release not found') + except Exception as e: + logging.warning('Error getting OS info via lsb_release: %s', e) + + return os_info + + def _get_freedesktop_os_release_info(self, os_info): + os_release = platform.freedesktop_os_release() + os_info['pretty_name'] = os_release.get('PRETTY_NAME', '') + os_info['version_codename'] = os_release.get('VERSION_CODENAME', '') + + def _get_display_server(self): + """Get the display server type (X11, Wayland, etc.).""" + session_type = os.environ.get('XDG_SESSION_TYPE', '') + wayland_display = os.environ.get('WAYLAND_DISPLAY', '') + display = os.environ.get('DISPLAY', '') + + return { + 'session_type': session_type, + 'wayland_display': wayland_display, + 'display': display + } + + def _get_desktop_environment(self): + """Get the current desktop environment.""" + return { + 'xdg_current_desktop': os.environ.get('XDG_CURRENT_DESKTOP', ''), + 'xdg_session_desktop': os.environ.get('XDG_SESSION_DESKTOP', ''), + 'desktop_session': os.environ.get('DESKTOP_SESSION', '') + } + + def _get_input_method(self): + """Get current input method configuration.""" + return { + 'gtk_im_module': os.environ.get('GTK_IM_MODULE', ''), + 'qt_im_module': os.environ.get('QT_IM_MODULE', ''), + 'xmodifiers': os.environ.get('XMODIFIERS', ''), + } + + def _get_installed_keyboards(self): + """Get list of installed Keyman keyboards.""" + keyboards = { + 'user': [], + 'shared': [], + 'os': [] + } + + for area, location in [ + ('user', InstallLocation.User), + ('shared', InstallLocation.Shared), + ('os', InstallLocation.OS) + ]: + try: + installed = get_installed_kmp(location) + for pkg_id, data in installed.items(): + keyboards[area].append({ + 'id': pkg_id, + 'name': data.get('name', ''), + 'version': data.get('kmpversion', '') + }) + except Exception as e: + logging.warning('Error getting keyboards from %s: %s', area, e) + + return keyboards + + def _get_ibus_engines(self): + """Get list of IBus preload engines.""" + try: + from keyman_config.gsettings import GSettings + ibus_settings = GSettings('org.freedesktop.ibus.general') + engines = ibus_settings.get('preload-engines') + return engines or [] + except Exception as e: + logging.warning('Error getting IBus engines: %s', e) + return [] + + def _get_gnome_input_sources(self): + """Get Gnome input sources if running Gnome.""" + try: + from keyman_config.gnome_keyboards_util import GnomeKeyboardsUtil, is_gnome_desktop + if is_gnome_desktop(): + util = GnomeKeyboardsUtil() + sources = util.read_input_sources() + return sources or [] + except Exception as e: + logging.warning('Error getting Gnome input sources: %s', e) + return [] + + def _get_browser_versions(self): + """Get versions of Firefox, Chromium, Chrome installed via various package managers.""" + return { + 'apt': self._get_apt_browser_versions(), + 'rpm': self._get_rpm_browser_versions(), + 'pacman': self._get_pacman_browser_versions(), + 'snap': self._get_snap_browser_versions(), + 'flatpak': self._get_flatpak_browser_versions(), + } + + def _get_snap_browser_versions(self): + """Get browser versions installed via snap.""" + snap_browsers = {} + for browser in ['firefox', 'chromium']: + try: + result = subprocess.run( + ['snap', 'info', browser], + capture_output=True, + check=False, + ) + if result.returncode == 0: + output = result.stdout.decode('utf-8') + for line in output.split('\n'): + if line.startswith('installed:'): + version = line.split(':', 1)[1].strip() + if version != '-': + snap_browsers[browser] = version + break + except FileNotFoundError: + logging.debug('snap not found') + except Exception as e: + logging.debug('Error checking snap package %s: %s', browser, e) + return snap_browsers + + def _get_flatpak_browser_versions(self): + """Get browser versions installed via flatpak.""" + flatpak_browsers = {} + for app_id in ['org.mozilla.firefox', 'org.chromium.Chromium', 'com.google.Chrome']: + try: + result = subprocess.run( + ['flatpak', 'info', app_id], + capture_output=True, + check=False, + ) + if result.returncode == 0: + output = result.stdout.decode('utf-8') + for line in output.split('\n'): + if line.strip().startswith('Version:'): + version = line.split(':', 1)[1].strip() + app_name = app_id.split('.')[-1].lower() + flatpak_browsers[app_name] = version + break + except FileNotFoundError: + logging.debug('flatpak not found') + except Exception as e: + logging.debug('Error checking flatpak app %s: %s', app_id, e) + return flatpak_browsers + + def _has_command(self, cmd): + result = shutil.which(cmd) + if not result: + logging.debug(f'{cmd} not found') + return False + return True + + def _get_apt_browser_versions(self): + """Get browser versions installed via apt.""" + apt_browsers = {} + if not self._has_command('dpkg-query'): + return apt_browsers + + for browser in ['firefox', 'chromium-browser', 'chromium', 'google-chrome-stable']: + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f', '${Version}', browser], + capture_output=True, + check=False, + ) + if result.returncode == 0: + if version := result.stdout.decode('utf-8').strip(): + apt_browsers[browser] = version + except Exception as e: + logging.debug('Error checking apt package %s: %s', browser, e) + return apt_browsers + + def _get_rpm_browser_versions(self): + """Get browser versions installed via RPM-based package managers (dnf/yum).""" + rpm_browsers = {} + + pm_command = next( + (cmd for cmd in ['dnf', 'yum'] if self._has_command(cmd)), None + ) + if not pm_command: + logging.debug('Neither dnf nor yum package manager found') + return rpm_browsers + + for browser in ['firefox', 'chromium', 'google-chrome-stable']: + try: + result = subprocess.run( + [pm_command, 'info', 'installed', browser], + capture_output=True, + check=False, + ) + if result.returncode == 0: + output = result.stdout.decode('utf-8') + for line in output.split('\n'): + if line.startswith('Version'): + version = line.split(':', 1)[1].strip() + rpm_browsers[browser] = version + break + except Exception as e: + logging.debug('Error checking rpm package %s: %s', browser, e) + + return rpm_browsers + + def _get_pacman_browser_versions(self): + """Get browser versions installed via Pacman (Arch Linux).""" + pacman_browsers = {} + + if not self._has_command('pacman'): + return pacman_browsers + + for browser in ['firefox', 'chromium', 'google-chrome']: + try: + result = subprocess.run( + ['pacman', '-Q', browser], + capture_output=True, + check=False, + ) + if result.returncode == 0: + output = result.stdout.decode('utf-8').strip() + # pacman -Q outputs: "package version" + parts = output.split() + if len(parts) >= 2: + version = parts[1] + pacman_browsers[browser] = version + except Exception as e: + logging.debug('Error checking pacman package %s: %s', browser, e) + + return pacman_browsers + + def _get_default_browser(self): + """Get the default browser with executable path.""" + try: + result = subprocess.run( + ['xdg-mime', 'query', 'default', 'text/html'], + capture_output=True, check=False) + if result.returncode == 0: + if desktop_id := result.stdout.decode('utf-8').strip(): + if desktop_info := self._parse_desktop_file(desktop_id): + return desktop_info + return desktop_id + except FileNotFoundError: + logging.debug('xdg-mime not found') + except Exception as e: + logging.warning('Error getting default browser: %s', e) + return None + + def _parse_desktop_file(self, desktop_id): + """Parse a .desktop file and extract the Exec command and file path.""" + # Common locations for .desktop files + search_paths = [ + os.path.expanduser('~/.local/share/applications'), + '/usr/local/share/applications', + '/usr/share/applications' + ] + + if 'XDG_DATA_DIRS' in os.environ: + data_dirs = os.environ['XDG_DATA_DIRS'] + new_search_paths = [] + new_search_paths.extend( + os.path.join(data_dir, 'applications') + for data_dir in data_dirs.split(os.pathsep) + ) + search_paths = new_search_paths + search_paths + + if 'XDG_DATA_HOME' in os.environ: + search_paths.insert(0, os.path.join(os.environ['XDG_DATA_HOME'], 'applications')) + + for base_path in search_paths: + desktop_file = os.path.join(base_path, desktop_id) + if os.path.isfile(desktop_file): + try: + with open(desktop_file, 'r', encoding='utf-8') as f: + exec_cmd = None + for line in f: + if line.startswith('Exec='): + exec_cmd = line.split('=', 1)[1].strip() + # Remove field codes like %U, %F, %u, %f + exec_cmd = exec_cmd.split('%')[0].strip() + break + return f"{desktop_file} ({exec_cmd})" if exec_cmd else f"{desktop_file}" + except Exception as e: + logging.debug('Error parsing desktop file %s: %s', desktop_file, e) + return None + + def _report_browsers(self, pkg_managers, browsers): + lines = [] + for (key, description) in pkg_managers: + if browsers.get(key): + lines.append(f' {description}:') + lines.extend( + f" - {name}: {version}" + for name, version in browsers[key].items() + ) + return lines or None + + def _create_report(self): # sourcery skip: low-code-quality + """Generate a complete diagnostic report as a formatted string.""" + keyman_info = self._get_keyman_version() + lines = [ + "=" * 60, + "Keyman Diagnostic Report", + "=" * 60, + "", + "--- Keyman Version ---", + *( + f" Version: {keyman_info['version_with_tag']}", + f" Tier: {keyman_info['tier']}", + ), + ] + if keyman_info['package_version']: + lines.append(f" Package version: {keyman_info['package_version']}") + if pkg_version := self._get_package_version(): + lines.append(f" Installed package (dpkg): {pkg_version}") + + # IBus Version + ibus_version = get_ibus_version() + if ibus_version is not None: + lines.extend(("", "--- IBus ---", f" IBus version: {ibus_version or 'unknown'}")) + if engines := self._get_ibus_engines(): + lines.append(f" Preload engines: {', '.join(engines)}") + + if fcitx_version := self._get_fcitx_version(): + lines.extend(("", "--- Fcitx5 ---", f" Fcitx5 version: {fcitx_version}")) + + # OS Information + lines.extend(("", "--- Operating System ---")) + os_info = self._get_os_info() + if 'pretty_name' in os_info and os_info['pretty_name']: + lines.append(f" OS: {os_info['pretty_name']}") + else: + lines.append(f" Platform: {os_info['platform']}") + if os_info.get('version_codename'): + lines.append(f" Codename: {os_info['version_codename']}") + lines.extend(( + f" Kernel: {os_info['release']}", + f" Architecture: {os_info['machine']}", + + # Display Server + "", + "--- Display Server ---" + )) + display_info = self._get_display_server() + lines.append(f" Session type: {display_info['session_type'] or 'Not set'}") + if display_info['wayland_display']: + lines.append(f" Wayland display: {display_info['wayland_display']}") + if display_info['display']: + lines.append(f" X11 display: {display_info['display']}") + + # Desktop Environment + lines.extend(("", "--- Desktop Environment ---")) + desktop_info = self._get_desktop_environment() + if desktop_info['xdg_current_desktop']: + lines.append(f" Current desktop: {desktop_info['xdg_current_desktop']}") + if desktop_info['xdg_session_desktop']: + lines.append(f" Session desktop: {desktop_info['xdg_session_desktop']}") + if desktop_info['desktop_session']: + lines.append(f" Desktop session: {desktop_info['desktop_session']}") + + # Input Method + lines.extend(("", "--- Input Method Configuration ---")) + im_info = self._get_input_method() + lines.extend(( + f" GTK_IM_MODULE: {im_info['gtk_im_module'] or 'Not set'}", + f" QT_IM_MODULE: {im_info['qt_im_module'] or 'Not set'}", + f" XMODIFIERS: {im_info['xmodifiers'] or 'Not set'}", + )) + if gnome_sources := self._get_gnome_input_sources(): + lines.extend(("", "--- Gnome Input Sources ---")) + for source in gnome_sources: + if isinstance(source, tuple) and len(source) == 2: + lines.append(f" {source[0]}: {source[1]}") + else: + lines.append(f" {source}") + + # Installed Keyboards + lines.extend(("", "--- Installed Keyman Keyboards ---")) + keyboards = self._get_installed_keyboards() + for area in ['user', 'shared', 'os']: + if keyboards[area]: + lines.append(f" {area.capitalize()} keyboards:") + lines.extend( + f" - {kb['name']} (id: {kb['id']}, version: {kb['version']})" + for kb in keyboards[area] + ) + if not any(keyboards.values()): + lines.append("No Keyman keyboards installed") + + # Browsers + lines.extend(("", "--- Web Browsers ---")) + browsers = self._get_browser_versions() + pkg_managers = [ + ('apt', 'APT packages'), + ('rpm', 'RPM packages (dnf/yum)'), + ('pacman', 'Pacman packages (Arch)'), + ('snap', 'Snap packages'), + ('flatpak', 'Flatpak packages'), + ] + if browser_report := self._report_browsers(pkg_managers, browsers): + lines.extend(browser_report) + else: + lines.append("No Firefox, Chromium, or Chrome installations found") + + # Default browser + if default_browser := self._get_default_browser(): + lines.extend((" Default browser:", f" {default_browser}")) + else: + lines.append(" No default browser detected") + + lines.extend(( + "", + "=" * 60, + "End of Diagnostic Report", + "=" * 60 + )) + return '\n'.join(lines) + + +def get_diagnostic_report(): + """Convenience function to generate a diagnostic report.""" + return DiagReport()._create_report() diff --git a/linux/keyman-config/keyman_config/ibus_util.py b/linux/keyman-config/keyman_config/ibus_util.py index 3401b7d3b9..4e8c084efe 100644 --- a/linux/keyman-config/keyman_config/ibus_util.py +++ b/linux/keyman-config/keyman_config/ibus_util.py @@ -144,19 +144,37 @@ def verify_ibus_daemon(start): def get_ibus_version(): - ibus_version = subprocess.run(('ibus', 'version'), check=False, - stdout=subprocess.PIPE).stdout - match = re.search(r'^IBus (.*)\n$', ibus_version.decode('utf-8')) - if match: - logging.info('Running IBus version %s', match.group(1)) - return match.group(1) - logging.warning('Unable to determine IBus version') - return '' + """ + Get the installed IBus version string if available. This checks the `ibus` command + output and parses the reported version. + + Returns the version as a string on success, an empty string if the version + cannot be determined, or None if the `ibus` command is not available. + + Returns: + str | None: The detected IBus version string, an empty string if + parsing fails, or None if the `ibus` command is not found. + """ + try: + ibus_version = subprocess.run(('ibus', 'version'), check=False, + stdout=subprocess.PIPE).stdout + if match := re.search(r'^IBus (.*)\n$', ibus_version.decode('utf-8')): + logging.info('Running IBus version %s', match[1]) + return match[1] + logging.warning('Unable to determine IBus version') + return '' + except FileNotFoundError: + logging.warning('ibus command not found') + return None def _start_ibus_daemon(realuser): try: - if version.parse(get_ibus_version()) >= version.parse('1.5.28'): + ibus_ver = get_ibus_version() + if not ibus_ver: + return + + if version.parse(ibus_ver) >= version.parse('1.5.28'): # IBus ~1.5.28 added the `start` command, so we use that if possible # and let IBus deal with the necessary parameters args = ['ibus', 'start', '-d'] diff --git a/linux/keyman-config/keyman_config/support_widget.py b/linux/keyman-config/keyman_config/support_widget.py new file mode 100644 index 0000000000..600f557271 --- /dev/null +++ b/linux/keyman-config/keyman_config/support_widget.py @@ -0,0 +1,172 @@ +#!/usr/bin/python3 + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +from keyman_config import _ +from keyman_config.diag_report import get_diagnostic_report + + +class SupportWidget(Gtk.Box): + """Widget for the Support tab in km-config that shows diagnostic information.""" + + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + # Header label + label = Gtk.Label(_("Support")) + label.set_padding(5, 5) + label.set_halign(Gtk.Align.START) + self.pack_start(label, False, False, 10) + + # Description + desc_label = Gtk.Label( + _("Generate a diagnostic report to help troubleshoot issues with Keyman. " + "You can copy this report and include it when reporting issues.")) + desc_label.set_line_wrap(True) + desc_label.set_halign(Gtk.Align.START) + desc_label.set_padding(5, 5) + self.pack_start(desc_label, False, False, 0) + + # Button box + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + button_box.set_margin_start(5) + button_box.set_margin_top(10) + + self.generate_button = Gtk.Button.new_with_mnemonic(_("_Generate Report")) + self.generate_button.set_tooltip_text(_("Generate a diagnostic report")) + self.generate_button.connect("clicked", self.on_generate_clicked) + button_box.pack_start(self.generate_button, False, False, 0) + + self.copy_button = Gtk.Button.new_with_mnemonic(_("_Copy to Clipboard")) + self.copy_button.set_tooltip_text(_("Copy the report to clipboard")) + self.copy_button.connect("clicked", self.on_copy_clicked) + self.copy_button.set_sensitive(False) + button_box.pack_start(self.copy_button, False, False, 0) + + self.save_button = Gtk.Button.new_with_mnemonic(_("_Save to File...")) + self.save_button.set_tooltip_text(_("Save the report to a file")) + self.save_button.connect("clicked", self.on_save_clicked) + self.save_button.set_sensitive(False) + button_box.pack_start(self.save_button, False, False, 0) + + self.pack_start(button_box, False, False, 0) + + # Scrolled window with text view for the report + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_margin_start(5) + scrolled.set_margin_end(5) + scrolled.set_margin_top(10) + scrolled.set_margin_bottom(5) + + self.text_view = Gtk.TextView() + self.text_view.set_editable(False) + self.text_view.set_cursor_visible(False) + self.text_view.set_monospace(True) + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) + self.text_buffer = self.text_view.get_buffer() + + scrolled.add(self.text_view) + self.pack_start(scrolled, True, True, 0) + + self.report_text = "" + + def on_generate_clicked(self, button): + """Generate the diagnostic report and display it.""" + # Show a spinner or progress indication + self.generate_button.set_sensitive(False) + self.text_buffer.set_text(_("Generating report...")) + + # Use idle_add to allow the UI to update + from gi.repository import GLib + GLib.idle_add(self._generate_report) + + def _generate_report(self): + """Actually generate the report (called from idle).""" + try: + self.report_text = get_diagnostic_report() + self.text_buffer.set_text(self.report_text) + self.copy_button.set_sensitive(True) + self.save_button.set_sensitive(True) + except Exception as e: + self.text_buffer.set_text(_("Error generating report: ") + str(e)) + finally: + self.generate_button.set_sensitive(True) + return False # Don't repeat + + def on_copy_clicked(self, button): + """Copy the report to clipboard.""" + if self.report_text: + clipboard = Gtk.Clipboard.get_default(self.get_display()) + clipboard.set_text(self.report_text, -1) + # Show a brief notification + self._show_toast(_("Report copied to clipboard")) + + def on_save_clicked(self, button): + """Save the report to a file.""" + if not self.report_text: + return + + dialog = Gtk.FileChooserDialog( + title=_("Save Diagnostic Report"), + parent=self.get_toplevel(), + action=Gtk.FileChooserAction.SAVE) + dialog.add_buttons( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK) + dialog.set_do_overwrite_confirmation(True) + dialog.set_current_name("keyman-diagnostic-report.txt") + + filter_text = Gtk.FileFilter() + filter_text.set_name(_("Text files")) + filter_text.add_pattern("*.txt") + dialog.add_filter(filter_text) + + filter_all = Gtk.FileFilter() + filter_all.set_name(_("All files")) + filter_all.add_pattern("*") + dialog.add_filter(filter_all) + + response = dialog.run() + if response == Gtk.ResponseType.OK: + filename = dialog.get_filename() + try: + with open(filename, 'w', encoding='utf-8') as f: + f.write(self.report_text) + f.write('\n') + self._show_toast(_("Report saved to ") + filename) + except IOError as e: + error_dialog = Gtk.MessageDialog( + parent=self.get_toplevel(), + flags=0, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text=_("Error saving file")) + error_dialog.format_secondary_text(str(e)) + error_dialog.run() + error_dialog.destroy() + + dialog.destroy() + + def _show_toast(self, message): + """Show a brief notification message.""" + # For simplicity, we'll just update a label temporarily + # A more sophisticated implementation could use GtkRevealer or similar + from gi.repository import GLib + + original_text = _("_Generate Report") + self.generate_button.set_use_underline(False) + self.generate_button.set_label(message) + self.generate_button.set_sensitive(False) + + def restore(): + self.generate_button.set_label(original_text) + self.generate_button.set_sensitive(True) + self.generate_button.set_use_underline(True) + return False + + GLib.timeout_add(2000, restore) diff --git a/linux/keyman-config/keyman_config/view_installed.py b/linux/keyman-config/keyman_config/view_installed.py index b252260532..e39224b837 100755 --- a/linux/keyman-config/keyman_config/view_installed.py +++ b/linux/keyman-config/keyman_config/view_installed.py @@ -30,6 +30,7 @@ from keyman_config.keyboard_layouts_widget import KeyboardLayoutsWidget from keyman_config.list_installed_kmp import get_installed_kmp from keyman_config.options_widget import OptionsWidget from keyman_config.sentry_handling import SentryErrorHandling +from keyman_config.support_widget import SupportWidget class ViewInstalledWindowBase(Gtk.Window): @@ -154,6 +155,7 @@ class ViewInstalledWindow(ViewInstalledWindowBase): self.refresh_installed_kmp() stack.add_titled(KeyboardLayoutsWidget(self, self.store, self.restart), "KeyboardLayouts", _("Keyboard Layouts")) stack.add_titled(OptionsWidget(self.sentry), "Options", _("Options")) + stack.add_titled(SupportWidget(), "Support", _("Support")) return outerHbox def _add_app_buttons(self): diff --git a/linux/keyman-config/km-diag-report b/linux/keyman-config/km-diag-report new file mode 100755 index 0000000000..b1533009ca --- /dev/null +++ b/linux/keyman-config/km-diag-report @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +import argparse +import sys + +from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry +from keyman_config.diag_report import get_diagnostic_report + + +def main(): + parser = argparse.ArgumentParser( + description='Generate a diagnostic report of Keyman and system configuration. ' + 'This report can be used for troubleshooting Keyman issues.') + parser.add_argument('-o', '--output', metavar='FILE', + help='write report to FILE instead of stdout') + add_standard_arguments(parser) + + args = parser.parse_args() + + initialize_logging(args) + initialize_sentry() + + report = get_diagnostic_report() + + if args.output: + try: + with open(args.output, 'w', encoding='utf-8') as f: + f.write(report) + f.write('\n') + print(f"Diagnostic report written to {args.output}") + except IOError as e: + print(f"Error writing to file: {e}", file=sys.stderr) + sys.exit(1) + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/linux/keyman-config/km-diag-report.bash-completion b/linux/keyman-config/km-diag-report.bash-completion new file mode 100644 index 0000000000..56ed558e0b --- /dev/null +++ b/linux/keyman-config/km-diag-report.bash-completion @@ -0,0 +1,18 @@ +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. + +_km-diag-report_completions() +{ + local cur opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + opts="-h --help -o --output -v --verbose -vv --veryverbose --version" + + if [[ ${cur} == -* ]] ; then + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi +} + +complete -F _km-diag-report_completions km-diag-report diff --git a/linux/keyman-config/maninc/km-config.inc b/linux/keyman-config/maninc/km-config.inc index 9783cd6b60..5495960067 100644 --- a/linux/keyman-config/maninc/km-config.inc +++ b/linux/keyman-config/maninc/km-config.inc @@ -1,5 +1,5 @@ [ Date: Fri, 20 Feb 2026 10:34:55 +0700 Subject: [PATCH 03/57] chore(ios): Cleanup `RELEASE_OEM_FIRSTVOICES` var in build.sh scripts Fixes #9051 * Fixes unbound variable in tools/prepRelease.sh * Checks `RELEASE_OEM_FIRSTVOICES` env var for build targets --- ios/build.sh | 21 ++++++++++++++++++++- ios/tools/prepRelease.sh | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ios/build.sh b/ios/build.sh index fb4df58ed8..81b477932c 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -1,4 +1,12 @@ #!/usr/bin/env bash +# +# Build Keyman Engine for iPhone and iPad, Keyman for iPhone and iPad, OEM FirstVoices iOS app, +# Samples: KMSample1 and KMSample2 +# +# OEM FirstVoices iOS app requires the following env vars set: +# RELEASE_OEM should be set +# RELEASE_OEM_FIRSTVOICES should be true + ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -7,6 +15,17 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/utils.inc.sh" +# +# Restrict available OEM publish targets to those that can be built on the current system +# + +oemtargets=() +if [[ ! -z "${RELEASE_OEM+x}" ]]; then + if [[ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]]; then + oemtargets+=(":fv=../oem/firstvoices/ios Builds OEM FirstVoices for iOS platforms") + fi +fi + builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices - iPhone and iPad." \ "@/resources/tools/check-markdown test:help" \ "clean" \ @@ -18,7 +37,7 @@ builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices ":help Online documentation" \ ":sample1=samples/KMSample1 Builds the first KeymanEngine sample app" \ ":sample2=samples/KMSample2 Builds the second KeymanEngine sample app" \ - ":fv=../oem/firstvoices/ios Builds OEM FirstVoices for iOS platforms" \ + "${oemtargets[@]}" \ "--sim-artifact+ Also outputs a simulator-friendly test artifact corresponding to the build" builder_parse "$@" diff --git a/ios/tools/prepRelease.sh b/ios/tools/prepRelease.sh index 8065aad2f8..c54748b548 100755 --- a/ios/tools/prepRelease.sh +++ b/ios/tools/prepRelease.sh @@ -105,7 +105,7 @@ cd "$WORK_DIR" # FirstVoices app # -if [ "${RELEASE_OEM_FIRSTVOICES}" = true ]; then +if [[ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]]; then FIRSTVOICESAPP_IPA="../oem/firstvoices/ios/build/Build/Products/Release-iphoneos/FirstVoices.ipa" FIRSTVOICESAPP_IPA_DST="firstvoices-ios-${KEYMAN_VERSION_FOR_FILENAME}.ipa" @@ -129,6 +129,6 @@ fi write_download_info "${UPLOAD_DIR}" "${KMEI_DST_NAME}" "Keyman Engine for iOS" zip ios write_download_info "${UPLOAD_DIR}" "${KEYMANAPP_IPA_DST}" "Keyman for iPhone and iPad" ipa ios -if [[ ${RELEASE_OEM_FIRSTVOICES} = true ]]; then +if [[ ${RELEASE_OEM_FIRSTVOICES-false} = true ]]; then write_download_info "${UPLOAD_DIR}" "${FIRSTVOICESAPP_IPA_DST}" "FirstVoices Keyboards" ipa ios fi From 0bd6f59382dda18f730ae4a59301b70ec9af2d8f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 24 Feb 2026 15:35:44 +0100 Subject: [PATCH 04/57] fix(developer): handle window already destroyed when closing editor Fixes: #15617 Fixes: KEYMAN-DEVELOPER-1JD Fixes: KEYMAN-DEVELOPER-36K Fixes: KEYMAN-DEVELOPER-2P5 Test-bot: skip --- common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas index bffcd952d2..19f84b40c3 100644 --- a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas +++ b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas @@ -303,7 +303,7 @@ end; function TframeCEFHost.HasFocus: Boolean; begin AssertVclThread; - Result := cefwp.HandleAllocated and IsChild(cefwp.Handle, GetFocus); + Result := Assigned(cefwp) and cefwp.HandleAllocated and IsChild(cefwp.Handle, GetFocus); end; procedure TframeCEFHost.Handle_CEF_SHOW(var message: TMessage); @@ -574,7 +574,8 @@ end; procedure TframeCEFHost.Handle_CEF_DESTROY(var Message: TMessage); begin AssertVclThread; - cefwp.DestroyChildWindow; + if Assigned(cefwp) then + cefwp.DestroyChildWindow; FreeAndNil(cefwp); end; From 79d13a4ad24196f07cea32b0a8a6f3e3c01df25f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 24 Feb 2026 16:17:06 +0100 Subject: [PATCH 05/57] fix(developer): package compiler handle missing `info` section in .kps file Fixes: #15619 Fixes: KEYMAN-DEVELOPER-27R Test-bot: skip --- developer/src/kmc-package/src/compiler/kmp-compiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 4999336cee..141547354b 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -206,7 +206,8 @@ export class KmpCompiler implements KeymanCompiler { fileVersion: null, keymanDeveloperVersion: KEYMAN_VERSION.VERSION }, - options: {} + options: {}, + info: {}, }; // From 9fca089dc89932f586a45d85280f4db1de2c9ad9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 24 Feb 2026 16:48:03 +0100 Subject: [PATCH 06/57] fix(developer): handle .kps file that has no `` element Fixes: #15621 Fixes: KEYMAN-DEVELOPER-37D Test-bot: skip --- .../web/utils/src/developer-utils-messages.ts | 6 +++++ .../utils/src/types/kps/kps-file-reader.ts | 25 +++++++++++-------- .../fixtures/kps/error_not_a_package_file.kps | 4 +++ .../utils/test/kps/kps-file-reader.tests.ts | 12 +++++++++ .../test/package-compiler.tests.ts | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 developer/src/common/web/utils/test/fixtures/kps/error_not_a_package_file.kps diff --git a/developer/src/common/web/utils/src/developer-utils-messages.ts b/developer/src/common/web/utils/src/developer-utils-messages.ts index 917af99e70..c3d797ad99 100644 --- a/developer/src/common/web/utils/src/developer-utils-messages.ts +++ b/developer/src/common/web/utils/src/developer-utils-messages.ts @@ -79,4 +79,10 @@ export class DeveloperUtilsMessages { this.ERROR_ProjectFileCouldNotBeRead, `Project file could not be read` ); + + static ERROR_NotAPackageFile = SevError | 0x000D; + static Error_NotAPackageFile = () => m( + this.ERROR_NotAPackageFile, + `Package source file is not a valid .kps file because it is missing the root element.`, + ); }; diff --git a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts index c41a30ebb4..203617864d 100644 --- a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts @@ -38,16 +38,21 @@ export class KpsFileReader { return null; } + if(!kpsPackage.Package) { + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_NotAPackageFile()); + return null; + } + return this.boxArrays(kpsPackage); } private boxArrays(data: KpsPackage): KpsPackage { - boxXmlArray(data.Package?.Files, 'File'); - boxXmlArray(data.Package?.Keyboards, 'Keyboard'); - boxXmlArray(data.Package?.LexicalModels, 'LexicalModel'); - boxXmlArray(data.Package?.RelatedPackages, 'RelatedPackage'); + boxXmlArray(data.Package.Files, 'File'); + boxXmlArray(data.Package.Keyboards, 'Keyboard'); + boxXmlArray(data.Package.LexicalModels, 'LexicalModel'); + boxXmlArray(data.Package.RelatedPackages, 'RelatedPackage'); - if(data.Package?.Keyboards?.Keyboard) { + if(data.Package.Keyboards?.Keyboard) { for(const k of data.Package.Keyboards.Keyboard) { boxXmlArray(k.Examples, 'Example'); boxXmlArray(k.Languages, 'Language'); @@ -56,17 +61,17 @@ export class KpsFileReader { } } - if(data.Package?.LexicalModels?.LexicalModel) { + if(data.Package.LexicalModels?.LexicalModel) { for(const lm of data.Package.LexicalModels.LexicalModel) { boxXmlArray(lm.Languages, 'Language'); } } - boxXmlArray(data.Package?.RelatedPackages, 'RelatedPackage'); - boxXmlArray(data.Package?.StartMenu?.Items, 'Item'); - boxXmlArray(data.Package?.Strings, 'String'); + boxXmlArray(data.Package.RelatedPackages, 'RelatedPackage'); + boxXmlArray(data.Package.StartMenu?.Items, 'Item'); + boxXmlArray(data.Package.Strings, 'String'); - if(data.Package?.Info) { + if(data.Package.Info) { // If no properties are defined on an subitem, then it will be a // string, and needs to be boxed into a KpsInfoItem object const info: any = data.Package.Info; diff --git a/developer/src/common/web/utils/test/fixtures/kps/error_not_a_package_file.kps b/developer/src/common/web/utils/test/fixtures/kps/error_not_a_package_file.kps new file mode 100644 index 0000000000..05af827783 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/kps/error_not_a_package_file.kps @@ -0,0 +1,4 @@ + + + + diff --git a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts index 117a48a281..05f16f8e2a 100644 --- a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts @@ -83,4 +83,16 @@ describe('kps-file-reader', function () { assert.lengthOf(callbacks.messages, 1); assert.isTrue(callbacks.hasMessage(DeveloperUtilsMessages.ERROR_InvalidPackageFile)); }); + + // ERROR_NotAPackageFile + + it(`should generate ERROR_NotAPackageFile when the package source file is valid XML but does not have a root element`, function () { + const input = fs.readFileSync(makePathToFixture('kps', 'error_not_a_package_file.kps')); + const reader = new KpsFileReader(callbacks); + const kps = reader.read(input); + + assert.isNull(kps); + assert.isTrue(callbacks.hasMessage(DeveloperUtilsMessages.ERROR_NotAPackageFile)); + }); + }); diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index 33b00e6f00..9dcad78054 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -315,7 +315,7 @@ describe('KmpCompiler', function () { assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson)); }); - it(`should reject an package that contains itself`, function () { + it(`should reject a package that contains itself`, function () { const inputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kps'); const outputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kmp'); const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); From f07f95452fa76d6c15b3ac5e8e130dca9be33eda Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 25 Feb 2026 19:19:11 +0100 Subject: [PATCH 07/57] docs(core): add keyhandling doc This documents the state of `km_core_actions.emit_keystroke` for different keys pressed. Also some cleanup in other docs. This documents the state after merging #15609 (for ldml keyboards) and NN (for kmn keyboards). Follows: #15609 Build-bot: skip Test-bot: skip --- core/docs/api/index.md | 3 +- core/docs/api/json-schema.md | 6 ++-- core/docs/api/keyhandling.md | 42 +++++++++++++++++++++++++++ core/docs/api/options.md | 4 +-- core/docs/api/processor.md | 4 +-- core/docs/api/state.md | 14 +++++---- core/include/keyman/keyman_core_api.h | 8 +++-- 7 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 core/docs/api/keyhandling.md diff --git a/core/docs/api/index.md b/core/docs/api/index.md index 94bb517654..427e6f1ded 100644 --- a/core/docs/api/index.md +++ b/core/docs/api/index.md @@ -19,6 +19,7 @@ This is an internal API intended for use only within Keyman Engine. * [Options](options) * [Processor](processor) * [State and Actions](state) +* [Key Handling](keyhandling) * [JSON introspection Schema](json-schema) * [Building Keyman Core](building) @@ -122,4 +123,4 @@ Caps Lock. [km_core_event_flags]: processor#km_core_event_flags "km_core_event_flags enum" [km_core_process_event]: processor#km_core_process_event "km_core_process_event function" [km_core_event]: processor#km_core_event "km_core_event function" -[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" \ No newline at end of file +[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" diff --git a/core/docs/api/json-schema.md b/core/docs/api/json-schema.md index 756d5c4969..4fca89e677 100644 --- a/core/docs/api/json-schema.md +++ b/core/docs/api/json-schema.md @@ -4,10 +4,10 @@ title: JSON Introspection Schema The [`km_core_state_to_json()`](state#km_core_state_to_json) call generates a JSON document describing the internal state of the keyboard -processor, this is the schema describing that document. +processor. This is the schema describing that document. **WARNING**: The structure and format of the JSON document is independently -versioned is not considered part of C API. It is intended solely for use in +versioned and is not considered part of the C API. It is intended solely for use in diagnostics or by development and debugging tools which may need to be aware of keyboard processor engine implementation details. @@ -74,4 +74,4 @@ keyboard processor engine implementation details. "context": { "$ref": "#/definitions/context" } } } -``` \ No newline at end of file +``` diff --git a/core/docs/api/keyhandling.md b/core/docs/api/keyhandling.md new file mode 100644 index 0000000000..6dcc3177b0 --- /dev/null +++ b/core/docs/api/keyhandling.md @@ -0,0 +1,42 @@ +# Key Handling + +For each key press the processor will called twice, for the key down event +as well as for the key up event. Depending on the type of key pressed the +processor might handle the key itself or pass it on to the application. +The value of `emit_keystroke` in `km_core_actions` struct tells if the +processor handled the key (`emit_keystroke=0`) or not (`emit_keystroke=1`). +It is important that the same value gets set for both key down and key up +events, otherwise the application might miss some events which then looks +to the user like keys are stuck. + +Usually the processor won't handle any frame keys and let the application +deal with it. This allows shortcut keys like Ctrl+C +to work. The only exception is the Backspace key which is +handled internally if enough context is available. Regular keys will be +handled by the processor. + +Note that there is a difference between CLDR/LDML and KMN keyboards if +there are no rules/transforms defined for a key: KMN keyboards will +output the cap value of the key (e.g. pressing a will output +`a` if no rule is defined), whereas CLDR/LDML keyboards will suppress +any output for that key. + +The following table lists the state of `km_core_actions.emit_keystroke` +on return of `km_core_process_event` when the following type of key is +pressed: + +|Type | KeyDown | KeyUp | +|-----------------------------|---------------|----------| +|Key with rule | FALSE | FALSE | +|Key w/o rule | FALSE | FALSE | +|Framekeys: | | | +|Enter | TRUE | TRUE | +|Backspace¹ | FALSE | FALSE | +|Backspace² | TRUE | TRUE | +|Ctrl+Key | TRUE | TRUE | +|Modifier key Shift| TRUE | TRUE | +|Modifier key Ctrl | TRUE | TRUE | +|Modifier key LAlt | TRUE | TRUE | + +1: context available
+2: without or with empty context diff --git a/core/docs/api/options.md b/core/docs/api/options.md index f51528eafe..c5539d7ae1 100644 --- a/core/docs/api/options.md +++ b/core/docs/api/options.md @@ -3,7 +3,7 @@ title: Options - Keyman Core API --- A state’s default options are set from the keyboard at creation time and the -environment. The Platform layer is then is expected to apply any persisted +environment. The Platform layer is then expected to apply any persisted options it is maintaining. Options are passed into and out of API functions as simple C arrays of [km_core_option_item] terminated with a `KM_CORE_OPTIONS_END` sentinel value. A state's options are exposed and manipulatable via the @@ -281,4 +281,4 @@ km_core_state_options_to_json(km_core_state const *state, [km_core_event_flags]: processor#km_core_event_flags "km_core_event_flags enum" [km_core_process_event]: processor#km_core_process_event "km_core_process_event function" [km_core_event]: processor#km_core_event "km_core_event function" -[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" \ No newline at end of file +[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" diff --git a/core/docs/api/processor.md b/core/docs/api/processor.md index c72d743bb9..e8a8f7c0f4 100644 --- a/core/docs/api/processor.md +++ b/core/docs/api/processor.md @@ -15,8 +15,8 @@ enum km_core_event_flags { KM_CORE_EVENT_FLAG_DEFAULT = 0, KM_CORE_EVENT_FLAG_TOUCH = 1, }; - ``` + ## Values `KM_CORE_EVENT_FLAG_DEFAULT` @@ -195,4 +195,4 @@ enum km_core_event_code { [km_core_event_flags]: processor#km_core_event_flags "km_core_event_flags enum" [km_core_process_event]: processor#km_core_process_event "km_core_process_event function" [km_core_event]: processor#km_core_event "km_core_event function" -[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" \ No newline at end of file +[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" diff --git a/core/docs/api/state.md b/core/docs/api/state.md index 0292d9f8a5..32d3dc9b32 100644 --- a/core/docs/api/state.md +++ b/core/docs/api/state.md @@ -15,7 +15,8 @@ owned by the state object. ## Description -Describes the +Describes a change to the hardware caps lock indicator state requested by +Keyman Core to the Platform layer. ## Specification ```c @@ -50,6 +51,7 @@ This API replaces the Action items APIs, which are now deprecated and will be removed in the future. ## Specification + ```c typedef struct { unsigned int code_points_to_delete; @@ -60,8 +62,8 @@ typedef struct { km_core_caps_state new_caps_lock_state; const km_core_usv* deleted_context; } km_core_actions; - ``` + ## Members `code_points_to_delete` @@ -78,15 +80,17 @@ typedef struct { `emit_keystroke` : Emit the (unmodified) input keystroke to the application, 0 = no, 1 = yes. + On most platforms this signals whether the processor handled the event + (0) or not (1). See also [key handling](keyhandling). `new_caps_lock_state` : -1=unchanged, 0=off, 1=on `deleted_context` : Reference copy of actual UTF32 codepoints deleted from end of context - (closest to caret) exactly code_points_to_delete in length (plus null + (closest to caret) exactly `code_points_to_delete` in length (plus null terminator). Used to determine encoding conversion differences when - deleting; only set when using [km_core_state_get_actions], otherwise nullptr. + deleting; only set when using [km_core_state_get_actions], otherwise `nullptr`. ------------------------------------------------------------------------------- @@ -282,4 +286,4 @@ An opaque pointer to a state object. [km_core_event_flags]: processor#km_core_event_flags "km_core_event_flags enum" [km_core_process_event]: processor#km_core_process_event "km_core_process_event function" [km_core_event]: processor#km_core_event "km_core_event function" -[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" \ No newline at end of file +[km_core_event_code]: processor#km_core_event_code "km_core_event_code enum" diff --git a/core/include/keyman/keyman_core_api.h b/core/include/keyman/keyman_core_api.h index 7863689296..c26cbdf9ff 100644 --- a/core/include/keyman/keyman_core_api.h +++ b/core/include/keyman/keyman_core_api.h @@ -576,15 +576,17 @@ typedef struct { `emit_keystroke` : Emit the (unmodified) input keystroke to the application, 0 = no, 1 = yes. + On most platforms this signals whether the processor handled the event + (0) or not (1). See also [key handling](keyhandling). `new_caps_lock_state` : -1=unchanged, 0=off, 1=on `deleted_context` : Reference copy of actual UTF32 codepoints deleted from end of context - (closest to caret) exactly code_points_to_delete in length (plus null + (closest to caret) exactly `code_points_to_delete` in length (plus null terminator). Used to determine encoding conversion differences when - deleting; only set when using [km_core_state_get_actions], otherwise nullptr. + deleting; only set when using [km_core_state_get_actions], otherwise `nullptr`. ------------------------------------------------------------------------------- @@ -744,7 +746,7 @@ title: Options - Keyman Core API --- A state’s default options are set from the keyboard at creation time and the -environment. The Platform layer is then is expected to apply any persisted +environment. The Platform layer is then expected to apply any persisted options it is maintaining. Options are passed into and out of API functions as simple C arrays of [km_core_option_item] terminated with a `KM_CORE_OPTIONS_END` sentinel value. A state's options are exposed and manipulatable via the From 058e68db5a25dcab667121fbe622f69aa11716a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:36:05 +0000 Subject: [PATCH 08/57] chore(deps-dev): bump basic-ftp from 5.0.5 to 5.2.0 Bumps [basic-ftp](https://github.com/patrickjuchli/basic-ftp) from 5.0.5 to 5.2.0. - [Release notes](https://github.com/patrickjuchli/basic-ftp/releases) - [Changelog](https://github.com/patrickjuchli/basic-ftp/blob/master/CHANGELOG.md) - [Commits](https://github.com/patrickjuchli/basic-ftp/compare/v5.0.5...v5.2.0) --- updated-dependencies: - dependency-name: basic-ftp dependency-version: 5.2.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61a304996b..30da122658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4943,9 +4943,9 @@ "license": "MIT" }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "dev": true, "license": "MIT", "engines": { From 7d8e3978a1799232759cab024c858d9db62fc04c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 25 Feb 2026 14:55:14 +0100 Subject: [PATCH 09/57] fix(developer): ngrok upgrade The existing ngrok package is no longer maintained, so this commit switches to @ngrok/ngrok. However, this means a number of deployment changes, as the new module uses a node binary module rather than a standalone executable, and the path to the module is assumed to be on the Node search path. The new module also requires VC++ redistributable, so installation of that has been added to the Server Options dialog. Use of ngrok with Keyman Developer Server should be in theory possible with non-Windows platforms with this change, if the user installs the appropriate @ngrok binary package (e.g. @ngrok/ngrok-darwin-universal) globally before starting the server. Fixes: #15625 Build-bot: skip release:developer --- developer/src/server/build.sh | 7 - developer/src/server/package.json | 2 +- developer/src/server/src/index.ts | 92 ++-- developer/src/server/src/ngrok.d.ts | 1 - .../Keyman.Developer.UI.UfrmServerOptions.dfm | 346 ++++++------ .../Keyman.Developer.UI.UfrmServerOptions.pas | 237 +++++--- .../Keyman.Developer.System.ServerAPI.pas | 26 + .../main/Keyman.Developer.System.Main.pas | 2 + package-lock.json | 509 +++++++++--------- 9 files changed, 666 insertions(+), 556 deletions(-) delete mode 100644 developer/src/server/src/ngrok.d.ts diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index 73eb5c9b45..f2ff0c35d6 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -52,10 +52,6 @@ function clean_server() { function configure_server() { node_select_version_and_npm_ci - # See https://github.com/bubenshchykov/ngrok/issues/254, https://github.com/bubenshchykov/ngrok/pull/255 - # TODO: this is horrible; is there a way we can avoid this? - rm -f "$KEYMAN_ROOT"/node_modules/ngrok/bin/ngrok - rm -f "$KEYMAN_ROOT"/node_modules/ngrok/bin/ngrok.exe } function build_addins() { @@ -130,9 +126,6 @@ function installer_server() { pushd "$PRODBUILDTEMP" npm install --omit=dev --omit=optional - # See https://github.com/bubenshchykov/ngrok/issues/254, https://github.com/bubenshchykov/ngrok/pull/255 - rm -f node_modules/ngrok/bin/ngrok - rm -f node_modules/ngrok/bin/ngrok.exe popd # Dependencies are required in build/ but we need to copy them in manually diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 37192ca6f3..56231d8953 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -10,11 +10,11 @@ "license": "MIT", "dependencies": { "@keymanapp/developer-utils": "*", + "@ngrok/ngrok": "^1.7.0", "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", "multer": "^2.0.2", - "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", "sax": ">=0.6.0", diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index 85de2fcce6..a91b36d545 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -1,5 +1,4 @@ import * as fs from 'node:fs'; -import * as os from 'node:os'; import * as path from 'node:path'; import chalk from 'chalk'; import express from 'express'; @@ -77,7 +76,7 @@ export async function run() { setupRoutes(app, upload, wsServer, environment); - /* Start the server */ + /* Start the web server */ let server = null; try { @@ -87,6 +86,8 @@ export async function run() { // TODO handle and cleanup EADDRINUSE, throw anything else } + /* Attach the web socket server */ + server.on('upgrade', (request, socket, head) => { wsServer.handleUpgrade(request, socket, head, socket => { wsServer.emit('connection', socket, request); @@ -96,45 +97,60 @@ export async function run() { /* Launch ngrok if enabled */ configuration.ngrokEndpoint = ''; + if(configuration.useNgrok) { + await startNGrok(); + } - if(configuration.useNgrok - && os.platform() == 'win32' - && fs.existsSync(path.join(configuration.ngrokBinPath, 'ngrok.exe')) - ) { - const ngrok: any = await import('ngrok'); - (async function() { - configuration.ngrokEndpoint = await ngrok.connect({ - proto: 'http', - addr: configuration.port, - authtoken: configuration.ngrokToken, - binPath: () => configuration.ngrokBinPath, - onLogEvent: (msg: string) => { - if(options.ngrokLog) { - console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); - } - }, - onStatusChange: (state: string) => { - if(state == 'connected') { - setTimeout(async () => { - const api = ngrok.getApi(); - const tunnels = await api.listTunnels(); - configuration.ngrokEndpoint = tunnels.tunnels[0]?.public_url ?? ''; - console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); - }, 1000); - } else if(state == 'closed') { - configuration.ngrokEndpoint = ''; - console.log(chalk.blueBright('ngrok tunnel closed')); - } - } - }); - console.log(chalk.blueBright('ngrok tunnel initially established at %s'), configuration.ngrokEndpoint); - tray.start(configuration.port, configuration.ngrokEndpoint); - })(); + /* Load the tray icon */ + + tray.start(configuration.port, configuration.ngrokEndpoint); +} + +async function loadNGrok() { + try { + return await import('@ngrok/ngrok'); + } catch(e) { + // ngrok can fail import if it cannot load its dependent binary library, + // either because it is missing or because it has missing dependencies + // itself. But we don't want to crash the server if that happens + console.error(e); + return null; } - else { - /* Load the tray icon */ - tray.start(configuration.port, configuration.ngrokEndpoint); +} + +async function startNGrok() { + console.log('Attempting to start ngrok'); + const ngrok = await loadNGrok(); + if(!ngrok) { + return false; } + + let started = false; + const listener = await ngrok.forward({ + proto: 'http', + addr: configuration.port, + authtoken: configuration.ngrokToken, + onLogEvent: (msg: string) => { + if(options.ngrokLog) { + console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); + } + }, + onStatusChange: (state: string) => { + if(state == 'connected' && started) { + // We only announce reconnection after initial start + configuration.ngrokEndpoint = listener.url(); + console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), configuration.ngrokEndpoint); + } else if(state == 'closed') { + configuration.ngrokEndpoint = ''; + console.log(chalk.blueBright('ngrok tunnel closed')); + } + } + }); + started = true; + configuration.ngrokEndpoint = listener.url(); + console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); + + return true; } function getRunningInstancePid(pidFilename: string) { diff --git a/developer/src/server/src/ngrok.d.ts b/developer/src/server/src/ngrok.d.ts deleted file mode 100644 index 61f00d7a0b..0000000000 --- a/developer/src/server/src/ngrok.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'ngrok'; \ No newline at end of file diff --git a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.dfm b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.dfm index 07f0979d87..ca4ba95a6d 100644 --- a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.dfm +++ b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.dfm @@ -1,165 +1,181 @@ -object frmServerOptions: TfrmServerOptions - Left = 0 - Top = 0 - BorderIcons = [biSystemMenu] - BorderStyle = bsDialog - Caption = 'Keyman Developer Server Options' - ClientHeight = 378 - ClientWidth = 433 - Color = clBtnFace - Font.Charset = DEFAULT_CHARSET - Font.Color = clWindowText - Font.Height = -11 - Font.Name = 'Tahoma' - Font.Style = [] - OldCreateOrder = False - Position = poScreenCenter - OnCreate = FormCreate - PixelsPerInch = 96 - TextHeight = 13 - object Label1: TLabel - Left = 8 - Top = 96 - Width = 403 - Height = 39 - AutoSize = False - Caption = - 'ngrok is a service that allows you to temporarily make your loca' + - 'l Server instance public accessible, so that you can view it fro' + - 'm any device or share it with other users for testing your keybo' + - 'ards, models and packages.' - WordWrap = True - end - object lblDefaultPort: TLabel - Left = 8 - Top = 19 - Width = 58 - Height = 13 - Caption = '&Default port' - FocusControl = editDefaultPort - end - object cmdOK: TButton - Left = 272 - Top = 346 - Width = 73 - Height = 25 - Caption = 'OK' - Default = True - ModalResult = 1 - TabOrder = 5 - OnClick = cmdOKClick - end - object cmdCancel: TButton - Left = 352 - Top = 346 - Width = 73 - Height = 25 - Cancel = True - Caption = 'Cancel' - ModalResult = 2 - TabOrder = 6 - end - object gbSetup: TGroupBox - Left = 8 - Top = 144 - Width = 417 - Height = 129 - Caption = 'Setup' - TabOrder = 3 - object lblAuthToken: TLabel - Left = 16 - Top = 93 - Width = 100 - Height = 13 - Caption = '&Authentication token' - FocusControl = editAuthToken - end - object lblVersion: TLabel - Left = 183 - Top = 29 - Width = 43 - Height = 13 - Caption = '(version)' - end - object lblGetToken: TLabel - Left = 360 - Top = 93 - Width = 47 - Height = 13 - Cursor = crHandPoint - Caption = 'Get token' - Font.Charset = DEFAULT_CHARSET - Font.Color = clBlue - Font.Height = -11 - Font.Name = 'Tahoma' - Font.Style = [fsUnderline] - ParentFont = False - OnClick = lblGetTokenClick - end - object editAuthToken: TEdit - Left = 122 - Top = 90 - Width = 231 - Height = 21 - TabOrder = 2 - end - object cmdDownload: TButton - Left = 16 - Top = 24 - Width = 161 - Height = 25 - Caption = 'Download or &update ngrok' - TabOrder = 0 - OnClick = cmdDownloadClick - end - object cmdCreateAccount: TButton - Left = 16 - Top = 55 - Width = 161 - Height = 25 - Caption = '&Create free ngrok account...' - TabOrder = 1 - OnClick = cmdCreateAccountClick - end - end - object gbAdvanced: TGroupBox - Left = 8 - Top = 279 - Width = 417 - Height = 61 - Caption = 'Advanced Options' - TabOrder = 4 - object chkServerShowConsoleWindow: TCheckBox - Left = 16 - Top = 27 - Width = 257 - Height = 17 - Caption = '&Show Server console window on start' - TabOrder = 0 - end - end - object editDefaultPort: TEdit - Left = 84 - Top = 16 - Width = 61 - Height = 21 - NumbersOnly = True - TabOrder = 0 - end - object chkUseNgrok: TCheckBox - Left = 8 - Top = 73 - Width = 313 - Height = 17 - Caption = 'Use &ngrok to provide public url for Server' - TabOrder = 2 - end - object chkLeaveServerRunning: TCheckBox - Left = 8 - Top = 43 - Width = 257 - Height = 17 - Caption = '&Leave Server running after closing IDE' - TabOrder = 1 - end -end +object frmServerOptions: TfrmServerOptions + Left = 0 + Top = 0 + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'Keyman Developer Server Options' + ClientHeight = 407 + ClientWidth = 433 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + OnCreate = FormCreate + PixelsPerInch = 96 + TextHeight = 13 + object Label1: TLabel + Left = 8 + Top = 96 + Width = 403 + Height = 39 + AutoSize = False + Caption = + 'ngrok is a service that allows you to temporarily make your loca' + + 'l Server instance public accessible, so that you can view it fro' + + 'm any device or share it with other users for testing your keybo' + + 'ards, models and packages.' + WordWrap = True + end + object lblDefaultPort: TLabel + Left = 8 + Top = 19 + Width = 58 + Height = 13 + Caption = '&Default port' + FocusControl = editDefaultPort + end + object cmdOK: TButton + Left = 272 + Top = 374 + Width = 73 + Height = 25 + Caption = 'OK' + Default = True + ModalResult = 1 + TabOrder = 5 + OnClick = cmdOKClick + end + object cmdCancel: TButton + Left = 352 + Top = 374 + Width = 73 + Height = 25 + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 6 + end + object gbSetup: TGroupBox + Left = 8 + Top = 144 + Width = 417 + Height = 157 + Caption = 'Setup' + TabOrder = 3 + object lblAuthToken: TLabel + Left = 16 + Top = 117 + Width = 100 + Height = 13 + Caption = '&Authentication token' + FocusControl = editAuthToken + end + object lblNgrokInstallState: TLabel + Left = 183 + Top = 29 + Width = 87 + Height = 13 + Caption = '(installation state)' + end + object lblGetToken: TLabel + Left = 360 + Top = 117 + Width = 47 + Height = 13 + Cursor = crHandPoint + Caption = 'Get token' + Font.Charset = DEFAULT_CHARSET + Font.Color = clBlue + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [fsUnderline] + ParentFont = False + OnClick = lblGetTokenClick + end + object lblVCRedistInstallState: TLabel + Left = 183 + Top = 57 + Width = 87 + Height = 13 + Caption = '(installation state)' + end + object editAuthToken: TEdit + Left = 122 + Top = 114 + Width = 231 + Height = 21 + TabOrder = 3 + end + object cmdDownload: TButton + Left = 16 + Top = 24 + Width = 161 + Height = 25 + Caption = 'Download or &update ngrok' + TabOrder = 0 + OnClick = cmdDownloadClick + end + object cmdCreateAccount: TButton + Left = 16 + Top = 79 + Width = 161 + Height = 25 + Caption = '&Create free ngrok account...' + TabOrder = 2 + OnClick = cmdCreateAccountClick + end + object cmdInstallVCRedist: TButton + Left = 16 + Top = 52 + Width = 161 + Height = 25 + Caption = '&Install required redistributable' + TabOrder = 1 + OnClick = cmdInstallVCRedistClick + end + end + object gbAdvanced: TGroupBox + Left = 8 + Top = 307 + Width = 417 + Height = 61 + Caption = 'Advanced Options' + TabOrder = 4 + object chkServerShowConsoleWindow: TCheckBox + Left = 16 + Top = 27 + Width = 257 + Height = 17 + Caption = '&Show Server console window on start' + TabOrder = 0 + end + end + object editDefaultPort: TEdit + Left = 84 + Top = 16 + Width = 61 + Height = 21 + NumbersOnly = True + TabOrder = 0 + end + object chkUseNgrok: TCheckBox + Left = 8 + Top = 73 + Width = 313 + Height = 17 + Caption = 'Use &ngrok to provide public url for Server' + TabOrder = 2 + end + object chkLeaveServerRunning: TCheckBox + Left = 8 + Top = 43 + Width = 257 + Height = 17 + Caption = '&Leave Server running after closing IDE' + TabOrder = 1 + end +end diff --git a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas index 8a5170e0d1..f2dbf9ad41 100644 --- a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas +++ b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas @@ -4,8 +4,10 @@ interface uses System.Classes, + System.Net.HttpClient, System.SysUtils, System.Variants, + System.Win.Registry, Vcl.Controls, Vcl.Dialogs, Vcl.Forms, @@ -28,35 +30,40 @@ type gbAdvanced: TGroupBox; chkServerShowConsoleWindow: TCheckBox; cmdCreateAccount: TButton; - lblVersion: TLabel; + lblNgrokInstallState: TLabel; Label1: TLabel; lblGetToken: TLabel; lblDefaultPort: TLabel; editDefaultPort: TEdit; chkUseNgrok: TCheckBox; chkLeaveServerRunning: TCheckBox; + cmdInstallVCRedist: TButton; + lblVCRedistInstallState: TLabel; procedure FormCreate(Sender: TObject); procedure cmdOKClick(Sender: TObject); procedure cmdDownloadClick(Sender: TObject); procedure cmdCreateAccountClick(Sender: TObject); procedure lblGetTokenClick(Sender: TObject); + procedure cmdInstallVCRedistClick(Sender: TObject); private + FInstallStateChanged: Boolean; FDownloadProgress: TfrmDownloadProgress; + FDownloadObject: string; procedure UpdateVersionLabel; function NgrokPath: string; procedure DownloadNgrok(AOwner: TfrmDownloadProgress; var Result: Boolean); procedure HttpReceiveData(const Sender: TObject; AContentLength, AReadCount: Int64; var Abort: Boolean); - { Private declarations } + procedure DownloadVCRedist(AOwner: TfrmDownloadProgress; + var Result: Boolean); + function DoDownload(AOwner: TfrmDownloadProgress; const objectName, url: string): IHTTPResponse; + function RedistInstallerPath: string; public - { Public declarations } end; implementation uses - System.Net.HttpClient, - System.Zip, Winapi.ShlObj, KeymanDeveloperOptions, @@ -73,8 +80,18 @@ const // to be stable, as they are used in other apps also. SUrlNgrokAuthToken = 'https://dashboard.ngrok.com/get-started/your-authtoken'; SUrlNgrokSignup = 'https://dashboard.ngrok.com/signup'; - SUrlNgrokDownload = 'https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-windows-386.zip'; - SNGrokExe = 'ngrok.exe'; + + // Note, we manually update the version of ngrok as required in subsequent + // releases, rather than trying to handle a potentially moving target + SNgrokVersion = 'v1.7.0'; + SUrlNgrokDownload = 'https://github.com/ngrok/ngrok-javascript/releases/download/'+SNgrokVersion+'/ngrok.win32-ia32-msvc.node'; + SNgrokNodeModuleFilename = 'ngrok-win32-ia32-msvc.node'; + + // VC++ Redistributable - presence, filename, and permanent URL + SUrlVCRedistDownload = 'https://aka.ms/vc14/vc_redist.x86.exe'; + SVCRedistExeFilename = 'VC_redist.x86.exe'; + SRegKey_VCRedist = 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86'; + SRegValue_VCRedist_Version = 'Version'; procedure TfrmServerOptions.cmdCreateAccountClick(Sender: TObject); begin @@ -86,104 +103,153 @@ begin Abort := False; if Assigned(FDownloadProgress) then begin - FDownloadProgress.HTTPStatus(nil, 'Downloading ngrok', AReadCount, AContentLength); + FDownloadProgress.HTTPStatus(nil, 'Downloading '+FDownloadObject, AReadCount, AContentLength); FDownloadProgress.HTTPCheckCancel(nil, Abort); end; end; -procedure TfrmServerOptions.DownloadNgrok(AOwner: TfrmDownloadProgress; var Result: Boolean); +function TfrmServerOptions.DoDownload(AOwner: TfrmDownloadProgress; const objectName, url: string): IHTTPResponse; var http: THttpClient; res: IHTTPResponse; - fs: TFileStream; - zip: TZipFile; - ms: TStream; - zh: TZipHeader; begin - Result := False; + Result := nil; + FDownloadObject := objectName; FDownloadProgress := AOwner; http := THttpClient.Create; try http.OnReceiveData := HttpReceiveData; try - res := http.Get(SUrlNgrokDownload); + res := http.Get(url); except on E:ENetHTTPClientException do begin - ShowMessage(Format('Unable to download %s: %s', [SNGrokExe, E.Message])); + ShowMessage(Format('Unable to download %s from %s: %s', [objectName, url, E.Message])); Exit; end; end; if res.StatusCode <> 200 then begin - ShowMessage(Format('Unable to download %s: %d %s', - [SNGrokExe, res.StatusCode, res.StatusText])); + ShowMessage(Format('Unable to download %s from %s: %d %s', + [objectName, url, res.StatusCode, res.StatusText])); Exit; end; - // Download is a zip file with a single executable, so let's extract it - ms := nil; - try - zip := TZipFile.Create; - try - try - zip.Open(res.ContentStream, zmRead); - zip.Read(SNGrokExe, ms, zh); - ms.Position := 0; - finally - zip.Free; - end; - except - on E:Exception do - begin - ShowMessage(Format('Unable to extract %s from downloaded archive: %s', - [SNGrokExe, E.Message])); - Exit; - end; - end; - try - fs := TFileStream.Create(NgrokPath, fmCreate); - except - on E:Exception do - begin - ShowMessage(Format('Unable to save %s: %s', [SNGrokExe, E.Message])); - Exit; - end; - end; - try - fs.CopyFrom(ms, 0); - finally - fs.Free; - end; - finally - ms.Free; - end; + Result := res; finally http.Free; FDownloadProgress := nil; + FDownloadObject := ''; end; end; +procedure TfrmServerOptions.DownloadVCRedist(AOwner: TfrmDownloadProgress; var Result: Boolean); +var + res: IHTTPResponse; + fs: TFileStream; +begin + Result := False; + + res := DoDownload(AOwner, 'Microsoft Visual C++ Redistributable', SUrlVCRedistDownload); + if not Assigned(res) then + Exit; + + // Save the redistributable installer + + try + fs := TFileStream.Create(RedistInstallerPath, fmCreate); + try + fs.CopyFrom(res.ContentStream, 0); + finally + fs.Free; + end; + except + on E:Exception do + begin + ShowMessage(Format('Unable to save %s: %s', [RedistInstallerPath, E.Message])); + Exit; + end; + end; + + // Execute the installer - user will need to follow prompts to complete installation + + Result := TUtilExecute.WaitForProcess('"'+RedistInstallerPath+'"', ExtractFileDir(RedistInstallerPath), SW_SHOWNORMAL); + FInstallStateChanged := True; +end; + +procedure TfrmServerOptions.DownloadNgrok(AOwner: TfrmDownloadProgress; var Result: Boolean); +var + res: IHTTPResponse; + fs: TFileStream; +begin + Result := False; + + res := DoDownload(AOwner, 'ngrok', SUrlNgrokDownload); + if not Assigned(res) then + Exit; + + // Download is a single file, needs to be saved to AppData\Keyman\Keyman Developer\Server\bin\@ngrok\filename + + try + fs := TFileStream.Create(NgrokPath, fmCreate); + except + on E:Exception do + begin + ShowMessage(Format('Unable to save %s: %s', [SNgrokNodeModuleFilename, E.Message])); + Exit; + end; + end; + try + fs.CopyFrom(res.ContentStream, 0); + finally + fs.Free; + end; + + Result := True; + FInstallStateChanged := True; +end; + procedure TfrmServerOptions.cmdDownloadClick(Sender: TObject); var DownloadProgress: TfrmDownloadProgress; begin - TServerDebugAPI.StopServer; + if FKeymanDeveloperOptions.ServerUseNgrok then + begin + // We need to stop the server if NGrok has been enabled because the module + // may be locked and in use + TServerDebugAPI.StopServer; + end; + DownloadProgress := TfrmDownloadProgress.Create(Self); try - DownloadProgress := TfrmDownloadProgress.Create(Self); - try - DownloadProgress.Callback := DownloadNgrok; - DownloadProgress.ShowModal; - finally - DownloadProgress.Free; - end; + DownloadProgress.Callback := DownloadNgrok; + DownloadProgress.ShowModal; finally + DownloadProgress.Free; + UpdateVersionLabel; + if FKeymanDeveloperOptions.ServerUseNgrok then + begin + // Restart the server after ngrok module is installed (although ngrok may + // not run if the dependency is not also installed) + TServerDebugAPI.StartServer; + end; + end; +end; + +procedure TfrmServerOptions.cmdInstallVCRedistClick(Sender: TObject); +var + DownloadProgress: TfrmDownloadProgress; +begin + DownloadProgress := TfrmDownloadProgress.Create(Self); + try + DownloadProgress.Callback := DownloadVCRedist; + DownloadProgress.ShowModal; + finally + DownloadProgress.Free; UpdateVersionLabel; - TServerDebugAPI.StartServer; end; end; @@ -205,7 +271,8 @@ begin (FKeymanDeveloperOptions.ServerKeepAlive <> KeepAlive) or (FKeymanDeveloperOptions.ServerUseNgrok <> UseNgrok) or (FKeymanDeveloperOptions.ServerNgrokToken <> ngrokToken) or - (FKeymanDeveloperOptions.ServerServerShowConsoleWindow <> ngrokKeepVisible); + (FKeymanDeveloperOptions.ServerServerShowConsoleWindow <> ngrokKeepVisible) or + FInstallStateChanged; if Changed then begin @@ -226,7 +293,8 @@ begin chkUseNgrok.Checked := FKeymanDeveloperOptions.ServerUseNgrok; chkLeaveServerRunning.Checked := FKeymanDeveloperOptions.ServerKeepAlive; - lblVersion.Caption := ''; + lblNgrokInstallState.Caption := ''; + lblVCRedistInstallState.Caption := ''; editAuthToken.Text := FKeymanDeveloperOptions.ServerNgrokToken; chkServerShowConsoleWindow.Checked := FKeymanDeveloperOptions.ServerServerShowConsoleWindow; @@ -239,29 +307,36 @@ begin TUtilExecute.URL(SUrlNgrokAuthToken); end; + function TfrmServerOptions.NgrokPath: string; begin - Result := GetFolderPath(CSIDL_APPDATA) + SFolderKeymanDeveloper + '\Server\bin\'; + Result := TServerDebugAPI.ServerBinPath + '\@ngrok'; ForceDirectories(Result); - Result := Result + SNgrokExe; + Result := Result + '\' + SNgrokNodeModuleFilename; +end; + +function TfrmServerOptions.RedistInstallerPath: string; +begin + Result := TServerDebugAPI.ServerBinPath + SVCRedistExeFilename; end; procedure TfrmServerOptions.UpdateVersionLabel; var - logtext: string; - ec: Integer; + reg: TRegistry; begin - if not FileExists(NgrokPath) then - lblVersion.Caption := 'Ngrok not installed' - else if TUtilExecute.Console(Format('"%s" --version', [NgrokPath]), ExtractFilePath(ParamStr(0)), - logtext, ec) then - begin - if ec = 0 - then lblVersion.Caption := logtext.Trim - else lblVersion.Caption := Format('Error %d running ngrok: %s', [ec, logtext.Trim]) - end - else - lblVersion.Caption := Format('Unable to run ngrok: %s', [SysErrorMessage(GetLastError)]); + if FileExists(NgrokPath) + then lblNgrokInstallState.Caption := 'Installed' + else lblNgrokInstallState.Caption := 'Not installed'; + + lblVCRedistInstallState.Caption := 'Not installed'; + reg := TRegistry.Create; + try + reg.RootKey := HKEY_LOCAL_MACHINE; + if reg.OpenKeyReadOnly(SRegKey_VCRedist) and reg.ValueExists(SRegValue_VCRedist_Version) then + lblVCRedistInstallState.Caption := reg.ReadString(SRegValue_VCRedist_Version); + finally + reg.Free; + end; end; end. diff --git a/developer/src/tike/http/Keyman.Developer.System.ServerAPI.pas b/developer/src/tike/http/Keyman.Developer.System.ServerAPI.pas index c666b49642..565b4ed068 100644 --- a/developer/src/tike/http/Keyman.Developer.System.ServerAPI.pas +++ b/developer/src/tike/http/Keyman.Developer.System.ServerAPI.pas @@ -25,6 +25,8 @@ type class procedure StartServer; static; class procedure StopServer; static; class procedure GetServerURLs(v: TStrings); static; + class function ServerBinPath: string; static; + class procedure CleanupOldNgrok; static; // API endpoints class function IsKeyboardRegistered(const Filename: string): Boolean; static; @@ -65,6 +67,7 @@ uses Keyman.Developer.System.KeymanDeveloperPaths, Keyman.System.KeyboardUtils, Keyman.System.LexicalModelUtils, + RegistryKeys, utilexecute, utilsystem, Winapi.ShlObj, @@ -83,6 +86,12 @@ begin then sw := SW_SHOWNORMAL else sw := SW_HIDE; + // We have an explicit NODE_PATH env var so that Node can + // find our downloaded version of ngrok in %AppData%\Server\bin\@ngrok + // without us risking messing up the user's own node environment (if + // they have one!) + SetEnvironmentVariable('NODE_PATH', PChar(ServerBinPath)); + if not TUtilExecute.Execute(Format('"%s" .', [TKeymanDeveloperPaths.NodePath]), TKeymanDeveloperPaths.ServerPath, sw) then begin @@ -92,6 +101,12 @@ begin LastGetStatusTime := 0; end; +class function TServerDebugAPI.ServerBinPath: string; +begin + Result := TKeymanDeveloperPaths.ServerDataPath + 'bin'; + ForceDirectories(Result); +end; + function QueryFullProcessImageName( hProcess: THandle; dwFlags: DWORD; @@ -189,6 +204,17 @@ begin Result := True; end; +class procedure TServerDebugAPI.CleanupOldNgrok; +begin + // #15625: In February 2026, we moved from npm ngrok module to @ngrok/ngrok. + // This npm module uses a different binary version of ngrok, so we should + // cleanup the old ngrok.exe if we find it. + if FileExists(TServerDebugAPI.ServerBinPath + '\ngrok.exe') then + begin + System.SysUtils.DeleteFile(TServerDebugAPI.ServerBinPath + '\ngrok.exe'); + end; +end; + class procedure TServerDebugAPI.GetServerURLs(v: TStrings); const IPv4Loopback = '127.0.0.1'; {do not localize} diff --git a/developer/src/tike/main/Keyman.Developer.System.Main.pas b/developer/src/tike/main/Keyman.Developer.System.Main.pas index 9f6536a14a..d60897e2f2 100644 --- a/developer/src/tike/main/Keyman.Developer.System.Main.pas +++ b/developer/src/tike/main/Keyman.Developer.System.Main.pas @@ -13,6 +13,7 @@ uses uCEFTypes, KeymanDeveloperOptions, + Keyman.Developer.System.ServerAPI, Keyman.Developer.System.TikeCommandLine, Keyman.System.CEFManager, Keyman.System.KeymanSentryClient, @@ -68,6 +69,7 @@ begin begin InitThemeLibrary; SetThemeAppProperties(STAP_ALLOW_NONCLIENT or STAP_ALLOW_CONTROLS or STAP_ALLOW_WEBCONTENT); + TServerDebugAPI.CleanupOldNgrok; Application.CreateForm(TmodWebHttpServer, modWebHttpServer); try Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); diff --git a/package-lock.json b/package-lock.json index 61a304996b..08297d611b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1146,11 +1146,11 @@ "license": "MIT", "dependencies": { "@keymanapp/developer-utils": "*", + "@ngrok/ngrok": "^1.7.0", "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", "multer": "^2.0.2", - "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -2007,6 +2007,235 @@ "resolve": "~1.22.2" } }, + "node_modules/@ngrok/ngrok": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-1.7.0.tgz", + "integrity": "sha512-P06o9TpxrJbiRbHQkiwy/rUrlXRupc+Z8KT4MiJfmcdWxvIdzjCaJOdnNkcOTs6DMyzIOefG5tvk/HLdtjqr0g==", + "license": "(MIT OR Apache-2.0)", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@ngrok/ngrok-android-arm64": "1.7.0", + "@ngrok/ngrok-darwin-arm64": "1.7.0", + "@ngrok/ngrok-darwin-universal": "1.7.0", + "@ngrok/ngrok-darwin-x64": "1.7.0", + "@ngrok/ngrok-freebsd-x64": "1.7.0", + "@ngrok/ngrok-linux-arm-gnueabihf": "1.7.0", + "@ngrok/ngrok-linux-arm64-gnu": "1.7.0", + "@ngrok/ngrok-linux-arm64-musl": "1.7.0", + "@ngrok/ngrok-linux-x64-gnu": "1.7.0", + "@ngrok/ngrok-linux-x64-musl": "1.7.0", + "@ngrok/ngrok-win32-arm64-msvc": "1.7.0", + "@ngrok/ngrok-win32-ia32-msvc": "1.7.0", + "@ngrok/ngrok-win32-x64-msvc": "1.7.0" + } + }, + "node_modules/@ngrok/ngrok-android-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-1.7.0.tgz", + "integrity": "sha512-8tco3ID6noSaNy+CMS7ewqPoIkIM6XO5COCzsUp3Wv3XEbMSyn65RN6cflX2JdqLfUCHcMyD0ahr9IEiHwqmbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-+dmJSOzSO+MNDVrPOca2yYDP1W3KfP4qOlAkarIeFRIfqonQwq3QCBmcR7HAlZocLsSqEwyG6KP4RRvAuT0WGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-universal": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-1.7.0.tgz", + "integrity": "sha512-fDEfewyE2pWGFBhOSwQZObeHUkc65U1l+3HIgSOe094TMHsqmyJD0KTCgW9KSn0VP4OvDZbAISi1T3nvqgZYhQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-1.7.0.tgz", + "integrity": "sha512-+fwMi5uHd9G8BS42MMa9ye6exI5lwTcjUO6Ut497Vu0qgLONdVRenRqnEePV+Q3KtQR7NjqkMnomVfkr9MBjtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-freebsd-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-2OGgbrjy3yLRrqAz5N6hlUKIWIXSpR5RjQa2chtZMsSbszQ6c9dI+uVQfOKAeo05tHMUgrYAZ7FocC+ig0dzdQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-SN9YIfEQiR9xN90QVNvdgvAemqMLoFVSeTWZs779145hQMhvF9Qd9rnWi6J+2uNNK10OczdV1oc/nq1es7u/3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-KDMgzPKFU2kbpVSaA2RZBBia5IPdJEe063YlyVFnSMJmPYWCUnMwdybBsucXfV9u1Lw/ZjKTKotIlbTWGn3HGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-e66vUdVrBlQ0lT9ZdamB4U604zt5Gualt8/WVcUGzbu8s5LajWd6g/mzZCUjK4UepjvMpfgmCp1/+rX7Rk8d5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-1.7.0.tgz", + "integrity": "sha512-M6gF0DyOEFqXLfWxObfL3bxYZ4+PnKBHuyLVaqNfFN9Y5utY2mdPOn5422Ppbk4XoIK5/YkuhRqPJl/9FivKEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-4Ijm0dKeoyzZTMaYxR2EiNjtlK81ebflg/WYIO1XtleFrVy4UJEGnxtxEidYoT4BfCqi4uvXiK2Mx216xXKvog==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-arm64-msvc/-/ngrok-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-u7qyWIJI2/YG1HTBnHwUR1+Z2tyGfAsUAItJK/+N1G0FeWJhIWQvSIFJHlaPy4oW1Dc8mSDBX9qvVsiQgLaRFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-/UdYUsLNv/Q8j9YJsyIfq/jLCoD8WP+NidouucTUzSoDtmOsXBBT3itLrmPiZTEdEgKiFYLuC1Zon8XQQvbVLA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-UFJg/duEWzZlLkEs61Gz6/5nYhGaKI62I8dvUGdBR3NCtIMagehnFaFxmnXZldyHmCM8U0aCIFNpWRaKcrQkoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3214,16 +3443,6 @@ "node": ">=8" } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", @@ -3231,16 +3450,6 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -3278,16 +3487,6 @@ "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "*", - "@types/node": "*", - "@types/responselike": "*" - } - }, "node_modules/@types/chai": { "version": "4.3.14", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz", @@ -3393,10 +3592,6 @@ "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", "dev": true }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "license": "MIT" - }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -3426,10 +3621,6 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/json-buffer": { - "version": "3.0.0", - "license": "MIT" - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3442,13 +3633,6 @@ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", "dev": true }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/koa": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", @@ -3488,6 +3672,7 @@ "version": "20.12.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3521,13 +3706,6 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/semver": { "version": "7.3.13", "dev": true, @@ -3592,6 +3770,7 @@ }, "node_modules/@types/yauzl": { "version": "2.9.2", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5057,6 +5236,7 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -5253,29 +5433,6 @@ "node": ">= 6.0.0" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.2", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5548,13 +5705,6 @@ "node": ">=0.8" } }, - "node_modules/clone-response": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5696,17 +5846,6 @@ "node": ">=4.0.0" } }, - "node_modules/compress-brotli": { - "version": "1.3.6", - "license": "MIT", - "dependencies": { - "@types/json-buffer": "~3.0.0", - "json-buffer": "~3.0.1" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -5933,6 +6072,7 @@ }, "node_modules/debug": { "version": "4.3.1", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -5952,29 +6092,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/decompress-response": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -6072,13 +6189,6 @@ "node": ">=8" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6280,6 +6390,7 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -7629,6 +7740,7 @@ }, "node_modules/extract-zip": { "version": "2.0.1", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -7721,6 +7833,7 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", + "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -8063,6 +8176,7 @@ }, "node_modules/get-stream": { "version": "5.2.0", + "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -8314,29 +8428,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.5", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "dev": true, @@ -8456,11 +8547,6 @@ "node": ">=10.4" } }, - "node_modules/hpagent": { - "version": "0.1.2", - "license": "MIT", - "optional": true - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -8510,7 +8596,8 @@ "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true }, "node_modules/http-errors": { "version": "2.0.0", @@ -8585,17 +8672,6 @@ } } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -9374,10 +9450,6 @@ "node": ">=18" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "license": "MIT" - }, "node_modules/json-schema-migrate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", @@ -9450,14 +9522,6 @@ "resolved": "web", "link": true }, - "node_modules/keyv": { - "version": "4.2.2", - "license": "MIT", - "dependencies": { - "compress-brotli": "^1.3.6", - "json-buffer": "3.0.1" - } - }, "node_modules/koa": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", @@ -9696,10 +9760,6 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "license": "MIT" - }, "node_modules/lodash.get": { "version": "4.4.2", "dev": true, @@ -9814,13 +9874,6 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", @@ -9988,13 +10041,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "3.1.2", "dev": true, @@ -10502,6 +10548,7 @@ }, "node_modules/ms": { "version": "2.1.2", + "dev": true, "license": "MIT" }, "node_modules/multer": { @@ -10551,28 +10598,6 @@ "node": ">= 0.4.0" } }, - "node_modules/ngrok": { - "version": "5.0.0-beta.2", - "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-5.0.0-beta.2.tgz", - "integrity": "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ==", - "hasInstallScript": true, - "dependencies": { - "extract-zip": "^2.0.1", - "got": "^11.8.5", - "lodash.clonedeep": "^4.5.0", - "uuid": "^7.0.0 || ^8.0.0", - "yaml": "^2.2.2" - }, - "bin": { - "ngrok": "bin/ngrok" - }, - "engines": { - "node": ">=14.2" - }, - "optionalDependencies": { - "hpagent": "^0.1.2" - } - }, "node_modules/nise": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", @@ -10816,16 +10841,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -10967,13 +10982,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-event": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", @@ -11302,6 +11310,7 @@ }, "node_modules/pend": { "version": "1.2.0", + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -11544,6 +11553,7 @@ }, "node_modules/pump": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -11642,16 +11652,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -11776,10 +11776,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -11838,13 +11834,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/responselike": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - } - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -13222,7 +13211,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "node_modules/unique-filename": { "version": "3.0.0", @@ -13656,14 +13646,6 @@ "version": "4.0.0", "license": "ISC" }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "16.2.0", "dev": true, @@ -13764,6 +13746,7 @@ }, "node_modules/yauzl": { "version": "2.10.0", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", From 01e3e32caec3f8de2b4a9025413c5de58dd4f5e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:09:46 +0000 Subject: [PATCH 10/57] chore(deps-dev): bump rollup from 4.22.4 to 4.59.0 Bumps [rollup](https://github.com/rollup/rollup) from 4.22.4 to 4.59.0. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.22.4...v4.59.0) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.59.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 301 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 227 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61a304996b..88116fc193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2511,208 +2511,350 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", - "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", - "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", - "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", - "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", - "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", - "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", - "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", - "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", - "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", - "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", - "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", - "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", - "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", - "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3356,10 +3498,11 @@ "dev": true }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.13", @@ -11898,12 +12041,13 @@ } }, "node_modules/rollup": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -11913,22 +12057,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.4", - "@rollup/rollup-android-arm64": "4.22.4", - "@rollup/rollup-darwin-arm64": "4.22.4", - "@rollup/rollup-darwin-x64": "4.22.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", - "@rollup/rollup-linux-arm-musleabihf": "4.22.4", - "@rollup/rollup-linux-arm64-gnu": "4.22.4", - "@rollup/rollup-linux-arm64-musl": "4.22.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", - "@rollup/rollup-linux-riscv64-gnu": "4.22.4", - "@rollup/rollup-linux-s390x-gnu": "4.22.4", - "@rollup/rollup-linux-x64-gnu": "4.22.4", - "@rollup/rollup-linux-x64-musl": "4.22.4", - "@rollup/rollup-win32-arm64-msvc": "4.22.4", - "@rollup/rollup-win32-ia32-msvc": "4.22.4", - "@rollup/rollup-win32-x64-msvc": "4.22.4", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, From 058df11f0c6f4681f0244b12a484c367c983629e Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 26 Feb 2026 11:39:58 +0100 Subject: [PATCH 11/57] docs(developer): refresh ngrok help --- developer/docs/help/context/options.md | 85 ++++++++++++------ developer/docs/help/context/server.md | 28 +++--- developer/docs/help/images/ui/NGROK_Phone.png | Bin 44729 -> 0 bytes .../ui/frmKeyman_Developer_Server_Options.png | Bin 18644 -> 132776 bytes .../docs/help/images/ui/frmNGROK_Config.png | Bin 30282 -> 0 bytes ...r_1.png => tools_options_server_ngrok.png} | Bin ...er_2.png => web_and_mobile_test_ngrok.png} | Bin 7 files changed, 69 insertions(+), 44 deletions(-) delete mode 100644 developer/docs/help/images/ui/NGROK_Phone.png delete mode 100644 developer/docs/help/images/ui/frmNGROK_Config.png rename developer/docs/help/images/ui/{frmNGROK_Server_1.png => tools_options_server_ngrok.png} (100%) rename developer/docs/help/images/ui/{frmNGROK_Server_2.png => web_and_mobile_test_ngrok.png} (100%) diff --git a/developer/docs/help/context/options.md b/developer/docs/help/context/options.md index ac1825382c..63bc65e62f 100644 --- a/developer/docs/help/context/options.md +++ b/developer/docs/help/context/options.md @@ -125,42 +125,73 @@ Update database ![Options dialog - Server tab](../images/ui/frmOptions_Server.png) -There are two configurations in the Keyman Developer Server section: -1. Configure Server... -2. List local URLs for Server +### Configure Server dialog -Configure Server... +After clicking the **Configure Server...** button, a new window will pop-up: -: After clicking the button, a new window will pop-up +![Keyman Developer Server Options dialog](../images/ui/frmKeyman_Developer_Server_Options.png) - ![Keyman Developer Server Options dialog](../images/ui/frmKeyman_Developer_Server_Options.png) +You can customize the Keyman Developer Server for testing your keyboard online +and on other devices. Features of the Keyman Developer Server include: - You can customize the Keyman Developer server for testing the usability of your keyboard. Features of the Keyman Developer server include: +* Seamless integration with the IDE (Start, Stop, Live reload, Recompile...) +* Sharing keyboards for testing online and on other devices +* Allowing testing across devices and networks - * Seamless integration with the IDE (Start, Stop, Live reload, Recompile...) - * Port configuration - * Allow testing cross devices - * Wider range support for NGROK... +Keyman Developer Server integrates with a free tool called +[ngrok](https://ngrok.com) that provides a public URL to your instance of Keyman +Developer Server. This URL is dynamic and can change over time, and is only +available while Keyman Developer Server is running. This URL can be accessed +from any mobile device that is online, whether or not it is on your local +network. This means you can also quickly share keyboards, packages, and models +with other users, with a minimum of difficulty. - [Find out more](https://github.com/keymanapp/keyman/pull/6073) +* [Learn more about ngrok](server#ngrok) - Understanding the three tick boxes will enhance the usability of Keyman Developer: - - **Leave Server running after closing IDE**: allows keyboard testing on local URLs after closing Keyman Developer. - - **Use ngrok to provide public url for web debugger**: configure NGROK as instructed, once it looks something like this: - ![Keyman Developer NGROK](../images/ui/frmNGROK_Config.png) +**Options available in this dialog:** - Please restart the machine, and the URL (*link ending in .ngrok-free.app*) will appear in the specified locations: - ![Keyman Developer NGROK URL 1](../images/ui/frmNGROK_Server_1.png) - ![Keyman Developer NGROK URL 2](../images/ui/frmNGROK_Server_2.png) +- **Leave Server running after closing IDE**: allows keyboard testing on local + URLs even if Keyman Developer is not running. - Now, freely share it across multiple devices using the link or QR code. - ![Keyboard Test on Mobile](../images/ui/NGROK_Phone.png) +- **Use ngrok to provide public url for web debugger**: enable ngrok + integration; you may need to download additional components; see below: - - **Show server console window on start**: once ticked, Keyman Developer's server console will appear as an independent window as a node application with the default message: - `Starting Keyman Developer Server 17.0.290-beta, listening on port 8008.` +- **Download or update ngrok**: If the text next to this button shows 'Not + installed', press the button to download the required component -List local URLs for Server -: Below Configure Server, untick the box, and it will quit displaying any local URLs for keyboard testing. - ![Keyman Developer Server Options dialog](../images/ui/frmList_Local_URLs.png) +- **Install required redistributable**: ngrok requires an additional Microsoft + VC++ redistributable component; if the text next to this button shows 'Not + installed', press the button and follow the prompts to install the + redistributable component. -Explore more: [Keyman Developer Server](server#toc-configuring-keyman-developer-server) +After enabling ngrok, a URL with ending a domain such as `.ngrok-free.app` will +appear in the Tools/Server menu and the Web and Mobile debugger pages in the +Keyboard Editor and the Package Editor: + +![Tools/Options/Server menu showing ngrok](../images/ui/tools_options_server_ngrok.png) + +![Web and Mobile test showing ngrok](../images/ui/web_and_mobile_test_ngrok.png) + +Once this is active, you can access Keyman Developer Server across multiple +devices and networks using the link or QR code. + +- **Show server console window on start**: once ticked, Keyman Developer's + server console will appear as an independent window as a node application, + which may be helpful if you encounter errors as they will appear in the + console. + + A default message will appear when the server console starts, similar to: + `Starting Keyman Developer Server -beta, listening on port 8008.` + +### List local URLs for Server + +When selected, Keyman Developer will show you more local URLs in the +Tools/Server menu for accessing Keyman Developer Server from your browser. Some +of these URLs will work only on your local computer, and will not work on other +devices. Deselect this option in order to show only the most relevant access +URLs. (On some networks, it may be helpful to see all the available URLs.) + + +## Related links + +* [Keyman Developer Server](server#toc-configuring-keyman-developer-server) diff --git a/developer/docs/help/context/server.md b/developer/docs/help/context/server.md index bccf637af1..20b3581ae1 100644 --- a/developer/docs/help/context/server.md +++ b/developer/docs/help/context/server.md @@ -296,7 +296,7 @@ context as well. The devices listed are representative of the various types of platforms that Keyman supports, to allow testing on each of the available form factors. -## Making your Keyman Developer Server instance globally available with ngrok +## Making your Keyman Developer Server instance globally available with ngrok {#ngrok} One of the fiddliest parts of testing keyboards and models with mobile devices is transferring the files to your phone or tablet. Keyman Developer Server will @@ -324,28 +324,30 @@ configure ngrok manually. click **Configure Server...**. You should be presented with a window similar to this: - ![](images/config.png) + ![](../images/ui/frmKeyman_Developer_Server_Options.png) 2. Tick the box **Use ngrok to provide public url for Server** -3. If ngrok is available, a version number will be shown to the right of the +3. If ngrok is available, the text 'Installed' will be shown to the right of the **Download or update ngrok** button. Otherwise, click this button to download the latest version. -4. You'll need a free ngrok account, so click the **Create free ngrok +4. **Install required redistributable**: ngrok requires an additional Microsoft + VC++ redistributable component; if the text next to this button shows 'Not + installed', press the button and follow the prompts to install the + redistributable component. +5. You'll need a free ngrok account, so click the **Create free ngrok account...** button to open the ngrok website, and follow the steps there to finish the account creation process. -5. After creating the account, you should be presented with an authentication +6. After creating the account, you should be presented with an authentication token. You can also access this by clicking the **Get token** link in the Keyman Developer Server Options dialog box. Paste this token into the **Authentication token** edit box. -6. Select the most appropriate region for your location, to minimize the latency - (delay) when using the ngrok service. 7. Click **OK** to save changes, and start the ngrok service within Keyman Developer Server. Once ngrok is configured, you should see an additional **Server** menu item in Keyman Developer's **Tools** menu, with three items: -**Open `https://some-url.ngrok.io` in browser** +**Open `https://some-url.ngrok-free.app` in browser** : Opens the public URL to your current Keyman Developer Server instance in the default browser on your computer. @@ -357,15 +359,7 @@ Keyman Developer's **Tools** menu, with three items: **Configure...** -: Opens the Keyman Developer Server Options dialog box. - -# Configuring Keyman Developer Server - -Keyman Developer Server has a number of configuration options. On Windows, using -Keyman Developer is the easiest way to manage these options. Open **Tools | -Options**, select **Server** tab, and click **Configure Server...**. - -![](images/config.png) +: Opens the [Keyman Developer Server Options](options#server-tab) dialog box. ## Configuration Files diff --git a/developer/docs/help/images/ui/NGROK_Phone.png b/developer/docs/help/images/ui/NGROK_Phone.png deleted file mode 100644 index b33ef1e316620b3de1ed491989690f5a011ea7e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44729 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�koL7VsQ!W$hq*;>PB9I0RrJy`mFCbqt4 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 z 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 Uuk-Kp-T;3RBC^6|f_nb{A3j>(hyVZp literal 18644 zcmdSAcTkgS-z^*qvK7H?0i>gVNSEG=A|f5BCRBxhbb$cU6*jQxQbXv{2~Ao81cIov zgdREs6bTTjgaC$6PTbG?o#)K+otg8_neU(Pk4z@l%$@tX?y|16e!sP1jSaOKudrVM z004|SPam5A0OuNMpP@f4(ME1OTg{?>5xUag$C{)&}S(Hpa6j&8{x_$^Y!<|Lc~R?6rhINvY+ta$Wl;8JX#;9b7Ee5OJ|nnz*! z-x9)&RCXUP@5Qgi8ZS$S9|nXULY$nQ004be&ZjG0rz=F|op!$PUDogyg69D5C5BJ8 z13DXd&H?($ox{(li*v~U!1B$mtAO{x!)l>N<6##8yaj(a0p89R=Uk@^UKVRC!2Xh! zzyx30tWbG>XdCh*pY5!bYsW%qQMh2}=gUw*rTZW?<+arxL#K&Vxtk5fVF=@eYYStzEkNEG58-TSZqi-iYd8IR<(1J|CQ~hq4sa_+yo1@RRiEcb| zI_Fe(qRJaLPS@Ow>_+(3!~I~Mg6q%1ZE_|V8jS{pgA`>wColKdnd)h^&egm8B07V5 zk*AOYI(dnj6;&t72t>lGYki~9$nNizxy8fiKphul0`AjX!O!~sEDI|t>^*CT#A#U6 zW32Lz)0m!)eG}|>LFHk;I_q63JulBT9(71|Tl}Rdy*9k{#)4jka&+c+$`Jg}Nj*P9 z!p^TYvzgwFR2r^TQ}@=+d}>N$!?xJ67~ZlW|hJkoub~-%5#$NQ?t6GR`a2405{O@$q zl(s`Vvz;7Ld~$(lhIL82jyccz(bNLjGLjg5T-&ZnEj>0i=fAc<{SZPqxn6hPEovKd zh5Pk$R==oDzcS@bMMZYUvWfOyvBg^YhJ(_RSMGN_%)8DqgbS}lp5jDZ{9daU*(CfBqZwNKn z2DX75m#GD1F0e9X(u%~mA~hle)2C z`c_Y?woVd0h(q$jL`m?@A?Y$8S0G6?@T~JNTh&4g>Jm{Hf5b)QFF2=t- zNC*QgOPrA;ln;WPj$FPLgbp+EQej~vL;CTPqeD|_=-D~`QP#Ypoxaq3(D=K}U%Nk6 z5ZWv-kQ0o!T{sfS0|3a3Jrovt?-FD?sZr*4KWnCQXB7YNG|deF*nFQ9AblS25vMda z_y^#VIB(WDfKRghB|x8Q1RH?&F#lw~E?mqmbU%ZO4!|3pPa#NzQ^Oa9bw0&;5e6FXYdQBpxWQDTA8W=#zUf0>zF zaGLo04?ErJa_S%4k>whxon}e ypI6b-;);pTD*0`Vp(lBzb%VXnSf<$li4ctip_ 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 diff --git a/developer/docs/help/images/ui/frmNGROK_Config.png b/developer/docs/help/images/ui/frmNGROK_Config.png deleted file mode 100644 index ffbbecef230126b67504bdcd2f3d38baddfa8072..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30282 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 diff --git a/developer/docs/help/images/ui/frmNGROK_Server_1.png b/developer/docs/help/images/ui/tools_options_server_ngrok.png similarity index 100% rename from developer/docs/help/images/ui/frmNGROK_Server_1.png rename to developer/docs/help/images/ui/tools_options_server_ngrok.png diff --git a/developer/docs/help/images/ui/frmNGROK_Server_2.png b/developer/docs/help/images/ui/web_and_mobile_test_ngrok.png similarity index 100% rename from developer/docs/help/images/ui/frmNGROK_Server_2.png rename to developer/docs/help/images/ui/web_and_mobile_test_ngrok.png From 76866b275b738f273b34052cbfc22598621529c9 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 26 Feb 2026 16:58:08 +0100 Subject: [PATCH 12/57] feat(linux): address code review comments - add date to header and filename - add state of ibus - add privacy statement and link to privacy policy --- .../keyman_config/diag_report.py | 23 ++++++++++++++++--- .../keyman_config/support_widget.py | 16 ++++++++++--- .../keyman-config/tests/diag_report_tests.py | 4 ++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/linux/keyman-config/keyman_config/diag_report.py b/linux/keyman-config/keyman_config/diag_report.py index a2ec76f403..314abd9878 100644 --- a/linux/keyman-config/keyman_config/diag_report.py +++ b/linux/keyman-config/keyman_config/diag_report.py @@ -4,6 +4,7 @@ Keyman is copyright (C) SIL Global. MIT License. Collects diagnostic information about the system for troubleshooting. ''' +from datetime import datetime import logging import os import platform @@ -11,7 +12,7 @@ import shutil import subprocess from keyman_config.get_kmp import InstallLocation -from keyman_config.ibus_util import get_ibus_version +from keyman_config.ibus_util import get_ibus_version, verify_ibus_daemon, IbusDaemon from keyman_config.list_installed_kmp import get_installed_kmp from keyman_config.version import ( __version__, @@ -389,12 +390,28 @@ class DiagReport: ) return lines or None + def _get_ibus_state(self): + ibus_state = verify_ibus_daemon(False) + match ibus_state: + case IbusDaemon.RUNNING: + return "Running" + case IbusDaemon.NOT_RUNNING: + return "Not running" + case IbusDaemon.MORE_THAN_ONE: + return "More than one instance running" + case IbusDaemon.ERROR_USER: + return "invalid/unknown user" + case IbusDaemon.ERROR: + return "got exception finding ibus-daemon" + case _: + return f"Unknown state: {ibus_state}" + def _create_report(self): # sourcery skip: low-code-quality """Generate a complete diagnostic report as a formatted string.""" keyman_info = self._get_keyman_version() lines = [ "=" * 60, - "Keyman Diagnostic Report", + f"Keyman for Linux Diagnostic Report ({datetime.now().strftime('%Y-%m-%d')})", "=" * 60, "", "--- Keyman Version ---", @@ -411,7 +428,7 @@ class DiagReport: # IBus Version ibus_version = get_ibus_version() if ibus_version is not None: - lines.extend(("", "--- IBus ---", f" IBus version: {ibus_version or 'unknown'}")) + lines.extend(("", "--- IBus ---", f" IBus version: {ibus_version or 'unknown'} ({self._get_ibus_state()})")) if engines := self._get_ibus_engines(): lines.append(f" Preload engines: {', '.join(engines)}") diff --git a/linux/keyman-config/keyman_config/support_widget.py b/linux/keyman-config/keyman_config/support_widget.py index 600f557271..c9d868d66f 100644 --- a/linux/keyman-config/keyman_config/support_widget.py +++ b/linux/keyman-config/keyman_config/support_widget.py @@ -1,6 +1,10 @@ #!/usr/bin/python3 +''' +Keyman is copyright (C) SIL Global. MIT License. +''' import gi +from datetime import datetime gi.require_version('Gtk', '3.0') @@ -23,9 +27,14 @@ class SupportWidget(Gtk.Box): self.pack_start(label, False, False, 10) # Description - desc_label = Gtk.Label( + desc_label = Gtk.Label() + desc_label.set_markup( _("Generate a diagnostic report to help troubleshoot issues with Keyman. " - "You can copy this report and include it when reporting issues.")) + "You can copy this report and include it when reporting issues.\n\n" + "This report does not contain personally identifiable information, " + "but it will include system information and Keyman configuration details. " + "See the privacy policy.")) + desc_label.set_use_markup(True) desc_label.set_line_wrap(True) desc_label.set_halign(Gtk.Align.START) desc_label.set_padding(5, 5) @@ -119,7 +128,8 @@ class SupportWidget(Gtk.Box): Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK) dialog.set_do_overwrite_confirmation(True) - dialog.set_current_name("keyman-diagnostic-report.txt") + date_str = datetime.now().strftime("%Y-%m-%d") + dialog.set_current_name(f"keyman-diagnostic-report_{date_str}.txt") filter_text = Gtk.FileFilter() filter_text.set_name(_("Text files")) diff --git a/linux/keyman-config/tests/diag_report_tests.py b/linux/keyman-config/tests/diag_report_tests.py index 3a151df1a6..08212b1990 100644 --- a/linux/keyman-config/tests/diag_report_tests.py +++ b/linux/keyman-config/tests/diag_report_tests.py @@ -187,7 +187,7 @@ class DiagReportTests(unittest.TestCase): # Verify self.assertIsInstance(result, str) - self.assertIn('Keyman Diagnostic Report', result) + self.assertIn('Keyman for Linux Diagnostic Report', result) self.assertIn('Keyman Version', result) self.assertIn('IBus', result) self.assertIn('Operating System', result) @@ -205,7 +205,7 @@ class DiagReportTests(unittest.TestCase): # Verify self.assertIsInstance(result, str) - self.assertIn('Keyman Diagnostic Report', result) + self.assertIn('Keyman for Linux Diagnostic Report', result) @patch.object(DiagReport, '_parse_desktop_file') @patch('subprocess.run') From 5d2f9bd65ea6a6076de31603f452aabd0d5db6ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:58:16 +0000 Subject: [PATCH 13/57] chore(deps): bump minimatch and @microsoft/api-extractor Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.5 and updates ancestor dependencies [minimatch](https://github.com/isaacs/minimatch) and [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor). These dependencies need to be updated together. Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 9.0.4 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 9.0.5 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 5.1.6 to 5.1.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `@microsoft/api-extractor` from 7.47.3 to 7.57.6 - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.57.6/apps/api-extractor) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect - dependency-name: minimatch dependency-version: 5.1.9 dependency-type: indirect - dependency-name: "@microsoft/api-extractor" dependency-version: 7.57.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 383 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 216 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61a304996b..e7a8f559f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ }, "devDependencies": { "@microsoft/api-documenter": "^7.28.0", - "@microsoft/api-extractor": "^7.47.3", + "@microsoft/api-extractor": "^7.57.6", "@types/chai": "^4.3.14", "@types/node": "^20.4.1", "@typescript-eslint/eslint-plugin": "^7.13.1", @@ -1778,24 +1778,26 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.47.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.3.tgz", - "integrity": "sha512-WKPcmg0vcBnuAX0vArXMbCtCwyN0xuyH1zoRBhuU//X0gP35KA7QInRSeuN0nFV/AKI5NGXapr8dS2lwlCDmgQ==", + "version": "7.57.6", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.57.6.tgz", + "integrity": "sha512-0rFv/D8Grzw1Mjs2+8NGUR+o4h9LVm5zKRtMeWnpdB5IMJF4TeHCL1zR5LMCIudkOvyvjbhMG5Wjs0B5nqsrRQ==", "dev": true, + "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.29.3", - "@microsoft/tsdoc": "~0.15.0", - "@microsoft/tsdoc-config": "~0.17.0", - "@rushstack/node-core-library": "5.5.0", - "@rushstack/rig-package": "0.5.2", - "@rushstack/terminal": "0.13.2", - "@rushstack/ts-command-line": "4.22.2", - "lodash": "~4.17.15", - "minimatch": "~3.0.3", + "@microsoft/api-extractor-model": "7.33.4", + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3", + "@rushstack/rig-package": "0.7.2", + "@rushstack/terminal": "0.22.3", + "@rushstack/ts-command-line": "5.3.3", + "diff": "~8.0.2", + "lodash": "~4.17.23", + "minimatch": "10.2.1", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.4.2" + "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" @@ -1814,44 +1816,28 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/@microsoft/api-extractor-model": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.29.3.tgz", - "integrity": "sha512-kEWjLr2ygL3ku9EGyjeTnL2S5IxyH9NaF1k1UoI0Nzwr4xEJBSWCVsWuF2+0lPUrRPA6mTY95fR264SJ5ETKQA==", + "version": "7.33.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.4.tgz", + "integrity": "sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==", "dev": true, + "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.15.0", - "@microsoft/tsdoc-config": "~0.17.0", - "@rushstack/node-core-library": "5.5.0" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/@microsoft/tsdoc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", - "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", - "dev": true - }, - "node_modules/@microsoft/api-extractor/node_modules/@microsoft/tsdoc-config": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", - "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", - "dev": true, - "dependencies": { - "@microsoft/tsdoc": "0.15.0", - "ajv": "~8.12.0", - "jju": "~1.4.0", - "resolve": "~1.22.2" + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3" } }, "node_modules/@microsoft/api-extractor/node_modules/@rushstack/node-core-library": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.5.0.tgz", - "integrity": "sha512-Cl3MYQ74Je5Y/EngMxcA3SpHjGZ/022nKbAO1aycGfQ+7eKyNCBu0oywj5B1f367GCzuHBgy+3BlVLKysHkXZw==", + "version": "5.20.3", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.20.3.tgz", + "integrity": "sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "~8.13.0", + "ajv": "~8.18.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", - "fs-extra": "~7.0.1", + "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", @@ -1866,29 +1852,30 @@ } } }, - "node_modules/@microsoft/api-extractor/node_modules/@rushstack/node-core-library/node_modules/ajv": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "node_modules/@microsoft/api-extractor/node_modules/@rushstack/problem-matcher": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", + "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "license": "MIT", + "peerDependencies": { + "@types/node": "*" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@microsoft/api-extractor/node_modules/@rushstack/terminal": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.13.2.tgz", - "integrity": "sha512-t8i0PsGvBHmFBY8pryO3badqFlxQsm2rw3KYrzjcmVkG/WGklKg1qVkr9beAS1Oe8XWDRgj6SkoHkpNjs7aaNw==", + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.3.tgz", + "integrity": "sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==", "dev": true, + "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.5.0", + "@rushstack/node-core-library": "5.20.3", + "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1901,22 +1888,41 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/@rushstack/ts-command-line": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.2.tgz", - "integrity": "sha512-xkvrGd6D9dPlI3I401Thc640WNsEPB1sGEmy12a2VJaPQPwhE6Ik0gEVPZJ/2G1w213eaCAdxUY1xpiTulsmpA==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.3.tgz", + "integrity": "sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==", "dev": true, + "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.13.2", + "@rushstack/terminal": "0.22.3", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, + "node_modules/@microsoft/api-extractor/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@microsoft/api-extractor/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -1934,20 +1940,58 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, - "node_modules/@microsoft/api-extractor/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "node_modules/@microsoft/api-extractor/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "balanced-match": "^4.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@microsoft/api-extractor/node_modules/string-argv": { @@ -1955,6 +1999,7 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } @@ -1964,6 +2009,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1974,19 +2020,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", @@ -1995,18 +2028,35 @@ "license": "MIT" }, "node_modules/@microsoft/tsdoc-config": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.0.tgz", - "integrity": "sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", + "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", - "ajv": "~8.12.0", + "ajv": "~8.18.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2778,21 +2828,6 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@rushstack/problem-matcher": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.1.1.tgz", @@ -2809,10 +2844,11 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", - "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", + "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" @@ -3779,10 +3815,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3805,12 +3842,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5163,10 +5201,11 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5214,12 +5253,13 @@ "dev": true }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7024,10 +7064,11 @@ } }, "node_modules/eslint-config-love/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7070,12 +7111,13 @@ } }, "node_modules/eslint-config-love/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7692,6 +7734,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", @@ -7913,35 +7972,18 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=14.14" } }, "node_modules/fs-minipass": { @@ -9996,7 +10038,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10327,13 +10371,13 @@ } }, "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10368,9 +10412,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -10666,10 +10710,11 @@ } }, "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -10720,12 +10765,13 @@ } }, "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -13111,9 +13157,10 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7f033959d8..8e33f398b2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "devDependencies": { "@microsoft/api-documenter": "^7.28.0", - "@microsoft/api-extractor": "^7.47.3", + "@microsoft/api-extractor": "^7.57.6", "@types/chai": "^4.3.14", "@types/node": "^20.4.1", "@typescript-eslint/eslint-plugin": "^7.13.1", From ede902df4a58160a109b108105236dc853f0865b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:56:42 +0000 Subject: [PATCH 14/57] chore(deps): bump minimatch Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.1.1 to 10.2.4. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v10.1.1...v10.2.4) --- updated-dependencies: - dependency-name: minimatch dependency-version: 10.2.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- .../trayicon/addon-src/package-lock.json | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json index ab91216cd4..e801c957d0 100644 --- a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -18,25 +18,6 @@ "node-gyp": "^12.1.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -90,6 +71,14 @@ "node": ">= 14" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -98,6 +87,17 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -318,14 +318,14 @@ } }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -717,19 +717,6 @@ } }, "dependencies": { - "@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" - }, - "@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "requires": { - "@isaacs/balanced-match": "^4.0.1" - } - }, "@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -768,6 +755,11 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" }, + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -776,6 +768,14 @@ "file-uri-to-path": "1.0.0" } }, + "brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "requires": { + "balanced-match": "^4.0.2" + } + }, "cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -936,11 +936,11 @@ } }, "minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "requires": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" } }, "minipass": { From b2dbd357b8c58d1cccd4eaa28b341562bf3f6745 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 2 Mar 2026 10:31:20 +0100 Subject: [PATCH 15/57] fix(developer): address review comments Co-authored-by: Darcy Wong Co-authored-by: Eberhard Beilharz --- developer/src/server/src/config.ts | 4 -- developer/src/server/src/index.ts | 52 +++++++++++-------- .../Keyman.Developer.UI.UfrmServerOptions.pas | 5 +- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/developer/src/server/src/config.ts b/developer/src/server/src/config.ts index 8d6c967855..e60ec6d0a0 100644 --- a/developer/src/server/src/config.ts +++ b/developer/src/server/src/config.ts @@ -8,7 +8,6 @@ export class Configuration { public readonly lockFilename: string; public readonly pidFilename: string; public readonly configFilename: string; - public readonly ngrokBinPath: string; /* Configuration values - set in config.json by TIKE */ @@ -17,7 +16,6 @@ export class Configuration { /* ngrok Configuration */ public readonly useNgrok: boolean; - public readonly ngrokControlPort: number; public readonly ngrokToken: string; public readonly ngrokVisible: boolean; @@ -42,8 +40,6 @@ export class Configuration { // ngrok configuration this.useNgrok = cfg?.useNgrok ?? false; - this.ngrokBinPath = this.appDataPath + 'bin/'; - this.ngrokControlPort = cfg?.ngrokControlPort ?? 8009; this.ngrokToken = cfg?.ngrokToken ?? ''; this.ngrokVisible = cfg?.ngrokVisible ?? false; } diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index a91b36d545..36fd8431d8 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -125,30 +125,36 @@ async function startNGrok() { return false; } - let started = false; - const listener = await ngrok.forward({ - proto: 'http', - addr: configuration.port, - authtoken: configuration.ngrokToken, - onLogEvent: (msg: string) => { - if(options.ngrokLog) { - console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); + try { + let started = false; + const listener = await ngrok.forward({ + proto: 'http', + addr: configuration.port, + authtoken: configuration.ngrokToken, + onLogEvent: (msg: string) => { + if(options.ngrokLog) { + console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); + } + }, + onStatusChange: (state: string) => { + if(state == 'connected' && started) { + // We only announce reconnection after initial start + configuration.ngrokEndpoint = listener.url() ?? ''; + console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), configuration.ngrokEndpoint); + } else if(state == 'closed') { + configuration.ngrokEndpoint = ''; + console.log(chalk.blueBright('ngrok tunnel closed')); + } } - }, - onStatusChange: (state: string) => { - if(state == 'connected' && started) { - // We only announce reconnection after initial start - configuration.ngrokEndpoint = listener.url(); - console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), configuration.ngrokEndpoint); - } else if(state == 'closed') { - configuration.ngrokEndpoint = ''; - console.log(chalk.blueBright('ngrok tunnel closed')); - } - } - }); - started = true; - configuration.ngrokEndpoint = listener.url(); - console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); + }); + started = true; + configuration.ngrokEndpoint = listener.url(); + console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); + } catch(e) { + configuration.ngrokEndpoint = ''; + console.error(chalk.red('ngrok tunnel failed to connect with an error: %s'), e); + return false; + } return true; } diff --git a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas index f2dbf9ad41..4a2225e564 100644 --- a/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas +++ b/developer/src/tike/dialogs/Keyman.Developer.UI.UfrmServerOptions.pas @@ -82,7 +82,8 @@ const SUrlNgrokSignup = 'https://dashboard.ngrok.com/signup'; // Note, we manually update the version of ngrok as required in subsequent - // releases, rather than trying to handle a potentially moving target + // releases, rather than trying to handle a potentially moving target. This + // should match developer/src/server/package.json version for ngrok. SNgrokVersion = 'v1.7.0'; SUrlNgrokDownload = 'https://github.com/ngrok/ngrok-javascript/releases/download/'+SNgrokVersion+'/ngrok.win32-ia32-msvc.node'; SNgrokNodeModuleFilename = 'ngrok-win32-ia32-msvc.node'; @@ -317,7 +318,7 @@ end; function TfrmServerOptions.RedistInstallerPath: string; begin - Result := TServerDebugAPI.ServerBinPath + SVCRedistExeFilename; + Result := TServerDebugAPI.ServerBinPath + '\' + SVCRedistExeFilename; end; procedure TfrmServerOptions.UpdateVersionLabel; From 343cf4567261abf4ca4a768502b4630c95ca5969 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 2 Mar 2026 10:42:33 +0100 Subject: [PATCH 16/57] fix(developer): use correct return values and nullish checks in package-validation Turned on strictNullChecks to verify the file and found a few other problems in this file. However, there are many null check errors reported across the kmc-package source which should be addressed in a future patch. (This is a broader problem for the entire Typescript source of Keyman.) Fixes: #15627 Test-bot: skip Build-bot: skip build:developer --- .../kmc-package/src/compiler/package-validation.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index 725822be82..33fca76b97 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -70,7 +70,7 @@ export class PackageValidation { } private checkForModelsAndKeyboardsInSamePackage(kmpJson: KmpJsonFile.KmpJsonFile): boolean { - if(kmpJson.lexicalModels?.length > 0 && kmpJson.keyboards?.length > 0) { + if((kmpJson.lexicalModels?.length ?? 0) > 0 && (kmpJson.keyboards?.length ?? 0) > 0) { this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageCannotContainBothModelsAndKeyboards()); return false; } @@ -120,7 +120,7 @@ export class PackageValidation { } for(const keyboard of kmpJson.keyboards) { - if(!this.checkForDuplicatedOrNonMinimalLanguages('keyboard', keyboard.id, keyboard.languages)) { + if(!this.checkForDuplicatedOrNonMinimalLanguages('keyboard', keyboard.id, keyboard.languages ?? [])) { return false; } // Note: package-version-validation verifies that there is a corresponding @@ -131,7 +131,7 @@ export class PackageValidation { } private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile, outputFilename: string): boolean { - for(const file of kmpJson.files) { + for(const file of kmpJson.files ?? []) { if(!this.checkContentFile(file, outputFilename)) { return false; } @@ -202,7 +202,7 @@ export class PackageValidation { return true; } - private checkPackageInfo(file: KmpJsonFile.KmpJsonFile) { + private checkPackageInfo(file: KmpJsonFile.KmpJsonFile): boolean { if(!file.info || !file.info.name || !file.info.name.description.trim()) { this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageNameCannotBeBlank()); return false; @@ -214,11 +214,11 @@ export class PackageValidation { /* c8 ignore next 3 */ if (match === null) { this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url})); - return null; + return false; } if(!isValidEmail(match[2])) { this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url})); - return null; + return false; } } From 9666227b48ef8ca9896894fe1a0c8e7c83e370ac Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 2 Mar 2026 14:02:50 +0100 Subject: [PATCH 17/57] chore(developer): add null guard to TframeCEFHost.SetFocus Co-authored-by: Eberhard Beilharz --- common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas index 19f84b40c3..abc1b0f169 100644 --- a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas +++ b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas @@ -373,7 +373,7 @@ end; procedure TframeCEFHost.SetFocus; begin AssertVclThread; - if not FIsClosing and cefwp.CanFocus and Assigned(cef) then + if not FIsClosing and Assigned(cefwp) and Assigned(cef) and cefwp.CanFocus then begin GetParentForm(Self).ActiveControl := Self; cef.SetFocus(True); From 3e41d7094fc57fef1e717b9a52636bc2e819ba7d Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 2 Mar 2026 14:38:45 +0100 Subject: [PATCH 18/57] chore(developer): cleanup kps file reader per review comments Co-authored-by: Darcy Wong Co-authored-by: Eberhard Beilharz --- .../utils/src/types/kps/kps-file-reader.ts | 21 +++++++------------ .../utils/test/kps/kps-file-reader.tests.ts | 14 ++++++++++++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts index 203617864d..379bb3668f 100644 --- a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts @@ -22,23 +22,16 @@ export class KpsFileReader { public read(file: Uint8Array): KpsPackage { const data = new TextDecoder().decode(file); - const kpsPackage = (() => { - let a: KpsPackage; - - try { - a = new KeymanXMLReader('kps') - .parse(data) as KpsPackage; - } catch(e) { - this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidPackageFile({e})); - } - return a; - })(); - - if(!kpsPackage) { + let kpsPackage: KpsPackage = null; + try { + kpsPackage = new KeymanXMLReader('kps') + .parse(data) as KpsPackage; + } catch(e) { + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidPackageFile({e})); return null; } - if(!kpsPackage.Package) { + if(!kpsPackage?.Package) { this.callbacks.reportMessage(DeveloperUtilsMessages.Error_NotAPackageFile()); return null; } diff --git a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts index 05f16f8e2a..e195f23c77 100644 --- a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts @@ -17,9 +17,21 @@ import { KpsFileWriter } from '../../src/types/kps/kps-file-writer.js'; import { SymbolUtils } from '../../src/symbol-utils.js'; import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js'; -const callbacks = new TestCompilerCallbacks(); describe('kps-file-reader', function () { + + const callbacks = new TestCompilerCallbacks(); + + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } + }); + it('kps-file-reader should read a valid file', function() { const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')); const reader = new KpsFileReader(callbacks); From 42eec30825096faa6ad1e239ce9b9484d9de76d1 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 24 Feb 2026 15:02:39 +0100 Subject: [PATCH 19/57] fix(developer): handle CRLF as CR internally in LDML debugger While the debugger memo internally uses CRLF, in all references, CRLF should be converted to CR for consistent text manipulation operations. This follows a similar fix in the kmn debugger in #13334. Also addresses review comments from @ermshiperete. Fixes: #15601 Relates-to: #13334 --- ...veloper.UI.Debug.UfrmLdmlKeyboardDebug.pas | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas index 4a7aaf6066..cd893a34d7 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas @@ -391,7 +391,7 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word); dk: TDeadKeyInfo; begin Result.Selection := memo.Selection; - Result.Length := Length(memo.Text); + Result.Length := Length(memo.GetTextCR); for dk in FDeadkeys do begin if dk.Position >= Result.Selection.Start @@ -406,7 +406,7 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word); L: Integer; s: string; begin - s := memo.Text; + s := memo.GetTextCR; L := Length(s); for dk in FDeadkeys do if dk.SavedPosition < 0 then @@ -430,10 +430,6 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word); if GetKeyState(VK_CONTROL) >= 0 then begin - if (vk = VK_RETURN) and (GetKeyState(VK_MENU) >= 0) then - begin - memo.SelText := #13#10; - end; Exit; end; @@ -591,13 +587,13 @@ begin RealignMemoSelectionState(state); selection := memo.Selection; end; - lhs := Copy(memo.Text, 1, selection.Start); + lhs := Copy(memo.GetTextCR, 1, selection.Start); end else lhs := ''; - context := Copy(memo.Text, lhs.Length + 1, selection.Start - lhs.Length); - rhs := Copy(memo.Text, lhs.Length + context.Length + 1, MaxInt); + context := Copy(memo.GetTextCR, lhs.Length + 1, selection.Start - lhs.Length); + rhs := Copy(memo.GetTextCR, lhs.Length + context.Length + 1, MaxInt); // Reinsert the context @@ -844,19 +840,21 @@ end; procedure TfrmLdmlKeyboardDebug.UpdateCharacterGrid; // I4808 var start, len: Integer; + text: string; begin if csDestroying in ComponentState then Exit; start := memo.SelStart; len := memo.SelLength; - if start + len > Length(memo.Text) then + text := memo.GetTextCR; + if start + len > text.Length then begin // RichEdit has a virtual final character, which is selected when // pressing Ctrl+A, etc. - len := Length(memo.Text) - start; + len := text.Length - start; end; - TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len, + TCharacterGridRenderer.Fill(sgChars, text, FDeadkeys, start, len, memo.Selection.Anchor, True); TCharacterGridRenderer.Size(sgChars, memo.Font); end; From dddd50005212db6c84634cbdf813ceee6f7ddee5 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Mon, 2 Mar 2026 09:16:38 -0500 Subject: [PATCH 20/57] maint(fv): update Cartfile for Xcode 26 --- oem/firstvoices/ios/Cartfile | 2 +- oem/firstvoices/ios/Cartfile.resolved | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile index 9ffc07d330..8305e26d3f 100644 --- a/oem/firstvoices/ios/Cartfile +++ b/oem/firstvoices/ios/Cartfile @@ -1,4 +1,4 @@ github "weichsel/ZIPFoundation" ~> 0.9 github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" -github "getsentry/sentry-cocoa" ~> 8.38.0 +github "getsentry/sentry-cocoa" ~> 8.58.0 diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved index ca05da1ae9..54775dca04 100644 --- a/oem/firstvoices/ios/Cartfile.resolved +++ b/oem/firstvoices/ios/Cartfile.resolved @@ -1,4 +1,4 @@ github "ashleymills/Reachability.swift" "v5.2.4" -github "devicekit/DeviceKit" "5.5.0" -github "getsentry/sentry-cocoa" "8.44.0" -github "weichsel/ZIPFoundation" "0.9.19" +github "devicekit/DeviceKit" "5.7.0" +github "getsentry/sentry-cocoa" "8.58.0" +github "weichsel/ZIPFoundation" "0.9.20" From d8a5f694daaab455cf8652d70399ef83e131e65c Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 20 Feb 2026 12:12:24 +0100 Subject: [PATCH 21/57] refactor(core): fix whitespace Build-bot: skip Test-bot: skip --- core/src/ldml/ldml_processor.cpp | 2 +- core/tests/unit/km_core_keyboard_api.tests.cpp | 2 +- core/tests/unit/kmx/meson.build | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 17e4d634eb..7de84315b6 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -243,7 +243,7 @@ void ldml_event_state::emit_backspace() { // else loop again assert(end.type != KM_CORE_CT_END); // inappropriate here. state->context().pop_back(); - } + } /* We couldn't find a character at end of context (context is empty), so we'll pass the backspace keystroke on to the app to process; the diff --git a/core/tests/unit/km_core_keyboard_api.tests.cpp b/core/tests/unit/km_core_keyboard_api.tests.cpp index 06e604c023..262390d7c6 100644 --- a/core/tests/unit/km_core_keyboard_api.tests.cpp +++ b/core/tests/unit/km_core_keyboard_api.tests.cpp @@ -18,7 +18,7 @@ protected: km_core_keyboard_dispose(this->keyboard); this->keyboard = nullptr; } - } + } }; TEST_F(KmCoreKeyboardApiTests, LoadFromBlob) { diff --git a/core/tests/unit/kmx/meson.build b/core/tests/unit/kmx/meson.build index 22158b6a83..71e2aed1b5 100644 --- a/core/tests/unit/kmx/meson.build +++ b/core/tests/unit/kmx/meson.build @@ -239,7 +239,7 @@ kbd_log = custom_target(test_kbd + '.kmx'.underscorify(), input: kbd_src, command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@', '--out-file', kbd_obj] ) - test('imx_list', imx_e, depends: kbd_log, args: [kbd_obj] ) +test('imx_list', imx_e, depends: kbd_log, args: [kbd_obj] ) external_e = executable('ext_event_tests', ['kmx_external_event.tests.cpp', common_test_files], cpp_args: defns + warns, @@ -258,4 +258,4 @@ kbd_log = custom_target(test_kbd + '.kmx'.underscorify(), input: kbd_src, command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@', '--out-file', kbd_obj] ) - test('ext_event', external_e, depends: kbd_log, args: [kbd_obj] ) +test('ext_event', external_e, depends: kbd_log, args: [kbd_obj] ) From 2bd6cfd735b9fc56f40ad0899ed9d46b4e08f987 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 2 Mar 2026 17:27:15 +0100 Subject: [PATCH 22/57] refactor(core): rename fields to start with underscore Test-bot: skip --- core/src/ldml/ldml_processor.cpp | 8 ++++---- core/src/ldml/ldml_processor.hpp | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 7de84315b6..9ef36b62fc 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -532,10 +532,10 @@ ldml_event_state::ldml_event_state( uint8_t i, uint16_t e) { this->state = s; - this->vk = v; - this->modifier_state = m; - this->is_key_down = i; - this->event_flags = e; + this->_vk = v; + this->_modifier_state = m; + this->_is_key_down = i; + this->_event_flags = e; actions.persist_options = new km_core_option_item[1]; actions.persist_options[0] = NULL_OPTIONS[0]; diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index 6bf8a8fa31..b673a9275c 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -178,10 +178,10 @@ public: size_t context_to_string(std::u32string &str, bool include_markers = true); private: - km_core_virtual_key vk; - uint16_t modifier_state; - uint8_t is_key_down; - uint16_t event_flags; + km_core_virtual_key _vk; + uint16_t _modifier_state; + uint8_t _is_key_down; + uint16_t _event_flags; km_core_state *state; // our in-flight action struct. @@ -194,12 +194,12 @@ public: // implementation km_core_virtual_key ldml_event_state::get_vk() const { - return vk; + return _vk; } uint16_t ldml_event_state::get_modifier_state() const { - return modifier_state; + return _modifier_state; } } // namespace core From e06868a6221cfc4644905c6d0a7f606519293840 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 2 Mar 2026 12:01:33 -0600 Subject: [PATCH 23/57] auto: increment master version to 19.0.208 Test-bot: skip Build-bot: skip --- HISTORY.md | 6 ++++++ VERSION.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index f42af6d60b..60b5e193cf 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 19.0.207 alpha 2026-03-02 + +* chore(ios): Cleanup `RELEASE_OEM_FIRSTVOICES` var in build.sh scripts (#15605) +* fix(developer): package compiler handle missing `info` section in .kps file (#15620) +* fix(developer): handle window already destroyed when closing editor (#15618) + ## 19.0.206 alpha 2026-02-25 * maint(ios): upgrade to Xcode 26.2 (#15498) diff --git a/VERSION.md b/VERSION.md index 3027f050d0..b2383c811f 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.207 \ No newline at end of file +19.0.208 \ No newline at end of file From 529a49431e4e8d65f0bc9feee57e2680bf22d2e9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 3 Mar 2026 16:44:06 +0100 Subject: [PATCH 24/57] maint(developer): make `TestCompilerCallbacks` usage more consistent and simplify Simplify usage of `TestCompilerCallbacks` by making it responsible for the `beforeEach` and `afterEach` incantations itself. There are a couple of more complex usages of `TestCompilerCallbacks` which have been excluded from this change on purpose. Renamed `testCallbacks` to `callbacks` in the one place where it was different. Hoist @types/node and @types/mocha because inconsistent versions of @types/mocha were causing compiler errors with this change. Also added a cast to `fs.readFileSync` to `Uint8Array` to eliminate compiler warnings/errors in test files. Fixes: #15654 Test-bot: skip --- common/web/types/package.json | 2 - .../web/test-helpers/TestCompilerCallbacks.ts | 22 ++- .../utils/test/kpj/kpj-file-reader.tests.ts | 10 +- developer/src/kmc-analyze/package.json | 2 - .../kmc-copy/test/copier-messages.tests.ts | 12 +- developer/src/kmc-copy/test/copier.tests.ts | 12 +- .../test/lexical-model-generator.tests.ts | 3 +- developer/src/kmc-keyboard-info/package.json | 2 - .../keyboard-info-compiler-messages.tests.ts | 12 +- .../test/keyboard-info-compiler.tests.ts | 18 +- developer/src/kmc-kmn/package.json | 2 - developer/src/kmc-kmn/test/compiler.tests.ts | 6 +- developer/src/kmc-kmn/test/features.tests.ts | 7 +- .../test/kmn-compiler-messages.tests.ts | 2 +- .../kmc-kmn/test/kmw/kmw-compiler.tests.ts | 9 +- .../kmc-kmn/test/kmw/kmw-messages.tests.ts | 2 +- developer/src/kmc-kmn/test/wasm-uset.tests.ts | 7 +- developer/src/kmc-ldml/package.json | 2 - developer/src/kmc-model-info/package.json | 2 - .../test/model-info-compiler.tests.ts | 8 +- developer/src/kmc-model/package.json | 2 - .../compile-model-with-pseudoclosure.tests.ts | 2 +- .../src/kmc-model/test/compile-model.tests.ts | 2 +- .../src/kmc-model/test/compile-trie.tests.ts | 5 +- .../src/kmc-model/test/messages.tests.ts | 12 +- .../kmc-model/test/parse-wordlist.tests.ts | 27 ++- .../src/kmc-model/test/punctuation.tests.ts | 3 +- developer/src/kmc-package/package.json | 2 - .../src/kmc-package/test/messages.tests.ts | 7 +- .../test/package-compiler.tests.ts | 10 +- .../src/kmc-package/test/versioning.tests.ts | 2 +- ...indows-package-installer-compiler.tests.ts | 7 +- developer/src/kmc/package.json | 2 - developer/src/server/package.json | 2 - package-lock.json | 161 +++++++++--------- package.json | 1 + .../predictive-text/templates/package.json | 1 - .../predictive-text/wordbreakers/package.json | 1 - .../main/headless/languageProcessor.tests.js | 3 +- 39 files changed, 156 insertions(+), 238 deletions(-) diff --git a/common/web/types/package.json b/common/web/types/package.json index 4c2e2bfd2e..2b2122f682 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -36,8 +36,6 @@ "restructure": "3.0.1" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "ajv": "^8.12.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", diff --git a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts index 04e5734f18..711944f12d 100644 --- a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts +++ b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts @@ -5,6 +5,7 @@ import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSy CompilerFileSystemAsyncCallbacks, CompilerErrorSeverity} from '@keymanapp/developer-utils'; import { fileURLToPath } from 'url'; +import { Suite } from 'mocha'; const { TEST_SAVE_FIXTURES } = process.env; @@ -12,16 +13,27 @@ const { TEST_SAVE_FIXTURES } = process.env; * A CompilerCallbacks implementation for testing */ export class TestCompilerCallbacks implements CompilerCallbacks { - /* TestCompilerCallbacks */ - + private readonly suite: Suite; messages: CompilerEvent[] = []; readonly _net: TestCompilerNetAsyncCallbacks; readonly _fsAsync: DefaultCompilerFileSystemAsyncCallbacks = new DefaultCompilerFileSystemAsyncCallbacks(this); - constructor(basePath?: string) { + constructor(suite?: Suite, basePath?: string) { if(basePath) { this._net = new TestCompilerNetAsyncCallbacks(basePath); } + this.suite = suite; + if(this.suite) { + const _this = this; + this.suite.beforeEach(function() { + _this.clear(); + }); + this.suite.afterEach(function() { + if(this.currentTest?.isFailed()) { + _this.printMessages(); + } + }) + } } clear() { @@ -55,7 +67,7 @@ export class TestCompilerCallbacks implements CompilerCallbacks { loadFile(filename: string): Uint8Array { try { - return fs.readFileSync(filename); + return fs.readFileSync(filename) as Uint8Array; } catch(e) { if (e.code === 'ENOENT') { return null; @@ -162,7 +174,7 @@ class TestCompilerNetAsyncCallbacks implements CompilerNetAsyncCallbacks { // missing file, this is okay return null; } - const data: Uint8Array = fs.readFileSync(p); + const data = fs.readFileSync(p) as Uint8Array; return data; } diff --git a/developer/src/common/web/utils/test/kpj/kpj-file-reader.tests.ts b/developer/src/common/web/utils/test/kpj/kpj-file-reader.tests.ts index 358edb01da..a70d4dd42c 100644 --- a/developer/src/common/web/utils/test/kpj/kpj-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kpj/kpj-file-reader.tests.ts @@ -6,13 +6,15 @@ import { KPJFileReader } from "../../src/types/kpj/kpj-file-reader.js"; import { KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from '../../src/types/kpj/keyman-developer-project.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -const callbacks = new TestCompilerCallbacks(); describe('kpj-file-reader', function () { + + const callbacks = new TestCompilerCallbacks(this); + it('kpj-file-reader should read a valid file', async function() { const kpjPath = 'khmer_angkor.kpj'; const path = makePathToFixture('kpj', kpjPath); - const input = fs.readFileSync(path); + const input = fs.readFileSync(path) as Uint8Array; const reader = new KPJFileReader(callbacks); const kpj = reader.read(input); reader.validate(kpj); @@ -124,7 +126,7 @@ describe('kpj-file-reader', function () { it('should load a v1.0 keyboard project with missing ', async function() { const path = makePathToFixture('kpj', 'project-missing-file', 'project_missing_file.kpj'); - const input = fs.readFileSync(path); + const input = fs.readFileSync(path) as Uint8Array; const reader = new KPJFileReader(callbacks); const kpj = reader.read(input); reader.validate(kpj); @@ -140,7 +142,7 @@ describe('kpj-file-reader', function () { it('should load a v1.0 keyboard project with missing ', async function() { const path = makePathToFixture('kpj', 'project-missing-file', 'project_missing_files.kpj'); - const input = fs.readFileSync(path); + const input = fs.readFileSync(path) as Uint8Array; const reader = new KPJFileReader(callbacks); const kpj = reader.read(input); reader.validate(kpj); diff --git a/developer/src/kmc-analyze/package.json b/developer/src/kmc-analyze/package.json index c7e2ebabe2..432bb26e01 100644 --- a/developer/src/kmc-analyze/package.json +++ b/developer/src/kmc-analyze/package.json @@ -29,8 +29,6 @@ }, "devDependencies": { "@keymanapp/resources-gosh": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" diff --git a/developer/src/kmc-copy/test/copier-messages.tests.ts b/developer/src/kmc-copy/test/copier-messages.tests.ts index 0312910860..ea0e9bf624 100644 --- a/developer/src/kmc-copy/test/copier-messages.tests.ts +++ b/developer/src/kmc-copy/test/copier-messages.tests.ts @@ -14,17 +14,7 @@ import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js'; import { makePathToFixture } from './helpers/index.js'; describe('CopierMessages', function () { - const callbacks = new TestCompilerCallbacks(makePathToFixture('online')); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this, makePathToFixture('online')); it('should have a valid CopierMessages object', function() { return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier); diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts index 697e58e0b5..2acf5bf12c 100644 --- a/developer/src/kmc-copy/test/copier.tests.ts +++ b/developer/src/kmc-copy/test/copier.tests.ts @@ -23,7 +23,7 @@ function normalizeNewLine(s: string): string { } describe('KeymanProjectCopier', function() { - const callbacks = new TestCompilerCallbacks(makePathToFixture('online')); + const callbacks = new TestCompilerCallbacks(this, makePathToFixture('online')); this.beforeAll(function() { if(TEST_SAVE_ARTIFACTS) { @@ -32,16 +32,6 @@ describe('KeymanProjectCopier', function() { } }); - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); - const tests = [ // diff --git a/developer/src/kmc-generate/test/lexical-model-generator.tests.ts b/developer/src/kmc-generate/test/lexical-model-generator.tests.ts index 39908f25c9..f54dc5290c 100644 --- a/developer/src/kmc-generate/test/lexical-model-generator.tests.ts +++ b/developer/src/kmc-generate/test/lexical-model-generator.tests.ts @@ -35,6 +35,8 @@ function getFilenames(p: string, base?: string): string[] { describe('LexicalModelGenerator', function () { let clock: sinon.SinonFakeTimers; + const callbacks = new TestCompilerCallbacks(this); + before(function() { // We will always be 12 April 2024 to match test fixtures clock = sinon.useFakeTimers(new Date(2024, 3, 12)); @@ -46,7 +48,6 @@ describe('LexicalModelGenerator', function () { it('should generate a lexical model from provided options', async function() { const generator = new LexicalModelGenerator(); - const callbacks = new TestCompilerCallbacks(); const opts: GeneratorOptions = {...options}; opts.id = 'sample.en.sample'; opts.targets = [KeymanTargets.KeymanTarget.any]; diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index 5b0c463b3a..db256a6c78 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -29,8 +29,6 @@ "@keymanapp/langtags": "*" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts index fcd9dfb8c1..7bb355faef 100644 --- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts +++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts @@ -8,8 +8,6 @@ import { makePathToFixture } from './helpers/index.js'; import { KeyboardInfoCompiler } from '../src/keyboard-info-compiler.js'; import { KeyboardInfoFile } from '../src/keyboard-info-file.js'; -const callbacks = new TestCompilerCallbacks(); - const KHMER_ANGKOR_JS = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); const KHMER_ANGKOR_KPS = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); const KHMER_ANGKOR_KMP = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); @@ -24,15 +22,7 @@ const KHMER_ANGKOR_SOURCES = { describe('KeyboardInfoCompilerMessages', function () { - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }) + const callbacks = new TestCompilerCallbacks(this); it('should have a valid KeyboardInfoCompilerMessages object', function() { return verifyCompilerMessagesObject(KeyboardInfoCompilerMessages, CompilerErrorNamespace.KeyboardInfoCompiler); diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts index 0bb17e539e..b9345e7508 100644 --- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts +++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts @@ -10,18 +10,6 @@ import { KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/com import { CompilerCallbacks } from '@keymanapp/developer-utils'; import { KeyboardInfoFile, KeyboardInfoFileLanguage, KeyboardInfoFilePlatform } from './keyboard-info-file.js'; -const callbacks = new TestCompilerCallbacks(); - -beforeEach(function() { - callbacks.clear(); -}); - -afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } -}); - const KHMER_ANGKOR_KPJ = makePathToFixture('khmer_angkor', 'khmer_angkor.kpj'); const KHMER_ANGKOR_JS = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); const KHMER_ANGKOR_KPS = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); @@ -87,6 +75,9 @@ const JAVA_DISPLAY_FONT_INFO = { family: "Java", source: [ JAVA_DISPLAY_FONT ] } const JAVA_OSK_FONT_INFO = { family: "Java Kbd", source: [ JAVA_OSK_FONT ] }; describe('keyboard-info-compiler', function () { + + const callbacks = new TestCompilerCallbacks(this); + it('compile a .keyboard_info file correctly', async function() { const kpjFilename = KHMER_ANGKOR_KPJ; const buildKeyboardInfoFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.keyboard_info'); @@ -844,6 +835,9 @@ describe('keyboard-info-compiler', function () { }); describe('fillLanguageMetadata', function() { + + const callbacks = new TestCompilerCallbacks(this); + const tests: { bcp47: string, lang: KeyboardInfoFileLanguage, commonScript: string }[] = [ // 'und' language subtag diff --git a/developer/src/kmc-kmn/package.json b/developer/src/kmc-kmn/package.json index f9c82fb4e9..88a172dfbb 100644 --- a/developer/src/kmc-kmn/package.json +++ b/developer/src/kmc-kmn/package.json @@ -32,8 +32,6 @@ }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.9", diff --git a/developer/src/kmc-kmn/test/compiler.tests.ts b/developer/src/kmc-kmn/test/compiler.tests.ts index cb190f519d..7b67e3dd58 100644 --- a/developer/src/kmc-kmn/test/compiler.tests.ts +++ b/developer/src/kmc-kmn/test/compiler.tests.ts @@ -11,6 +11,8 @@ const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/'; const baselineDir = keyboardsDir + 'baseline/'; describe('Compiler class', function() { + const callbacks = new TestCompilerCallbacks(this); + it('should throw on failure', async function() { const compiler = new KmnCompiler(); const callbacks : any = null; // ERROR @@ -25,14 +27,12 @@ describe('Compiler class', function() { it('should start', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); }); it('should compile a basic keyboard', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false})); assert(compiler.verifyInitialized()); @@ -60,7 +60,6 @@ describe('Compiler class', function() { it('should build all baseline fixtures', async function() { this.timeout(10000); // there are quite a few fixtures, sometimes CI agents are slow const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false})); assert(compiler.verifyInitialized()); @@ -90,7 +89,6 @@ describe('Compiler class', function() { it('should compile a keyboard with visual keyboard', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await compiler.init(callbacks, { saveDebug: true, shouldAddCompilerVersion: false, diff --git a/developer/src/kmc-kmn/test/features.tests.ts b/developer/src/kmc-kmn/test/features.tests.ts index 1f82a03ed5..6d8f0bde19 100644 --- a/developer/src/kmc-kmn/test/features.tests.ts +++ b/developer/src/kmc-kmn/test/features.tests.ts @@ -7,19 +7,14 @@ import { KMX, KmxFileReader } from '@keymanapp/common-types'; describe('Keyboard compiler features', function() { let compiler: KmnCompiler = null; - let callbacks: TestCompilerCallbacks = null; + const callbacks = new TestCompilerCallbacks(this); this.beforeAll(async function() { compiler = new KmnCompiler(); - callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, {saveDebug: true})); assert(compiler.verifyInitialized()); }); - beforeEach(function() { - callbacks.clear(); - }); - // Test each Keyman file version target const versions = [ diff --git a/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts b/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts index 055535513d..f241c02606 100644 --- a/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts +++ b/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts @@ -7,7 +7,7 @@ import { KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/k import { makePathToFixture } from './helpers/index.js'; describe('KmnCompilerMessages', function () { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid KmnCompilerMessages object', function() { return verifyCompilerMessagesObject(KmnCompilerMessages, CompilerErrorNamespace.KmnCompiler); diff --git a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts index a1008416aa..9fb4563d29 100644 --- a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts +++ b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts @@ -25,7 +25,7 @@ const generateTestFilenames = (id: string) => ({ }); describe('KeymanWeb Compiler', function() { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); const kmnCompiler: KmnCompiler = new KmnCompiler(); this.beforeAll(async function() { @@ -35,13 +35,6 @@ describe('KeymanWeb Compiler', function() { })); }); - this.afterEach(function() { - if(this.currentTest?.isFailed() || debug) { - callbacks.printMessages(); - } - callbacks.clear(); - }); - it('should compile a complex keyboard', async function() { await run_test_keyboard(kmnCompiler, 'khmer_angkor'); }); diff --git a/developer/src/kmc-kmn/test/kmw/kmw-messages.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-messages.tests.ts index a69db77020..175afdfe7a 100644 --- a/developer/src/kmc-kmn/test/kmw/kmw-messages.tests.ts +++ b/developer/src/kmc-kmn/test/kmw/kmw-messages.tests.ts @@ -7,7 +7,7 @@ import { KmnCompiler } from '../../src/main.js'; import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; describe('KmwCompilerMessages', function () { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid KmwCompilerMessages object', function() { return verifyCompilerMessagesObject(KmwCompilerMessages, CompilerErrorNamespace.KmwCompiler); diff --git a/developer/src/kmc-kmn/test/wasm-uset.tests.ts b/developer/src/kmc-kmn/test/wasm-uset.tests.ts index ad67b35246..11a3e9a487 100644 --- a/developer/src/kmc-kmn/test/wasm-uset.tests.ts +++ b/developer/src/kmc-kmn/test/wasm-uset.tests.ts @@ -6,6 +6,8 @@ import { KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js'; import { compilerErrorFormatCode } from '@keymanapp/developer-utils'; describe('Compiler UnicodeSet function', function() { + const callbacks = new TestCompilerCallbacks(this); + it('should fixup "short" \\u{} escapes', function () { assert.equal(KmnCompiler.fixNewPattern(`\\u{A}`), `\\u000A`); // " assert.equal(KmnCompiler.fixNewPattern(`\\u{22}`), `\\u0022`); // " @@ -24,14 +26,12 @@ describe('Compiler UnicodeSet function', function() { it('should start', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); }); it('should compile a basic uset', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); @@ -49,7 +49,6 @@ describe('Compiler UnicodeSet function', function() { }); it('should compile a more complex uset', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); @@ -69,7 +68,6 @@ describe('Compiler UnicodeSet function', function() { }); it('should compile an even more complex uset', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); @@ -96,7 +94,6 @@ describe('Compiler UnicodeSet function', function() { }); it('should fail in various ways', async function() { const compiler = new KmnCompiler(); - const callbacks = new TestCompilerCallbacks(); assert(await compiler.init(callbacks, null)); assert(compiler.verifyInitialized()); // map from string to failing error diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index 9e2a1c6831..1a586eec78 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -35,8 +35,6 @@ "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", "@types/common-tags": "^1.8.4", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "abnf": "^4.3.1", "c8": "^7.12.0", diff --git a/developer/src/kmc-model-info/package.json b/developer/src/kmc-model-info/package.json index c747a922c4..7d1e7f2fef 100644 --- a/developer/src/kmc-model-info/package.json +++ b/developer/src/kmc-model-info/package.json @@ -34,8 +34,6 @@ "@keymanapp/developer-utils": "*" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" diff --git a/developer/src/kmc-model-info/test/model-info-compiler.tests.ts b/developer/src/kmc-model-info/test/model-info-compiler.tests.ts index 4b0a85624e..5f87ad0f21 100644 --- a/developer/src/kmc-model-info/test/model-info-compiler.tests.ts +++ b/developer/src/kmc-model-info/test/model-info-compiler.tests.ts @@ -6,13 +6,9 @@ import { makePathToFixture } from './helpers/index.js'; import { ModelInfoCompiler } from '../src/model-info-compiler.js'; import { KmpCompiler } from '@keymanapp/kmc-package'; -const callbacks = new TestCompilerCallbacks(); - -beforeEach(function() { - callbacks.clear(); -}); - describe('model-info-compiler', function () { + const callbacks = new TestCompilerCallbacks(this); + it('compile a .model_info file correctly', async function() { const kpjFilename = makePathToFixture('sil.cmo.bw', 'sil.cmo.bw.model.kpj'); const kpsFilename = makePathToFixture('sil.cmo.bw', 'source', 'sil.cmo.bw.model.kps'); diff --git a/developer/src/kmc-model/package.json b/developer/src/kmc-model/package.json index 481822e686..52ef40c425 100644 --- a/developer/src/kmc-model/package.json +++ b/developer/src/kmc-model/package.json @@ -37,8 +37,6 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/models-templates": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "esbuild": "^0.25.0" diff --git a/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts b/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts index c4d3334b6a..6a8b55af7e 100644 --- a/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts +++ b/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts @@ -7,7 +7,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { LexicalModelTypes } from '@keymanapp/common-types'; describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); const MODEL_ID = 'example.qaa.trivial'; const PATH = makePathToFixture(MODEL_ID); diff --git a/developer/src/kmc-model/test/compile-model.tests.ts b/developer/src/kmc-model/test/compile-model.tests.ts index e5c9e86c15..5c6a8ab104 100644 --- a/developer/src/kmc-model/test/compile-model.tests.ts +++ b/developer/src/kmc-model/test/compile-model.tests.ts @@ -7,7 +7,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KeymanFileTypes } from '@keymanapp/common-types'; describe('LexicalModelCompiler', function () { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); this.timeout(5000); diff --git a/developer/src/kmc-model/test/compile-trie.tests.ts b/developer/src/kmc-model/test/compile-trie.tests.ts index 81de56987e..24f05bb7ec 100644 --- a/developer/src/kmc-model/test/compile-trie.tests.ts +++ b/developer/src/kmc-model/test/compile-trie.tests.ts @@ -11,10 +11,7 @@ import { TrieModel } from '@keymanapp/models-templates'; import { LexicalModelTypes } from '@keymanapp/common-types'; describe('LexicalModelCompiler', function () { - const callbacks = new TestCompilerCallbacks(); - this.beforeEach(function() { - callbacks.clear(); - }); + const callbacks = new TestCompilerCallbacks(this); describe('#generateLexicalModelCode', function () { it('should compile a trivial word list', async function () { diff --git a/developer/src/kmc-model/test/messages.tests.ts b/developer/src/kmc-model/test/messages.tests.ts index f9d3eb07b8..8d3bb10c08 100644 --- a/developer/src/kmc-model/test/messages.tests.ts +++ b/developer/src/kmc-model/test/messages.tests.ts @@ -8,17 +8,7 @@ import { makePathToFixture } from './helpers/index.js'; describe('ModelCompilerMessages', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid ModelCompilerMessages object', function() { return verifyCompilerMessagesObject(ModelCompilerMessages, CompilerErrorNamespace.ModelCompiler); diff --git a/developer/src/kmc-model/test/parse-wordlist.tests.ts b/developer/src/kmc-model/test/parse-wordlist.tests.ts index ada01fbc2b..ba4f6cb92b 100644 --- a/developer/src/kmc-model/test/parse-wordlist.tests.ts +++ b/developer/src/kmc-model/test/parse-wordlist.tests.ts @@ -22,11 +22,10 @@ const SENCOTEN_WORDLIST = { describe('parsing a word list', function () { - const testCallbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); beforeEach(function () { - testCallbacks.clear(); - setCompilerCallbacks(testCallbacks); + setCompilerCallbacks(callbacks); }); afterEach(function () { @@ -43,12 +42,12 @@ describe('parsing a word list', function () { const withoutBOM: WordList = {}; parseWordListFromContents(withoutBOM, file); assert.deepEqual(withoutBOM, expected, "expected regular file to parse properly"); - assert.isEmpty(testCallbacks.messages); + assert.isEmpty(callbacks.messages); const withBOM: WordList = {}; parseWordListFromContents(withBOM, `${BOM}${file}`) assert.deepEqual(withBOM, expected, "expected BOM to be ignored"); - assert.isEmpty(testCallbacks.messages); + assert.isEmpty(callbacks.messages); }); it('should read word lists in UTF-8', function () { @@ -58,7 +57,7 @@ describe('parsing a word list', function () { parseWordListFromFilename(wordlist, filename); assert.deepEqual(wordlist, SENCOTEN_WORDLIST); - assert.isEmpty(testCallbacks.messages); + assert.isEmpty(callbacks.messages); }); it('should read word lists in UTF-16 little-endian (with BOM)', function () { @@ -69,7 +68,7 @@ describe('parsing a word list', function () { parseWordListFromFilename(wordlist, filename); assert.deepEqual(wordlist, SENCOTEN_WORDLIST); - assert.isEmpty(testCallbacks.messages); + assert.isEmpty(callbacks.messages); }); it('should NOT read word lists in UTF-16 big-endian (with BOM)', function () { @@ -106,22 +105,22 @@ describe('parsing a word list', function () { assert.deepEqual(repeatedWords, expected); - assert.lengthOf(testCallbacks.messages, 4); + assert.lengthOf(callbacks.messages, 4); // hello has been seen multiple times: - assert.isTrue(testCallbacks.hasMessage(ModelCompilerMessages.HINT_DuplicateWordInSameFile)); + assert.isTrue(callbacks.hasMessage(ModelCompilerMessages.HINT_DuplicateWordInSameFile)); // helló and hello + U+0301 have both been seen: - assert.isTrue(testCallbacks.hasMessage(ModelCompilerMessages.HINT_MixedNormalizationForms)); + assert.isTrue(callbacks.hasMessage(ModelCompilerMessages.HINT_MixedNormalizationForms)); // Let's parse another file: - testCallbacks.clear(); + callbacks.clear(); // Now, parse a DIFFERENT file, but with an NFD entry. parseWordListFromContents(repeatedWords, "hello\u0301\t5\n"); - assert.lengthOf(testCallbacks.messages, 1); + assert.lengthOf(callbacks.messages, 1); // hello + U+0301 (NFD) has been seen, but... - assert.isTrue(testCallbacks.hasMessage(ModelCompilerMessages.HINT_MixedNormalizationForms)); + assert.isTrue(callbacks.hasMessage(ModelCompilerMessages.HINT_MixedNormalizationForms)); // BUT! We have not seen a duplicate **within the same file** - assert.isFalse(testCallbacks.hasMessage(ModelCompilerMessages.HINT_DuplicateWordInSameFile)); + assert.isFalse(callbacks.hasMessage(ModelCompilerMessages.HINT_DuplicateWordInSameFile)); assert.deepEqual(repeatedWords, { hello: expected['hello'], diff --git a/developer/src/kmc-model/test/punctuation.tests.ts b/developer/src/kmc-model/test/punctuation.tests.ts index c3679f08d9..e0f1d661b4 100644 --- a/developer/src/kmc-model/test/punctuation.tests.ts +++ b/developer/src/kmc-model/test/punctuation.tests.ts @@ -6,12 +6,13 @@ import { makePathToFixture, compileModelSourceCode } from './helpers/index.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; describe('LexicalModelCompiler', function () { + const callbacks = new TestCompilerCallbacks(this); + describe('specifying punctuation', function () { const MODEL_ID = 'example.qaa.trivial'; const PATH = makePathToFixture(MODEL_ID); it('should compile punctuation into the generated code', async function () { - const callbacks = new TestCompilerCallbacks(); const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); diff --git a/developer/src/kmc-package/package.json b/developer/src/kmc-package/package.json index edfba42073..e441b5d57b 100644 --- a/developer/src/kmc-package/package.json +++ b/developer/src/kmc-package/package.json @@ -36,8 +36,6 @@ }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" diff --git a/developer/src/kmc-package/test/messages.tests.ts b/developer/src/kmc-package/test/messages.tests.ts index 21f1e0e4a2..934226e024 100644 --- a/developer/src/kmc-package/test/messages.tests.ts +++ b/developer/src/kmc-package/test/messages.tests.ts @@ -10,15 +10,10 @@ import { makePathToFixture } from './helpers/index.js'; import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; import { CompilerErrorNamespace, CompilerOptions } from '@keymanapp/developer-utils'; -const callbacks = new TestCompilerCallbacks(); describe('PackageCompilerMessages', function () { - this.afterEach(function() { - if(this.currentTest.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid PackageCompilerMessages object', function() { return verifyCompilerMessagesObject(PackageCompilerMessages, CompilerErrorNamespace.PackageCompiler); diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index 9dcad78054..77fbf75cdb 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -21,22 +21,14 @@ describe('KmpCompiler', function () { 'example.qaa.sencoten', 'withfolders.qaa.sencoten', ]; - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); let kmpCompiler: KmpCompiler = null; this.beforeAll(async function() { - callbacks.clear(); kmpCompiler = new KmpCompiler(); assert.isTrue(await kmpCompiler.init(callbacks, null)); }); - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - callbacks.clear(); - }); - for (const modelID of MODELS) { const kpsPath = modelID.includes('withfolders') ? makePathToFixture(modelID, 'source', `${modelID}.model.kps`) : makePathToFixture(modelID, `${modelID}.model.kps`); diff --git a/developer/src/kmc-package/test/versioning.tests.ts b/developer/src/kmc-package/test/versioning.tests.ts index adcba30019..91ec523e65 100644 --- a/developer/src/kmc-package/test/versioning.tests.ts +++ b/developer/src/kmc-package/test/versioning.tests.ts @@ -11,6 +11,7 @@ import { makePathToFixture } from './helpers/index.js'; // test results documented below. describe('package versioning', function () { + const callbacks = new TestCompilerCallbacks(this); const cases: [string,string][] = [ ['test-single-version-1-package', 'test1.kps'], @@ -33,7 +34,6 @@ describe('package versioning', function () { for(const [ caseTitle, filename ] of cases) { it(caseTitle, async function () { - const callbacks = new TestCompilerCallbacks(); const kmpCompiler = new KmpCompiler(); assert.isTrue(await kmpCompiler.init(callbacks, null)); diff --git a/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts b/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts index f61b65d8aa..c4843942a7 100644 --- a/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts +++ b/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts @@ -9,6 +9,8 @@ import { makePathToFixture } from './helpers/index.js'; import { WindowsPackageInstallerCompiler, WindowsPackageInstallerSources } from '../src/compiler/windows-package-installer-compiler.js'; describe('WindowsPackageInstallerCompiler', function () { + const callbacks = new TestCompilerCallbacks(this); + it(`should build an SFX archive`, async function () { this.timeout(10000); // this test can take a little while to run @@ -22,7 +24,6 @@ describe('WindowsPackageInstallerCompiler', function () { appName: 'Testing', }; - const callbacks = new TestCompilerCallbacks(); const compiler = new WindowsPackageInstallerCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); @@ -44,7 +45,7 @@ describe('WindowsPackageInstallerCompiler', function () { const zipBuffer = sfxBuffer.slice(setupExeSize); // Verify setup.exe sfx loader - const setupExeFixture = fs.readFileSync(sources.setupExeFilename); + const setupExeFixture = fs.readFileSync(sources.setupExeFilename) as Uint8Array; assert.deepEqual(setupExe, setupExeFixture); // Load the zip from the buffer @@ -68,7 +69,7 @@ khmer_angkor.kmp assert.equal(setupInf.trim(), setupInfFixture.trim()); const verifyFile = async (filename: string) => { - const fixture = fs.readFileSync(filename); + const fixture = fs.readFileSync(filename) as Uint8Array; const file = await zipFile.file(path.basename(filename)).async('uint8array'); assert.deepEqual(file, fixture, `File in zip '${filename}' did not match fixture`); }; diff --git a/developer/src/kmc/package.json b/developer/src/kmc/package.json index 39d839bc84..4f15f5e98c 100644 --- a/developer/src/kmc/package.json +++ b/developer/src/kmc/package.json @@ -53,8 +53,6 @@ ], "devDependencies": { "@sentry/cli": "^2.31.0", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "esbuild": "^0.25.0", "typescript": "^5.4.5" diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 37192ca6f3..ee58b4d649 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -29,9 +29,7 @@ "@keymanapp/keyman-version": "*", "@keymanapp/resources-gosh": "*", "@types/express": "^4.17.13", - "@types/mocha": "^9.1.0", "@types/multer": "^1.4.12", - "@types/node": "^20.4.1", "@types/ws": "^8.2.2", "copyfiles": "^2.4.1", "node-gyp": "^10.2.0", diff --git a/package-lock.json b/package-lock.json index 61a304996b..ca632dbea4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@microsoft/api-documenter": "^7.28.0", "@microsoft/api-extractor": "^7.47.3", "@types/chai": "^4.3.14", + "@types/mocha": "^10.0.10", "@types/node": "^20.4.1", "@typescript-eslint/eslint-plugin": "^7.13.1", "@web/dev-server-esbuild": "^1.0.2", @@ -149,8 +150,6 @@ "restructure": "3.0.1" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "ajv": "^8.12.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", @@ -160,11 +159,6 @@ "typescript": "^5.4.5" } }, - "common/web/types/node_modules/@types/mocha": { - "version": "5.2.7", - "dev": true, - "license": "MIT" - }, "common/web/types/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -285,8 +279,6 @@ }, "devDependencies": { "@sentry/cli": "^2.31.0", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "esbuild": "^0.25.0", "typescript": "^5.4.5" @@ -302,19 +294,11 @@ }, "devDependencies": { "@keymanapp/resources-gosh": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" } }, - "developer/src/kmc-analyze/node_modules/@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "developer/src/kmc-analyze/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -581,19 +565,11 @@ "@keymanapp/langtags": "*" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" } }, - "developer/src/kmc-keyboard-info/node_modules/@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "developer/src/kmc-keyboard-info/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -666,8 +642,6 @@ }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.9", @@ -677,12 +651,6 @@ "typescript": "^5.4.5" } }, - "developer/src/kmc-kmn/node_modules/@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "developer/src/kmc-kmn/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -759,8 +727,6 @@ "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", "@types/common-tags": "^1.8.4", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "abnf": "^4.3.1", "c8": "^7.12.0", @@ -770,12 +736,6 @@ "typescript": "^5.4.5" } }, - "developer/src/kmc-ldml/node_modules/@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "developer/src/kmc-ldml/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -849,8 +809,6 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/models-templates": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "esbuild": "^0.25.0" @@ -864,18 +822,11 @@ "@keymanapp/developer-utils": "*" }, "devDependencies": { - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" } }, - "developer/src/kmc-model-info/node_modules/@types/mocha": { - "version": "5.2.7", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-model-info/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -932,11 +883,6 @@ "node": ">=4" } }, - "developer/src/kmc-model/node_modules/@types/mocha": { - "version": "5.2.7", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-model/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -1004,18 +950,11 @@ }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", - "@types/mocha": "^5.2.7", - "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" } }, - "developer/src/kmc-package/node_modules/@types/mocha": { - "version": "5.2.7", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-package/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -1072,11 +1011,6 @@ "node": ">=4" } }, - "developer/src/kmc/node_modules/@types/mocha": { - "version": "5.2.7", - "dev": true, - "license": "MIT" - }, "developer/src/kmc/node_modules/ansi-styles": { "version": "3.2.1", "license": "MIT", @@ -1162,9 +1096,7 @@ "@keymanapp/keyman-version": "*", "@keymanapp/resources-gosh": "*", "@types/express": "^4.17.13", - "@types/mocha": "^9.1.0", "@types/multer": "^1.4.12", - "@types/node": "^20.4.1", "@types/ws": "^8.2.2", "copyfiles": "^2.4.1", "node-gyp": "^10.2.0", @@ -1175,11 +1107,6 @@ "node-hide-console-window": "^2.2.0" } }, - "developer/src/server/node_modules/@types/mocha": { - "version": "9.1.1", - "dev": true, - "license": "MIT" - }, "developer/src/server/node_modules/@types/multer": { "version": "1.4.12", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", @@ -3480,7 +3407,9 @@ "license": "MIT" }, "node_modules/@types/mocha": { - "version": "7.0.2", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, "license": "MIT" }, @@ -4679,6 +4608,15 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -5842,6 +5780,15 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9846,6 +9793,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/make-fetch-happen": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", @@ -12939,6 +12895,47 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsc-watch": { "version": "4.6.2", "dev": true, @@ -13779,6 +13776,18 @@ "node": ">= 4.0.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -14097,7 +14106,6 @@ "devDependencies": { "@keymanapp/common-types": "*", "@keymanapp/web-utils": "*", - "@types/mocha": "^7.0.2", "c8": "^7.12.0", "typescript": "^5.4.5" } @@ -14115,7 +14123,6 @@ "devDependencies": { "@keymanapp/common-types": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^7.0.2", "c8": "^7.12.0", "typescript": "^5.4.5" } diff --git a/package.json b/package.json index 7f033959d8..8bad452796 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@microsoft/api-documenter": "^7.28.0", "@microsoft/api-extractor": "^7.47.3", "@types/chai": "^4.3.14", + "@types/mocha": "^10.0.10", "@types/node": "^20.4.1", "@typescript-eslint/eslint-plugin": "^7.13.1", "@web/dev-server-esbuild": "^1.0.2", diff --git a/web/src/engine/predictive-text/templates/package.json b/web/src/engine/predictive-text/templates/package.json index cbe3fb8747..fc1deedbfb 100644 --- a/web/src/engine/predictive-text/templates/package.json +++ b/web/src/engine/predictive-text/templates/package.json @@ -53,7 +53,6 @@ "devDependencies": { "@keymanapp/common-types": "*", "@keymanapp/web-utils": "*", - "@types/mocha": "^7.0.2", "c8": "^7.12.0", "typescript": "^5.4.5" }, diff --git a/web/src/engine/predictive-text/wordbreakers/package.json b/web/src/engine/predictive-text/wordbreakers/package.json index 0f73f19f69..5ebe40654b 100644 --- a/web/src/engine/predictive-text/wordbreakers/package.json +++ b/web/src/engine/predictive-text/wordbreakers/package.json @@ -56,7 +56,6 @@ "devDependencies": { "@keymanapp/common-types": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^7.0.2", "c8": "^7.12.0", "typescript": "^5.4.5" }, diff --git a/web/src/test/auto/headless/engine/main/headless/languageProcessor.tests.js b/web/src/test/auto/headless/engine/main/headless/languageProcessor.tests.js index 6d866ef8ed..e5b8856016 100644 --- a/web/src/test/auto/headless/engine/main/headless/languageProcessor.tests.js +++ b/web/src/test/auto/headless/engine/main/headless/languageProcessor.tests.js @@ -22,11 +22,10 @@ global.keyman = {}; // So that keyboard-based checks against the global `keyman` // Test the KeyboardProcessor interface. describe('LanguageProcessor', function() { let languageProcessor; - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); beforeEach(function() { languageProcessor = new LanguageProcessor(LMWorker, new TranscriptionCache()); - callbacks.clear(); }); afterEach(function() { From b0bfc6be6f6b624304d3708fce0986e929e4f36e Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 3 Mar 2026 12:02:02 -0600 Subject: [PATCH 25/57] auto: increment master version to 19.0.209 Test-bot: skip Build-bot: skip --- HISTORY.md | 7 +++++++ VERSION.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 60b5e193cf..cd5a060aa0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Keyman Version History +## 19.0.208 alpha 2026-03-03 + +* docs(core): add keyhandling doc (#15629) +* fix(developer): handle CRLF as CR internally in LDML debugger (#15616) +* fix(developer): handle .kps file that has no `` element (#15622) +* fix(developer): use correct return values and nullish checks in package-validation (#15653) + ## 19.0.207 alpha 2026-03-02 * chore(ios): Cleanup `RELEASE_OEM_FIRSTVOICES` var in build.sh scripts (#15605) diff --git a/VERSION.md b/VERSION.md index b2383c811f..adbee0615c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.208 \ No newline at end of file +19.0.209 \ No newline at end of file From a0ecd51c600bf518bbb30843441087e4310d3c0e Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 4 Mar 2026 04:57:56 +0100 Subject: [PATCH 26/57] fix(developer): apply keyboard description in Windows keyboard import Fixes: #15630 Test-bot: skip --- .../Keyman.Developer.UI.ImportWindowsKeyboardDialogManager.pas | 1 + 1 file changed, 1 insertion(+) diff --git a/developer/src/tike/main/Keyman.Developer.UI.ImportWindowsKeyboardDialogManager.pas b/developer/src/tike/main/Keyman.Developer.UI.ImportWindowsKeyboardDialogManager.pas index d0e526c230..7713cf9b82 100644 --- a/developer/src/tike/main/Keyman.Developer.UI.ImportWindowsKeyboardDialogManager.pas +++ b/developer/src/tike/main/Keyman.Developer.UI.ImportWindowsKeyboardDialogManager.pas @@ -70,6 +70,7 @@ begin iwk.Version := f.Version; iwk.BCP47Tags := f.BCP47Tags; iwk.Author := f.Author; + iwk.Description := f.Description; iwk.Targets := f.Targets; if not iwk.Execute then From 7a8fa1bf7f0848126b563e8606827f547a35158e Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 4 Mar 2026 05:45:21 +0100 Subject: [PATCH 27/57] fix(developer): prevent new projects from overwriting existing projects The new project UI process detects if the target project folder already exists in a given path, and asks if the author wants to overwrite it. However, this has been inconsistently implemented -- in the clone keyboard case, the process would fail with a message "error KM0B004: Output path already exists, not overwriting". In other cases, no files would be removed, but existing files would be overwritten where there was a collision, resulting in a messy project folder. I have opted to prevent this situation in a consistent manner, requiring the author to remove the folder themselves in Windows Explorer, even though this is higher friction, because it pushes them into verifying that they actually want to delete the contents of the folder. I also took the opportunity to DRY out this verification process in the six different New Project dialogs. Fixes: #15063 --- .../Keyman.Developer.UI.Project.ProjectUI.pas | 24 +++++++++++++++++++ ...oject.UfrmCloneGitHubProjectParameters.pas | 20 ++-------------- ....UfrmCloneKeymanCloudProjectParameters.pas | 21 ++-------------- ...roject.UfrmCloneLocalProjectParameters.pas | 20 ++-------------- ...t.UfrmNewLDMLKeyboardProjectParameters.pas | 21 ++-------------- ....Project.UfrmNewModelProjectParameters.pas | 21 ++-------------- ...er.UI.Project.UfrmNewProjectParameters.pas | 23 +++--------------- 7 files changed, 37 insertions(+), 113 deletions(-) diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas index 4bd05c588c..d1eb8c20bb 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas @@ -29,12 +29,16 @@ function LoadGlobalProjectUI(pt: TProjectType; AFilename: string): TProjectUI; procedure FreeGlobalProjectUI; function IsGlobalProjectUIReady: Boolean; function CreateTempGlobalProjectUI(pt: TProjectType): TProjectUI; +function VerifyNewProjectPathWithUser(const BasePath, KeyboardID: string): Boolean; implementation uses System.Hash, System.SysUtils, + System.UITypes, + Vcl.Controls, + Vcl.Dialogs, Winapi.Windows, Keyman.Developer.System.KeymanDeveloperPaths, @@ -153,4 +157,24 @@ begin hLockFile := INVALID_HANDLE_VALUE; end; +function VerifyNewProjectPathWithUser(const BasePath, KeyboardID: string): Boolean; +var + ProjectFolder: string; +begin + if not DirectoryExists(BasePath) then + begin + if MessageDlg('The target folder '+BasePath+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then + Exit(False); + end; + + ProjectFolder := IncludeTrailingPathDelimiter(BasePath) + KeyboardID; + if DirectoryExists(ProjectFolder) then + begin + MessageDlg('The project folder '+ProjectFolder+' already exists. Please choose a different folder or delete it in File Explorer.', mtError, [mbOk], 0); + Exit(False); + end; + + Result := True; +end; + end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas index ad042fc872..3ccf96289f 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas @@ -97,6 +97,7 @@ uses Keyman.Developer.System.HelpTopics, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.UI.Project.ProjectUI, Keyman.System.KeyboardUtils, Keyman.System.LexicalModelUtils, UfrmMain; @@ -331,29 +332,12 @@ begin end; function TfrmCloneGitHubProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin if IsSourceProjectALexicalModelProject then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editProjectID.Text); end; procedure TfrmCloneGitHubProjectParameters.UpdateProjectFilename; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas index 557fd24ec2..b0e491f0c6 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas @@ -94,6 +94,7 @@ uses Keyman.Developer.System.HelpTopics, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.UI.Project.ProjectUI, Keyman.System.KeyboardUtils, UfrmMain, Upload_Settings; @@ -283,27 +284,9 @@ begin end; function TfrmCloneKeymanCloudProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editKeyboardID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); end; procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string); diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas index cfc24801b3..d986cf56ac 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas @@ -89,6 +89,7 @@ uses Keyman.Developer.System.HelpTopics, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.UI.Project.ProjectUI, Keyman.System.KeyboardUtils, Keyman.System.LexicalModelUtils, UfrmMain; @@ -248,29 +249,12 @@ begin end; function TfrmCloneLocalProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin if IsSourceProjectALexicalModelProject then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editProjectID.Text); end; procedure TfrmCloneLocalProjectParameters.UpdateProjectFilename; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas index 4ffede1c08..61ad52e4f2 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas @@ -127,6 +127,7 @@ uses Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.System.LDMLKeyboardProjectTemplate, Keyman.Developer.System.ProjectTemplate, + Keyman.Developer.UI.Project.ProjectUI, Keyman.System.KeyboardUtils, UfrmMain; @@ -350,8 +351,6 @@ begin end; function TfrmNewLDMLKeyboardProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin if not FormValidation.Update then begin @@ -360,23 +359,7 @@ begin end; Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editKeyboardID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); end; { Languages Grid } diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas index 0c238c16dc..36b06f72d4 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas @@ -157,6 +157,7 @@ uses Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.System.ModelProjectTemplate, Keyman.Developer.System.ProjectTemplate, + Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmSelectBCP47Language, UfrmMain; @@ -472,8 +473,6 @@ begin end; function TfrmNewModelProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin if not FormValidation.Update then begin @@ -482,23 +481,7 @@ begin end; Result := TLexicalModelUtils.DoesProjectFilenameFollowLexicalModelConventions(editModelID.Text + Ext_LexicalModelProject); - - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editModelID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editModelID.Text); end; { Dynamically update other fields based on filled details } diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas index c5323a7894..749638650b 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas @@ -146,6 +146,7 @@ uses Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.System.KeyboardProjectTemplate, Keyman.Developer.System.ProjectTemplate, + Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmSelectBCP47Language, Keyman.System.KeyboardUtils, UfrmMain; @@ -482,8 +483,6 @@ begin end; function TfrmNewProjectParameters.Validate: Boolean; -var - ProjectFolder: string; begin if not FormValidation.Update then begin @@ -492,24 +491,8 @@ begin end; Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - - if Result then - begin - if not DirectoryExists(editPath.Text) then - begin - if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then - Exit(False); - end; - - ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editKeyboardID.Text; - if DirectoryExists(ProjectFolder) then - begin - if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, - mbOkCancel, 0) = mrCancel then - Exit(False); - end; - end; -end; + Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); +end; { Languages Grid } From 26cf278cc1688adfd5a1ef83cc82e16b82bc76bb Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 4 Mar 2026 06:27:36 +0100 Subject: [PATCH 28/57] fix(developer): improve UI feedback on upgrade Show a progress dialog when running the Keyman Developer upgrade (`-au` parameter). Fixes: #13958 Test-bot: skip --- developer/src/setup/UfrmRun.pas | 38 ++++++++++++--------------- developer/src/setup/bootstrapmain.pas | 32 +++++++++++----------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/developer/src/setup/UfrmRun.pas b/developer/src/setup/UfrmRun.pas index 291dd7b374..08aef3dc4e 100644 --- a/developer/src/setup/UfrmRun.pas +++ b/developer/src/setup/UfrmRun.pas @@ -55,11 +55,13 @@ type InstallSize: Integer; end; + TInstallUILevel = (uiSilent, uiProgressOnly, uiFull); + TfrmRun = class(TSetupForm) public procedure cmdExitClick(Sender: TObject); procedure lblTitleClick(Sender: TObject); - procedure cmdInstallKeymanDesktopClick(Sender: TObject); + procedure cmdInstallKeymanDeveloperClick(Sender: TObject); procedure FormCreate(Sender: TObject); override; procedure FormShow(Sender: TObject); override; @@ -71,17 +73,13 @@ type FDownloadFilename: WideString; FDownloadURL: WideString; FNewVersion, FInstalledVersion, FInstallerVersion: TMSIInfo; - //FInstalledVersion: WideString; FContinueSetup: Boolean; FWhiteBrush: THandle; FErrorLog: TStream; FPromptForReboot: Boolean; // I3355 // I3500 - FSilent: Boolean; + FUILevel: TInstallUILevel; FOfflineByRequest: Boolean; FOnline: Boolean; - //FInstalledProductCode: WideString; - //FInstallerProductCode: WideString; - //FNewProductCode: WideString; procedure LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean = True); @@ -107,7 +105,7 @@ type function DialogID: Integer; override; constructor Create(AOwner: TComponent); override; destructor Destroy; override; - function DoInstall(Silent, PromptForReboot: Boolean): Boolean; // I3355 // I3500 + function DoInstall(UILevel: TInstallUILevel; PromptForReboot: Boolean): Boolean; // I3355 // I3500 property ContinueSetup: Boolean read FContinueSetup write FContinueSetup; property Offline: Boolean read FOfflineByRequest write SetOfflineByRequest; end; @@ -225,7 +223,7 @@ end; function TfrmRun.InstallNewVersion: Boolean; begin Result := False; - if not FSilent and (MessageDlgW(FInstallInfo.Text(ssQueryUpdateVersion, [FNewVersion.InstallSize div 1024, FNewVersion.Version]), // I2680 + if (FUILevel = uiFull) and (MessageDlgW(FInstallInfo.Text(ssQueryUpdateVersion, [FNewVersion.InstallSize div 1024, FNewVersion.Version]), // I2680 mtConfirmation, mbOkCancel, 0) = mrCancel) then Exit; @@ -320,12 +318,12 @@ end; procedure TfrmRun.WMUserFormShown(var Message: TMessage); begin if FContinueSetup then - DoInstall(False, True); // I3355 // I3500 + DoInstall(uiFull, True); // I3355 // I3500 end; -procedure TfrmRun.cmdInstallKeymanDesktopClick(Sender: TObject); +procedure TfrmRun.cmdInstallKeymanDeveloperClick(Sender: TObject); begin - if DoInstall(False, True) then // I1901 and I2680.4 // I3355 // I3500 + if DoInstall(uiFull, True) then // I1901 and I2680.4 // I3355 // I3500 ModalResult := mrOk; end; @@ -392,11 +390,10 @@ begin end; end; -function TfrmRun.DoInstall(Silent, PromptForReboot: Boolean): Boolean; // I1901 // I3355 // I3500 +function TfrmRun.DoInstall(UILevel: TInstallUILevel; PromptForReboot: Boolean): Boolean; // I1901 // I3355 // I3500 begin FPromptForReboot := PromptForReboot; // I3355 // I3500 - FSilent := Silent; - + FUILevel := UILevel; SetCursor(LoadCursor(0, IDC_WAIT)); try @@ -671,8 +668,10 @@ begin FCacheFileName := CacheMSIFile(ExtPath + FInstallInfo.MSIFileName); // I3310 - if FSilent then + if FUILevel = uiSilent then MsiSetInternalUI(INSTALLUILEVEL_NONE, nil) + else if FUILevel = uiProgressOnly then + MsiSetInternalUI(INSTALLUILEVEL_BASIC, nil) else MsiSetInternalUI(INSTALLUILEVEL_FULL, nil); res := MsiInstallProductW(PWideChar(FCacheFileName), PWideChar('DISABLEFINALOPTIONS=1 '+ReinstallMode)); @@ -694,16 +693,13 @@ begin end; end; - if not FSilent then + if FUILevel = uiFull then MsiSetInternalUI(INSTALLUILEVEL_FULL, nil); if FNewVersion.Filename <> '' then begin //Run the setup app silently // I2680.5 TUtilExecute.WaitForProcess('"'+ExtPath + FNewVersion.FileName+'" -s -o', ExtPath, SW_SHOW, WaitFor); // I3349 - //if not FSilent then - // MsiSetInternalUI(INSTALLUILEVEL_BASIC, nil); - //CheckMSIResult(MsiApplyPatchW(PWideChar(ExtPath + FNewVersion.Filename), nil, INSTALLTYPE_DEFAULT, '')); end; end; @@ -712,7 +708,7 @@ function TfrmRun.ProcessCommand(ID, NotificationCode: Integer; begin Result := True; case ID of - IDOK: cmdInstallKeymanDesktopClick(Self); + IDOK: cmdInstallKeymanDeveloperClick(Self); IDCANCEL: cmdExitClick(Self); else Result := False; end; @@ -727,7 +723,7 @@ procedure TfrmRun.LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean const nl: WideString = #13#10; begin - if not FSilent and ShowDialogIfNotSilent then + if (FUILevel = uiFull) and ShowDialogIfNotSilent then ShowMessageW(msg); if not Assigned(FErrorLog) then begin diff --git a/developer/src/setup/bootstrapmain.pas b/developer/src/setup/bootstrapmain.pas index 469a6a64e1..1a8380f368 100644 --- a/developer/src/setup/bootstrapmain.pas +++ b/developer/src/setup/bootstrapmain.pas @@ -1,18 +1,18 @@ (* Name: bootstrapmain Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 4 Jun 2007 Modified Date: 12 Aug 2014 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 04 Jun 2007 - mcdurdin - Initial version 05 Jun 2007 - mcdurdin - I817 - Fix -x option 19 Jun 2007 - mcdurdin - I817 - Translate to Unicode and remove Forms dependence @@ -123,12 +123,12 @@ begin DeletePath(TempPath); // I3310 end; -procedure ProcessCommandLine(var FPromptForReboot, FSilent, FOffline, FExtractOnly, FContinueSetup: Boolean; var FExtractPath: WideString); // I3355 // I3500 +procedure ProcessCommandLine(var FUILevel: TInstallUILevel; var FPromptForReboot, FOffline, FExtractOnly, FContinueSetup: Boolean; var FExtractPath: WideString); // I3355 // I3500 var i: Integer; begin FPromptForReboot := True; // I3355 // I3500 - FSilent := False; + FUILevel := uiFull; FOffline := False; FExtractOnly := False; FContinueSetup := False; @@ -139,13 +139,13 @@ begin FContinueSetup := True else if WideSameText(ParamStr(i), '-au') then // I2849 begin - FSilent := True; + FUILevel := uiProgressOnly; FOffline := True; end else if WideSameText(ParamStr(i), '-s') then begin FPromptForReboot := False; // I3355 // I3500 - FSilent := True; + FUILevel := uiSilent; end else if WideSameText(ParamStr(i), '-o') then FOffline := True @@ -169,7 +169,7 @@ var FExtractOnly: Boolean; FContinueSetup: Boolean; FPromptForReboot: Boolean; // I3355 // I3500 - FSilent: Boolean; + FUILevel: TInstallUILevel; FOffline: Boolean; FExtractOnly_Path: WideString; BEGIN @@ -193,11 +193,11 @@ BEGIN { Display the dialog } //Cancel_From_InfoWin := False; - ProcessCommandLine(FPromptForReboot, FSilent, FOffline, FExtractOnly, FContinueSetup, FExtractOnly_Path); // I3355 // I3500 + ProcessCommandLine(FUILevel, FPromptForReboot, FOffline, FExtractOnly, FContinueSetup, FExtractOnly_Path); // I3355 // I3500 if FExtractOnly then begin - DoExtractOnly(FSilent, FExtractOnly_Path); + DoExtractOnly(FUILevel = uiSilent, FExtractOnly_Path); Exit; end; @@ -224,8 +224,8 @@ BEGIN try ContinueSetup := FContinueSetup; Offline := FOffline; - if FSilent - then DoInstall(True, FPromptForReboot) // I3355 // I3500 + if FUILevel <> uiFull + then DoInstall(FUILevel, FPromptForReboot) // I3355 // I3500 else ShowModal; finally Free; From 1db954a7fa28ebfcf60d575262234d9ea2ef99fd Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 4 Mar 2026 14:09:57 +0700 Subject: [PATCH 29/57] chore(android): Cleanup unused extracted text --- .../src/main/java/com/keyman/android/SystemKeyboard.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 97f8ab7d38..a8d71a450f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -43,7 +43,6 @@ import io.sentry.Sentry; public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener { private static View inputView = null; - private static ExtractedText exText = null; private KMHardwareKeyboardInterpreter interpreter = null; private int inputType = InputType.TYPE_NULL; private int lastOrientation = Configuration.ORIENTATION_UNDEFINED; @@ -206,8 +205,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven if (icText != null && icText.text != null) { boolean didUpdateText = KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString()); boolean didUpdateSelection = KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM); - if (!didUpdateText || !didUpdateSelection) - exText = icText; } } @@ -278,10 +275,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onKeyboardLoaded(KeyboardType keyboardType) { - if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { - if (exText != null) - exText = null; - } + // Do nothing } @Override From 59f02a6a55fd47d15066f6049f281f306b974e72 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 4 Mar 2026 11:19:12 +0100 Subject: [PATCH 30/57] fix(developer): retrieve debug memo anchor without side effects Use RichEdit's `ITextDocument` interface to retrieve the selection anchor/caret information, and avoid the side-effects that can arise with the hacky `EM_GETSEL` / `EM_SETSEL` pattern we used previously. Changing the selection with `EM_SETSEL` in order to find the anchor point causes notification messages to be generated that can arrive at an unexpected time in some text selection scenarios, which ended up with us having a saved selection in the debugger pointing to the wrong text range. The name `Anchor` was a misnomer, because it was actually returning the caret position, not the anchor for the selection, which is the far side of the selection from the anchor! Fixes: #11706 Fixes: KEYMAN-DEVELOPER-18A --- .../KeymanDeveloperDebuggerMemo.pas | 55 +- .../components/developer_components.dpk | 3 +- .../components/developer_components.dproj | 1 + .../src/common/delphi/components/tom_TLB.pas | 942 ++++++++++++++++++ ...veloper.UI.Debug.UfrmLdmlKeyboardDebug.pas | 26 +- developer/src/tike/child/UfrmDebug.pas | 43 +- developer/src/tike/tike.dpr | 33 +- developer/src/tike/tike.dproj | 1 + 8 files changed, 1041 insertions(+), 63 deletions(-) create mode 100644 developer/src/common/delphi/components/tom_TLB.pas diff --git a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas index 54e2c87ee8..dd46b57d7a 100644 --- a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas +++ b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas @@ -21,6 +21,7 @@ interface uses System.Classes, + Winapi.ActiveX, Winapi.Messages, Winapi.RichEdit, Winapi.Windows, @@ -35,7 +36,7 @@ type TMemoSelection = record Start, Finish: Integer; - Anchor: Integer; + Caret: Integer; // In a non-empty selection, Anchor is at the other end of the selection function ToString: string; end; @@ -43,7 +44,6 @@ type private FOnMessage: TKeymanDeveloperDebuggerMessageEvent; FAllowUnicodeInput: Boolean; - FSelectionChanging: Boolean; FIsDebugging: Boolean; procedure SetAllowUnicode(const Value: Boolean); function GetSelection: TMemoSelection; @@ -60,7 +60,6 @@ type property AllowUnicode: Boolean read FAllowUnicodeInput write SetAllowUnicode default True; property OnMessage: TKeymanDeveloperDebuggerMessageEvent read FOnMessage write FOnMessage; property Selection: TMemoSelection read GetSelection write SetSelection; - property SelectionChanging: Boolean read FSelectionChanging; property IsDebugging: Boolean read FIsDebugging write FIsDebugging; end; @@ -69,6 +68,7 @@ procedure Register; implementation uses + tom_TLB, System.SysUtils; { TKeymanDeveloperDebuggerMemo } @@ -107,26 +107,29 @@ begin end; function TKeymanDeveloperDebuggerMemo.GetSelection: TMemoSelection; +var + Unk: IUnknown; + TextDoc: ITextDocument; + TextSel: ITextSelection; begin - Result.Anchor := 0; - - // EM_GETSEL doesn't tell us the anchor position, but we can figure - // it out with this kludge. I am not aware of side effects from this - // at this time. SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Start), NativeUInt(@Result.Finish)); - if Result.Start <> Result.Finish then + Result.Caret := Result.Finish; + + if Result.Finish <> Result.Start then begin - // We only need to play the selection test game if there is a non-zero - // selection length - FSelectionChanging := True; - Lines.BeginUpdate; - try - SendMessage(Handle, EM_SETSEL, -1, 0); - SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Anchor), 0); - SetSelection(Result); - finally - Lines.EndUpdate; - FSelectionChanging := False; + if SendMessage(Handle, EM_GETOLEINTERFACE, 0, LPARAM(@Unk)) <> 0 then + begin + if Unk.QueryInterface(IID_ITextDocument, TextDoc) = S_OK then + begin + TextSel := TextDoc.Selection; + if TextSel <> nil then + begin + if (TextSel.Flags and tomSelStartActive) <> 0 then + begin + Result.Caret := Result.Start; + end; + end; + end; end; end; end; @@ -153,7 +156,7 @@ end; procedure TKeymanDeveloperDebuggerMemo.SetSelection( const Value: TMemoSelection); begin - if Value.Anchor = Value.Start + if Value.Caret = Value.Start then SendMessage(Handle, EM_SETSEL, Value.Finish, Value.Start) else SendMessage(Handle, EM_SETSEL, Value.Start, Value.Finish); end; @@ -174,10 +177,10 @@ begin end; { TMemoSelection } - + function TMemoSelection.ToString: string; -begin - Result := Format('%d %d %d', [Start, Finish, Anchor]); -end; - +begin + Result := Format('TMemoSelection(start=%d finish=%d caret=%d)', [Start, Finish, Caret]); +end; + end. diff --git a/developer/src/common/delphi/components/developer_components.dpk b/developer/src/common/delphi/components/developer_components.dpk index 4a0b7f6386..3f27243187 100644 --- a/developer/src/common/delphi/components/developer_components.dpk +++ b/developer/src/common/delphi/components/developer_components.dpk @@ -44,6 +44,7 @@ contains TextFileFormat in '..\general\TextFileFormat.pas', LeftTabbedPageControl in 'LeftTabbedPageControl.pas', MenuImgList in 'MenuImgList.pas', - StringGridEditControlled in 'StringGridEditControlled.pas'; + StringGridEditControlled in 'StringGridEditControlled.pas', + tom_TLB in 'tom_TLB.pas'; end. diff --git a/developer/src/common/delphi/components/developer_components.dproj b/developer/src/common/delphi/components/developer_components.dproj index dabd46bf8a..38aa37350e 100644 --- a/developer/src/common/delphi/components/developer_components.dproj +++ b/developer/src/common/delphi/components/developer_components.dproj @@ -139,6 +139,7 @@ + Cfg_2 Base diff --git a/developer/src/common/delphi/components/tom_TLB.pas b/developer/src/common/delphi/components/tom_TLB.pas new file mode 100644 index 0000000000..7055de4009 --- /dev/null +++ b/developer/src/common/delphi/components/tom_TLB.pas @@ -0,0 +1,942 @@ +unit tom_TLB; + +// ************************************************************************ // +// WARNING +// ------- +// The types declared in this file were generated from data read from a +// Type Library. If this type library is explicitly or indirectly (via +// another type library referring to this type library) re-imported, or the +// 'Refresh' command of the Type Library Editor activated while editing the +// Type Library, the contents of this file will be regenerated and all +// manual modifications will be lost. +// ************************************************************************ // + +// $Rev: 52393 $ +// File generated on 4/03/2026 10:48:24 AM from Type Library described below. + +// ************************************************************************ // +// Type Lib: C:\Windows\SysWOW64\RICHED20.dll (1) +// LIBID: {8CC497C9-A1DF-11CE-8098-00AA0047BE5D} +// LCID: 0 +// Helpfile: +// HelpString: +// DepndLst: +// (1) v2.0 stdole, (C:\Windows\SysWOW64\stdole2.tlb) +// SYS_KIND: SYS_WIN32 +// Errors: +// Hint: Symbol 'Type' renamed to 'type_' +// Hint: Parameter 'Unit' of ITextSelection.MoveLeft changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextSelection.MoveRight changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextSelection.MoveUp changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextSelection.MoveDown changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextSelection.HomeKey changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextSelection.EndKey changed to 'Unit_' +// Hint: Member 'End' of 'ITextRange' changed to 'End_' +// Hint: Parameter 'Unit' of ITextRange.Expand changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.GetIndex changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.SetIndex changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.StartOf changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.EndOf changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.Move changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.MoveStart changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.MoveEnd changed to 'Unit_' +// Hint: Parameter 'Unit' of ITextRange.Delete changed to 'Unit_' +// Hint: Parameter 'Type' of ITextRange.ChangeCase changed to 'Type_' +// Hint: Parameter 'Type' of ITextRange.GetPoint changed to 'Type_' +// Hint: Parameter 'Type' of ITextRange.SetPoint changed to 'Type_' +// Hint: Member 'Protected' of 'ITextFont' changed to 'Protected_' +// Hint: Parameter 'Type' of ITextDocument2.GetClientRect changed to 'Type_' +// ************************************************************************ // +{$TYPEDADDRESS OFF} // Unit must be compiled without type-checked pointers. +{$WARN SYMBOL_PLATFORM OFF} +{$WRITEABLECONST ON} +{$VARPROPSETTER ON} +{$ALIGN 4} + +interface + +uses Winapi.Windows, System.Classes, System.Variants, System.Win.StdVCL, Vcl.Graphics, Winapi.ActiveX; + + +// *********************************************************************// +// GUIDS declared in the TypeLibrary. Following prefixes are used: +// Type Libraries : LIBID_xxxx +// CoClasses : CLASS_xxxx +// DISPInterfaces : DIID_xxxx +// Non-DISP interfaces: IID_xxxx +// *********************************************************************// +const + // TypeLibrary Major and minor versions + tomMajorVersion = 1; + tomMinorVersion = 0; + + LIBID_tom: TGUID = '{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}'; + + IID_ITextDocument: TGUID = '{8CC497C0-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextRange: TGUID = '{8CC497C2-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextSelection: TGUID = '{8CC497C1-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextFont: TGUID = '{8CC497C3-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextPara: TGUID = '{8CC497C4-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextStoryRanges: TGUID = '{8CC497C5-A1DF-11CE-8098-00AA0047BE5D}'; + IID_ITextDocument2: TGUID = '{01C25500-4268-11D1-883A-3C8B00C10000}'; + IID_ITextMsgFilter: TGUID = '{A3787420-4267-11D1-883A-3C8B00C10000}'; + +// *********************************************************************// +// Declaration of Enumerations defined in Type Library +// *********************************************************************// +// Constants for enum __MIDL___MIDL_itf_tom_0000_0000_0001 +type + __MIDL___MIDL_itf_tom_0000_0000_0001 = TOleEnum; +const + tomFalse = $00000000; + tomTrue = $FFFFFFFF; + tomUndefined = $FF676981; + tomToggle = $FF676982; + tomAutoColor = $FF676983; + tomDefault = $FF676984; + tomSuspend = $FF676985; + tomResume = $FF676986; + tomApplyNow = $00000000; + tomApplyLater = $00000001; + tomTrackParms = $00000002; + tomCacheParms = $00000003; + tomBackward = $C0000001; + tomForward = $3FFFFFFF; + tomMove = $00000000; + tomExtend = $00000001; + tomNoSelection = $00000000; + tomSelectionIP = $00000001; + tomSelectionNormal = $00000002; + tomSelectionFrame = $00000003; + tomSelectionColumn = $00000004; + tomSelectionRow = $00000005; + tomSelectionBlock = $00000006; + tomSelectionInlineShape = $00000007; + tomSelectionShape = $00000008; + tomSelStartActive = $00000001; + tomSelAtEOL = $00000002; + tomSelOvertype = $00000004; + tomSelActive = $00000008; + tomSelReplace = $00000010; + tomEnd = $00000000; + tomStart = $00000020; + tomCollapseEnd = $00000000; + tomCollapseStart = $00000001; + tomClientCoord = $00000100; + tomNone = $00000000; + tomSingle = $00000001; + tomWords = $00000002; + tomDouble = $00000003; + tomDotted = $00000004; + tomDash = $00000005; + tomDashDot = $00000006; + tomDashDotDot = $00000007; + tomWave = $00000008; + tomThick = $00000009; + tomHair = $0000000A; + tomLineSpaceSingle = $00000000; + tomLineSpace1pt5 = $00000001; + tomLineSpaceDouble = $00000002; + tomLineSpaceAtLeast = $00000003; + tomLineSpaceExactly = $00000004; + tomLineSpaceMultiple = $00000005; + tomAlignLeft = $00000000; + tomAlignCenter = $00000001; + tomAlignRight = $00000002; + tomAlignJustify = $00000003; + tomAlignDecimal = $00000003; + tomAlignBar = $00000004; + tomAlignInterWord = $00000003; + tomAlignInterLetter = $00000004; + tomAlignScaled = $00000005; + tomAlignGlyphs = $00000006; + tomAlignSnapGrid = $00000007; + tomSpaces = $00000000; + tomDots = $00000001; + tomDashes = $00000002; + tomLines = $00000003; + tomThickLines = $00000004; + tomEquals = $00000005; + tomTabBack = $FFFFFFFD; + tomTabNext = $FFFFFFFE; + tomTabHere = $FFFFFFFF; + tomListNone = $00000000; + tomListBullet = $00000001; + tomListNumberAsArabic = $00000002; + tomListNumberAsLCLetter = $00000003; + tomListNumberAsUCLetter = $00000004; + tomListNumberAsLCRoman = $00000005; + tomListNumberAsUCRoman = $00000006; + tomListNumberAsSequence = $00000007; + tomListParentheses = $00010000; + tomListPeriod = $00020000; + tomListPlain = $00030000; + tomCharacter = $00000001; + tomWord = $00000002; + tomSentence = $00000003; + tomParagraph = $00000004; + tomLine = $00000005; + tomStory = $00000006; + tomScreen = $00000007; + tomSection = $00000008; + tomColumn = $00000009; + tomRow = $0000000A; + tomWindow = $0000000B; + tomCell = $0000000C; + tomCharFormat = $0000000D; + tomParaFormat = $0000000E; + tomTable = $0000000F; + tomObject = $00000010; + tomMatchWord = $00000002; + tomMatchCase = $00000004; + tomMatchPattern = $00000008; + tomUnknownStory = $00000000; + tomMainTextStory = $00000001; + tomFootnotesStory = $00000002; + tomEndnotesStory = $00000003; + tomCommentsStory = $00000004; + tomTextFrameStory = $00000005; + tomEvenPagesHeaderStory = $00000006; + tomPrimaryHeaderStory = $00000007; + tomEvenPagesFooterStory = $00000008; + tomPrimaryFooterStory = $00000009; + tomFirstPageHeaderStory = $0000000A; + tomFirstPageFooterStory = $0000000B; + tomNoAnimation = $00000000; + tomLasVegasLights = $00000001; + tomBlinkingBackground = $00000002; + tomSparkleText = $00000003; + tomMarchingBlackAnts = $00000004; + tomMarchingRedAnts = $00000005; + tomShimmer = $00000006; + tomWipeDown = $00000007; + tomWipeRight = $00000008; + tomAnimationMax = $00000008; + tomLowerCase = $00000000; + tomUpperCase = $00000001; + tomTitleCase = $00000002; + tomSentenceCase = $00000004; + tomToggleCase = $00000005; + tomReadOnly = $00000100; + tomShareDenyRead = $00000200; + tomShareDenyWrite = $00000400; + tomPasteFile = $00001000; + tomCreateNew = $00000010; + tomCreateAlways = $00000020; + tomOpenExisting = $00000030; + tomOpenAlways = $00000040; + tomTruncateExisting = $00000050; + tomRTF = $00000001; + tomText = $00000002; + tomHTML = $00000003; + tomWordDocument = $00000004; + tomBold = $80000001; + tomItalic = $80000002; + tomUnderline = $80000004; + tomStrikeout = $80000008; + tomProtected = $80000010; + tomLink = $80000020; + tomSmallCaps = $80000040; + tomAllCaps = $80000080; + tomHidden = $80000100; + tomOutline = $80000200; + tomShadow = $80000400; + tomEmboss = $80000800; + tomImprint = $80001000; + tomDisabled = $80002000; + tomRevised = $80004000; + tomNormalCaret = $00000000; + tomKoreanBlockCaret = $00000001; + tomIncludeInset = $00000001; + tomIgnoreCurrentFont = $00000000; + tomMatchFontCharset = $00000001; + tomMatchFontSignature = $00000002; + tomCharset = $80000000; + tomRE10Mode = $00000001; + tomNoIME = $00080000; + tomSelfIME = $00040000; + +type + +// *********************************************************************// +// Forward declaration of types defined in TypeLibrary +// *********************************************************************// + ITextDocument = interface; + ITextDocumentDisp = dispinterface; + ITextRange = interface; + ITextRangeDisp = dispinterface; + ITextSelection = interface; + ITextSelectionDisp = dispinterface; + ITextFont = interface; + ITextFontDisp = dispinterface; + ITextPara = interface; + ITextParaDisp = dispinterface; + ITextStoryRanges = interface; + ITextStoryRangesDisp = dispinterface; + ITextDocument2 = interface; + ITextDocument2Disp = dispinterface; + ITextMsgFilter = interface; + +// *********************************************************************// +// Declaration of structures, unions and aliases. +// *********************************************************************// + wireHWND = ^_RemotableHandle; + POleVariant1 = ^OleVariant; {*} + PInteger1 = ^Integer; {*} + + tomConstants = __MIDL___MIDL_itf_tom_0000_0000_0001; + + __MIDL_IWinTypes_0009 = record + case Integer of + 0: (hInproc: Integer); + 1: (hRemote: Integer); + end; + + _RemotableHandle = record + fContext: Integer; + u: __MIDL_IWinTypes_0009; + end; + + UINT_PTR = LongWord; + LONG_PTR = Integer; + +// *********************************************************************// +// Interface: ITextDocument +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C0-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextDocument = interface(IDispatch) + ['{8CC497C0-A1DF-11CE-8098-00AA0047BE5D}'] + function Get_Name: WideString; safecall; + function Get_Selection: ITextSelection; safecall; + function Get_StoryCount: Integer; safecall; + function Get_StoryRanges: ITextStoryRanges; safecall; + function Get_Saved: Integer; safecall; + procedure Set_Saved(pValue: Integer); safecall; + function Get_DefaultTabStop: Single; safecall; + procedure Set_DefaultTabStop(pValue: Single); safecall; + procedure New; safecall; + procedure Open(const pVar: OleVariant; Flags: Integer; CodePage: Integer); safecall; + procedure Save(const pVar: OleVariant; Flags: Integer; CodePage: Integer); safecall; + function Freeze: Integer; safecall; + function Unfreeze: Integer; safecall; + procedure BeginEditCollection; safecall; + procedure EndEditCollection; safecall; + function Undo(Count: Integer): Integer; safecall; + function Redo(Count: Integer): Integer; safecall; + function Range(cp1: Integer; cp2: Integer): ITextRange; safecall; + function RangeFromPoint(x: Integer; y: Integer): ITextRange; safecall; + property Name: WideString read Get_Name; + property Selection: ITextSelection read Get_Selection; + property StoryCount: Integer read Get_StoryCount; + property StoryRanges: ITextStoryRanges read Get_StoryRanges; + property Saved: Integer read Get_Saved write Set_Saved; + property DefaultTabStop: Single read Get_DefaultTabStop write Set_DefaultTabStop; + end; + +// *********************************************************************// +// DispIntf: ITextDocumentDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C0-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextDocumentDisp = dispinterface + ['{8CC497C0-A1DF-11CE-8098-00AA0047BE5D}'] + property Name: WideString readonly dispid 0; + property Selection: ITextSelection readonly dispid 1; + property StoryCount: Integer readonly dispid 2; + property StoryRanges: ITextStoryRanges readonly dispid 3; + property Saved: Integer dispid 4; + property DefaultTabStop: Single dispid 5; + procedure New; dispid 6; + procedure Open(const pVar: OleVariant; Flags: Integer; CodePage: Integer); dispid 7; + procedure Save(const pVar: OleVariant; Flags: Integer; CodePage: Integer); dispid 8; + function Freeze: Integer; dispid 9; + function Unfreeze: Integer; dispid 10; + procedure BeginEditCollection; dispid 11; + procedure EndEditCollection; dispid 12; + function Undo(Count: Integer): Integer; dispid 13; + function Redo(Count: Integer): Integer; dispid 14; + function Range(cp1: Integer; cp2: Integer): ITextRange; dispid 15; + function RangeFromPoint(x: Integer; y: Integer): ITextRange; dispid 16; + end; + +// *********************************************************************// +// Interface: ITextRange +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C2-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextRange = interface(IDispatch) + ['{8CC497C2-A1DF-11CE-8098-00AA0047BE5D}'] + function Get_Text: WideString; safecall; + procedure Set_Text(const pbstr: WideString); safecall; + function Get_Char: Integer; safecall; + procedure Set_Char(pch: Integer); safecall; + function Get_Duplicate: ITextRange; safecall; + function Get_FormattedText: ITextRange; safecall; + procedure Set_FormattedText(const ppRange: ITextRange); safecall; + function Get_Start: Integer; safecall; + procedure Set_Start(pcpFirst: Integer); safecall; + function Get_End_: Integer; safecall; + procedure Set_End_(pcpLim: Integer); safecall; + function Get_Font: ITextFont; safecall; + procedure Set_Font(const pFont: ITextFont); safecall; + function Get_Para: ITextPara; safecall; + procedure Set_Para(const pPara: ITextPara); safecall; + function Get_StoryLength: Integer; safecall; + function Get_StoryType: Integer; safecall; + procedure Collapse(bStart: Integer); safecall; + function Expand(Unit_: Integer): Integer; safecall; + function GetIndex(Unit_: Integer): Integer; safecall; + procedure SetIndex(Unit_: Integer; Index: Integer; Extend: Integer); safecall; + procedure SetRange(cpActive: Integer; cpOther: Integer); safecall; + function InRange(const pRange: ITextRange): Integer; safecall; + function InStory(const pRange: ITextRange): Integer; safecall; + function IsEqual(const pRange: ITextRange): Integer; safecall; + procedure Select; safecall; + function StartOf(Unit_: Integer; Extend: Integer): Integer; safecall; + function EndOf(Unit_: Integer; Extend: Integer): Integer; safecall; + function Move(Unit_: Integer; Count: Integer): Integer; safecall; + function MoveStart(Unit_: Integer; Count: Integer): Integer; safecall; + function MoveEnd(Unit_: Integer; Count: Integer): Integer; safecall; + function MoveWhile(const Cset: OleVariant; Count: Integer): Integer; safecall; + function MoveStartWhile(const Cset: OleVariant; Count: Integer): Integer; safecall; + function MoveEndWhile(const Cset: OleVariant; Count: Integer): Integer; safecall; + function MoveUntil(const Cset: OleVariant; Count: Integer): Integer; safecall; + function MoveStartUntil(const Cset: OleVariant; Count: Integer): Integer; safecall; + function MoveEndUntil(const Cset: OleVariant; Count: Integer): Integer; safecall; + function FindText(const bstr: WideString; cch: Integer; Flags: Integer): Integer; safecall; + function FindTextStart(const bstr: WideString; cch: Integer; Flags: Integer): Integer; safecall; + function FindTextEnd(const bstr: WideString; cch: Integer; Flags: Integer): Integer; safecall; + function Delete(Unit_: Integer; Count: Integer): Integer; safecall; + procedure Cut(out pVar: OleVariant); safecall; + procedure Copy(out pVar: OleVariant); safecall; + procedure Paste(const pVar: OleVariant; Format: Integer); safecall; + function CanPaste(const pVar: OleVariant; Format: Integer): Integer; safecall; + function CanEdit: Integer; safecall; + procedure ChangeCase(Type_: Integer); safecall; + procedure GetPoint(Type_: Integer; out px: Integer; out py: Integer); safecall; + procedure SetPoint(x: Integer; y: Integer; Type_: Integer; Extend: Integer); safecall; + procedure ScrollIntoView(Value: Integer); safecall; + function GetEmbeddedObject: IUnknown; safecall; + property Text: WideString read Get_Text write Set_Text; + property Char: Integer read Get_Char write Set_Char; + property Duplicate: ITextRange read Get_Duplicate; + property FormattedText: ITextRange read Get_FormattedText write Set_FormattedText; + property Start: Integer read Get_Start write Set_Start; + property End_: Integer read Get_End_ write Set_End_; + property Font: ITextFont read Get_Font write Set_Font; + property Para: ITextPara read Get_Para write Set_Para; + property StoryLength: Integer read Get_StoryLength; + property StoryType: Integer read Get_StoryType; + end; + +// *********************************************************************// +// DispIntf: ITextRangeDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C2-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextRangeDisp = dispinterface + ['{8CC497C2-A1DF-11CE-8098-00AA0047BE5D}'] + property Text: WideString dispid 0; + property Char: Integer dispid 513; + property Duplicate: ITextRange readonly dispid 514; + property FormattedText: ITextRange dispid 515; + property Start: Integer dispid 516; + property End_: Integer dispid 517; + property Font: ITextFont dispid 518; + property Para: ITextPara dispid 519; + property StoryLength: Integer readonly dispid 520; + property StoryType: Integer readonly dispid 521; + procedure Collapse(bStart: Integer); dispid 528; + function Expand(Unit_: Integer): Integer; dispid 529; + function GetIndex(Unit_: Integer): Integer; dispid 530; + procedure SetIndex(Unit_: Integer; Index: Integer; Extend: Integer); dispid 531; + procedure SetRange(cpActive: Integer; cpOther: Integer); dispid 532; + function InRange(const pRange: ITextRange): Integer; dispid 533; + function InStory(const pRange: ITextRange): Integer; dispid 534; + function IsEqual(const pRange: ITextRange): Integer; dispid 535; + procedure Select; dispid 536; + function StartOf(Unit_: Integer; Extend: Integer): Integer; dispid 537; + function EndOf(Unit_: Integer; Extend: Integer): Integer; dispid 544; + function Move(Unit_: Integer; Count: Integer): Integer; dispid 545; + function MoveStart(Unit_: Integer; Count: Integer): Integer; dispid 546; + function MoveEnd(Unit_: Integer; Count: Integer): Integer; dispid 547; + function MoveWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 548; + function MoveStartWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 549; + function MoveEndWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 550; + function MoveUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 551; + function MoveStartUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 552; + function MoveEndUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 553; + function FindText(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 560; + function FindTextStart(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 561; + function FindTextEnd(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 562; + function Delete(Unit_: Integer; Count: Integer): Integer; dispid 563; + procedure Cut(out pVar: OleVariant); dispid 564; + procedure Copy(out pVar: OleVariant); dispid 565; + procedure Paste(const pVar: OleVariant; Format: Integer); dispid 566; + function CanPaste(const pVar: OleVariant; Format: Integer): Integer; dispid 567; + function CanEdit: Integer; dispid 568; + procedure ChangeCase(Type_: Integer); dispid 569; + procedure GetPoint(Type_: Integer; out px: Integer; out py: Integer); dispid 576; + procedure SetPoint(x: Integer; y: Integer; Type_: Integer; Extend: Integer); dispid 577; + procedure ScrollIntoView(Value: Integer); dispid 578; + function GetEmbeddedObject: IUnknown; dispid 579; + end; + +// *********************************************************************// +// Interface: ITextSelection +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C1-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextSelection = interface(ITextRange) + ['{8CC497C1-A1DF-11CE-8098-00AA0047BE5D}'] + function Get_Flags: Integer; safecall; + procedure Set_Flags(pFlags: Integer); safecall; + function Get_type_: Integer; safecall; + function MoveLeft(Unit_: Integer; Count: Integer; Extend: Integer): Integer; safecall; + function MoveRight(Unit_: Integer; Count: Integer; Extend: Integer): Integer; safecall; + function MoveUp(Unit_: Integer; Count: Integer; Extend: Integer): Integer; safecall; + function MoveDown(Unit_: Integer; Count: Integer; Extend: Integer): Integer; safecall; + function HomeKey(Unit_: Integer; Extend: Integer): Integer; safecall; + function EndKey(Unit_: Integer; Extend: Integer): Integer; safecall; + procedure TypeText(const bstr: WideString); safecall; + property Flags: Integer read Get_Flags write Set_Flags; + property type_: Integer read Get_type_; + end; + +// *********************************************************************// +// DispIntf: ITextSelectionDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C1-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextSelectionDisp = dispinterface + ['{8CC497C1-A1DF-11CE-8098-00AA0047BE5D}'] + property Flags: Integer dispid 257; + property type_: Integer readonly dispid 258; + function MoveLeft(Unit_: Integer; Count: Integer; Extend: Integer): Integer; dispid 259; + function MoveRight(Unit_: Integer; Count: Integer; Extend: Integer): Integer; dispid 260; + function MoveUp(Unit_: Integer; Count: Integer; Extend: Integer): Integer; dispid 261; + function MoveDown(Unit_: Integer; Count: Integer; Extend: Integer): Integer; dispid 262; + function HomeKey(Unit_: Integer; Extend: Integer): Integer; dispid 263; + function EndKey(Unit_: Integer; Extend: Integer): Integer; dispid 264; + procedure TypeText(const bstr: WideString); dispid 265; + property Text: WideString dispid 0; + property Char: Integer dispid 513; + property Duplicate: ITextRange readonly dispid 514; + property FormattedText: ITextRange dispid 515; + property Start: Integer dispid 516; + property End_: Integer dispid 517; + property Font: ITextFont dispid 518; + property Para: ITextPara dispid 519; + property StoryLength: Integer readonly dispid 520; + property StoryType: Integer readonly dispid 521; + procedure Collapse(bStart: Integer); dispid 528; + function Expand(Unit_: Integer): Integer; dispid 529; + function GetIndex(Unit_: Integer): Integer; dispid 530; + procedure SetIndex(Unit_: Integer; Index: Integer; Extend: Integer); dispid 531; + procedure SetRange(cpActive: Integer; cpOther: Integer); dispid 532; + function InRange(const pRange: ITextRange): Integer; dispid 533; + function InStory(const pRange: ITextRange): Integer; dispid 534; + function IsEqual(const pRange: ITextRange): Integer; dispid 535; + procedure Select; dispid 536; + function StartOf(Unit_: Integer; Extend: Integer): Integer; dispid 537; + function EndOf(Unit_: Integer; Extend: Integer): Integer; dispid 544; + function Move(Unit_: Integer; Count: Integer): Integer; dispid 545; + function MoveStart(Unit_: Integer; Count: Integer): Integer; dispid 546; + function MoveEnd(Unit_: Integer; Count: Integer): Integer; dispid 547; + function MoveWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 548; + function MoveStartWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 549; + function MoveEndWhile(const Cset: OleVariant; Count: Integer): Integer; dispid 550; + function MoveUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 551; + function MoveStartUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 552; + function MoveEndUntil(const Cset: OleVariant; Count: Integer): Integer; dispid 553; + function FindText(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 560; + function FindTextStart(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 561; + function FindTextEnd(const bstr: WideString; cch: Integer; Flags: Integer): Integer; dispid 562; + function Delete(Unit_: Integer; Count: Integer): Integer; dispid 563; + procedure Cut(out pVar: OleVariant); dispid 564; + procedure Copy(out pVar: OleVariant); dispid 565; + procedure Paste(const pVar: OleVariant; Format: Integer); dispid 566; + function CanPaste(const pVar: OleVariant; Format: Integer): Integer; dispid 567; + function CanEdit: Integer; dispid 568; + procedure ChangeCase(Type_: Integer); dispid 569; + procedure GetPoint(Type_: Integer; out px: Integer; out py: Integer); dispid 576; + procedure SetPoint(x: Integer; y: Integer; Type_: Integer; Extend: Integer); dispid 577; + procedure ScrollIntoView(Value: Integer); dispid 578; + function GetEmbeddedObject: IUnknown; dispid 579; + end; + +// *********************************************************************// +// Interface: ITextFont +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C3-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextFont = interface(IDispatch) + ['{8CC497C3-A1DF-11CE-8098-00AA0047BE5D}'] + function Get_Duplicate: ITextFont; safecall; + procedure Set_Duplicate(const ppFont: ITextFont); safecall; + function CanChange: Integer; safecall; + function IsEqual(const pFont: ITextFont): Integer; safecall; + procedure Reset(Value: Integer); safecall; + function Get_Style: Integer; safecall; + procedure Set_Style(pValue: Integer); safecall; + function Get_AllCaps: Integer; safecall; + procedure Set_AllCaps(pValue: Integer); safecall; + function Get_Animation: Integer; safecall; + procedure Set_Animation(pValue: Integer); safecall; + function Get_BackColor: Integer; safecall; + procedure Set_BackColor(pValue: Integer); safecall; + function Get_Bold: Integer; safecall; + procedure Set_Bold(pValue: Integer); safecall; + function Get_Emboss: Integer; safecall; + procedure Set_Emboss(pValue: Integer); safecall; + function Get_ForeColor: Integer; safecall; + procedure Set_ForeColor(pValue: Integer); safecall; + function Get_Hidden: Integer; safecall; + procedure Set_Hidden(pValue: Integer); safecall; + function Get_Engrave: Integer; safecall; + procedure Set_Engrave(pValue: Integer); safecall; + function Get_Italic: Integer; safecall; + procedure Set_Italic(pValue: Integer); safecall; + function Get_Kerning: Single; safecall; + procedure Set_Kerning(pValue: Single); safecall; + function Get_LanguageID: Integer; safecall; + procedure Set_LanguageID(pValue: Integer); safecall; + function Get_Name: WideString; safecall; + procedure Set_Name(const pbstr: WideString); safecall; + function Get_Outline: Integer; safecall; + procedure Set_Outline(pValue: Integer); safecall; + function Get_Position: Single; safecall; + procedure Set_Position(pValue: Single); safecall; + function Get_Protected_: Integer; safecall; + procedure Set_Protected_(pValue: Integer); safecall; + function Get_Shadow: Integer; safecall; + procedure Set_Shadow(pValue: Integer); safecall; + function Get_Size: Single; safecall; + procedure Set_Size(pValue: Single); safecall; + function Get_SmallCaps: Integer; safecall; + procedure Set_SmallCaps(pValue: Integer); safecall; + function Get_Spacing: Single; safecall; + procedure Set_Spacing(pValue: Single); safecall; + function Get_StrikeThrough: Integer; safecall; + procedure Set_StrikeThrough(pValue: Integer); safecall; + function Get_Subscript: Integer; safecall; + procedure Set_Subscript(pValue: Integer); safecall; + function Get_Superscript: Integer; safecall; + procedure Set_Superscript(pValue: Integer); safecall; + function Get_Underline: Integer; safecall; + procedure Set_Underline(pValue: Integer); safecall; + function Get_Weight: Integer; safecall; + procedure Set_Weight(pValue: Integer); safecall; + property Duplicate: ITextFont read Get_Duplicate write Set_Duplicate; + property Style: Integer read Get_Style write Set_Style; + property AllCaps: Integer read Get_AllCaps write Set_AllCaps; + property Animation: Integer read Get_Animation write Set_Animation; + property BackColor: Integer read Get_BackColor write Set_BackColor; + property Bold: Integer read Get_Bold write Set_Bold; + property Emboss: Integer read Get_Emboss write Set_Emboss; + property ForeColor: Integer read Get_ForeColor write Set_ForeColor; + property Hidden: Integer read Get_Hidden write Set_Hidden; + property Engrave: Integer read Get_Engrave write Set_Engrave; + property Italic: Integer read Get_Italic write Set_Italic; + property Kerning: Single read Get_Kerning write Set_Kerning; + property LanguageID: Integer read Get_LanguageID write Set_LanguageID; + property Name: WideString read Get_Name write Set_Name; + property Outline: Integer read Get_Outline write Set_Outline; + property Position: Single read Get_Position write Set_Position; + property Protected_: Integer read Get_Protected_ write Set_Protected_; + property Shadow: Integer read Get_Shadow write Set_Shadow; + property Size: Single read Get_Size write Set_Size; + property SmallCaps: Integer read Get_SmallCaps write Set_SmallCaps; + property Spacing: Single read Get_Spacing write Set_Spacing; + property StrikeThrough: Integer read Get_StrikeThrough write Set_StrikeThrough; + property Subscript: Integer read Get_Subscript write Set_Subscript; + property Superscript: Integer read Get_Superscript write Set_Superscript; + property Underline: Integer read Get_Underline write Set_Underline; + property Weight: Integer read Get_Weight write Set_Weight; + end; + +// *********************************************************************// +// DispIntf: ITextFontDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C3-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextFontDisp = dispinterface + ['{8CC497C3-A1DF-11CE-8098-00AA0047BE5D}'] + property Duplicate: ITextFont dispid 0; + function CanChange: Integer; dispid 769; + function IsEqual(const pFont: ITextFont): Integer; dispid 770; + procedure Reset(Value: Integer); dispid 771; + property Style: Integer dispid 772; + property AllCaps: Integer dispid 773; + property Animation: Integer dispid 774; + property BackColor: Integer dispid 775; + property Bold: Integer dispid 776; + property Emboss: Integer dispid 777; + property ForeColor: Integer dispid 784; + property Hidden: Integer dispid 785; + property Engrave: Integer dispid 786; + property Italic: Integer dispid 787; + property Kerning: Single dispid 788; + property LanguageID: Integer dispid 789; + property Name: WideString dispid 790; + property Outline: Integer dispid 791; + property Position: Single dispid 792; + property Protected_: Integer dispid 793; + property Shadow: Integer dispid 800; + property Size: Single dispid 801; + property SmallCaps: Integer dispid 802; + property Spacing: Single dispid 803; + property StrikeThrough: Integer dispid 804; + property Subscript: Integer dispid 805; + property Superscript: Integer dispid 806; + property Underline: Integer dispid 807; + property Weight: Integer dispid 808; + end; + +// *********************************************************************// +// Interface: ITextPara +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C4-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextPara = interface(IDispatch) + ['{8CC497C4-A1DF-11CE-8098-00AA0047BE5D}'] + function Get_Duplicate: ITextPara; safecall; + procedure Set_Duplicate(const ppPara: ITextPara); safecall; + function CanChange: Integer; safecall; + function IsEqual(const pPara: ITextPara): Integer; safecall; + procedure Reset(Value: Integer); safecall; + function Get_Style: Integer; safecall; + procedure Set_Style(pValue: Integer); safecall; + function Get_Alignment: Integer; safecall; + procedure Set_Alignment(pValue: Integer); safecall; + function Get_Hyphenation: Integer; safecall; + procedure Set_Hyphenation(pValue: Integer); safecall; + function Get_FirstLineIndent: Single; safecall; + function Get_KeepTogether: Integer; safecall; + procedure Set_KeepTogether(pValue: Integer); safecall; + function Get_KeepWithNext: Integer; safecall; + procedure Set_KeepWithNext(pValue: Integer); safecall; + function Get_LeftIndent: Single; safecall; + function Get_LineSpacing: Single; safecall; + function Get_LineSpacingRule: Integer; safecall; + function Get_ListAlignment: Integer; safecall; + procedure Set_ListAlignment(pValue: Integer); safecall; + function Get_ListLevelIndex: Integer; safecall; + procedure Set_ListLevelIndex(pValue: Integer); safecall; + function Get_ListStart: Integer; safecall; + procedure Set_ListStart(pValue: Integer); safecall; + function Get_ListTab: Single; safecall; + procedure Set_ListTab(pValue: Single); safecall; + function Get_ListType: Integer; safecall; + procedure Set_ListType(pValue: Integer); safecall; + function Get_NoLineNumber: Integer; safecall; + procedure Set_NoLineNumber(pValue: Integer); safecall; + function Get_PageBreakBefore: Integer; safecall; + procedure Set_PageBreakBefore(pValue: Integer); safecall; + function Get_RightIndent: Single; safecall; + procedure Set_RightIndent(pValue: Single); safecall; + procedure SetIndents(StartIndent: Single; LeftIndent: Single; RightIndent: Single); safecall; + procedure SetLineSpacing(LineSpacingRule: Integer; LineSpacing: Single); safecall; + function Get_SpaceAfter: Single; safecall; + procedure Set_SpaceAfter(pValue: Single); safecall; + function Get_SpaceBefore: Single; safecall; + procedure Set_SpaceBefore(pValue: Single); safecall; + function Get_WidowControl: Integer; safecall; + procedure Set_WidowControl(pValue: Integer); safecall; + function Get_TabCount: Integer; safecall; + procedure AddTab(tbPos: Single; tbAlign: Integer; tbLeader: Integer); safecall; + procedure ClearAllTabs; safecall; + procedure DeleteTab(tbPos: Single); safecall; + procedure GetTab(iTab: Integer; out ptbPos: Single; out ptbAlign: Integer; + out ptbLeader: Integer); safecall; + property Duplicate: ITextPara read Get_Duplicate write Set_Duplicate; + property Style: Integer read Get_Style write Set_Style; + property Alignment: Integer read Get_Alignment write Set_Alignment; + property Hyphenation: Integer read Get_Hyphenation write Set_Hyphenation; + property FirstLineIndent: Single read Get_FirstLineIndent; + property KeepTogether: Integer read Get_KeepTogether write Set_KeepTogether; + property KeepWithNext: Integer read Get_KeepWithNext write Set_KeepWithNext; + property LeftIndent: Single read Get_LeftIndent; + property LineSpacing: Single read Get_LineSpacing; + property LineSpacingRule: Integer read Get_LineSpacingRule; + property ListAlignment: Integer read Get_ListAlignment write Set_ListAlignment; + property ListLevelIndex: Integer read Get_ListLevelIndex write Set_ListLevelIndex; + property ListStart: Integer read Get_ListStart write Set_ListStart; + property ListTab: Single read Get_ListTab write Set_ListTab; + property ListType: Integer read Get_ListType write Set_ListType; + property NoLineNumber: Integer read Get_NoLineNumber write Set_NoLineNumber; + property PageBreakBefore: Integer read Get_PageBreakBefore write Set_PageBreakBefore; + property RightIndent: Single read Get_RightIndent write Set_RightIndent; + property SpaceAfter: Single read Get_SpaceAfter write Set_SpaceAfter; + property SpaceBefore: Single read Get_SpaceBefore write Set_SpaceBefore; + property WidowControl: Integer read Get_WidowControl write Set_WidowControl; + property TabCount: Integer read Get_TabCount; + end; + +// *********************************************************************// +// DispIntf: ITextParaDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C4-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextParaDisp = dispinterface + ['{8CC497C4-A1DF-11CE-8098-00AA0047BE5D}'] + property Duplicate: ITextPara dispid 0; + function CanChange: Integer; dispid 1025; + function IsEqual(const pPara: ITextPara): Integer; dispid 1026; + procedure Reset(Value: Integer); dispid 1027; + property Style: Integer dispid 1028; + property Alignment: Integer dispid 1029; + property Hyphenation: Integer dispid 1030; + property FirstLineIndent: Single readonly dispid 1031; + property KeepTogether: Integer dispid 1032; + property KeepWithNext: Integer dispid 1033; + property LeftIndent: Single readonly dispid 1040; + property LineSpacing: Single readonly dispid 1041; + property LineSpacingRule: Integer readonly dispid 1042; + property ListAlignment: Integer dispid 1043; + property ListLevelIndex: Integer dispid 1044; + property ListStart: Integer dispid 1045; + property ListTab: Single dispid 1046; + property ListType: Integer dispid 1047; + property NoLineNumber: Integer dispid 1048; + property PageBreakBefore: Integer dispid 1049; + property RightIndent: Single dispid 1056; + procedure SetIndents(StartIndent: Single; LeftIndent: Single; RightIndent: Single); dispid 1057; + procedure SetLineSpacing(LineSpacingRule: Integer; LineSpacing: Single); dispid 1058; + property SpaceAfter: Single dispid 1059; + property SpaceBefore: Single dispid 1060; + property WidowControl: Integer dispid 1061; + property TabCount: Integer readonly dispid 1062; + procedure AddTab(tbPos: Single; tbAlign: Integer; tbLeader: Integer); dispid 1063; + procedure ClearAllTabs; dispid 1064; + procedure DeleteTab(tbPos: Single); dispid 1065; + procedure GetTab(iTab: Integer; out ptbPos: Single; out ptbAlign: Integer; + out ptbLeader: Integer); dispid 1072; + end; + +// *********************************************************************// +// Interface: ITextStoryRanges +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C5-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextStoryRanges = interface(IDispatch) + ['{8CC497C5-A1DF-11CE-8098-00AA0047BE5D}'] + function _NewEnum: IUnknown; safecall; + function Item(Index: Integer): ITextRange; safecall; + function Get_Count: Integer; safecall; + property Count: Integer read Get_Count; + end; + +// *********************************************************************// +// DispIntf: ITextStoryRangesDisp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {8CC497C5-A1DF-11CE-8098-00AA0047BE5D} +// *********************************************************************// + ITextStoryRangesDisp = dispinterface + ['{8CC497C5-A1DF-11CE-8098-00AA0047BE5D}'] + function _NewEnum: IUnknown; dispid -4; + function Item(Index: Integer): ITextRange; dispid 0; + property Count: Integer readonly dispid 2; + end; + +// *********************************************************************// +// Interface: ITextDocument2 +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {01C25500-4268-11D1-883A-3C8B00C10000} +// *********************************************************************// + ITextDocument2 = interface(ITextDocument) + ['{01C25500-4268-11D1-883A-3C8B00C10000}'] + procedure AttachMsgFilter(const pFilter: IUnknown); safecall; + procedure SetEffectColor(Index: Integer; cr: LongWord); safecall; + procedure GetEffectColor(Index: Integer; out pcr: LongWord); safecall; + function Get_CaretType: Integer; safecall; + procedure Set_CaretType(pCaretType: Integer); safecall; + function GetImmContext: Integer; safecall; + procedure ReleaseImmContext(Context: Integer); safecall; + procedure GetPreferredFont(cp: Integer; CodePage: Integer; Option: Integer; + curCodepage: Integer; curFontSize: Integer; out pbstr: WideString; + out pPitchAndFamily: Integer; out pNewFontSize: Integer); safecall; + function Get_NotificationMode: Integer; safecall; + procedure Set_NotificationMode(pMode: Integer); safecall; + procedure GetClientRect(Type_: Integer; out pLeft: Integer; out pTop: Integer; + out pRight: Integer; out pBottom: Integer); safecall; + function Get_SelectionEx: ITextSelection; safecall; + procedure GetWindow(out phWnd: Integer); safecall; + procedure GetFEFlags(out pFlags: Integer); safecall; + procedure UpdateWindow; safecall; + procedure CheckTextLimit(cch: Integer; var pcch: Integer); safecall; + procedure IMEInProgress(Mode: Integer); safecall; + procedure SysBeep; safecall; + procedure Update(Mode: Integer); safecall; + procedure Notify(Notify: Integer); safecall; + property CaretType: Integer read Get_CaretType write Set_CaretType; + property NotificationMode: Integer read Get_NotificationMode write Set_NotificationMode; + property SelectionEx: ITextSelection read Get_SelectionEx; + end; + +// *********************************************************************// +// DispIntf: ITextDocument2Disp +// Flags: (4544) Dual NonExtensible OleAutomation Dispatchable +// GUID: {01C25500-4268-11D1-883A-3C8B00C10000} +// *********************************************************************// + ITextDocument2Disp = dispinterface + ['{01C25500-4268-11D1-883A-3C8B00C10000}'] + procedure AttachMsgFilter(const pFilter: IUnknown); dispid 21; + procedure SetEffectColor(Index: Integer; cr: LongWord); dispid 22; + procedure GetEffectColor(Index: Integer; out pcr: LongWord); dispid 23; + property CaretType: Integer dispid 24; + function GetImmContext: Integer; dispid 25; + procedure ReleaseImmContext(Context: Integer); dispid 26; + procedure GetPreferredFont(cp: Integer; CodePage: Integer; Option: Integer; + curCodepage: Integer; curFontSize: Integer; out pbstr: WideString; + out pPitchAndFamily: Integer; out pNewFontSize: Integer); dispid 27; + property NotificationMode: Integer dispid 28; + procedure GetClientRect(Type_: Integer; out pLeft: Integer; out pTop: Integer; + out pRight: Integer; out pBottom: Integer); dispid 29; + property SelectionEx: ITextSelection readonly dispid 30; + procedure GetWindow(out phWnd: Integer); dispid 31; + procedure GetFEFlags(out pFlags: Integer); dispid 32; + procedure UpdateWindow; dispid 33; + procedure CheckTextLimit(cch: Integer; var pcch: Integer); dispid 34; + procedure IMEInProgress(Mode: Integer); dispid 35; + procedure SysBeep; dispid 36; + procedure Update(Mode: Integer); dispid 37; + procedure Notify(Notify: Integer); dispid 38; + property Name: WideString readonly dispid 0; + property Selection: ITextSelection readonly dispid 1; + property StoryCount: Integer readonly dispid 2; + property StoryRanges: ITextStoryRanges readonly dispid 3; + property Saved: Integer dispid 4; + property DefaultTabStop: Single dispid 5; + procedure New; dispid 6; + procedure Open(const pVar: OleVariant; Flags: Integer; CodePage: Integer); dispid 7; + procedure Save(const pVar: OleVariant; Flags: Integer; CodePage: Integer); dispid 8; + function Freeze: Integer; dispid 9; + function Unfreeze: Integer; dispid 10; + procedure BeginEditCollection; dispid 11; + procedure EndEditCollection; dispid 12; + function Undo(Count: Integer): Integer; dispid 13; + function Redo(Count: Integer): Integer; dispid 14; + function Range(cp1: Integer; cp2: Integer): ITextRange; dispid 15; + function RangeFromPoint(x: Integer; y: Integer): ITextRange; dispid 16; + end; + +// *********************************************************************// +// Interface: ITextMsgFilter +// Flags: (128) NonExtensible +// GUID: {A3787420-4267-11D1-883A-3C8B00C10000} +// *********************************************************************// + ITextMsgFilter = interface(IUnknown) + ['{A3787420-4267-11D1-883A-3C8B00C10000}'] + function AttachDocument(var hwnd: _RemotableHandle; const pTextDoc: ITextDocument2): HResult; stdcall; + function HandleMessage(var pmsg: SYSUINT; var pwparam: UINT_PTR; var plparam: LONG_PTR; + out plres: LONG_PTR): HResult; stdcall; + function AttachMsgFilter(const pMsgFilter: ITextMsgFilter): HResult; stdcall; + end; + +implementation + +uses System.Win.ComObj; + +end. diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas index cd893a34d7..b4b38fe28d 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas @@ -74,6 +74,7 @@ type procedure memoClick(Sender: TObject); // I4808 procedure memoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FormResize(Sender: TObject); + procedure memoDblClick(Sender: TObject); private FFileName: string; debugkeyboard: TDebugKeyboard; @@ -108,6 +109,7 @@ type function SetKeyEventContext: Boolean; function HandleMemoKeydown(var Message: TMessage): Boolean; procedure Run(vkey: Word); + procedure UpdateMemoSelection; protected function GetHelpTopic: string; override; @@ -206,13 +208,13 @@ end; procedure TfrmLdmlKeyboardDebug.memoGotFocus(Sender: TObject); begin - memoSelMove(memo); + UpdateMemoSelection; end; procedure TfrmLdmlKeyboardDebug.memoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin - memoSelMove(memo); + UpdateMemoSelection; end; function TfrmLdmlKeyboardDebug.HandleMemoKeydown(var Message: TMessage): Boolean; @@ -631,7 +633,7 @@ begin SendMessage(memo.Handle, (WM_USER + 97) {EM_SETTEXTEX}, NativeUint(@ste), NativeUInt(PChar(str))); selection.Start := lhs.Length + output.Length; selection.Finish := selection.Start; - selection.Anchor := selection.Start; + selection.Caret := selection.Start; memo.Selection := selection; finally memo.Lines.EndUpdate; @@ -816,22 +818,32 @@ end; procedure TfrmLdmlKeyboardDebug.memoChange(Sender: TObject); begin UpdateDeadkeys; - memoSelMove(memo); + UpdateMemoSelection; end; procedure TfrmLdmlKeyboardDebug.memoClick(Sender: TObject); begin - memoSelMove(memo); + UpdateMemoSelection; end; +procedure TfrmLdmlKeyboardDebug.memoDblClick(Sender: TObject); +begin + UpdateMemoSelection; +end; + procedure TfrmLdmlKeyboardDebug.memoSelMove(Sender: TObject); +begin + UpdateMemoSelection; +end; + +procedure TfrmLdmlKeyboardDebug.UpdateMemoSelection; begin if memo.Focused then begin frmKeymanDeveloper.barStatus.Panels[0].Text := 'Debugger Active'; end; - if not memo.ReadOnly and not memo.SelectionChanging then + if not memo.ReadOnly then begin UpdateCharacterGrid; // I4808 end; @@ -855,7 +867,7 @@ begin len := text.Length - start; end; TCharacterGridRenderer.Fill(sgChars, text, FDeadkeys, start, len, - memo.Selection.Anchor, True); + memo.Selection.Caret, True); TCharacterGridRenderer.Size(sgChars, memo.Font); end; diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index 6137cab712..354592e72f 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -86,6 +86,7 @@ type procedure memoClick(Sender: TObject); // I4808 procedure memoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FormResize(Sender: TObject); + procedure memoDblClick(Sender: TObject); private FDebugVisible: Boolean; @@ -118,6 +119,7 @@ type function GetStatusText: string; procedure SetStatusText(Value: string); procedure UpdateDebugStatusForm; // I4809 + procedure UpdateMemoSelection; private FDebugCore: TDebugCore; @@ -147,6 +149,7 @@ type procedure SetCurrentEvent(Value: Integer); procedure FinishBatch; procedure StartBatch; + procedure RecordBreadcrumb(const message: string); protected function GetHelpTopic: string; override; @@ -306,13 +309,13 @@ begin if UIStatus = duiReadyForInput then UIStatus := duiFocusedForInput; - memoSelMove(memo); + UpdateMemoSelection; end; procedure TfrmDebug.memoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin - memoSelMove(memo); + UpdateMemoSelection; end; procedure TfrmDebug.memoLostFocus(Sender: TObject); @@ -463,9 +466,8 @@ begin modifier := modifier and not KM_CORE_MODIFIER_LCTRL; end; - TKeymanSentryClient.Instance.Breadcrumb('default', - Format('ProcessKeyEvent: vk=%x mod=%x scan=%x beforeText="%s" beforeSelection=%s',[vkey, modifier, scan, Copy(memo.GetTextCR, 1, 256), memo.Selection.ToString]), - 'debugger'); + RecordBreadcrumb( + Format('ProcessKeyEvent: vk=%x mod=%x scan=%x beforeText="%s" beforeSelection=%s savedSelection=%s',[vkey, modifier, scan, Copy(memo.GetTextCR, 1, 256), memo.Selection.ToString, FSavedSelection.ToString])); if not SetKeyEventContext then Exit(False); @@ -601,12 +603,17 @@ end; { Stores } +procedure TfrmDebug.RecordBreadcrumb(const message: string); +begin + // TODO(v20): eliminate breadcrumbs once underlying bug #11706 is confirmed fixed + TKeymanSentryClient.Instance.Breadcrumb('default', message, 'debugger'); + // OutputDebugString(PChar(message)); +end; + procedure TfrmDebug.ExecuteEvent(n: Integer); begin - TKeymanSentryClient.Instance.Breadcrumb('default', - Format('ExecuteEvent(%d/%d): [%s]; b:%s; s:%s', - [n, FEvents.Count, FEvents[n].ToString, memo.Selection.ToString, FSavedSelection.ToString]), - 'debugger'); + RecordBreadcrumb(Format('ExecuteEvent(%d/%d): [%s] beforeSelection=%s savedSelection=%s', + [n, FEvents.Count, FEvents[n].ToString, memo.Selection.ToString, FSavedSelection.ToString])); memo.ReadOnly := False; memo.Selection := FSavedSelection; @@ -1323,22 +1330,32 @@ end; procedure TfrmDebug.memoChange(Sender: TObject); begin UpdateDeadkeys; - memoSelMove(memo); + UpdateMemoSelection; end; procedure TfrmDebug.memoClick(Sender: TObject); begin - memoSelMove(memo); + UpdateMemoSelection; +end; + +procedure TfrmDebug.memoDblClick(Sender: TObject); +begin + UpdateMemoSelection; end; procedure TfrmDebug.memoSelMove(Sender: TObject); +begin + UpdateMemoSelection; +end; + +procedure TfrmDebug.UpdateMemoSelection; begin if memo.Focused then begin frmKeymanDeveloper.barStatus.Panels[0].Text := 'Debugger Active'; end; - if not memo.ReadOnly and not memo.SelectionChanging then + if not memo.ReadOnly then begin FSavedSelection := memo.Selection; UpdateCharacterGrid; // I4808 @@ -1363,7 +1380,7 @@ begin len := t.Length - start; end; - TCharacterGridRenderer.Fill(sgChars, t, FDeadkeys, start, len, memo.Selection.Anchor); + TCharacterGridRenderer.Fill(sgChars, t, FDeadkeys, start, len, memo.Selection.Caret); TCharacterGridRenderer.Size(sgChars, memo.Font); end; diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr index 19f7ed7756..80cd982b86 100644 --- a/developer/src/tike/tike.dpr +++ b/developer/src/tike/tike.dpr @@ -1,6 +1,6 @@ -program tike; - -uses +program tike; + +uses Vcl.Forms, Vcl.Dialogs, Winapi.Windows, @@ -304,17 +304,18 @@ uses Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters}, Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas' {frmCloneLocalProjectParameters}, Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas' {frmCloneKeymanCloudProjectParameters}, - Keyman.Developer.UI.FormValidation in 'main\Keyman.Developer.UI.FormValidation.pas'; + Keyman.Developer.UI.FormValidation in 'main\Keyman.Developer.UI.FormValidation.pas', + tom_TLB in '..\common\delphi\components\tom_TLB.pas'; -{$R *.RES} -{$R ICONS.RES} -{$R VERSION.RES} -{$R MANIFEST.RES} - -// CEF3 needs to set the LARGEADDRESSAWARE flag which allows 32-bit processes to use up to 3GB of RAM. -// If you don't add this flag the rederer process will crash when you try to load large images. -{$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE} - -begin - RunKeymanDeveloper; -end. +{$R *.RES} +{$R ICONS.RES} +{$R VERSION.RES} +{$R MANIFEST.RES} + +// CEF3 needs to set the LARGEADDRESSAWARE flag which allows 32-bit processes to use up to 3GB of RAM. +// If you don't add this flag the rederer process will crash when you try to load large images. +{$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE} + +begin + RunKeymanDeveloper; +end. diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj index ba4a86c430..2580d0591e 100644 --- a/developer/src/tike/tike.dproj +++ b/developer/src/tike/tike.dproj @@ -598,6 +598,7 @@ dfm + Cfg_2 From ba10f486945d9bdcc3f05af97350beeccdf41bff Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 4 Mar 2026 12:01:49 -0600 Subject: [PATCH 31/57] auto: increment master version to 19.0.210 Test-bot: skip Build-bot: skip --- HISTORY.md | 6 ++++++ VERSION.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index cd5a060aa0..e54c394c40 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 19.0.209 alpha 2026-03-04 + +* maint(ios): update fv Cartfile for Xcode 26 (#15655) +* feat(linux): implement diagnostic report (#15576) +* refactor(core): cleanup (#15656) + ## 19.0.208 alpha 2026-03-03 * docs(core): add keyhandling doc (#15629) diff --git a/VERSION.md b/VERSION.md index adbee0615c..d8fa739aae 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.209 \ No newline at end of file +19.0.210 \ No newline at end of file From 0700a3e444dc8e62b742e786c328a73280c9a32f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:15:02 +0000 Subject: [PATCH 32/57] chore(deps): bump multer from 2.0.2 to 2.1.1 Bumps [multer](https://github.com/expressjs/multer) from 2.0.2 to 2.1.1. - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.0.2...v2.1.1) --- updated-dependencies: - dependency-name: multer dependency-version: 2.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- developer/src/server/package.json | 2 +- package-lock.json | 29 ++++++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 37192ca6f3..fa44520f5d 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -13,7 +13,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", - "multer": "^2.0.2", + "multer": "^2.1.1", "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", diff --git a/package-lock.json b/package-lock.json index 61a304996b..c8629dc659 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1149,7 +1149,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", - "multer": "^2.0.2", + "multer": "^2.1.1", "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", @@ -10008,6 +10008,7 @@ }, "node_modules/minimist": { "version": "1.2.6", + "dev": true, "license": "MIT" }, "node_modules/minipass": { @@ -10154,6 +10155,7 @@ }, "node_modules/mkdirp": { "version": "0.5.6", + "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -10505,21 +10507,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/nanocolors": { @@ -10832,15 +10835,6 @@ "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", "dev": true }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -13640,6 +13634,7 @@ }, "node_modules/xtend": { "version": "4.0.2", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" From 1a07aadc05e6fd4e3bfbbf630909e934ef8966ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:08:11 +0000 Subject: [PATCH 33/57] chore(deps): bump tar Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.10. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.10) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- .../src/win32/trayicon/addon-src/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json index ab91216cd4..eca5a1c510 100644 --- a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -642,9 +642,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -1166,9 +1166,9 @@ } }, "tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", From 724a8ea0f53df91bd2bef5ff4ad9aa250a9da99b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 5 Mar 2026 13:15:43 +0100 Subject: [PATCH 34/57] maint(developer): cleanup additional TestCompilerCallbacks references Address a review comment from #15665 and cleanup additional references that I missed the first time around, along with the `fs.readFileSync` `Uint8Array` cast. Follows: #15665 Test-bot: skip --- .../utils/test/kpj/kpj-file-writer.tests.ts | 13 ++++++----- .../utils/test/kps/kps-file-reader.tests.ts | 22 +++++-------------- .../test/analyzer-messages.tests.ts | 12 +--------- .../test/osk-character-use-format.tests.ts | 12 +--------- .../test/osk-character-use-messages.tests.ts | 14 ++---------- .../test/osk-character-use.tests.ts | 12 +--------- .../test/abstract-generator.tests.ts | 20 +++++------------ .../test/basic-generator.tests.ts | 4 ++-- .../test/generator-messages.tests.ts | 16 +------------- .../test/keyman-keyboard-generator.tests.ts | 3 ++- .../test/ldml-keyboard-generator.tests.ts | 3 ++- .../test/package-compiler.tests.ts | 10 --------- developer/src/kmc/test/build.tests.ts | 10 +++++---- .../kmc/test/extendedCompilerOptions.tests.ts | 8 +++---- developer/src/kmc/test/fileLists.tests.ts | 13 ++++++----- .../src/kmc/test/generateKmnKeyboard.tests.ts | 7 +++--- .../kmc/test/generateLdmlKeyboard.tests.ts | 7 +++--- .../kmc/test/generateLexicalModel.tests.ts | 7 +++--- developer/src/kmc/test/project-build.tests.ts | 6 ++++- 19 files changed, 65 insertions(+), 134 deletions(-) diff --git a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts index d1a7cdddb1..b2c7464e6c 100644 --- a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts +++ b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts @@ -1,4 +1,7 @@ -import * as fs from 'fs'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import * as fs from 'node:fs'; import 'mocha'; import {assert} from 'chai'; import { makePathToFixture } from '../helpers/index.js'; @@ -8,13 +11,13 @@ import { KPJFileWriter } from '../../src/types/kpj/kpj-file-writer.js'; import { KeymanDeveloperProjectOptions } from '../../src/types/kpj/keyman-developer-project.js'; import { SymbolUtils } from '../../src/symbol-utils.js'; -const callbacks = new TestCompilerCallbacks(); - describe('kpj-file-writer', function () { + const callbacks = new TestCompilerCallbacks(this); + it('kpj-file-writer should write a valid v1.0 file', async function() { const kpjPath = 'khmer_angkor.kpj'; const path = makePathToFixture('kpj', kpjPath); - const input = fs.readFileSync(path); + const input = fs.readFileSync(path) as Uint8Array; const reader = new KPJFileReader(callbacks); const inputKpj = reader.read(input); reader.validate(inputKpj); @@ -53,7 +56,7 @@ describe('kpj-file-writer', function () { it('kpj-file-writer should write a valid v2.0 file', async function() { const kpjPath = 'khmer_angkor.kpj'; const path = makePathToFixture('kpj', kpjPath); - const input = fs.readFileSync(path); + const input = fs.readFileSync(path) as Uint8Array; const reader = new KPJFileReader(callbacks); const inputKpj = reader.read(input); reader.validate(inputKpj); diff --git a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts index e195f23c77..d73f9a526a 100644 --- a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts @@ -4,7 +4,7 @@ * Created by mcdurdin on 2024-10-16 */ -import * as fs from 'fs'; +import * as fs from 'node:fs'; import 'mocha'; import {assert} from 'chai'; @@ -20,20 +20,10 @@ import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js'; describe('kps-file-reader', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('kps-file-reader should read a valid file', function() { - const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')); + const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')) as Uint8Array; const reader = new KpsFileReader(callbacks); const kps = reader.read(input); @@ -70,7 +60,7 @@ describe('kps-file-reader', function () { }); it('kps-file-reader should round-trip with kps-file-writer', function() { - const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')); + const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')) as Uint8Array; const reader = new KpsFileReader(callbacks); // Remove XML metadata symbols to reduce clutter for testing purposes const kps = SymbolUtils.removeSymbols(reader.read(input)); @@ -87,7 +77,7 @@ describe('kps-file-reader', function () { // ERROR_InvalidPackageFile it('should generate ERROR_InvalidPackageFile if package source file contains invalid XML', async function() { - const input = fs.readFileSync(makePathToFixture('kps', 'error_invalid_package_file.kps')); + const input = fs.readFileSync(makePathToFixture('kps', 'error_invalid_package_file.kps')) as Uint8Array; const reader = new KpsFileReader(callbacks); const kps = reader.read(input); @@ -99,7 +89,7 @@ describe('kps-file-reader', function () { // ERROR_NotAPackageFile it(`should generate ERROR_NotAPackageFile when the package source file is valid XML but does not have a root element`, function () { - const input = fs.readFileSync(makePathToFixture('kps', 'error_not_a_package_file.kps')); + const input = fs.readFileSync(makePathToFixture('kps', 'error_not_a_package_file.kps')) as Uint8Array; const reader = new KpsFileReader(callbacks); const kps = reader.read(input); diff --git a/developer/src/kmc-analyze/test/analyzer-messages.tests.ts b/developer/src/kmc-analyze/test/analyzer-messages.tests.ts index 7839c7fe39..ce4e021968 100644 --- a/developer/src/kmc-analyze/test/analyzer-messages.tests.ts +++ b/developer/src/kmc-analyze/test/analyzer-messages.tests.ts @@ -11,17 +11,7 @@ import { AnalyzeOskCharacterUse } from '../src/osk-character-use/index.js'; describe('AnalyzerMessages', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid AnalyzerMessages object', function() { return verifyCompilerMessagesObject(AnalyzerMessages, CompilerErrorNamespace.Analyzer); diff --git a/developer/src/kmc-analyze/test/osk-character-use-format.tests.ts b/developer/src/kmc-analyze/test/osk-character-use-format.tests.ts index 9acd87403f..22852af2b4 100644 --- a/developer/src/kmc-analyze/test/osk-character-use-format.tests.ts +++ b/developer/src/kmc-analyze/test/osk-character-use-format.tests.ts @@ -7,23 +7,13 @@ import { AnalyzeOskCharacterUse } from '../src/osk-character-use/index.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; describe('AnalyzeOskCharacterUse output formats', function() { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); const dummyStrings = { 'a': [{ filename: 'file1.kvks', count: 1 }], 'b': [{ filename: 'file2.kvks', count: 2 }] }; - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if (this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); - it('generates .txt format correctly', function() { const a = new AnalyzeOskCharacterUse(callbacks, { includeCounts: true }); a.unitTestEndPoints.addStrings(['a', 'b'], 'testfile.kvks'); diff --git a/developer/src/kmc-analyze/test/osk-character-use-messages.tests.ts b/developer/src/kmc-analyze/test/osk-character-use-messages.tests.ts index 540ef3cb41..362b694c07 100644 --- a/developer/src/kmc-analyze/test/osk-character-use-messages.tests.ts +++ b/developer/src/kmc-analyze/test/osk-character-use-messages.tests.ts @@ -9,7 +9,7 @@ import { AnalyzerMessages } from '../src/analyzer-messages.js'; import { makePathToFixture } from './helpers/index.js'; describe('AnalyzeOskCharacterUse warnings', function() { - const callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(this); const MOCK_MAP_NO_COUNTS = makePathToFixture( 'osk-character-use', @@ -20,16 +20,6 @@ describe('AnalyzeOskCharacterUse warnings', function() { 'mock-map-with-counts.json' ); - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if (this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); - it('warns if previous map did not include counts but includeCounts=true', function() { const a = new AnalyzeOskCharacterUse(callbacks, { includeCounts: true @@ -37,7 +27,7 @@ describe('AnalyzeOskCharacterUse warnings', function() { const result = a.unitTestEndPoints.loadPreviousMap(MOCK_MAP_NO_COUNTS); assert.isNotNull(result, 'Expected map to be loaded successfully'); - + assert.isTrue( callbacks.hasMessage(AnalyzerMessages.WARN_PreviousMapDidNotIncludeCounts), 'Expected Warn_PreviousMapDidNotIncludeCounts warning' diff --git a/developer/src/kmc-analyze/test/osk-character-use.tests.ts b/developer/src/kmc-analyze/test/osk-character-use.tests.ts index dac60dcfb6..6bd7001099 100644 --- a/developer/src/kmc-analyze/test/osk-character-use.tests.ts +++ b/developer/src/kmc-analyze/test/osk-character-use.tests.ts @@ -17,17 +17,7 @@ const SIL_KHMER_KVKS = makePathToFixture('khmer', "sil_khmer.kvks"); const KBDKHMR_WITH_SIL_KHMER_JSON = makePathToFixture('khmer', "KbdKhmr-with-sil_khmer.json"); describe('AnalyzeOskCharacterUse', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest?.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('generates a mapping file from 2 input files', async function() { // functional test diff --git a/developer/src/kmc-generate/test/abstract-generator.tests.ts b/developer/src/kmc-generate/test/abstract-generator.tests.ts index 6b3667b2e9..5a0cf800dd 100644 --- a/developer/src/kmc-generate/test/abstract-generator.tests.ts +++ b/developer/src/kmc-generate/test/abstract-generator.tests.ts @@ -3,26 +3,16 @@ */ import 'mocha'; -import * as path from 'path'; -import * as os from 'os'; -import * as fs from 'fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import * as fs from 'node:fs'; import { assert } from 'chai'; import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { options } from './shared-options.js'; describe('AbstractGenerator', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('should write out files successfully', async function() { const ag = new AbstractGenerator(); @@ -40,7 +30,7 @@ describe('AbstractGenerator', function () { }; assert.isTrue(await ag.write(artifacts)); assert.isTrue(fs.existsSync(filename)); - const buf = fs.readFileSync(filename); + const buf = fs.readFileSync(filename) as Uint8Array; assert.deepEqual(buf, data); fs.unlinkSync(filename); diff --git a/developer/src/kmc-generate/test/basic-generator.tests.ts b/developer/src/kmc-generate/test/basic-generator.tests.ts index f5cff4044a..d40dc59b33 100644 --- a/developer/src/kmc-generate/test/basic-generator.tests.ts +++ b/developer/src/kmc-generate/test/basic-generator.tests.ts @@ -32,9 +32,10 @@ class BasicGeneratorTest extends BasicGenerator { } describe('BasicGenerator', function () { + const callbacks = new TestCompilerCallbacks(this); + it('should configure default settings', async function() { const bg = new BasicGenerator(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await bg.init(callbacks, options)); assert.isTrue(bg.test_preGenerate()); @@ -45,7 +46,6 @@ describe('BasicGenerator', function () { const bg = new BasicGeneratorTest(); bg.setup(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await bg.init(callbacks, options)); assert.isTrue(bg.test_preGenerate()); diff --git a/developer/src/kmc-generate/test/generator-messages.tests.ts b/developer/src/kmc-generate/test/generator-messages.tests.ts index b5d3a8837a..d497f43b43 100644 --- a/developer/src/kmc-generate/test/generator-messages.tests.ts +++ b/developer/src/kmc-generate/test/generator-messages.tests.ts @@ -22,17 +22,7 @@ import { LexicalModelGenerator } from '../src/lexical-model-generator.js'; import { options } from './shared-options.js'; describe('GeneratorMessages', function () { - const callbacks = new TestCompilerCallbacks(); - - this.beforeEach(function() { - callbacks.clear(); - }); - - this.afterEach(function() { - if(this.currentTest.isFailed()) { - callbacks.printMessages(); - } - }); + const callbacks = new TestCompilerCallbacks(this); it('should have a valid GeneratorMessages object', function() { return verifyCompilerMessagesObject(GeneratorMessages, CompilerErrorNamespace.Generator); @@ -78,7 +68,6 @@ describe('GeneratorMessages', function () { targets: ['invalid'] }; const bg = new BasicGenerator(); - const callbacks = new TestCompilerCallbacks(); assert(await bg.init(callbacks, testOptions)); assert.isFalse(bg.test_preGenerate()); assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidTarget)); @@ -89,7 +78,6 @@ describe('GeneratorMessages', function () { id: '???invalid', }; const kkg = new KeymanKeyboardGenerator(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await kkg.init(callbacks, testOptions)); assert.isNull(await kkg.run()); assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidKeymanKeyboardId)); @@ -100,7 +88,6 @@ describe('GeneratorMessages', function () { id: '???invalid', }; const lkg = new LdmlKeyboardGenerator(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await lkg.init(callbacks, testOptions)); assert.isNull(await lkg.run()); assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLdmlKeyboardId)); @@ -111,7 +98,6 @@ describe('GeneratorMessages', function () { id: 'example.???.invalid', }; const lmg = new LexicalModelGenerator(); - const callbacks = new TestCompilerCallbacks(); assert.isTrue(await lmg.init(callbacks, testOptions)); assert.isNull(await lmg.run()); assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLexicalModelId)); diff --git a/developer/src/kmc-generate/test/keyman-keyboard-generator.tests.ts b/developer/src/kmc-generate/test/keyman-keyboard-generator.tests.ts index 61bb710cb2..7c8f993d6b 100644 --- a/developer/src/kmc-generate/test/keyman-keyboard-generator.tests.ts +++ b/developer/src/kmc-generate/test/keyman-keyboard-generator.tests.ts @@ -33,6 +33,8 @@ function getFilenames(p: string, base?: string): string[] { describe('KeymanKeyboardGenerator', function () { let clock: sinon.SinonFakeTimers; + const callbacks = new TestCompilerCallbacks(this); + before(function() { // We will always be 12 April 2024 to match test fixtures clock = sinon.useFakeTimers(new Date(2024, 3, 12)); @@ -44,7 +46,6 @@ describe('KeymanKeyboardGenerator', function () { it('should generate a Keyman keyboard from provided options', async function() { const generator = new KeymanKeyboardGenerator(); - const callbacks = new TestCompilerCallbacks(); assert(await generator.init(callbacks, options)); const result = await generator.run(); assert.exists(result); diff --git a/developer/src/kmc-generate/test/ldml-keyboard-generator.tests.ts b/developer/src/kmc-generate/test/ldml-keyboard-generator.tests.ts index ca64e4d18d..d97dcecfb3 100644 --- a/developer/src/kmc-generate/test/ldml-keyboard-generator.tests.ts +++ b/developer/src/kmc-generate/test/ldml-keyboard-generator.tests.ts @@ -33,6 +33,8 @@ function getFilenames(p: string, base?: string): string[] { describe('LdmlKeyboardGenerator', function () { let clock: sinon.SinonFakeTimers; + const callbacks = new TestCompilerCallbacks(this); + before(function() { // We will always be 12 April 2024 to match test fixtures clock = sinon.useFakeTimers(new Date(2024, 3, 12)); @@ -44,7 +46,6 @@ describe('LdmlKeyboardGenerator', function () { it('should generate a LDML keyboard from provided options', async function() { const generator = new LdmlKeyboardGenerator(); - const callbacks = new TestCompilerCallbacks(); assert(await generator.init(callbacks, options)); const result = await generator.run(); assert.exists(result); diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index 77fbf75cdb..f2d71e77a6 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -136,8 +136,6 @@ describe('KmpCompiler', function () { */ it(`should transform a .kps file for a keyboard package to a correct kmp.json`, function () { - callbacks.clear(); - const kpsPath = makePathToFixture('kmp.json', 'ahom_star.kps'); const kmpJsonRefPath = makePathToFixture('kmp.json', 'kmp.json'); @@ -163,13 +161,9 @@ describe('KmpCompiler', function () { }); it(`should support .kps 17.0 metadata correctly`, function () { - callbacks.clear(); - const kpsPath = makePathToFixture('kmp_2.0', 'khmer_angkor.kps'); const kmpJsonRefPath = makePathToFixture('kmp_2.0', 'kmp.json'); - debugger; - const kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath); if(kmpJsonActual == null) { callbacks.printMessages(); @@ -197,8 +191,6 @@ describe('KmpCompiler', function () { it('should warn on absolute paths', async function() { this.timeout(10000); // building a zip file can sometimes be slow - callbacks.clear(); - const kpsPath = makePathToFixture('absolute_path', 'source', 'absolute_path.kps'); const kmpCompiler = new KmpCompiler(); assert.isTrue(await kmpCompiler.init(callbacks, null)); @@ -225,8 +217,6 @@ describe('KmpCompiler', function () { it('should normalize DOS pathnames from \\ to /', async function() { // this.timeout(10000); // building a zip file can sometimes be slow - callbacks.clear(); - const kpsPath = makePathToFixture('normalize_paths', 'source', 'khmer_angkor.kps'); const kmpCompiler = new KmpCompiler(); assert.isTrue(await kmpCompiler.init(callbacks, null)); diff --git a/developer/src/kmc/test/build.tests.ts b/developer/src/kmc/test/build.tests.ts index f8356db54b..b615c97f49 100644 --- a/developer/src/kmc/test/build.tests.ts +++ b/developer/src/kmc/test/build.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { clearOptions } from '../src/util/options.js'; import { assert } from 'chai'; @@ -5,8 +8,6 @@ import 'mocha'; import { BuildProject } from '../src/commands/buildClasses/BuildProject.js'; import { makePathToFixture } from './helpers/index.js'; -const callbacks = new TestCompilerCallbacks(); - interface CompilerWarningsAsErrorsTruthTable { cli: boolean; kpj: boolean; @@ -14,8 +15,9 @@ interface CompilerWarningsAsErrorsTruthTable { }; describe('compilerWarningsAsErrors', function () { - beforeEach(() => { - callbacks.clear(); + const callbacks = new TestCompilerCallbacks(this); + + this.beforeEach(function() { clearOptions(); }); diff --git a/developer/src/kmc/test/extendedCompilerOptions.tests.ts b/developer/src/kmc/test/extendedCompilerOptions.tests.ts index b2f04c81b7..b6950d6981 100644 --- a/developer/src/kmc/test/extendedCompilerOptions.tests.ts +++ b/developer/src/kmc/test/extendedCompilerOptions.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { assert } from 'chai'; import 'mocha'; @@ -10,10 +13,7 @@ interface MessageTest {input: string, result: CompilerMessageOverride}; interface InvalidMessageTest {input: string, code: number}; describe('commandOptionsMessageToCompilerOptionsMessage', function () { - const callbacks = new TestCompilerCallbacks(); - this.beforeEach(function() { - callbacks.clear(); - }); + const callbacks = new TestCompilerCallbacks(this); const valid: MessageTest[] = [ // Test the allowable coercions diff --git a/developer/src/kmc/test/fileLists.tests.ts b/developer/src/kmc/test/fileLists.tests.ts index 5092ff849d..94e379e864 100644 --- a/developer/src/kmc/test/fileLists.tests.ts +++ b/developer/src/kmc/test/fileLists.tests.ts @@ -1,11 +1,12 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { assert } from 'chai'; import 'mocha'; import { makePathToFixture } from './helpers/index.js'; import { expandFileList, expandFileLists } from '../src/util/fileLists.js'; -const callbacks = new TestCompilerCallbacks(); - // expandFileList expands each file name relative to the file list's supplied // filename, so we need to compare against the full path. const expectedFileList = [ @@ -21,11 +22,9 @@ const expectedFiles = [ 'file5.kmn' ]; -beforeEach(function() { - callbacks.clear(); -}); - describe('expandFileList', function () { + const callbacks = new TestCompilerCallbacks(this); + it('should report a missing filelist correctly', async function() { const path = makePathToFixture('file-lists', 'does-not-exist.txt'); @@ -45,6 +44,8 @@ describe('expandFileList', function () { }); describe('expandFileLists', function () { + const callbacks = new TestCompilerCallbacks(this); + it('should splice a filelist in correctly', async function() { // We just use this to test the splicing so no path resolution is made const files = [ diff --git a/developer/src/kmc/test/generateKmnKeyboard.tests.ts b/developer/src/kmc/test/generateKmnKeyboard.tests.ts index 811adc49b3..a5e39675d4 100644 --- a/developer/src/kmc/test/generateKmnKeyboard.tests.ts +++ b/developer/src/kmc/test/generateKmnKeyboard.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -28,17 +31,15 @@ describe('generateKeymanKeyboard', function() { description: 'Sample Keyboard Test', }; - const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(); + const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(this); let outPath: string = null; this.beforeEach(function() { - callbacks.clear(); outPath = fs.mkdtempSync(path.join(os.tmpdir(), 'kmc-')); }); this.afterEach(function() { if(this.currentTest.isFailed()) { - callbacks.printMessages(); console.error(`Output kept at ${outPath}`); } else { if(outPath) fs.rmSync(outPath, {recursive: true, force: true}); diff --git a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts index f28d1fec8f..3b92b31f87 100644 --- a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts +++ b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -28,17 +31,15 @@ describe('generateLdmlKeyboard', function() { description: 'Sample Keyboard Test', }; - const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(); + const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(this); let outPath: string = null; this.beforeEach(function() { - callbacks.clear(); outPath = fs.mkdtempSync(path.join(os.tmpdir(), 'kmc-')); }); this.afterEach(function() { if(this.currentTest.isFailed()) { - callbacks.printMessages(); console.error(`Output kept at ${outPath}`); } else { if(outPath) fs.rmSync(outPath, {recursive: true, force: true}); diff --git a/developer/src/kmc/test/generateLexicalModel.tests.ts b/developer/src/kmc/test/generateLexicalModel.tests.ts index 47a48098b8..7ed7f5eaf9 100644 --- a/developer/src/kmc/test/generateLexicalModel.tests.ts +++ b/developer/src/kmc/test/generateLexicalModel.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -27,17 +30,15 @@ describe('generateLexicalModel', function() { description: 'Sample Model Test', }; - const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(); + const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(this); let outPath: string = null; this.beforeEach(function() { - callbacks.clear(); outPath = fs.mkdtempSync(path.join(os.tmpdir(), 'kmc-')); }); this.afterEach(function() { if(this.currentTest.isFailed()) { - callbacks.printMessages(); console.error(`Output kept at ${outPath}`); } else { if(outPath) fs.rmSync(outPath, {recursive: true, force: true}); diff --git a/developer/src/kmc/test/project-build.tests.ts b/developer/src/kmc/test/project-build.tests.ts index 87a627b749..3e0bc56daf 100644 --- a/developer/src/kmc/test/project-build.tests.ts +++ b/developer/src/kmc/test/project-build.tests.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { assert } from 'chai'; import 'mocha'; @@ -6,9 +9,10 @@ import { makePathToFixture } from './helpers/index.js'; import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; import { clearOptions } from '../src/util/options.js'; -const callbacks = new TestCompilerCallbacks(); describe('BuildProject', function () { + const callbacks = new TestCompilerCallbacks(this); + it('should build a keyboard project', async function() { clearOptions(); const builder = new BuildProject(); From f1163860d17d527e2bc295d9f2f98856f0c81c73 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 5 Mar 2026 15:14:54 +0100 Subject: [PATCH 35/57] fix(developer): address review comments --- ...veloper.UI.Debug.UfrmLdmlKeyboardDebug.dfm | 186 ++++++++--------- developer/src/tike/child/UfrmDebug.dfm | 188 +++++++++--------- .../Keyman.UI.Debug.CharacterGridRenderer.pas | 8 +- 3 files changed, 193 insertions(+), 189 deletions(-) diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.dfm b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.dfm index cf5e4b735a..614ff28392 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.dfm +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.dfm @@ -1,92 +1,94 @@ -inherited frmLdmlKeyboardDebug: TfrmLdmlKeyboardDebug - Left = 85 - Top = 229 - BorderIcons = [biSystemMenu] - BorderStyle = bsSizeToolWin - Caption = 'Debug' - ClientHeight = 132 - ClientWidth = 755 - Font.Name = 'MS Sans Serif' - FormStyle = fsStayOnTop - KeyPreview = True - Position = poDefault - OnClose = FormClose - OnDestroy = FormDestroy - OnResize = FormResize - ExplicitWidth = 771 - ExplicitHeight = 171 - PixelsPerInch = 96 - TextHeight = 13 - object memo: TKeymanDeveloperDebuggerMemo - Left = 0 - Top = 0 - Width = 755 - Height = 73 - Cursor = crIBeam - Align = alClient - BorderStyle = bsNone - Font.Charset = ANSI_CHARSET - Font.Color = clWindowText - Font.Height = -11 - Font.Name = 'Arial' - Font.Style = [] - ParentFont = False - PlainText = True - PopupMenu = mnuPopup - ScrollBars = ssVertical - TabOrder = 0 - OnChange = memoChange - OnClick = memoClick - OnEnter = memoGotFocus - OnKeyUp = memoKeyUp - OnMessage = memoMessage - IsDebugging = False - end - object sgChars: TStringGrid - Left = 0 - Top = 73 - Width = 755 - Height = 59 - Align = alBottom - ColCount = 1 - DefaultDrawing = False - FixedCols = 0 - RowCount = 2 - FixedRows = 0 - ScrollBars = ssNone - TabOrder = 1 - OnDrawCell = sgCharsDrawCell - ColWidths = ( - 64) - RowHeights = ( - 24 - 24) - end - object mnuPopup: TPopupMenu - Images = frmKeymanDeveloper.lstImages - Left = 228 - Top = 73 - object mnuPopupFont: TMenuItem - Action = modActionsDebugger.actDebugViewFont - end - object mnuPopupResetToQuotedFont: TMenuItem - Action = modActionsDebugger.actDebugViewDefaultFont - end - object N1: TMenuItem - Caption = '-' - end - object mnuCut: TMenuItem - Action = modActionsMain.actEditCut - end - object mnuCopy: TMenuItem - Action = modActionsMain.actEditCopy - end - object mnuPaste: TMenuItem - Action = modActionsMain.actEditPaste - end - end - object appEvents: TApplicationEvents - Left = 368 - Top = 72 - end -end +inherited frmLdmlKeyboardDebug: TfrmLdmlKeyboardDebug + Left = 85 + Top = 229 + BorderIcons = [biSystemMenu] + BorderStyle = bsSizeToolWin + Caption = 'Debug' + ClientHeight = 132 + ClientWidth = 755 + Font.Name = 'MS Sans Serif' + FormStyle = fsStayOnTop + KeyPreview = True + Position = poDefault + OnClose = FormClose + OnDestroy = FormDestroy + OnResize = FormResize + ExplicitWidth = 771 + ExplicitHeight = 171 + PixelsPerInch = 96 + TextHeight = 13 + object memo: TKeymanDeveloperDebuggerMemo + Left = 0 + Top = 0 + Width = 755 + Height = 73 + Cursor = crIBeam + Align = alClient + BorderStyle = bsNone + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Arial' + Font.Style = [] + ParentFont = False + PlainText = True + PopupMenu = mnuPopup + ScrollBars = ssVertical + TabOrder = 0 + OnChange = memoChange + OnClick = memoClick + OnDblClick = memoDblClick + OnEnter = memoGotFocus + OnKeyUp = memoKeyUp + OnSelectionChange = memoSelMove + OnMessage = memoMessage + IsDebugging = False + end + object sgChars: TStringGrid + Left = 0 + Top = 73 + Width = 755 + Height = 59 + Align = alBottom + ColCount = 1 + DefaultDrawing = False + FixedCols = 0 + RowCount = 2 + FixedRows = 0 + ScrollBars = ssNone + TabOrder = 1 + OnDrawCell = sgCharsDrawCell + ColWidths = ( + 64) + RowHeights = ( + 24 + 24) + end + object mnuPopup: TPopupMenu + Images = frmKeymanDeveloper.lstImages + Left = 228 + Top = 73 + object mnuPopupFont: TMenuItem + Action = modActionsDebugger.actDebugViewFont + end + object mnuPopupResetToQuotedFont: TMenuItem + Action = modActionsDebugger.actDebugViewDefaultFont + end + object N1: TMenuItem + Caption = '-' + end + object mnuCut: TMenuItem + Action = modActionsMain.actEditCut + end + object mnuCopy: TMenuItem + Action = modActionsMain.actEditCopy + end + object mnuPaste: TMenuItem + Action = modActionsMain.actEditPaste + end + end + object appEvents: TApplicationEvents + Left = 368 + Top = 72 + end +end diff --git a/developer/src/tike/child/UfrmDebug.dfm b/developer/src/tike/child/UfrmDebug.dfm index 7642552667..8e7eeafeba 100644 --- a/developer/src/tike/child/UfrmDebug.dfm +++ b/developer/src/tike/child/UfrmDebug.dfm @@ -1,93 +1,95 @@ -inherited frmDebug: TfrmDebug - Left = 85 - Top = 229 - BorderIcons = [biSystemMenu] - BorderStyle = bsSizeToolWin - Caption = 'Debug' - ClientHeight = 132 - ClientWidth = 755 - Font.Name = 'MS Sans Serif' - FormStyle = fsStayOnTop - KeyPreview = True - Position = poDefault - OnClose = FormClose - OnDestroy = FormDestroy - OnResize = FormResize - ExplicitWidth = 771 - ExplicitHeight = 171 - PixelsPerInch = 96 - TextHeight = 13 - object memo: TKeymanDeveloperDebuggerMemo - Left = 0 - Top = 0 - Width = 755 - Height = 73 - Cursor = crIBeam - Align = alClient - BorderStyle = bsNone - Font.Charset = ANSI_CHARSET - Font.Color = clWindowText - Font.Height = -11 - Font.Name = 'Arial' - Font.Style = [] - ParentFont = False - PlainText = True - PopupMenu = mnuPopup - ScrollBars = ssVertical - TabOrder = 0 - OnChange = memoChange - OnClick = memoClick - OnEnter = memoGotFocus - OnExit = memoLostFocus - OnKeyUp = memoKeyUp - OnMessage = memoMessage - IsDebugging = False - end - object sgChars: TStringGrid - Left = 0 - Top = 73 - Width = 755 - Height = 59 - Align = alBottom - ColCount = 1 - DefaultDrawing = False - FixedCols = 0 - RowCount = 2 - FixedRows = 0 - ScrollBars = ssNone - TabOrder = 1 - OnDrawCell = sgCharsDrawCell - ColWidths = ( - 64) - RowHeights = ( - 24 - 24) - end - object mnuPopup: TPopupMenu - Images = frmKeymanDeveloper.lstImages - Left = 228 - Top = 73 - object mnuPopupFont: TMenuItem - Action = modActionsDebugger.actDebugViewFont - end - object mnuPopupResetToQuotedFont: TMenuItem - Action = modActionsDebugger.actDebugViewDefaultFont - end - object N1: TMenuItem - Caption = '-' - end - object mnuCut: TMenuItem - Action = modActionsMain.actEditCut - end - object mnuCopy: TMenuItem - Action = modActionsMain.actEditCopy - end - object mnuPaste: TMenuItem - Action = modActionsMain.actEditPaste - end - end - object appEvents: TApplicationEvents - Left = 368 - Top = 72 - end -end +inherited frmDebug: TfrmDebug + Left = 85 + Top = 229 + BorderIcons = [biSystemMenu] + BorderStyle = bsSizeToolWin + Caption = 'Debug' + ClientHeight = 132 + ClientWidth = 755 + Font.Name = 'MS Sans Serif' + FormStyle = fsStayOnTop + KeyPreview = True + Position = poDefault + OnClose = FormClose + OnDestroy = FormDestroy + OnResize = FormResize + ExplicitWidth = 771 + ExplicitHeight = 171 + PixelsPerInch = 96 + TextHeight = 13 + object memo: TKeymanDeveloperDebuggerMemo + Left = 0 + Top = 0 + Width = 755 + Height = 73 + Cursor = crIBeam + Align = alClient + BorderStyle = bsNone + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Arial' + Font.Style = [] + ParentFont = False + PlainText = True + PopupMenu = mnuPopup + ScrollBars = ssVertical + TabOrder = 0 + OnChange = memoChange + OnClick = memoClick + OnDblClick = memoDblClick + OnEnter = memoGotFocus + OnExit = memoLostFocus + OnKeyUp = memoKeyUp + OnSelectionChange = memoSelMove + OnMessage = memoMessage + IsDebugging = False + end + object sgChars: TStringGrid + Left = 0 + Top = 73 + Width = 755 + Height = 59 + Align = alBottom + ColCount = 1 + DefaultDrawing = False + FixedCols = 0 + RowCount = 2 + FixedRows = 0 + ScrollBars = ssNone + TabOrder = 1 + OnDrawCell = sgCharsDrawCell + ColWidths = ( + 64) + RowHeights = ( + 24 + 24) + end + object mnuPopup: TPopupMenu + Images = frmKeymanDeveloper.lstImages + Left = 228 + Top = 73 + object mnuPopupFont: TMenuItem + Action = modActionsDebugger.actDebugViewFont + end + object mnuPopupResetToQuotedFont: TMenuItem + Action = modActionsDebugger.actDebugViewDefaultFont + end + object N1: TMenuItem + Caption = '-' + end + object mnuCut: TMenuItem + Action = modActionsMain.actEditCut + end + object mnuCopy: TMenuItem + Action = modActionsMain.actEditCopy + end + object mnuPaste: TMenuItem + Action = modActionsMain.actEditPaste + end + end + object appEvents: TApplicationEvents + Left = 368 + Top = 72 + end +end diff --git a/developer/src/tike/debug/Keyman.UI.Debug.CharacterGridRenderer.pas b/developer/src/tike/debug/Keyman.UI.Debug.CharacterGridRenderer.pas index c38bf8f358..482a75db6c 100644 --- a/developer/src/tike/debug/Keyman.UI.Debug.CharacterGridRenderer.pas +++ b/developer/src/tike/debug/Keyman.UI.Debug.CharacterGridRenderer.pas @@ -22,7 +22,7 @@ type class procedure Fill(grid: TStringGrid; const text: string; deadkeys: TDebugDeadkeyInfoList; - SelStart, SelLength, SelAnchor: Integer; + SelStart, SelLength, Caret: Integer; DeadkeysAreCalledMarkers: Boolean = False); static; class procedure Render(grid: TStringGrid; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState; CharFont: TFont); static; @@ -37,7 +37,7 @@ uses class procedure TCharacterGridRenderer.Fill(grid: TStringGrid; const text: string; deadkeys: TDebugDeadkeyInfoList; - SelStart, SelLength, SelAnchor: Integer; + SelStart, SelLength, Caret: Integer; DeadkeysAreCalledMarkers: Boolean); type TCellType = (ctChar, ctDeadkey); @@ -171,10 +171,10 @@ begin X := 0; FillGrid(Before, 0, X); - if SelAnchor = SelStart then + if Caret = SelStart then FillGridCursor(X); FillGrid(Selection, CELL_SELECTED, X); - if SelAnchor <> SelStart then + if Caret <> SelStart then FillGridCursor(X); FillGrid(After, 0, X); end; From ad7966950d34b23d522520b035ba645eca01ae42 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 5 Mar 2026 15:19:01 +0100 Subject: [PATCH 36/57] chore(developer): address review comments Co-authored-by: Darcy Wong --- .../tike/project/Keyman.Developer.UI.Project.ProjectUI.pas | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas index d1eb8c20bb..866fb38ee6 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas @@ -29,7 +29,7 @@ function LoadGlobalProjectUI(pt: TProjectType; AFilename: string): TProjectUI; procedure FreeGlobalProjectUI; function IsGlobalProjectUIReady: Boolean; function CreateTempGlobalProjectUI(pt: TProjectType): TProjectUI; -function VerifyNewProjectPathWithUser(const BasePath, KeyboardID: string): Boolean; +function VerifyNewProjectPathWithUser(const BasePath, ProjectID: string): Boolean; implementation @@ -157,7 +157,7 @@ begin hLockFile := INVALID_HANDLE_VALUE; end; -function VerifyNewProjectPathWithUser(const BasePath, KeyboardID: string): Boolean; +function VerifyNewProjectPathWithUser(const BasePath, ProjectID: string): Boolean; var ProjectFolder: string; begin @@ -167,7 +167,7 @@ begin Exit(False); end; - ProjectFolder := IncludeTrailingPathDelimiter(BasePath) + KeyboardID; + ProjectFolder := IncludeTrailingPathDelimiter(BasePath) + ProjectID; if DirectoryExists(ProjectFolder) then begin MessageDlg('The project folder '+ProjectFolder+' already exists. Please choose a different folder or delete it in File Explorer.', mtError, [mbOk], 0); From bcba02671f2cc49f1b2bdbd9089aa376e33b59d9 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 5 Mar 2026 12:01:35 -0600 Subject: [PATCH 37/57] auto: increment master version to 19.0.211 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index e54c394c40..96cd3d9172 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.210 alpha 2026-03-05 + +* fix(developer): apply keyboard description in Windows keyboard import (#15667) +* fix(developer): improve UI feedback on upgrade (#15670) + ## 19.0.209 alpha 2026-03-04 * maint(ios): update fv Cartfile for Xcode 26 (#15655) diff --git a/VERSION.md b/VERSION.md index d8fa739aae..27040665fd 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.210 \ No newline at end of file +19.0.211 \ No newline at end of file From 645e1cd16f037c20427b954a42233caae3e32fa0 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 5 Mar 2026 21:46:40 +0100 Subject: [PATCH 38/57] chore(web): remove implicit any type when setting processingConfig.itemIdentifier Not exactly clear what triggered this? Perhaps related to npm package updates, but hard to see how. Test-bot: skip Build-bot: skip build:web --- .../src/engine/configuration/gestureRecognizerConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/engine/osk/gesture-processor/src/engine/configuration/gestureRecognizerConfiguration.ts b/web/src/engine/osk/gesture-processor/src/engine/configuration/gestureRecognizerConfiguration.ts index c3d6de5974..26abbd695d 100644 --- a/web/src/engine/osk/gesture-processor/src/engine/configuration/gestureRecognizerConfiguration.ts +++ b/web/src/engine/osk/gesture-processor/src/engine/configuration/gestureRecognizerConfiguration.ts @@ -120,7 +120,7 @@ export function preprocessRecognizerConfig( processingConfig.maxRoamingBounds = processingConfig.maxRoamingBounds ?? processingConfig.targetRoot; processingConfig.safeBounds = processingConfig.safeBounds ?? new PaddedZoneSource([2]); - processingConfig.itemIdentifier = processingConfig.itemIdentifier ?? (() => null); + processingConfig.itemIdentifier = processingConfig.itemIdentifier ?? ((): HoveredItemType => null); processingConfig.recordingMode = !!processingConfig.recordingMode; processingConfig.historyLength = (processingConfig.historyLength ?? 0) > 0 ? processingConfig.historyLength : 0; From 0ce96dbdcccefe8e19f7786754572bcddefa0d4b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 6 Mar 2026 09:57:36 +0100 Subject: [PATCH 39/57] maint(common): rollback from Typescript 5.8.2 to 5.4.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull request #15647 bumped our version of tsc from 5.4.5 to 5.8.2, because api-extractor wanted it. We have not tested yet with 5.8.2 and there are some incompatibility warnings and a new error that was patched in #15689. We will upgrade to a later version of Typescript in due course, but now is not the time. With this patch, we rollback our Typescript version to 5.4.x. Sample warning from build: ``` [web/src/engine/osk/gesture-processor/src/tools] ## build:test-module starting... ============= WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree. You may find that it works just fine, or you may not. SUPPORTED TYPESCRIPT VERSIONS: >=4.7.4 <5.5.0 YOUR TYPESCRIPT VERSION: 5.8.2 Please only submit bug reports when using the officially supported version. ============= ``` Report of typescript version usage with this patch: ``` $ npm ls typescript root@ D:\Projects\keyman\app ├─┬ @keymanapp/auto-history-action@ -> .\resources\build\version │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/common-test-resources@ -> .\common\test\resources │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/common-types@ -> .\common\web\types │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/developer-server@ -> .\developer\src\server │ ├─┬ tsc-watch@4.6.2 │ │ └── typescript@5.4.5 deduped │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/developer-test-helpers@ -> .\developer\src\common\web\test-helpers │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/developer-utils@ -> .\developer\src\common\web\utils │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/gesture-recognizer@ -> .\web\src\engine\osk\gesture-processor │ ├─┬ @keymanapp/web-utils@ -> .\web\src\engine\common\web-utils │ │ └── typescript@5.4.5 deduped │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/keyman-version@ -> .\common\web\keyman-version │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-analyze@ -> .\developer\src\kmc-analyze │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-copy@ -> .\developer\src\kmc-copy │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-generate@ -> .\developer\src\kmc-generate │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-keyboard-info@ -> .\developer\src\kmc-keyboard-info │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-kmn@ -> .\developer\src\kmc-kmn │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-ldml@ -> .\developer\src\kmc-ldml │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-model-info@ -> .\developer\src\kmc-model-info │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-model@ -> .\developer\src\kmc-model │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc-package@ -> .\developer\src\kmc-package │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/kmc@ -> .\developer\src\kmc │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/langtags@ -> .\common\web\langtags │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/lexical-model-layer@ -> .\web\src\engine\predictive-text\worker-main │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/lm-message-types@ -> .\web\src\engine\predictive-text\types │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/lm-worker@ -> .\web\src\engine\predictive-text\worker-thread │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/models-templates@ -> .\web\src\engine\predictive-text\templates │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/models-wordbreakers@ -> .\web\src\engine\predictive-text\wordbreakers │ └── typescript@5.4.5 deduped ├─┬ @keymanapp/sourcemap-path-remapper@ -> .\common\tools\sourcemap-path-remapper │ └── typescript@5.4.5 deduped ├─┬ @microsoft/api-extractor@7.57.6 │ └── typescript@5.8.2 ├─┬ @typescript-eslint/eslint-plugin@7.13.1 │ └─┬ ts-api-utils@1.3.0 │ └── typescript@5.4.5 deduped ├─┬ eslint-config-love@53.0.0 │ └── typescript@5.4.5 deduped ├─┬ keyman@ -> .\web │ ├─┬ @keymanapp/recorder-core@ -> .\web\src\tools\testing\recorder-core │ │ └── typescript@5.4.5 deduped │ └─┬ @keymanapp/web-sentry-manager@ -> .\web\src\engine\sentry-manager │ └── typescript@5.4.5 deduped └── typescript@5.4.5 ``` Test-bot: skip --- package-lock.json | 22 ++++++++++++++++++---- package.json | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76872a8a7c..35a28a2593 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "sinon": "^17.0.1", "source-map-support": "^0.5.21", "tslib": "^2.5.2", - "typescript": "^5.4.5" + "typescript": "~5.4.5" }, "engines": { "node": "20.16.0" @@ -2020,6 +2020,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", @@ -13304,9 +13318,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 8e33f398b2..f1a917d205 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "sinon": "^17.0.1", "source-map-support": "^0.5.21", "tslib": "^2.5.2", - "typescript": "^5.4.5" + "typescript": "~5.4.5" }, "scripts": {}, "workspaces": [ From cca8d4d21fb267476dcc0c3f25f1a0e968e53662 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 6 Mar 2026 10:21:04 +0100 Subject: [PATCH 40/57] docs: tweak build bot docs and update imsample gitignore Just minor maintenance. Build-bot: skip Test-bot: skip --- developer/src/samples/imsample/.gitignore | 2 ++ docs/build-bot.md | 5 +++-- resources/build/ci/trigger-definitions.inc.sh | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 developer/src/samples/imsample/.gitignore diff --git a/developer/src/samples/imsample/.gitignore b/developer/src/samples/imsample/.gitignore new file mode 100644 index 0000000000..fa2abae434 --- /dev/null +++ b/developer/src/samples/imsample/.gitignore @@ -0,0 +1,2 @@ +IMSample.exp +IMSample.lib \ No newline at end of file diff --git a/docs/build-bot.md b/docs/build-bot.md index 642f285894..fb3d12cbd0 100644 --- a/docs/build-bot.md +++ b/docs/build-bot.md @@ -60,8 +60,9 @@ Build-bot: :[Platforms] ... * `core`: build Keyman Core, on all three build platforms, and also WASM (note: `release` is equivalent to `build` buildLevel for `core`) -The list of platforms can be found in the `available_platforms` variable in -trigger-definitions.inc.sh. +The list of platforms is built from the `available_platforms_regex` variable in +trigger-definitions.inc.sh (which is a superset of `available_platforms`, with +the addition of `common` and `core`). ## Build Level diff --git a/resources/build/ci/trigger-definitions.inc.sh b/resources/build/ci/trigger-definitions.inc.sh index aeded92eb4..bac1cf99de 100644 --- a/resources/build/ci/trigger-definitions.inc.sh +++ b/resources/build/ci/trigger-definitions.inc.sh @@ -19,7 +19,7 @@ readonly build_level_release=release readonly valid_build_levels="$build_level_skip|$build_level_build|$build_level_release" # -# Target platforms +# Target platforms (note: 'common' matches all common* platforms in build bot commands) # available_platforms=(android common_web common_windows common_mac common_linux ios linux mac web windows developer) From 25c3490ecd28d8ae41ca9cdc8c1acde869bf9ebf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:58:59 +0000 Subject: [PATCH 41/57] chore(deps): bump fast-xml-parser from 5.3.4 to 5.3.8 Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.4 to 5.3.8. - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.4...v5.3.8) --- updated-dependencies: - dependency-name: fast-xml-parser dependency-version: 5.3.8 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- developer/src/common/web/utils/package.json | 2 +- package-lock.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json index 0c31f64e46..06d23d5364 100644 --- a/developer/src/common/web/utils/package.json +++ b/developer/src/common/web/utils/package.json @@ -11,7 +11,7 @@ "dependencies": { "@keymanapp/common-types": "*", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.3.4", + "fast-xml-parser": "^5.3.8", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", diff --git a/package-lock.json b/package-lock.json index 35a28a2593..2f9fac45bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,7 +242,7 @@ "dependencies": { "@keymanapp/common-types": "*", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.3.4", + "fast-xml-parser": "^5.3.8", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -7909,9 +7909,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.8.tgz", + "integrity": "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw==", "funding": [ { "type": "github", @@ -7920,7 +7920,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -12867,9 +12867,9 @@ "link": true }, "node_modules/strnum": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.0.tgz", - "integrity": "sha512-w0S//9BqZZGw0L0Y8uLSelFGnDJgTyyNQLmSlPnVz43zPAiqu3w4t8J8sDqqANOGeZIZ/9jWuPguYcEnsoHv4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", From 7df8eac192a4af144632598aa33fe8d8268222d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:07:21 +0000 Subject: [PATCH 42/57] chore(deps-dev): bump minimatch in /resources/build/pr-build-status Bumps [minimatch](https://github.com/isaacs/minimatch) from 9.0.5 to 9.0.9. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9) --- updated-dependencies: - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- resources/build/pr-build-status/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/build/pr-build-status/package-lock.json b/resources/build/pr-build-status/package-lock.json index e2980aed2c..0b48fb68b1 100644 --- a/resources/build/pr-build-status/package-lock.json +++ b/resources/build/pr-build-status/package-lock.json @@ -842,13 +842,13 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" From c0f09472a2b1bfd228085887b41819a01a06730d Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 6 Mar 2026 12:03:51 -0600 Subject: [PATCH 43/57] auto: increment master version to 19.0.212 Test-bot: skip Build-bot: skip --- HISTORY.md | 18 ++++++++++++++++++ VERSION.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 96cd3d9172..1a4d2e585a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,23 @@ # Keyman Version History +## 19.0.211 alpha 2026-03-06 + +* chore(deps-dev): bump basic-ftp from 5.0.5 to 5.2.0 (#15631) +* chore(deps-dev): bump rollup from 4.22.4 to 4.59.0 (#15632) +* chore(deps): bump minimatch and @microsoft/api-extractor (#15647) +* chore(deps): bump minimatch from 10.1.1 to 10.2.4 in /developer/src/server/src/win32/trayicon/addon-src (#15648) +* chore(deps): bump tar from 7.5.7 to 7.5.10 in /developer/src/server/src/win32/trayicon/addon-src (#15682) +* chore(deps): bump multer from 2.0.2 to 2.1.1 (#15681) +* chore(web): remove implicit any type when setting processingConfig.itemIdentifier (#15689) +* maint(common): rollback from Typescript 5.8.2 to 5.4.x (#15690) +* docs: tweak build bot docs and update imsample gitignore (#15691) +* maint(developer): make `TestCompilerCallbacks` usage more consistent and simplify (#15665) +* maint(developer): cleanup additional TestCompilerCallbacks references (#15684) +* fix(developer): prevent new projects from overwriting existing projects (#15669) +* fix(developer): retrieve debug memo anchor without side effects (#15672) +* fix(developer): ngrok upgrade (#15626) +* chore(deps-dev): bump minimatch from 9.0.5 to 9.0.9 in /resources/build/pr-build-status (#15650) + ## 19.0.210 alpha 2026-03-05 * fix(developer): apply keyboard description in Windows keyboard import (#15667) diff --git a/VERSION.md b/VERSION.md index 27040665fd..00884e84cc 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.211 \ No newline at end of file +19.0.212 \ No newline at end of file From 13ba5c8f8ea6523c785a4f10042d1c3338996428 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 9 Mar 2026 08:20:09 +0700 Subject: [PATCH 44/57] chore(android): Remove unused status vars --- .../src/main/java/com/keyman/android/SystemKeyboard.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index a8d71a450f..16330bfcda 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -203,8 +203,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven return value, so we test for that as well (#11479) */ if (icText != null && icText.text != null) { - boolean didUpdateText = KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString()); - boolean didUpdateSelection = KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM); + // Update the text selection but ignore the returned statuses + KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString()); + KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM); } } From 5129a482b4d98b9c2c7ec01fc6aac3f6e10348bf Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 9 Mar 2026 06:21:25 +0100 Subject: [PATCH 45/57] fix(developer): await copier result before reporting success or failure Without `await`, a 'success' message is always returned, because the Promise that is returned is not nullish. While the log message was misleading, the outcome was already correct, because the parent function `copyProject()` did correctly await the call to `doCopy()`. Fixes: #15699 Test-bot: skip --- developer/src/kmc/src/commands/copy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts index 9cfef2b957..b53c9eab93 100644 --- a/developer/src/kmc/src/commands/copy.ts +++ b/developer/src/kmc/src/commands/copy.ts @@ -81,7 +81,7 @@ async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string const dest = options.outPath; callbacks.reportMessage(InfrastructureMessages.Info_CopyingProject({source, dest})); - const result = runCopier(callbacks, source, options); + const result = await runCopier(callbacks, source, options); if(result) { callbacks.reportMessage(InfrastructureMessages.Info_ProjectCopiedSuccessfully({source, dest})); } else { From a8f5ee336283ef639cc08b3440637bd484b49563 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 9 Mar 2026 06:11:12 +0100 Subject: [PATCH 46/57] fix(developer): resolve input project path for `kmc copy` Relative paths would cause `kmc copy` to fail to find sources files for the project, because component paths would be constructed incorrectly. The cleanest fix is to ensure that we always full resolve local file paths before attempting to copy the project. Fixes: #15659 --- developer/src/kmc-copy/src/KeymanProjectCopier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index 545163aded..4218cc6e94 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -201,7 +201,7 @@ export class KeymanProjectCopier implements KeymanCompiler { */ private getLocalFolderProject(source: string): string { source = this.normalizePath(source); - const projectSource = this.normalizePath(this.callbacks.path.join(source, this.callbacks.path.basename(source) + KeymanFileTypes.Source.Project)); + const projectSource = this.normalizePath(this.callbacks.path.resolve(this.callbacks.path.join(source, this.callbacks.path.basename(source) + KeymanFileTypes.Source.Project))); if(!this.callbacks.fs.existsSync(projectSource)) { this.callbacks.reportMessage(CopierMessages.Error_CannotFindInputProject({project: source})); return null; @@ -216,7 +216,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * @returns */ private getLocalFileProject(source: string): string { - return this.normalizePath(source); + return this.normalizePath(this.callbacks.path.resolve(source)); } /** From 89c9c615e5d572ae10a50440fd248fb0d977f40a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 9 Mar 2026 07:31:51 +0100 Subject: [PATCH 47/57] fix(developer): correct INVALID_HANDLE_VALUE check in UnlockProject Fixes: #15674 Test-bot: skip Build-bot: skip --- .../src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas index 866fb38ee6..9f09db3d8f 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas @@ -148,7 +148,7 @@ end; procedure UnlockProject; begin - if not hLockFile = INVALID_HANDLE_VALUE then + if hLockFile = INVALID_HANDLE_VALUE then begin Exit; end; From f2f3e32e691b57873d18b27ebac88c01f6dc42e4 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 9 Mar 2026 07:38:13 +0100 Subject: [PATCH 48/57] fix(developer): use trimmed version of paths when verifying in New Project Fixes: #15673 Build-bot: skip Test-bot: skip --- ...an.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas | 2 +- ...veloper.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas | 2 +- ...man.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas | 2 +- ...eveloper.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas | 2 +- ...eyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas | 2 +- .../Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas index 3ccf96289f..8e5979da10 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas @@ -337,7 +337,7 @@ begin then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editProjectID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, ProjectID); end; procedure TfrmCloneGitHubProjectParameters.UpdateProjectFilename; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas index b0e491f0c6..c3f09a6406 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas @@ -286,7 +286,7 @@ end; function TfrmCloneKeymanCloudProjectParameters.Validate: Boolean; begin Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, KeyboardID); end; procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string); diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas index d986cf56ac..8f5a755e43 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas @@ -254,7 +254,7 @@ begin then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editProjectID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, ProjectID); end; procedure TfrmCloneLocalProjectParameters.UpdateProjectFilename; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas index 61ad52e4f2..5d9f13c311 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas @@ -359,7 +359,7 @@ begin end; Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, KeyboardID); end; { Languages Grid } diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas index 36b06f72d4..a4424f82df 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas @@ -481,7 +481,7 @@ begin end; Result := TLexicalModelUtils.DoesProjectFilenameFollowLexicalModelConventions(editModelID.Text + Ext_LexicalModelProject); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editModelID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, ModelID); end; { Dynamically update other fields based on filled details } diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas index 749638650b..f82d10fb12 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas @@ -491,7 +491,7 @@ begin end; Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); - Result := Result and VerifyNewProjectPathWithUser(editPath.Text, editKeyboardID.Text); + Result := Result and VerifyNewProjectPathWithUser(BasePath, KeyboardID); end; { Languages Grid } From 42bfe9d5709a8e2f70d303d643f81cac3110b5bf Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 9 Mar 2026 13:01:12 +0100 Subject: [PATCH 49/57] maint(linux): set git credential for linux help on the correct repo Test-bot: skip Build-bot: skip --- resources/teamcity/linux/keyman-linux-release.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/teamcity/linux/keyman-linux-release.sh b/resources/teamcity/linux/keyman-linux-release.sh index 83f85d7ab9..be29f77d97 100755 --- a/resources/teamcity/linux/keyman-linux-release.sh +++ b/resources/teamcity/linux/keyman-linux-release.sh @@ -117,8 +117,10 @@ function _publish_to_launchpad() { function _publish_linux_help() { builder_echo start "upload linux help" "Upload new Keyman Linux help to help.keyman.com" + cd "${HELP_KEYMAN_COM}" # shellcheck disable=SC2016 git config credential.helper '!f() { sleep 1; echo "username=${GITHUB_USER}"; echo "password=${GITHUB_TOKEN}"; }; f' + cd "${KEYMAN_ROOT}/resources/build" "${KEYMAN_ROOT}/resources/build/ci/help-keyman-com.sh" linux builder_echo end "upload linux help" success "Upload new Keyman Linux help to help.keyman.com" From 7e7dbc1f6d2b90d1ed63441e674a3881eb4d613f Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 9 Mar 2026 07:38:27 -0500 Subject: [PATCH 50/57] auto: cherry-pick 18.0.247 history to alpha Build-bot: skip Test-bot: skip --- HISTORY.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 1a4d2e585a..5928f341fd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1265,6 +1265,26 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.247 stable 2026-03-09 + +* chore(linux): Update debian changelog (#15556) +* chore(linux): update copyright year and standards version (#15558) +* fix(mac): retry for mac disk detach (#15577) +* maint(mac): upgrade to Xcode 26.2 (#15575) +* maint(ios): upgrade to Xcode 26.2 (#15614) +* fix(developer): handle CRLF as CR internally in LDML debugger (#15660) +* fix(developer): handle .kps file that has no `` element (#15661) +* fix(developer): use correct return values and nullish checks in package-validation (#15662) +* fix(developer): handle window already destroyed when closing editor (#15663) +* fix(developer): package compiler handle missing `info` section in .kps file (#15664) +* fix(developer): apply keyboard description in Windows keyboard import (#15668) +* fix(developer): improve UI feedback on upgrade (#15686) +* fix(developer): prevent new projects from overwriting existing projects (#15694) +* fix(developer): retrieve debug memo anchor without side effects (#15695) +* fix(developer): ngrok upgrade (#15633) +* fix(developer): await copier result before reporting success or failure (#15702) +* fix(developer): resolve input project path for `kmc copy` (#15703) + ## 18.0.246 stable 2026-02-04 * cherrypick(common): Update FirstVoices versions in keyboards.csv (#15273) From ad5c99ea59283019f26dccf9e54f178ca4a86b32 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 9 Mar 2026 13:02:20 -0500 Subject: [PATCH 51/57] auto: increment master version to 19.0.213 Test-bot: skip Build-bot: skip --- HISTORY.md | 9 +++++++++ VERSION.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 5928f341fd..138be3843a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ # Keyman Version History +## 19.0.212 alpha 2026-03-09 + +* chore(android): Cleanup unused Extracted Text (#15671) +* chore(deps): bump fast-xml-parser from 5.3.4 to 5.3.8 (#15649) +* fix(developer): resolve input project path for `kmc copy` (#15700) +* fix(developer): await copier result before reporting success or failure (#15701) +* fix(developer): correct INVALID_HANDLE_VALUE check in UnlockProject (#15704) +* fix(developer): use trimmed version of paths when verifying in New Project (#15705) + ## 19.0.211 alpha 2026-03-06 * chore(deps-dev): bump basic-ftp from 5.0.5 to 5.2.0 (#15631) diff --git a/VERSION.md b/VERSION.md index 00884e84cc..a814ae51dd 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.212 \ No newline at end of file +19.0.213 \ No newline at end of file From 160ed7b08bec905e3314d7ec81f5fcdfbed427ed Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 10 Mar 2026 13:16:49 +0100 Subject: [PATCH 52/57] maint(resources): use x86_64 emscripten on arm/windows builds Fixes: #15388 Test-bot: skip --- resources/locate_emscripten.inc.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/locate_emscripten.inc.sh b/resources/locate_emscripten.inc.sh index 67411b8391..50c8534ef4 100644 --- a/resources/locate_emscripten.inc.sh +++ b/resources/locate_emscripten.inc.sh @@ -4,6 +4,15 @@ # shellcheck disable=SC2154 . "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" +# On Windows/ARM64 machines, we build with the Emscripten x64 toolchain + +if builder_is_windows; then + if [[ "${MSYSTEM-x}" == CLANGARM64 ]]; then + builder_echo "Using x86_64 for emscripten" + export EMSDK_ARCH=x86_64 + fi +fi + # # We don't want to rely on emcc being on the path, because Emscripten puts far # too many things onto the path (in particular for us, node). From 3811980bf196533045658aeeb530efa2c19bda29 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 10 Mar 2026 17:58:46 +0100 Subject: [PATCH 53/57] chore(linux): Update debian changelog (cherry picked from commit 0d22c08f5907b418a45bf5cbc525fd1b598ea852) --- linux/debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/linux/debian/changelog b/linux/debian/changelog index 517b32e339..516340d09b 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,9 @@ +keyman (18.0.247-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz Tue, 10 Mar 2026 17:58:33 +0100 + keyman (18.0.246-1) unstable; urgency=medium * New upstream release. From 3174e4f963b0bac7cd617f62ea3c2ffeed306d62 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 10 Mar 2026 13:01:04 -0500 Subject: [PATCH 54/57] auto: increment master version to 19.0.214 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 138be3843a..1cfa8a2b28 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.213 alpha 2026-03-10 + +* maint(linux): set git credential for linux help on the correct repo (#15707) + ## 19.0.212 alpha 2026-03-09 * chore(android): Cleanup unused Extracted Text (#15671) diff --git a/VERSION.md b/VERSION.md index a814ae51dd..a3e5b9fada 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.213 \ No newline at end of file +19.0.214 \ No newline at end of file From 781bd4c548fa95a5669def4349dd8cf03d711590 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 12 Mar 2026 02:14:27 -0500 Subject: [PATCH 55/57] auto: cherry-pick 18.0.248 history to alpha Build-bot: skip Test-bot: skip --- HISTORY.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 138be3843a..cd87931eb7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1274,6 +1274,10 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.248 stable 2026-03-12 + +* fix(ios): Revert Sentry to 8.38.0 to fix FirstVoices crash on startup with 18.0.247 (#15726) + ## 18.0.247 stable 2026-03-09 * chore(linux): Update debian changelog (#15556) From 03caf87fcd90e791932083fe07bbfc93aac7b8e0 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 12 Mar 2026 13:02:20 -0500 Subject: [PATCH 56/57] auto: increment master version to 19.0.215 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 8f0fd51151..45405b8d55 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.214 alpha 2026-03-12 + +* chore(linux): Update debian changelog (#15718) +* maint(resources): use x86_64 emscripten on arm/windows builds (#15715) + ## 19.0.213 alpha 2026-03-10 * maint(linux): set git credential for linux help on the correct repo (#15707) diff --git a/VERSION.md b/VERSION.md index a3e5b9fada..9daf8a196e 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.214 \ No newline at end of file +19.0.215 \ No newline at end of file From ae60aaca53e8c8d24a13f1dcba93c86b75dcfe6c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 16 Mar 2026 04:58:37 +0100 Subject: [PATCH 57/57] chore(developer): remove old @types/mocha dep --- developer/src/kmc-convert/package.json | 1 - package-lock.json | 8 -------- 2 files changed, 9 deletions(-) diff --git a/developer/src/kmc-convert/package.json b/developer/src/kmc-convert/package.json index 39ac2a8c97..94a0cdf704 100644 --- a/developer/src/kmc-convert/package.json +++ b/developer/src/kmc-convert/package.json @@ -34,7 +34,6 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", diff --git a/package-lock.json b/package-lock.json index 61b0dca6e0..f7b5b1c7ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -372,7 +372,6 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", @@ -380,13 +379,6 @@ "typescript": "^5.4.5" } }, - "developer/src/kmc-convert/node_modules/@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-convert/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",