Merge branch 'keymanapp:master' into main

This commit is contained in:
memaynor 2026-04-08 13:10:38 +07:00 • committed by GitHub
commit 2834c1239c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1139 additions and 276 deletions

View file

@ -25,7 +25,7 @@ runs:
using: 'composite'
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts/keyman-srcpkg
@ -49,7 +49,7 @@ runs:
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Store binary packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-binarypkgs-${{ inputs.dist }}_${{ inputs.arch }}
path: |

195
.github/labeler.yml vendored
View file

@ -5,86 +5,173 @@
# common ones. The others are commented out. There is still some variance between
# folder names and labels; consider this documentation of that ;-)
docs: docs/**
docs:
- changed-files:
- any-glob-to-any-file: 'docs/**'
#
# Add labels based on changed files using actions/labeler
#
android/:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/: android/KMAPro/**
android/engine/: android/KMEA/**
android/samples/: android/Samples/**
- changed-files:
- any-glob-to-any-file:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/:
- changed-files:
- any-glob-to-any-file:
- android/KMAPro/**
android/engine/:
- changed-files:
- any-glob-to-any-file:
- android/KMEA/**
android/samples/:
- changed-files:
- any-glob-to-any-file:
- android/Samples/**
common/:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/: common/web/**
- changed-files:
- any-glob-to-any-file:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/:
- changed-files:
- any-glob-to-any-file:
- common/web/**
core/:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
- changed-files:
- any-glob-to-any-file:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
developer/:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
- changed-files:
- any-glob-to-any-file:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
developer/compilers/:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
- changed-files:
- any-glob-to-any-file:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
developer/ide/:
- developer/src/server/**
- developer/src/tike/**
- changed-files:
- any-glob-to-any-file:
- developer/src/server/**
- developer/src/tike/**
ios/:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/: ios/keyman/**
ios/engine/: ios/engine/**
ios/samples/: ios/samples/**
- changed-files:
- any-glob-to-any-file:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/:
- changed-files:
- any-glob-to-any-file:
- ios/keyman/**
ios/engine/:
- changed-files:
- any-glob-to-any-file:
- ios/engine/**
ios/samples/:
- changed-files:
- any-glob-to-any-file:
- ios/samples/**
linux/:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/: linux/keyman-config/**
linux/engine/: linux/ibus-keyman/**
- changed-files:
- any-glob-to-any-file:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/:
- changed-files:
- any-glob-to-any-file:
- linux/keyman-config/**
linux/engine/:
- changed-files:
- any-glob-to-any-file:
- linux/ibus-keyman/**
mac/:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
- changed-files:
- any-glob-to-any-file:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
# mac/config/:
# mac/engine/: mac/**
oem/: oem/**
oem/fv/: oem/firstvoices/**
oem/fv/android/: oem/firstvoices/android/**
oem/fv/ios/: oem/firstvoices/ios/**
oem/fv/windows/: oem/firstvoices/windows/**
oem/:
- changed-files:
- any-glob-to-any-file:
- oem/**
oem/fv/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/**
oem/fv/android/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/android/**
oem/fv/ios/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/ios/**
oem/fv/windows/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/windows/**
resources/: resources/**
resources/:
- changed-files:
- any-glob-to-any-file:
- resources/**
web/:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
- changed-files:
- any-glob-to-any-file:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
# web/bookmarklet/
web/engine/: web/source/**
web/ui/: web/source/kmwui*
web/samples/: web/samples/**
web/predictive-text/: web/src/engine/predictive-text/**
web/engine/:
- changed-files:
- any-glob-to-any-file:
- web/source/**
web/ui/:
- changed-files:
- any-glob-to-any-file:
- web/source/kmwui*
web/samples/:
- changed-files:
- any-glob-to-any-file:
- web/samples/**
web/predictive-text/:
- changed-files:
- any-glob-to-any-file:
- web/src/engine/predictive-text/**
windows/:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/: windows/src/desktop/**
windows/engine/: windows/src/engine/**
- changed-files:
- any-glob-to-any-file:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/:
- changed-files:
- any-glob-to-any-file:
- windows/src/desktop/**
windows/engine/:
- changed-files:
- any-glob-to-any-file:
- windows/src/engine/**

View file

@ -27,7 +27,7 @@ jobs:
steps:
- name: Restore artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts
@ -55,7 +55,7 @@ jobs:
- name: Checkout
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ steps.environment_step.outputs.GIT_SHA }}'
fetch-depth: 0
@ -84,7 +84,7 @@ jobs:
- name: Archive .symbols file
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libkeymancore.symbols
path: ${{ github.workspace }}/keyman/linux/debian/tmp/DEBIAN/symbols

View file

@ -41,7 +41,7 @@ jobs:
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Docker images
id: build
run: |
@ -67,7 +67,7 @@ jobs:
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test Docker images
id: test

View file

@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
@ -64,7 +64,7 @@ jobs:
npm -v
node -v
- uses: actions/setup-python@v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
@ -119,7 +119,7 @@ jobs:
- name: Install meson
shell: bash
run: |
pip3 install --user meson
pip3 install --user meson==1.10.1
- name: Run test
shell: bash

View file

@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: crowdin action
uses: crowdin/github-action@v2.7.0

View file

@ -43,7 +43,7 @@ jobs:
PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
@ -121,7 +121,7 @@ jobs:
echo "- $(find . -name keyman_\*.dsc)" >> $GITHUB_STEP_SUMMARY
- name: Store source package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-srcpkg
path: |
@ -140,7 +140,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -166,7 +166,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -204,13 +204,13 @@ jobs:
sudo rm -rf /usr/share/dotnet
- name: Download Source Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts
- name: Download Binary Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-binarypkgs-*
@ -281,7 +281,7 @@ jobs:
echo "::endgroup::"
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-signedpkgs
@ -343,7 +343,7 @@ jobs:
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-*
@ -362,7 +362,7 @@ jobs:
echo "SKIP_API_CHECK=${{ github.event.client_payload.skipApiCheck }}" >> artifacts/env
- name: Cache artifacts
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts

View file

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update labels based on changed files
uses: actions/labeler@v4
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Update labels based on PR title

View file

@ -50,7 +50,7 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'

View file

@ -1,40 +1,40 @@
# GENERATED FILE - DO NOT EDIT!
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
with:
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the

View file

@ -1,5 +1,39 @@
# Keyman Version History
## 19.0.221 alpha 2026-03-27
* fix(common): add missing `default` property to keyman-touch-layout.clean.spec.json (#15787)
* chore(deps): bump picomatch from 4.0.3 to 4.0.4 in /developer/src/server/src/win32/trayicon/addon-src (#15786)
* chore(deps): bump picomatch from 2.3.1 to 2.3.2 (#15784)
* chore(deps-dev): bump flatted from 3.2.5 to 3.4.2 (#15773)
* chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 in /developer/src/server/src/win32/trayicon/addon-src (#15791)
## 19.0.220 alpha 2026-03-26
* fix(mac): improved adherence to backspace rules for compliant apps (#15561)
## 19.0.219 alpha 2026-03-25
* maint(common): Update GitHub actions for Node 24 (#15762)
* fix(core): fix keydown/up handling for LDML keyboards (#15609)
## 19.0.218 alpha 2026-03-24
* fix(developer): define globalThis for compiled custom lexical models (#15777)
## 19.0.217 alpha 2026-03-17
* maint(mac): search for brew-installed rsync (#15765)
## 19.0.216 alpha 2026-03-16
* docs: tweak walkthrough content (#15712)
* chore(deps): bump tar from 7.5.10 to 7.5.11 in /developer/src/server/src/win32/trayicon/addon-src (#15723)
* docs(linux): add documentation how input methods work in GTK (#15749)
* docs(core): improve keyhandling doc (#15736)
* maint(resources): try meson 1.10.1 (#15754)
* maint(ios): clean carthage before builds (#15757)
## 19.0.215 alpha 2026-03-13
* chore(linux): fix dependency of Debian test suite (#15735)
@ -1288,6 +1322,15 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.249 stable 2026-03-27
* chore(linux): Update debian changelog (#15717)
* chore(linux): fix dependency of Debian test suite (#15734)
* chore(linux): Update debian changelog (#15731)
* maint(ios): Upgrade Sentry to 8.58.0 to support XCode 26 (#15755)
* fix(developer): define globalThis for compiled custom lexical models (#15778)
* fix(common): add missing `default` property to keyman-touch-layout.clean.spec.json (#15792)
## 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)

View file

@ -1 +1 @@
19.0.216
19.0.222

View file

@ -457,6 +457,10 @@ string") into the appropriate spec format.
# .keyman-touch-layout version history
## 2026-03-26 2.1.2 stable
* Add missing 'default' property for longpress (sk) keys to clean spec. No other
changes.
## 2024-02-23 2.1.1 stable
* Loosen `layer.id` requirements to any non-whitespace characters, recommend
only alphanumeric, -, _. clean spec enforces this recommendation.

View file

@ -85,9 +85,9 @@
"text": { "type": "string" },
"layer": { "$ref": "#/definitions/layer-id" },
"nextlayer": { "$ref": "#/definitions/layer-id" },
"font": { "$ref": "#/definitions/font-spec" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"sp": { "$ref": "#/definitions/key-sp" },
"font": { "$ref": "#/definitions/font-spec" },
"sp": { "$ref" : "#/definitions/key-sp" },
"pad": { "$ref" : "#/definitions/key-pad" },
"width": { "$ref" : "#/definitions/key-width" },
"sk": { "$ref": "#/definitions/subkeys" },
@ -140,11 +140,12 @@
"text": { "type": "string" },
"layer": { "$ref": "#/definitions/layer-id" },
"nextlayer": { "$ref": "#/definitions/layer-id" },
"font": { "$ref": "#/definitions/font-spec" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"sp": { "$ref": "#/definitions/key-sp" },
"sp": { "$ref" : "#/definitions/key-sp" },
"pad": { "$ref" : "#/definitions/key-pad" },
"width": { "$ref" : "#/definitions/key-width" }
"width": { "$ref" : "#/definitions/key-width" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"font": { "$ref": "#/definitions/font-spec" },
"default": { "type": "boolean" }
},
"required": ["id"],
"additionalProperties": false

View file

@ -1,42 +1,39 @@
# 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.
While Keyman Core does not generally emit actions from key up events,
the `emit_keystroke` value for a key up event will match the corresponding
key down event, in order to avoid 'stuck key' scenarios'.
Usually the processor won't handle any frame keys and let the application
deal with it. This allows shortcut keys like <kbd>Ctrl</kbd>+<kbd>C</kbd>
to work. The only exception is the <kbd>Backspace</kbd> key which is
handled internally if enough context is available. Regular keys will be
handled by the processor.
to work. One exception is the <kbd>Backspace</kbd> key which is
handled internally if enough context is available. Keyboards can define
rules for other frame keys, e.g. <kbd>Ctrl</kbd>+Key which are then
also handled internally. 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 <kbd>a</kbd> will output
`a` if no rule is defined), whereas CLDR/LDML keyboards will suppress
any output for that key.
any output for that key. However, `km_core_actions.emit_keystroke` will
be `FALSE` for both KMN and CLR/LDML keyboards.
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: | | |
|<kbd>Enter</kbd> | TRUE | TRUE |
|<kbd>Backspace</kbd>¹ | FALSE | FALSE |
|<kbd>Backspace</kbd>² | TRUE | TRUE |
|<kbd>Ctrl</kbd>+Key | TRUE | TRUE |
|Modifier key <kbd>Shift</kbd>| TRUE | TRUE |
|Modifier key <kbd>Ctrl</kbd> | TRUE | TRUE |
|Modifier key <kbd>LAlt</kbd> | TRUE | TRUE |
|Type | KeyDown/Up |
|-----------------------------|------------|
|Core key with rule | FALSE |
|Core key w/o rule | FALSE |
|Framekeys: | |
|<kbd>Enter</kbd> | TRUE |
|<kbd>Backspace</kbd>¹ | FALSE |
|<kbd>Backspace</kbd>² | TRUE |
|<kbd>Ctrl</kbd>+Key | TRUE |
|Modifier key <kbd>Shift</kbd>| TRUE |
|Modifier key <kbd>Ctrl</kbd> | TRUE |
|Modifier key <kbd>LAlt</kbd> | TRUE |
1: context available<br/>
2: without or with empty context

View file

@ -50,6 +50,9 @@ km_core_process_event(km_core_state *state,
if(state == nullptr) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
if (vk == KM_CORE_VKEY_BKSP && is_key_down) {
state->set_backspace_handled_internally(false);
}
km_core_status status = state->processor().process_event(state, vk, modifier_state, is_key_down, event_flags);
if (state_should_invalidate_context(state, vk, modifier_state, is_key_down, event_flags)) {
@ -78,6 +81,10 @@ km_core_process_event(km_core_state *state,
state->apply_actions_and_merge_app_context();
if (vk == KM_CORE_VKEY_BKSP) {
state->set_backspace_handled_internally(!state->action_struct().emit_keystroke);
}
return status;
}

View file

@ -179,21 +179,13 @@ ldml_processor::process_event(
ldml_state.clear();
try {
if (!is_key_down) {
process_key_up(ldml_state);
if (vk == KM_CORE_VKEY_BKSP) {
process_backspace(ldml_state);
} else if (is_key_down) {
process_key_down(ldml_state);
} else {
switch (vk) {
// Currently, only one VK gets spoecial treatment.
// Special handling for backspace VK
case KM_CORE_VKEY_BKSP:
process_backspace(ldml_state);
break;
default:
// all other VKs
process_key_down(ldml_state);
} // end of switch
} // end of normal processing
process_key_up(ldml_state);
}
// all key-up and key-down events end up here.
// commit the ldml state into the core state
ldml_state.commit();
@ -210,11 +202,31 @@ void
ldml_processor::process_key_up(ldml_event_state &ldml_state)
const {
// TODO-LDML: Implement caps lock handling
ldml_state.clear();
// Look up the key
bool found = false;
const std::u16string key_str = keys.lookup(ldml_state.get_vk(), ldml_state.get_modifier_state(), found);
if (!found) {
ldml_state.emit_passthrough_keystroke();
}
}
void
ldml_processor::process_backspace(ldml_event_state &ldml_state) const {
if (ldml_state.get_modifier_state() & K_MODIFIERFLAG) {
// we never process modifier+bksp
ldml_state.emit_passthrough_keystroke();
return;
}
if (!ldml_state.is_key_down()) {
if (!ldml_state.get_state()->backspace_handled_internally()) {
ldml_state.emit_passthrough_keystroke();
}
return;
}
if (!!bksp_transforms) {
// process with an empty string via the bksp transforms
auto matchedContext = process_output(ldml_state, std::u32string(), bksp_transforms.get());

View file

@ -176,6 +176,9 @@ public:
* @return the number of context items consumed
*/
size_t context_to_string(std::u32string &str, bool include_markers = true);
km_core_state* get_state() const { return state; }
uint8_t is_key_down() const { return _is_key_down; }
private:
km_core_virtual_key _vk;

View file

@ -52,6 +52,7 @@ state::state(km::core::abstract_processor & ap, km_core_option_item const *env)
env->key,
env->value);
}
_backspace_handled_internally = false;
_imx_callback = nullptr;
_imx_object = nullptr;
memset(const_cast<km_core_actions*>(&_action_struct), 0, sizeof(km_core_actions));
@ -117,4 +118,4 @@ void state::apply_actions_and_merge_app_context() {
}
this->_action_struct.deleted_context = km::core::get_deleted_context(app_context_for_deletion, this->_action_struct.code_points_to_delete);
}
}

View file

@ -130,6 +130,7 @@ protected:
core::debug_items _debug_items;
km_core_keyboard_imx_platform _imx_callback;
void *_imx_object;
bool _backspace_handled_internally;
public:
state(core::abstract_processor & kb, km_core_option_item const *env);
@ -174,6 +175,22 @@ public:
km_core_actions const &actions
);
void apply_actions_and_merge_app_context();
/**
* This is used to track whether the backspace key was handled internally
* during the keydown event. This is needed so that we can return the same
* value from the keyup event as we did for the keydown event.
*
* Backspace is the only key that we sometimes handle internally (if we
* have enough context) and sometimes not. By the time we get the keyup
* event the context already got updated and so we have no way of knowing
* whether or not the keydown handled it internally. Therefore this
* flag exists.
*
* Only used when processing KM_CORE_VKEY_BKSP with LDML keyboards.
*/
void set_backspace_handled_internally(bool handled) { _backspace_handled_internally = handled; }
bool backspace_handled_internally() const { return _backspace_handled_internally; }
};
} // namespace core
} // namespace km

View file

@ -0,0 +1,355 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
#include <gtest/gtest.h>
#include "path.hpp"
#include "state.hpp"
#include "kmx/kmx_processevent.h"
#include "emscripten_filesystem.h"
#include "kmnkbd/action_items.hpp"
#include "load_kmx_file.hpp"
using namespace km::core::kmx;
km::core::path test_dir;
km_core_option_item test_env_opts[] = {KM_CORE_OPTIONS_END};
#define KEY_DOWN TRUE
#define KEY_UP FALSE
struct TestData {
const char* test_name;
const char* keyboard_name;
km_core_virtual_key vkey;
uint16_t modifier_state;
// Initial context
km_core_cu const* context;
// Whether keydown handled the event. Only relevant for keyup tests.
bool keydown_handled;
// Expected actions for keydown and keyup
std::initializer_list<km_core_action_item> keydown_actions;
std::initializer_list<km_core_action_item> keyup_actions;
};
std::string GenerateTestName(const testing::TestParamInfo<TestData>& info) {
return info.param.test_name;
}
class ProcessEventTests : public testing::TestWithParam<TestData> {
protected:
km_core_keyboard* keyboard = nullptr;
km_core_state* state = nullptr;
KMX_ProcessEvent process_event;
void Initialize(TestData const& data) {
km::core::path kmxfile = km::core::path(test_dir / data.keyboard_name);
auto blob = km::tests::load_kmx_file(kmxfile.native().c_str());
EXPECT_EQ(km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), blob.data(), blob.size(), &this->keyboard), KM_CORE_STATUS_OK);
EXPECT_EQ(km_core_state_create(this->keyboard, test_env_opts, &this->state), KM_CORE_STATUS_OK);
EXPECT_TRUE(this->process_event.Load(blob.data(), blob.size()));
if (data.context) {
EXPECT_EQ(km_core_state_context_set_if_needed(this->state, data.context), KM_CORE_CONTEXT_STATUS_UPDATED);
}
((km::core::state*)this->state)->set_backspace_handled_internally(data.keydown_handled);
}
void TearDown() override {
if (this->state) {
km_core_state_dispose(this->state);
this->state = nullptr;
}
if (this->keyboard) {
km_core_keyboard_dispose(this->keyboard);
this->keyboard = nullptr;
}
}
};
void print_all_action_items(km_core_state const* state) {
size_t n = 0;
auto act = km_core_state_action_items(state, &n);
std::cout << "Action items:" << std::endl;
for (size_t i = 0; i < n; i++) {
print_action_item("", *act++);
}
std::cout << "---------------" << std::endl;
}
TEST_P(ProcessEventTests, ReturnsExpectedActionsForKeyDown) {
auto data = GetParam();
Initialize(data);
EXPECT_EQ(km_core_process_event(this->state, data.vkey, data.modifier_state,
KEY_DOWN, KM_CORE_EVENT_FLAG_DEFAULT), KM_CORE_STATUS_OK);
// print_all_action_items(this->state);
EXPECT_TRUE(action_items(this->state, data.keydown_actions));
}
TEST_P(ProcessEventTests, ReturnsExpectedActionsForKeyUp) {
auto data = GetParam();
Initialize(data);
EXPECT_EQ(km_core_process_event(this->state, data.vkey, data.modifier_state, KEY_UP, KM_CORE_EVENT_FLAG_DEFAULT), KM_CORE_STATUS_OK);
// print_all_action_items(this->state);
EXPECT_TRUE(action_items(this->state, data.keyup_actions));
}
union backspace_union {
km_core_backspace_item backspace;
uint32_t value;
};
// const backspace_union backspace_with_context = {{KM_CORE_BT_CHAR, 'x'}};
const backspace_union backspace_no_context = {{KM_CORE_BT_CHAR, 0}};
const TestData values[] = {
//--------------------------------------------------------------------
// KMN
// Key with rule
{"KMN_VKey_A", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_A, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'d'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
{"KMN_Ctrl_VKey_A", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_A, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Key without rule
{"KMN_VKey_X", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_X, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'x'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
{"KMN_Ctrl_VKey_X", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_X, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Frame key without rule
{"KMN_VKey_Enter", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_ENTER, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// // Backspace (with available context)
// // TODO: fix the implementation to make this test pass. Currently failing in KeyUp
// {"KMN_VKey_Backspace_Ctxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, u"x", true,
// { // KeyDown
// // Once we use C++ 20 we can use:
// //{KM_CORE_IT_BACK, { 0, }, {.backspace = {KM_CORE_BT_CHAR, 'x'}}},
// {KM_CORE_IT_BACK, { 0, }, {backspace_with_context.value}},
// {KM_CORE_IT_END}
// },
// { // KeyUp
// {KM_CORE_IT_END}
// }
// },
// Backspace (without context)
{"KMN_VKey_Backspace_NoCtxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (with context)
{"KMN_Ctrl_VKey_Backspace_Ctxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (no context)
{"KMN_Ctrl_VKey_Backspace_NoCtxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Modifier frame key
{"KMN_VKey_Shift", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_SHIFT, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
//--------------------------------------------------------------------
// LDML
// Key with rule
{"LDML_VKey_A", "ldml/keyboards/k_020_fr.kmx", KM_CORE_VKEY_A, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'q'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
{"LDML_Ctrl_VKey_A", "ldml/keyboards/k_020_fr.kmx", KM_CORE_VKEY_A, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Key without rule
{"LDML_VKey_X", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_X, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_END} // LDML: no output without rule
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
{"LDML_Ctrl_VKey_X", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_X, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END} // LDML: no output without rule
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Frame key without rule
{"LDML_VKey_Enter", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_ENTER, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Backspace (with available context)
{"LDML_VKey_Backspace_Ctxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, u"x", true,
{ // KeyDown
// Once we use C++ 20 we can use:
//{KM_CORE_IT_BACK, { 0, }, {.backspace = {KM_CORE_BT_CHAR, 0}}},
{KM_CORE_IT_BACK, { 0, }, {backspace_no_context.value}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
// Backspace (without context)
{"LDML_VKey_Backspace_NoCtxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (with context)
{"LDML_Ctrl_VKey_Backspace_Ctxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (no context)
{"LDML_Ctrl_VKey_Backspace_NoCtxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Modifier frame key
{"LDML_VKey_Shift", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_SHIFT, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
};
INSTANTIATE_TEST_SUITE_P(KMXProcessEvent, ProcessEventTests, testing::ValuesIn(values), GenerateTestName);
// provide our own `main` so that we can get the path of the exe so that
// we have a well-defined location to find our test keyboards
int main(int argc, char** argv) {
#ifdef __EMSCRIPTEN__
test_dir = get_wasm_file_path(km::core::path(argv[0]).parent());
#else
test_dir = km::core::path(argv[0]).parent();
#endif
std::cout << "test_dir=" << test_dir.c_str() << std::endl;
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -48,6 +48,18 @@ kmcorekeyboardapitests = executable('km_core_keyboard_api.tests',
test('km-core-keyboard-api-tests', kmcorekeyboardapitests)
# tests for km_core_process_event
km_core_process_event_tests_exe = executable(
'km_core_process_event_tests',
['km_core_process_event.tests.cpp', common_test_files],
include_directories: [inc, libsrc],
cpp_args: defns + warns,
link_args: [ links, extra_link_args ],
dependencies: [icu_uc, icu_i18n, gtest_dep, gmock_dep],
objects: lib.extract_all_objects(recursive: false),
)
test('km_core_process_event', km_core_process_event_tests_exe)
subdir('json')
subdir('utftest')
subdir('kmnkbd')

View file

@ -182,6 +182,7 @@ export class LexicalModelCompiler implements KeymanCompiler {
const sources: string[] = modelSource.sources.map(function(source) {
return new TextDecoder().decode(callbacks.loadFile(callbacks.path.join(sourcePath, source)));
});
func += `globalThis.exports = globalThis.exports ?? {};\n`;
func += this.transpileSources(sources).join('\n');
func += `LMLayerWorker.loadModel(new ${modelSource.rootClass}());\n`;
break;

View file

@ -19,6 +19,7 @@ describe('LexicalModelCompiler', function () {
'example.qaa.wordbreaker',
'example.qaa.joinwordbreaker',
'example.qaa.scriptusesspaces',
'example.qaa.custom',
];
for (const modelID of MODELS) {
@ -37,7 +38,11 @@ describe('LexicalModelCompiler', function () {
assert.isFalse(compilation.hasSyntaxError, 'model code had syntax error');
assert.isNull(compilation.error, `compilation error: ${compilation.error}`);
assert.equal(compilation.modelConstructorName, 'TrieModel');
if(modelID == 'example.qaa.custom') {
assert.isNull(compilation.modelConstructorName);
} else {
assert.equal(compilation.modelConstructorName, 'TrieModel');
}
});
}
});

View file

@ -0,0 +1,25 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
export class ExampleCustomModel implements LexicalModelTypes.LexicalModel {
configure(capabilities: LexicalModelTypes.Capabilities): LexicalModelTypes.Configuration {
return {
leftContextCodePoints: 16,
rightContextCodePoints: 0,
wordbreaksAfterSuggestions: false,
}
}
languageUsesCasing: boolean = true;
predict(transform: LexicalModelTypes.Transform, context: LexicalModelTypes.Context): LexicalModelTypes.Distribution<LexicalModelTypes.Suggestion> {
if(transform.deleteLeft == 0 && context.left.endsWith('te') && transform.insert == 'h') {
return [
{ p: 0.3, sample: { displayAs: 'the', transform: { deleteLeft: 2, insert: 'the' }, tag: 'correction' } },
{ p: 0.2, sample: { displayAs: 'them', transform: { deleteLeft: 2, insert: 'them' }, tag: 'correction' } },
{ p: 0.1, sample: { displayAs: 'tee hee', transform: { deleteLeft: 2, insert: 'tee hee' }, tag: 'correction' } },
];
} else {
return [];
}
}
}

View file

@ -0,0 +1,6 @@
const source: LexicalModelSource = {
format: 'custom-1.0',
rootClass: 'ExampleCustomModel',
sources: ['ExampleCustomModel.ts'],
};
export default source;

View file

@ -88,9 +88,9 @@
}
},
"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==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dependencies": {
"balanced-match": "^4.0.2"
},
@ -540,9 +540,9 @@
}
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"engines": {
"node": ">=12"
},
@ -769,9 +769,9 @@
}
},
"brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"requires": {
"balanced-match": "^4.0.2"
}
@ -1099,9 +1099,9 @@
}
},
"picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"proc-log": {
"version": "6.1.0",

View file

@ -91,12 +91,17 @@ var
begin
lm := TLexicalModelParser.Create(m.Text);
try
// TODO: LexicalModelParser does not support `rootClass` which is needed for
// custom lexical models, and the property `Wordlists` is inappropriate for
// the list of sources files. This is part of a bigger project for better
// custom model support in TIKE
//
// For now, this test excludes the `rootClass` property in order to pass
lm.Comment := 'Testing';
lm.Format := lmfCustom10;
lm.WordBreaker := lmwbAscii;
lm.Wordlists.Clear;
lm.Wordlists.Add('foo.tsv');
lm.Wordlists.Add('bar.tsv');
lm.Wordlists.Add('CustomModel.ts');
Assert.AreEqual(mcustom.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after
finally
lm.Free;

View file

@ -2,7 +2,7 @@
const source: LexicalModelSource = {
format: 'custom-1.0',
wordBreaker: 'ascii',
sources: ['foo.tsv', 'bar.tsv']
sources: ['CustomModel.ts']
};
export default source;

View file

@ -0,0 +1,132 @@
# Input Methods in GTK
This document describes how Keyman is connected to a client app, in other words
how a keypress in the app ends up being processed by the Keyman engine.
For text input, GTK (or rather the app) will add input method support to a widget.
How the input method gets loaded depends on the GTK version.
IBus provides input modules for GTK 2, GTK 3, GTK 4, X11, and Wayland.
However, the Wayland one is not used with Gnome because GTK provides
its own implementation.
Which input modules are available depends on the GTK version. Apps that
don't use the Gnome/GTK framework use different ways for input method
support.
## GTK 4
```mermaid
flowchart LR
subgraph GTK
app(application)
im-module["im-ibus
(IBusIMContext)"]
app --- im-module
end
subgraph ibus[ibus-daemon]
direction LR
IBusInputContext <--> IBusEngine
end
subgraph keyman
ibus-engine-keyman
end
GTK <--> ibus <--> keyman
style app color:#FFFFFF, stroke:#00C853, fill:#00C853
```
The im-module implements a [GIOExtensionPoint](https://docs.gtk.org/gio/struct.IOExtensionPoint.html)
"gtk-im-module". The type extends `GtkIMContext`.
When loading the app, GTK dynamically loads and starts the extension
specified by the environment variable `GTK_IM_MODULE` (or the default which is
probably the one with the highest priority).
External modules are located in `/usr/lib/x86_64-linux-gnu/gtk-4.0/4.0.0/immodules`
and named `lib*.so`.
Each module has a priority, a name and the type that implements it.
GTK4 has built-in support for Wayland (`GtkIMContextWayland`), Broadway
(`GtkIMContextBroadway`, irrelevant for Keyman) and gtk-im-context-simple
(`GtkIMContextSimple`, irrelevant for Keyman). `GtkIMContextWayland` is
implemented as part of GTK 4, the source code can be found in
<https://github.com/GNOME/gtk/blob/main/gtk/gtkimcontextwayland.c>.
The input method for ibus is implemented as an external module
(type `IBusIMContext`). Source code is in
<https://github.com/ibus/ibus/tree/main/client/gtk4>.
The IBus communication between `IBusIMContext`, ibus-daemon and the ibus
engine (aka Keyman) happens with DBus messages on a separate IBus DBus
instance.
## GTK 2/GTK 3
With older GTK versions basically the same components are involved,
although a different mechanism to discover and load the
im-modules is used.
GTK caches the available modules in a module database located in
`/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache` or whereever
the `GTK_IM_MODULE_FILE` environment variable points. This database is
read on application startup.
For each module the database contains an id, the name, the path, i18n domain(?)
and default locales.
GTK then determines what input module to load based on the `GTK_IM_MODULE`
environment variable (which can contain more than one module separated by
colons) or the `XSETTINGS` `gtk-im-module` property. It then checks that
the currently running display type is compatible with the module, and that
the current locale matches the locales supported by the module. An exact match
(`en_US` vs `en_US`) gets 4 points, a match of the language (`en_US` vs `en`)
gets 3 points, a match with a different region (`en_US` vs `en_UK`) 2 points,
and a wildcard (`en_US` vs `*`) 1 point.
Each input module implements a subclass of `GTKIMContext` plus the 4 required
entry points:
- `im_module_init()` - Initialize module
- `im_module_list()` - Describe available contexts
- `im_module_create()` - Create context instance
- `im_module_exit()` - Cleanup on unload
GTK uses `GModule` to dynamically load the module's `.so` file and then calls
`im_module_init()` on the module, followed by `im_module_create()` to instantiate
the actual `GtkIMContext` subclass.
If no module matches or loading fails, GTK falls back to `GtkIMContextSimple`.
GTK3 provides support for several input methods, more than with GTK4. Those
can be found in <https://github.com/GNOME/gtk/tree/gtk-3-24/modules/input>.
Among the built-in modules are `im-wayland` (for Wayland), `im-xim` (for X11),
`im-thai` (for Thai), and `im-ime` (for Windows).
The input method for ibus is implemented as an external module
(type `IBusIMContext`). Source code is in
<https://github.com/ibus/ibus/tree/main/client/gtk3>.
All modules are located in `/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules`
and named `*.so`, e.g. `im-ibus.so`.
After installing a new module the database needs to be updated with
`sudo /usr/lib/x86_64-linux-gnu/libgtk-3-0t64/gtk-query-immodules-3.0 --update-cache`.
## Tips
- GTK has a built-in
[inspection/debugging tool](https://developer.gnome.org/documentation/tools/inspector.html)
which can be opened with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>, or
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>, or by setting the
environment variable `GTK_DEBUG=interactive`.
- Available GTK3 IM modules can be listed with
`/usr/lib/x86_64-linux-gnu/libgtk-3-0t64/gtk-query-immodules-3.0` or by
looking at `/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache`.
## Links
- [GTK4 Input Handling](https://docs.gtk.org/gtk4/input-handling.html)

View file

@ -8,6 +8,7 @@
#import "KMInputMethodEventHandler.h"
#import <KeymanEngine4Mac/KeymanEngine4Mac.h>
#import <Carbon/Carbon.h> /* For kVK_ constants. */
#import <CoreFoundation/CoreFoundation.h>
#import "KeySender.h"
#import "TextApiCompliance.h"
#import "KMSettingsRepository.h"
@ -370,7 +371,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
contextString = attributedString.string;
//only uncomment for testing as we do not want to write context in logs
//os_log_debug([KMLogs testLog], " length: %lu result: %{public}@", contextString.length, contextString);
//os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString);
}
}
}
@ -405,18 +406,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self insertAndReplaceTextForOutput:output client:client];
} else if (output.isDeleteOnlyScenario) {
if ((event.keyCode == kVK_Delete) && output.codePointsToDeleteBeforeInsert == 1) {
// let the delete pass through in the original event rather than sending a new delete
NSString *message = @"applyOutputToTextInputClient, delete only scenario with passthrough";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
handledEvent = NO;
} else {
NSString *message = @"applyOutputToTextInputClient, delete only scenario";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self sendEvents:event forOutput:output];
}
handledEvent = [self handleDeleteOnlyScenario:output keyDownEvent:event client:client];
} else if (output.isDeleteAndInsertScenario) {
// TODO: fix issue #10246
/*
@ -511,6 +501,220 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
}
}
/**
* Handles deleting without an associated insert in one of three methods:
* 1. delete via replace: do the delete by replacing two (or more) characters with one.
* 2. backspace passthrough : if the original keydown event was a backspace, pass it through unhandled
* 3. generate event: generate keydown backspace events as necessary
*/
-(BOOL)handleDeleteOnlyScenario:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
// attempt to delete by replacing -- for compliant apps only
if ([self handleDeleteWithReplacement:output keyDownEvent:event client:client]) {
return YES;
}
// pass through if this was a backspace keydown event
if (event.keyCode == kVK_Delete && output.codePointsToDeleteBeforeInsert == 1) {
// let the delete pass through in the original event rather than sending a new delete
NSString *message = @"handleDeleteOnlyForOutput, delete only scenario with passthrough";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
// instruct system to handle the event
return NO;
}
else {
// otherwise generate a backspace
NSString *message = @"handleDeleteOnlyForOutput, send backspace event";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self sendEvents:event forOutput:output];
return YES;
}
}
/**
* For compliant apps only.
*
* This an attempt to make sure that deletion removes only the expected codepoints.
* When handling a transform which only deletes a character, or when allowing a
* backspace to pass through, the OS or application may not use the same rules
* around deletion as Keyman -- especially when deleting clusters such as letter +
* combining diacritic (e.g. `U+0062 U+0301`), where some applications may delete
* both together as they represent a single 'grapheme cluster'.
*
* (Note, the question of whether it is appropriate for backspace to delete a
* cluster rather than a codepoint from an end-user perspective is not relevant
* here, because what is important is that we match the rules that the keyboard has
* provided, which means we need a method of deleting a precise number of
* codepoints. The keyboard author can and should include rules for cluster
* deletion that meet end-user expectations.)
*
* The `insertText` API takes two parameters: a string to insert, and a range to
* replace with that string. However, we cannot simply pass through a zero-length
* insertion string along with the range to delete, because the `insertText` API
* treats this as an invalid call and ignores it.
*
* Instead, we can delete the desired number of codepoints only by using the
* `insertText` API to replace e.g. two codepoints with one.
*
* This method only works for compliant apps because non-compliant apps do not
* support the `insertText` API.
*
* Ref: https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:)
*/
-(BOOL)handleDeleteWithReplacement:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
BOOL handledEvent = NO;
NSString *context = [self readContext:event forClient:client];
// guard: only for compliant apps with sufficient context
if (!(self.apiCompliance.canReplaceText) || ([context length] <= output.textToDelete.length)) {
os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant or insufficient context");
return NO; // return without deleting/replacing
}
// guard: the logic of this method depends on locating textToDelete in the context
if (![self stringToDeleteMatchesContextSuffix:output.textToDelete context:context]) {
os_log_debug([KMLogs keyTraceLog], "cannot replace text, textToDelete not found at end of context");
return NO; // return without deleting/replacing
}
NSUInteger deletionTargetLength = output.textToDelete.length;
NSUInteger deletionTargetLocation = context.length-deletionTargetLength;
NSUInteger precedingCharacterLocation = deletionTargetLocation - 1;
// if the preceding character is the trailing half of a surrogate pair
// then delete by replacing with the entire surrogate pair
if ([self precededBySurrogatePair:precedingCharacterLocation context:context]) {
handledEvent = [self deleteByReplacingWithPrecedingSurrogate:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
} else {
// otherwise replace with only the preceding character
handledEvent = [self deleteByReplacingWithPrecedingCharacter:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
}
return handledEvent;
}
/**
* Check whether the string to be deleted is found at the tail end of the current context.
*/
-(BOOL) stringToDeleteMatchesContextSuffix:(NSString*)textToDelete context:(NSString*) context {
BOOL doesMatch = NO;
// get length of string to delete and compare to end of context
NSUInteger deleteLength = textToDelete.length;
NSUInteger locationOfDeletionTarget = context.length-deleteLength;
NSUInteger locationOfPrecedingCharacter = locationOfDeletionTarget - 1;
NSString *contextSuffix = [context substringFromIndex:context.length-deleteLength];
os_log_debug([KMLogs keyTraceLog], "stringToDeleteMatchesSuffix, textToDelete: '%{public}@', contextSuffix: '%{public}@', locationOfDeletionTarget: %u, locationOfprecedingCharacter: %u", textToDelete, contextSuffix, (int)locationOfDeletionTarget, (int)locationOfPrecedingCharacter);
doesMatch = [textToDelete isEqualToString:contextSuffix];
os_log_debug([KMLogs keyTraceLog], "stringToDeleteMatchesSuffix: %{public}@", doesMatch?@"YES":@"NO");
return doesMatch;
}
/**
* Check whether the preceding character, which is to be used for the replacement,
* is part of a surrogate pair that is distinct from the character being deleted.
*/
-(BOOL) precededBySurrogatePair: (NSUInteger)precedingLocation context:(NSString*) context {
BOOL precedingCharacterIsLowSurrogate = false;
unichar precedingCharacter = [context characterAtIndex:precedingLocation];
if (CFStringIsSurrogateHighCharacter(precedingCharacter)) {
// preceding character is high character -- unexpected from Keyman Core
// write to log and return false
precedingCharacterIsLowSurrogate = false;
NSString *message = [NSString stringWithFormat:@"Unexpected high surrogate found for preceding character at %ld", (long)precedingCharacter];
os_log_debug([KMLogs keyTraceLog], "%{public}@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
} else if (CFStringIsSurrogateLowCharacter(precedingCharacter)) {
// preceding character is low surrogate
precedingCharacterIsLowSurrogate = true;
}
return precedingCharacterIsLowSurrogate;
}
/**
* Replace both the text to delete and the character preceding it solely with the character that precedes it.
* Returns YES if executing the replace/delete and NO otherwise.
*/
-(BOOL) deleteByReplacingWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter");
// get the preceding character
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation, 1);
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
// guard: if preceding character is a control character, return NO
if ([self containsControlCharacter:replacementString]) {
NSString *message = @"replacementString contains control characters, cannot delete with replace";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
return NO;
}
// perform the replacement
NSUInteger replacementLength = [replacementString length] + deleteLength;
NSRange replacementRange = NSMakeRange(precedingCharacterLocation, replacementLength);
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
[client insertText:replacementString replacementRange:replacementRange];
return YES;
}
/**
* Replace both the text to delete and the surrogate pair preceding it with the surrogate pair preceding it.
* Returns YES if executing the replace/delete and NO otherwise.
*/
-(BOOL) deleteByReplacingWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingSurrogate");
// guard: return NO if there is no character before the precedingCharacterLocation
if (precedingCharacterLocation <= 0) {
NSString *message = @"no characters exist before precedingCharacterLocation, so it cannot be a surrogate pair";
os_log_debug([KMLogs keyTraceLog], "%@", message);
return NO;
}
// get the preceding character
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation - 1, 2);
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
unichar highCharacter = [replacementString characterAtIndex:0];
unichar lowCharacter = [replacementString characterAtIndex:1];
// verify that preceding characters comprise a surrogate pair
if ((CFStringIsSurrogateHighCharacter(highCharacter)) && (CFStringIsSurrogateLowCharacter(lowCharacter))) {
// found preceding surrogate as expected
} else {
NSString *message = [NSString stringWithFormat:@"Preceding characters of string do not comprise a surrogate pair: 0x%02x, 0x%02x", (unsigned int)highCharacter, (unsigned int)lowCharacter];
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
return NO;
}
// perform the replacement
NSUInteger replacementLength = [replacementString length] + deleteLength;
NSRange replacementRange = NSMakeRange(precedingCharacterRange.location, replacementLength);
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
[client insertText:replacementString replacementRange:replacementRange];
return YES;
}
-(BOOL) containsControlCharacter:(NSString*)text {
NSCharacterSet *controlSet = [NSCharacterSet controlCharacterSet];
NSRange range = [text rangeOfCharacterFromSet:controlSet];
return (range.location != NSNotFound);
}
/**
* Calculates the range where text will be inserted and replace existing text.
* Returning {NSNotFound, NSNotFound} for range signifies to insert at current location without replacement.

View file

@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
-(void) checkComplianceAfterInsert:(NSString *)insertedText deleted:(NSString *)deletedText;
-(BOOL)isComplianceUncertain;
-(BOOL)canReadText;
-(BOOL)canReplaceText;
-(BOOL)mustBackspaceUsingEvents;
@end

91
package-lock.json generated
View file

@ -4823,15 +4823,6 @@
"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,
@ -5955,15 +5946,6 @@
"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",
@ -7963,7 +7945,9 @@
}
},
"node_modules/flatted": {
"version": "3.2.5",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@ -9882,15 +9866,6 @@
"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",
@ -11288,10 +11263,11 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@ -12895,47 +12871,6 @@
"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,
@ -13772,18 +13707,6 @@
"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,

View file

@ -32,6 +32,6 @@ jobs:
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |

View file

@ -65,8 +65,26 @@ _tc_rsync() {
else
local RSYNC=rsync
if builder_is_macos; then
# We need to look for a newer version of rsync because the version bundled
# with macos does not work correctly; we assume that this is installed
# with Homebrew. On Intel, this is found in /usr/local/bin, and on M1, it
# is found in /opt/homebrew/bin.
RSYNC=/usr/local/bin/rsync
[[ -f /opt/homebrew/bin/rsync ]] && RSYNC=/opt/homebrew/bin/rsync
[[ -x /opt/homebrew/bin/rsync ]] && RSYNC=/opt/homebrew/bin/rsync
# On build agents, we unlink rsync from homebrew, because otherwise xcode
# builds which use rsync internally fail, so we need to specify the full
# path to rsync. Generally, rsync is not needed on developer machines as
# this is only used for release builds from the build agents. #15764
if [[ ! -x "${RSYNC}" ]]; then
# Try and find the rsync binary with `brew list`. It should be something
# like `/opt/homebrew/Cellar/rsync/3.4.1/bin/rsync`
RSYNC="$(brew list rsync | grep /bin/rsync\$)"
if [[ ! -x "${RSYNC}" ]]; then
builder_die "Could not find Homebrew-installed rsync anywhere"
fi
fi
fi
${RSYNC} "${rsync_args[@]}"

View file

@ -12,9 +12,13 @@ ba_mac_unlock_keychain() {
}
ba_mac_clean_xcode_derived_data() {
builder_echo start "clean" "Cleaning XCode DerivedData mess"
builder_echo start "clean" "Cleaning XCode DerivedData and Carthage cache mess"
rm -rf "${HOME}/Library/Developer/Xcode/DerivedData"
builder_echo end "clean" success "Finished cleaning XCode DerivedData mess"
# https://stackoverflow.com/a/45504898/1836776
rm -rf "${HOME}/Library/Caches/org.carthage.CarthageKit"
builder_echo end "clean" success "Finished cleaning XCode DerivedData and Carthage cache mess"
}
ba_mac_unmount_volumes_keyman() {