Merge branch 'epic/kmc-convert' into feat/developer/kmc-convert

This commit is contained in:
SabineSIL 2026-02-16 17:47:38 +01:00 committed by GitHub
commit 7c4ea1bb5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 11455 additions and 3972 deletions

View file

@ -1,5 +1,40 @@
# Keyman Version History
## 19.0.202 alpha 2026-02-12
* fix(linux): check for existing file before trying to install (#15573)
## 19.0.201 alpha 2026-02-09
* fix(linux): include missing artifacts in source tarball (#15566)
* chore(linux): use xz compression for source tarballs (#15568)
## 19.0.200 alpha 2026-02-06
* change(web): define, utilize cleaner Web test-resource import paths (#15564)
* maint(mac): upgrade to Xcode 26.2 (#15484)
## 19.0.199 alpha 2026-02-05
* chore(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 in /developer/src/server/src/win32/trayicon/addon-src (#15552)
* fix(windows): unresponsive splash screen (#15269)
* maint(common): Update langtags.json to 2026-02-03 release (v1.4) (#15459)
* feat(android): Use current display language for keyboard search (#15510)
* chore(linux): Update debian changelog (#15557)
* chore(linux): update copyright year and standards version (#15559)
## 19.0.198 alpha 2026-02-04
* fix(core): handle backspace decomposition (#15488)
* fix(core): normalization segment should end on NFC boundary, not NFD (#15506)
## 19.0.197 alpha 2026-02-02
* chore(deps): bump tar from 7.5.6 to 7.5.7 in /developer/src/server/src/win32/trayicon/addon-src (#15514)
* chore(deps): bump fast-xml-parser from 5.2.2 to 5.3.4 (#15526)
* fix(windows): handle symstore correctly from bash script (#15524)
* chore(ios): Add fv_tlingityooxatangi to FirstVoices for iOS app (#15485)
## 19.0.196 alpha 2026-01-30
* chore(common): Update Crowdin strings for `de` (#15511)
@ -1170,6 +1205,17 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.246 stable 2026-02-04
* cherrypick(common): Update FirstVoices versions in keyboards.csv (#15273)
* docs(ios): Use /cdn/dev assets for Engine guides (#15274)
* chore(linux): Update debian changelog (#15281)
* maint(linux): remove EOL Ubuntu 25.04 Plucky (#15424)
* docs(developer): update primerprep link (#15468)
* chore(ios): Add fv_tlingityooxatangi to FirstVoices for iOS app (#15496)
* fix(core): handle backspace decomposition (#15494)
* fix(core): normalization segment should end on NFC boundary, not NFD (#15551)
## 18.0.245 stable 2025-12-03
* chore(windows): VS2022 patch for stable-18.0 (#15081)

View file

@ -1 +1 @@
19.0.197
19.0.203

View file

@ -11,6 +11,7 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.View;
@ -38,7 +39,7 @@ public class KMPBrowserActivity extends BaseActivity {
private static final String TAG = "KMPBrowserActivity";
// URL for keyboard search web page presented to user when they add a keyboard in the app.
private static final String KMP_SEARCH_KEYBOARDS_FORMATSTR = "https://%s/go/android/%s/download-keyboards%s";
private static final String KMP_SEARCH_KEYBOARDS_FORMATSTR = "https://%s/go/android/%s/download-keyboards%s?lang=%s";
private static final String KMP_SEARCH_KEYBOARDS_LANGUAGES = "/languages/%s";
// Patterns for determining if a link should be opened in external browser
@ -163,13 +164,7 @@ public class KMPBrowserActivity extends BaseActivity {
}
});
// Tier determines the keyboard search host
String host = KMPLink.getHost();
// If language ID is provided, include it in the keyboard search
String languageID = getIntent().getStringExtra("languageCode");
String languageStr = (languageID != null) ? KMString.format(KMP_SEARCH_KEYBOARDS_LANGUAGES, languageID) : "";
String appMajorVersion = KMManager.getMajorVersion();
String kmpSearchUrl = KMString.format(KMP_SEARCH_KEYBOARDS_FORMATSTR, host, appMajorVersion, languageStr);
String kmpSearchUrl = determineSearchUrl();
webView.loadUrl(kmpSearchUrl);
}
@ -178,7 +173,8 @@ public class KMPBrowserActivity extends BaseActivity {
super.onResume();
if (webView != null) {
webView.reload();
String kmpSearchUrl = determineSearchUrl();
webView.loadUrl(kmpSearchUrl);
}
}
@ -221,6 +217,32 @@ public class KMPBrowserActivity extends BaseActivity {
}
}
/**
* Parse the keyman.com keyboard search URL while accounting for:
* host (production or staging)
* languageID - language ID to search for
* display language - request search results to be displayed in this language
* @return String
*/
private String determineSearchUrl() {
// Tier determines the keyboard search host
String host = KMPLink.getHost();
// If display language is provided, include it in the keyboard search. Otherwise fallback to default locale
String fallbackLang = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) ?
LocaleList.getDefault().get(0).toLanguageTag() : "en";
Bundle bundle = getIntent().getExtras();
String displayLang = (bundle != null) && bundle.containsKey("lang") ? bundle.getString("lang") : fallbackLang;
// If language ID is provided for keyboard search, include it in the keyboard search
String languageID = getIntent().getStringExtra("languageCode");
String languageStr = (languageID != null) ? KMString.format(KMP_SEARCH_KEYBOARDS_LANGUAGES, languageID) : "";
String appMajorVersion = KMManager.getMajorVersion();
String kmpSearchUrl = KMString.format(KMP_SEARCH_KEYBOARDS_FORMATSTR, host, appMajorVersion, languageStr, displayLang);
return kmpSearchUrl;
}
/**
* Check if a URL is a valid internal Keyman keyboard link
* @param url String of the URL to parse

View file

@ -8,9 +8,11 @@ import java.util.HashMap;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
@ -24,6 +26,7 @@ import android.widget.SimpleAdapter;
import android.widget.TextView;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.DisplayLanguages;
import com.keyman.engine.KMManager;
import com.keyman.engine.data.KeyboardController;
import com.keyman.engine.util.MapCompat;
@ -127,12 +130,19 @@ public class KeymanSettingsInstallActivity extends BaseActivity {
HashMap<String, String> hashMap = (HashMap<String, String>) parent.getItemAtPosition(position);
String itemTitle = MapCompat.getOrDefault(hashMap, titleKey, "");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String languageTag = prefs.getString(DisplayLanguages.displayLanguageKey, "");
// Install from keyman.com
if (itemTitle.equals(getString(R.string.install_from_keyman_dot_com))) {
if (KMManager.hasConnection(context)) {
Intent i = new Intent(context, KMPBrowserActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (languageTag != null && !languageTag.isEmpty()) {
Bundle bundle = new Bundle();
bundle.putString("lang", languageTag);
i.putExtras(bundle);
}
context.startActivity(i);
} else {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);

View file

@ -27,8 +27,10 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceManager;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.DisplayLanguages;
import com.keyman.engine.KeyboardPickerActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.ModelPickerActivity;
@ -201,11 +203,19 @@ public final class LanguageSettingsActivity extends BaseActivity {
if (KMManager.hasConnection(context)){
// Scenario 1: Connection to keyman.com catalog
// Pass the BCP47 language code to the KMPBrowserActivity
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String languageTag = prefs.getString(DisplayLanguages.displayLanguageKey, "");
Intent i = new Intent(context, KMPBrowserActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
i.putExtra("languageCode", lgCode);
i.putExtra("languageName", lgName);
if (languageTag != null && !languageTag.isEmpty()) {
Bundle bundle = new Bundle();
bundle.putString("lang", languageTag);
i.putExtras(bundle);
}
context.startActivity(i);
/*
} else if (KeyboardPickerActivity.hasKeyboardFromPackage()) {

View file

@ -105,7 +105,7 @@ bool km::core::actions_normalize(
boundary prior to the intersection of the cached_context and the output.
*/
if(!output.empty()) {
while(n > 0 && !km::core::util::has_nfd_boundary_before(output[0])) {
while(n > 0 && !km::core::util::has_nfc_boundary_before(output[0])) {
// The output may interact with the context further in normalization. We
// need to copy characters back further until we reach a normalization
// boundary.
@ -133,6 +133,7 @@ bool km::core::actions_normalize(
To adjust, we remove one codepoint at a time from the app_context until
its normalized form matches the cached_context normalized form.
*/
std::u32string context_final = U"";
while(!app_context_string.empty()) {
auto app_context_nfd = app_context_string;
@ -141,7 +142,16 @@ bool km::core::actions_normalize(
return false;
}
if(app_context_nfd == cached_context_string) {
/*
We are working in NFD as we backtrack in the context, but input variable
app_context_string is NFC. The last character of the context may be part
of a composed character, so we need to track decomposition. For example,
'' + BKSP should result in 'ta', but delete back 1 in app_context_string
will give us 't', so we need to remember 'a', and add it back afterwards
(see output_nfc variable).
*/
if(app_context_nfd == cached_context_string.substr(0, app_context_nfd.length())) {
context_final = cached_context_string.substr(app_context_nfd.length());
break;
}
@ -155,7 +165,7 @@ bool km::core::actions_normalize(
Normalize our output string
*/
auto output_nfc = output;
auto output_nfc = context_final + output;
if(!km::core::util::normalize_nfc(output_nfc)) {
DebugLog("nfc->normalize failed");
return false;
@ -197,6 +207,8 @@ bool km::core::actions_normalize(
actions.output = new_output;
actions.code_points_to_delete = nfu_to_delete;
// Outcome will be: <app_context><context_final><output>|
return true;
}

View file

@ -395,13 +395,18 @@ const char *Debug_ModifierName(KMX_UINT modifiers) {
}
const char *Debug_VirtualKey(KMX_WORD vk) {
if (!ShouldDebug()) {
return "";
}
return Debug_VirtualKey_Always(vk);
}
const char *Debug_VirtualKey_Always(KMX_WORD vk) {
#ifdef _MSC_VER
__declspec(thread)
#endif
static char buf[256];
if (!ShouldDebug()) {
return "";
}
if (vk < 256) {
snprintf(buf, 256, "['%s' 0x%x]", s_key_names[vk], vk);
@ -411,7 +416,6 @@ const char *Debug_VirtualKey(KMX_WORD vk) {
}
return buf;
}
const char *Debug_UnicodeString(PKMX_WCHAR s, int x) {
if (!ShouldDebug()) {
return "";

View file

@ -32,6 +32,7 @@ extern const char *s_key_names[];
int DebugLog_1(const char *file, int line, const char *function, const char *fmt, ...);
const char *Debug_VirtualKey(KMX_WORD vk);
const char *Debug_VirtualKey_Always(KMX_WORD vk);
/**
* @param s PKMX_WCHAR to output
* @param x temporary buffer (0 or 1) to write to

View file

@ -208,7 +208,7 @@ bool is_nfd(const std::u32string& str) {
#endif
}
bool has_nfd_boundary_before(km_core_usv cp) {
bool has_nfc_boundary_before(km_core_usv cp) {
#ifdef __EMSCRIPTEN__
// it's a negative table. entries in the table mean returning false. non-entries return true.
for (auto i=0;i<(km_noBoundaryBefore_entries*2);i+=2) {
@ -221,9 +221,9 @@ bool has_nfd_boundary_before(km_core_usv cp) {
return true; // fallthrough
#else
UErrorCode status = U_ZERO_ERROR;
auto nfd = getNFD(status);
if (nfd == nullptr) return false;
return nfd->hasBoundaryBefore(cp);
auto nfc = getNFC(status);
if (nfc == nullptr) return false;
return nfc->hasBoundaryBefore(cp);
#endif
}

View file

@ -39,7 +39,7 @@ bool is_nfd(const std::u16string& str);
bool is_nfd(const std::u32string& str);
/** @return true if cp can interacts with prior chars */
bool has_nfd_boundary_before(km_core_usv cp);
bool has_nfc_boundary_before(km_core_usv cp);
/** convenience function, caller owns storage */
km_core_usv *string_to_usv(const std::u32string& src);

View file

@ -28,7 +28,7 @@
int
write_nfd_table() {
write_nfc_table() {
#ifndef __EMSCRIPTEN__
std::cerr << "Note: This is unusual - this generator is usually only run under emscripten!" << std::endl;
#endif
@ -49,13 +49,13 @@ write_nfd_table() {
std::cout << std::endl;
// we're going to need an NFD normalizer
UErrorCode status = U_ZERO_ERROR;
const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status);
const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status);
assert(U_SUCCESS(status));
// collect the raw list of chars that do NOT have a boundary before them.
std::vector<km_core_usv> noBoundary;
for (km_core_usv ch = 0; ch < km::core::kmx::Uni_MAX_CODEPOINT; ch++) {
bool bb = nfd->hasBoundaryBefore(ch);
bool bb = nfc->hasBoundaryBefore(ch);
assert(!(ch == 0 && !bb)); // assert that we can use U+0000 as a terminator
if (bb) continue; //only emit nonboundary
noBoundary.push_back(ch);
@ -102,6 +102,6 @@ write_nfd_table() {
int
main(int /*argc*/, const char * /*argv*/[]) {
write_nfd_table();
write_nfc_table();
return 0;
}

View file

@ -311,6 +311,17 @@ void run_actions_normalize_tests() {
/* app_context: */ u"abcệ"
);
test_actions_normalize(
"One backspace to delete last NFD character (#15487)",
/* app context pre transform: */ u"abcê", // NFC
/* cached context post transform: */ u"abce",
/* cached context post transform: */ nullptr,
/* action del, output: */ 1, U"", // NFD input; delete 1: \u0302
// ---- results ----
/* action del, output: */ 1, U"e", // NFC output; delete 1: e
/* app_context: */ u"abce"
);
test_actions_normalize(
"One backspace for NFD converts into one char in NFC (ê) and recombine",
/* app context pre transform: */ u"abcê",
@ -347,6 +358,18 @@ void run_actions_normalize_tests() {
/* app_context: */ u"\u0323\u0300\u0302"
);
// #15505 - normalization of Bengali characters
test_actions_normalize(
"Bengali normalization of U+09C7 U+09D7 -> U+09CC",
/* app context pre transform: */ u"\u0995\u09C7",
/* cached context post transform: */ u"\u0995\u09C7\u09D7",
/* cached context post transform: */ nullptr,
/* action del, output: */ 0, U"\u09D7",
// ---- results ----
/* action del, output: */ 1, U"\u09CC",
/* app_context: */ u"\u0995\u09CC"
);
// Modifies the base as well as diacritic
test_actions_normalize(

View file

@ -0,0 +1,42 @@
<?xml version="1.0"?>
<!--
Note that the 'expected' lines are cumulative unless there's a reset event.
@@keys: [K_A][K_B][K_SLASH][K_BKSP]
@@expected: ab
Comment: bkspace should delete just the U+0301
@@keys: [K_C][K_E][K_SLASH][K_BKSP]
@@expected: abce
Comment: #15487: bksp will delete just acute (even though it will have combined to NFC, internally we do NFD bksp)
TODO: this seems to be working NFD on app context side also, do we need to set a flag? this may break other tests so care is needed
-->
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="en" conformsTo="45">
<info author="Marc Durdin" name="Test BKSP"/>
<version number="1.0.0"/>
<keys>
<import base="cldr" path="45/keys-Zyyy-punctuation.xml"/>
<import base="cldr" path="45/keys-Zyyy-currency.xml"/>
<key id="acute" output="\u{0301}"/>
</keys>
<layers formId="us">
<layer modifiers="none">
<row keys="grave 1 2 3 4 5 6 7 8 9 0 hyphen equal"/>
<row keys="q w e r t y u i o p open-square close-square backslash"/>
<row keys="a s d f g h j k l semi-colon apos"/>
<row keys="z x c v b n m comma period acute"/>
<row keys="space"/>
</layer>
<layer modifiers="shift">
<row keys="tilde bang at hash dollar percent caret amp asterisk open-paren close-paren underscore plus"/>
<row keys="Q W E R T Y U I O P open-curly close-curly pipe"/>
<row keys="A S D F G H J K L colon double-quote"/>
<row keys="Z X C V B N M open-angle close-angle question"/>
<row keys="space"/>
</layer>
</layers>
</keyboard3>

View file

@ -30,6 +30,7 @@ tests_without_testdata = [
'k_100_keytest',
'k_101_keytest',
'k_102_keytest',
'k_213_backspace_decomposition',
]
# These tests have a *-test.xml file as well.

View file

@ -40,6 +40,8 @@
namespace {
void print_context(std::u16string &text_store, km_core_state *&test_state, std::vector<km_core_context_item> &test_context);
bool g_beep_found = false;
km_core_option_item test_env_opts[] =
@ -82,13 +84,15 @@ apply_action(
std::vector<km_core_context_item> &context,
km::tests::LdmlTestSource &test_source,
std::vector<km_core_context_item> &test_context) {
// print_context(text_store, test_state, test_context);
switch (act.type) {
case KM_CORE_IT_END:
test_assert(false);
break;
case KM_CORE_IT_ALERT:
g_beep_found = true;
// std::cout << "beep" << std::endl;
std::cout << " + beep" << std::endl;
break;
case KM_CORE_IT_CHAR:
context.push_back(km_core_context_item{
@ -104,10 +108,10 @@ apply_action(
text_store.push_back(buf.ch[i]);
}
}
// std::cout << "char(" << act.character << ") size=" << cp->size() << std::endl;
std::cout << " + char(" << act.character << ")" << std::endl;
break;
case KM_CORE_IT_MARKER:
// std::cout << "deadkey(" << act.marker << ")" << std::endl;
std::cout << " + deadkey(" << act.marker << ")" << std::endl;
context.push_back(km_core_context_item{
KM_CORE_CT_MARKER,
{
@ -117,6 +121,8 @@ apply_action(
break;
case KM_CORE_IT_BACK:
{
std::cout << " + back(" << act.backspace.expected_type << ")" << std::endl;
// single char removed in context
km_core_usv ch = 0;
bool matched_text = false;
@ -167,10 +173,11 @@ apply_action(
}
break;
case KM_CORE_IT_PERSIST_OPT:
std::cout << " + TODO-LDML: persist_opt()" << std::endl;
break;
case KM_CORE_IT_INVALIDATE_CONTEXT:
{
std::cout << "action: context invalidated (markers cleared)" << std::endl;
std::cout << " + context invalidated (markers cleared)" << std::endl;
// TODO-LDML: We need the context for tests. So we will simulate recreating
// the context from the context string.
km_core_context_item* new_context_items = nullptr;
@ -186,11 +193,11 @@ apply_action(
}
break;
case KM_CORE_IT_EMIT_KEYSTROKE:
std::cout << "action: emit keystroke" << std::endl;
std::cout << " + emit keystroke" << std::endl;
// TODO-LDML: For now, this is a no-op. We could handle enter, etc.
break;
case KM_CORE_IT_CAPSLOCK:
std::cout << "action: capsLock " << act.capsLock << std::endl;
std::cout << " + capsLock " << act.capsLock << std::endl;
test_source.set_caps_lock_on(act.capsLock);
break;
default:
@ -199,11 +206,46 @@ apply_action(
}
}
/**
* verify the current context
*/
void
verify_context(std::u16string &text_store, km_core_state *&test_state, std::vector<km_core_context_item> &test_context) {
apply_actions(
km_core_state *test_state,
km_core_actions const *actions,
std::u16string &text_store,
std::vector<km_core_context_item> &context,
km::tests::LdmlTestSource &test_source,
std::vector<km_core_context_item> &test_context
) {
if(actions->do_alert) {
apply_action(test_state, {KM_CORE_IT_ALERT}, text_store, context, test_source, test_context);
}
if(actions->code_points_to_delete) {
for(unsigned int i = 0; i < actions->code_points_to_delete; i++) {
km_core_action_item act = {KM_CORE_IT_BACK};
act.backspace = {KM_CORE_BT_CHAR, 0};
apply_action(test_state, act, text_store, context, test_source, test_context);
}
}
if(actions->output) {
for(auto ch = actions->output; *ch; ch++) {
km_core_action_item act = {KM_CORE_IT_CHAR};
act.character = *ch;
apply_action(test_state, act, text_store, context, test_source, test_context);
}
}
if(actions->emit_keystroke) {
apply_action(test_state, {KM_CORE_IT_EMIT_KEYSTROKE}, text_store, context, test_source, test_context);
}
// TODO-LDML: other action types - persist, caps lock
}
void
print_context(std::u16string &text_store, km_core_state *&test_state, std::vector<km_core_context_item> &test_context) {
// Compare context and text store at each step - should be identical
size_t n = 0;
km_core_context_item *citems = nullptr;
@ -225,8 +267,18 @@ verify_context(std::u16string &text_store, km_core_state *&test_state, std::vect
}
}
std::cout << std::endl;
std::cout << "context : " << string_to_hex(buf) << " [" << buf << "]" << std::endl;
std::cout << "testcontext ";
std::cout << "context : " << string_to_hex(buf) << " [" << buf << "]" << std::endl;
delete[] buf;
km_core_context_item *citems_app = nullptr;
try_status(km_core_context_get(km_core_state_app_context(test_state), &citems_app));
try_status(context_items_to_utf16(citems_app, nullptr, &n));
buf = new km_core_cu[n];
try_status(context_items_to_utf16(citems_app, buf, &n));
std::cout << "app context : " << string_to_hex(buf) << " [" << buf << "]" << std::endl;
std::cout << "test_context : ";
std::cout.fill('0');
for (auto i = test_context.begin(); i < test_context.end(); i++) {
switch (i->type) {
@ -241,29 +293,60 @@ verify_context(std::u16string &text_store, km_core_state *&test_state, std::vect
}
}
std::cout << std::endl;
km_core_context_items_dispose(citems);
km_core_context_items_dispose(citems_app);
delete[] buf;
}
// Verify that both our local test_context and the core's test_state.context have
// not diverged
auto ci = citems;
for (auto test_ci = test_context.begin();; ci++, test_ci++) {
// skip over markers, they won't be in test_context
while (ci->type == KM_CORE_CT_MARKER) {
ci++;
/**
* verify the current context
*/
void
verify_context(std::u16string &text_store, km_core_state *&test_state, std::vector<km_core_context_item> &test_context, bool fully_normalized_mode, bool normalization_enabled) {
// Compare context and text store at each step - should be identical
print_context(text_store, test_state, test_context);
size_t n = 0;
km_core_context_item *citems = nullptr;
try_status(km_core_context_get(km_core_state_context(test_state), &citems));
try_status(context_items_to_utf16(citems, nullptr, &n));
km_core_cu *buf = new km_core_cu[n];
try_status(context_items_to_utf16(citems, buf, &n));
std::u16string buf_final(buf);
if(fully_normalized_mode) {
if(normalization_enabled) {
test_assert(km::core::util::normalize_nfc(buf_final));
}
// exit if BOTH are at end.
if (ci->type == KM_CORE_CT_END && test_ci == test_context.end()) {
break; // success
// note: we don't compare test_context with citems because they have different
// normalization forms in fully_normalized_mode
} else {
// Verify that both our local test_context and the core's test_state.context have
// not diverged
auto ci = citems;
for (auto test_ci = test_context.begin();; ci++, test_ci++) {
// skip over markers, they won't be in test_context
while (ci->type == KM_CORE_CT_MARKER) {
ci++;
}
// exit if BOTH are at end.
if (ci->type == KM_CORE_CT_END && test_ci == test_context.end()) {
break; // success
}
// fail if only ONE is at end
test_assert(ci->type != KM_CORE_CT_END && test_ci != test_context.end());
// fail if type and marker don't match.
test_assert(test_ci->type == ci->type && test_ci->marker == ci->marker);
}
// fail if only ONE is at end
test_assert(ci->type != KM_CORE_CT_END && test_ci != test_context.end());
// fail if type and marker don't match.
test_assert(test_ci->type == ci->type && test_ci->marker == ci->marker);
}
km_core_context_items_dispose(citems);
if (text_store != buf) {
if (text_store != buf_final) {
std::cerr << "text store has diverged from buf" << std::endl;
std::cerr << "text store: " << string_to_hex(text_store) << " [" << text_store << "]" << std::endl;
std::cerr << "buf_final : " << string_to_hex(buf_final) << " [" << buf << "]" << std::endl;
std::cerr << "buf : " << string_to_hex(buf) << " [" << buf << "]" << std::endl;
test_assert(false);
}
delete[] buf;
@ -327,7 +410,7 @@ verify_key_list(const km_core_keyboard_key *actual_list, const std::u16string &e
}
int
run_test(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlTestSource& test_source) {
run_test(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlTestSource& test_source, bool fully_normalized_mode) {
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr;
@ -341,18 +424,28 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
}
// setup normalization status
const bool normalization_disabled = !test_kb->supports_normalization();
test_source.set_normalization_disabled(normalization_disabled);
const bool normalization_enabled = test_kb->supports_normalization();
test_source.set_normalization_disabled(!normalization_enabled);
std::wcout
<< console_color::fg(console_color::BLUE) << "* normalization enabled = " << normalization_enabled
<< console_color::reset() << std::endl << std::endl;
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
g_beep_found = false;
std::vector<km_core_context_item> test_context;
km_core_context_item *citems = nullptr;
// setup test_context
try_status(context_items_from_utf16(test_source.get_context().c_str(), &citems));
try_status(km_core_context_set(km_core_state_context(test_state), citems));
if(fully_normalized_mode) {
km_core_state_context_set_if_needed(test_state, test_source.get_context().c_str());
} else {
try_status(km_core_context_set(km_core_state_context(test_state), citems));
}
// Make a copy of the setup context for the test
copy_context_items_to_vector(citems, test_context);
@ -364,7 +457,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
km::tests::ldml_action action;
// verify at beginning
verify_context(text_store, test_state, test_context);
verify_context(text_store, test_state, test_context, fully_normalized_mode, normalization_enabled);
int errorLine = 0; // nonzero if err.
@ -377,8 +470,8 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
break;
case km::tests::LDML_ACTION_KEY_EVENT: {
auto &p = action.k;
std::cout << "- key action: " << km::core::kmx::Debug_VirtualKey(p.vk) << "/modifier "
<< km::core::kmx::Debug_ModifierName(p.modifier_state) << " 0x" << p.modifier_state << std::dec << std::endl;
std::cout << std::endl << "- key action: " << km::core::kmx::Debug_VirtualKey_Always(p.vk) << " modifier: ["
<< km::core::kmx::Debug_ModifierName(p.modifier_state) << " 0x" << p.modifier_state << std::dec << "]" << std::endl;
// Because a normal system tracks caps lock state itself,
// we mimic that in the tests. We assume caps lock state is
// updated on key_down before the processor receives the
@ -388,6 +481,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
}
for (auto key_down = 1; key_down >= 0; key_down--) {
std::cout << " - key_down = " << key_down << std::endl;
// expected error only applies to key down
try_status(km_core_process_event(
test_state, p.vk, p.modifier_state | test_source.caps_lock_state(), key_down,
@ -398,11 +492,17 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
test_context.clear();
text_store.clear();
}
for (auto act = km_core_state_action_items(test_state, nullptr); act->type != KM_CORE_IT_END; act++) {
apply_action(test_state, *act, text_store, test_context, test_source, test_context);
if(fully_normalized_mode) {
auto actions = km_core_state_get_actions(test_state);
apply_actions(test_state, actions, text_store, test_context, test_source, test_context);
} else {
for (auto act = km_core_state_action_items(test_state, nullptr); act->type != KM_CORE_IT_END; act++) {
apply_action(test_state, *act, text_store, test_context, test_source, test_context);
}
}
verify_context(text_store, test_state, test_context, fully_normalized_mode, normalization_enabled);
}
verify_context(text_store, test_state, test_context);
} break;
case km::tests::LDML_ACTION_EMIT_STRING: {
std::cout << "- string emit action: " << action.string << std::endl;
@ -418,13 +518,17 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
}
km_core_context_items_dispose(nitems);
verify_context(text_store, test_state, test_context);
verify_context(text_store, test_state, test_context, fully_normalized_mode, normalization_enabled);
} break;
case km::tests::LDML_ACTION_CHECK_EXPECTED: {
if (!normalization_disabled) {
test_assert(km::core::util::normalize_nfd(action.string)); // TODO-LDML: should be NFC
if (normalization_enabled) {
if (!fully_normalized_mode) {
test_assert(km::core::util::normalize_nfd(action.string));
} else {
test_assert(km::core::util::normalize_nfc(action.string));
}
}
std::cout << "- check expected" << std::endl;
std::cout << std::endl << "# check expected" << std::endl;
std::cout << "expected : " << string_to_hex(action.string) << " [" << action.string << "]" << std::endl;
std::cout << "text store: " << string_to_hex(text_store) << " [" << text_store << "]" << std::endl;
// Compare internal context with expected result
@ -433,7 +537,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
}
} break;
case km::tests::LDML_ACTION_CHECK_KEYLIST: {
std::cout << "- checking keylist" << std::endl;
std::cout << std::endl << "# checking keylist" << std::endl;
// get keylist from kbd
const km_core_keyboard_key* actual_list = test_kb->get_key_list();
if (!verify_key_list(actual_list, action.string, test_source)) {
@ -462,7 +566,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
if (errorLine != 0) {
// re-verify at end (if there wasn't already a failure)
verify_context(text_store, test_state, test_context);
verify_context(text_store, test_state, test_context, fully_normalized_mode, normalization_enabled);
}
// cleanup
@ -476,15 +580,19 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
return __LINE__;
}
if(errorLine != 0) {
std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - error on line " << errorLine << console_color::reset() << std::endl;
}
return errorLine;
}
/**
* Run all tests for this keyboard
*/
int run_all_tests(const km::core::path &source, const km::core::path &compiled, const std::string &filter) {
std::wcout << console_color::fg(console_color::BLUE) << "source file = " << source << std::endl
<< "compiled file = " << compiled << console_color::reset() << std::endl;
int run_all_tests(const km::core::path &source, const km::core::path &compiled, const std::string &filter, bool fully_normalized_mode) {
std::wcout << console_color::fg(console_color::BLUE) << "* source file = " << source << std::endl
<< "* compiled file = " << compiled << console_color::reset() << std::endl;
if(!filter.empty()) {
std::wcout << "Running only tests matching (substring search): " << filter.c_str() << std::endl;
}
@ -497,14 +605,14 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled,
if (!filter.empty()) {
// Always skip the embedded test if there's a filter.
std::wcout << console_color::fg(console_color::YELLOW) << "SKIP: " << source.name() << " (embedded)" << console_color::reset()
std::wcout << console_color::fg(console_color::YELLOW) << "* SKIP: " << source.name() << " (embedded)" << console_color::reset()
<< std::endl;
embedded_result = 0; // no error
} else if (embedded_result == 0) {
// embedded loaded OK, try it
std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << source.name() << " (embedded)"
std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "* TEST: " << source.name() << " (embedded)"
<< console_color::reset() << std::endl;
embedded_result = run_test(source, compiled, embedded_test_source);
embedded_result = run_test(source, compiled, embedded_test_source, fully_normalized_mode);
if (embedded_result != 0) {
failures.push_back("in-XML (@@ comment) embedded test failed");
}
@ -533,7 +641,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled,
continue;
}
std::wcout << console_color::fg(console_color::BLUE) << "TEST: " << json_path.stem().c_str() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl;
int sub_test = run_test(source, compiled, *n.second);
int sub_test = run_test(source, compiled, *n.second, fully_normalized_mode);
if (sub_test != 0) {
std::wcout << console_color::fg(console_color::BRIGHT_RED) << "FAIL: " << json_path.stem() << "/" << console_color::bold() << n.first.c_str()
<< console_color::reset() << std::endl;
@ -632,7 +740,15 @@ int main(int argc, char *argv[]) {
filter = argv[first_arg++];
}
int rc = run_all_tests(ldml_file, kmx_file, filter);
std::cout << "1. Running tests in NFD mode" << std::endl;
int rc = run_all_tests(ldml_file, kmx_file, filter, false);
if (rc != EXIT_SUCCESS) {
std::wcerr << console_color::fg(console_color::BRIGHT_RED) << "FAILED" << console_color::reset() << std::endl;
return EXIT_FAILURE;
}
std::cout << std::endl << "2. Running tests in fully normalized mode" << std::endl;
rc = run_all_tests(ldml_file, kmx_file, filter, true);
if (rc != EXIT_SUCCESS) {
std::wcerr << console_color::fg(console_color::BRIGHT_RED) << "FAILED" << console_color::reset() << std::endl;
rc = EXIT_FAILURE;

View file

@ -423,7 +423,7 @@ LdmlTestSource::get_modifier(std::string const &m) {
std::string key_event::dump() const {
std::stringstream f;
f << "Key: {" << km::core::kmx::Debug_VirtualKey(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}";
f << "Key: {" << km::core::kmx::Debug_VirtualKey_Always(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}";
return f.str();
}

View file

@ -187,9 +187,20 @@ const std::string &block_unicode_ver) {
inline const char *boolstr(bool b) {
return b?"T":"f";
}
#endif
void test_has_boundary_before() {
std::cout << "= " << __FUNCTION__ << std::endl;
// Latin - #15505
test_assert(km::core::util::has_nfc_boundary_before(0x0065));
test_assert(!km::core::util::has_nfc_boundary_before(0x0301));
// Bengali - #15505
test_assert(km::core::util::has_nfc_boundary_before(0x0995));
test_assert(!km::core::util::has_nfc_boundary_before(0x09d7));
#ifdef __EMSCRIPTEN__
std::cout << "I see we are on Emscripten / wasm! Now we will do some additional tests." << std::endl;
std::string icu4c_unicode(U_UNICODE_VERSION), header_unicode(KM_HASBOUNDARYBEFORE_UNICODE_VERSION),
icu4c_icu(U_ICU_VERSION), header_icu(KM_HASBOUNDARYBEFORE_ICU_VERSION);
@ -198,26 +209,26 @@ void test_has_boundary_before() {
assert_basic_equal(icu4c_unicode, header_unicode);
assert_basic_equal(icu4c_icu, header_icu);
std::cout << std::endl << "Now, let's make sure has_nfd_boundary_before() matches ICU." << std::endl;
std::cout << std::endl << "Now, let's make sure has_nfc_boundary_before() matches ICU." << std::endl;
UErrorCode status = U_ZERO_ERROR;
const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status);
const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status);
UASSERT_SUCCESS(status);
// now, test that hasBoundaryBefore is the same
for (km_core_usv cp = 0; cp < km::core::kmx::Uni_MAX_CODEPOINT; cp++) {
auto km_hbb = km::core::util::has_nfd_boundary_before(cp);
auto icu_hbb = nfd->hasBoundaryBefore(cp);
auto km_hbb = km::core::util::has_nfc_boundary_before(cp);
auto icu_hbb = nfc->hasBoundaryBefore(cp);
if (km_hbb != icu_hbb) {
std::cerr << "Error: util_normalize_table.h said " << boolstr(km_hbb) << " but ICU said " << boolstr(icu_hbb) << " for "
<< "has_nfd_boundary_before(0x" << std::hex << cp << std::dec << ")" << std::endl;
<< "has_nfc_boundary_before(0x" << std::hex << cp << std::dec << ")" << std::endl;
}
test_assert(km_hbb == icu_hbb);
}
#endif
std::cout << "All OK!" << std::endl;
}
#endif
int test_all(const char *jsonpath, const char *packagepath, const char *blockspath) {
std::cout << "= " << __FUNCTION__ << std::endl;
@ -234,9 +245,7 @@ int test_all(const char *jsonpath, const char *packagepath, const char *blockspa
test_unicode_versions(versions, package, block_unicode_ver);
#ifdef __EMSCRIPTEN__
test_has_boundary_before();
#endif
return EXIT_SUCCESS;
}

View file

@ -11,7 +11,7 @@
"dependencies": {
"@keymanapp/common-types": "*",
"eventemitter3": "^5.0.0",
"fast-xml-parser": "^5.2.2",
"fast-xml-parser": "^5.3.4",
"path-browserify": "^1.0.1",
"restructure": "^3.0.1",
"sax": ">=0.6.0",

View file

@ -27,9 +27,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"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"
},
@ -642,9 +642,9 @@
}
},
"node_modules/tar": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
"integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
@ -723,9 +723,9 @@
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="
},
"@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"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"
}
@ -1166,9 +1166,9 @@
}
},
"tar": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
"integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"requires": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",

View file

@ -71,7 +71,7 @@ https://help.keyman.com/developer/engine/android/latest-version/
| KEYMAN_VERSION_ICU | 73.1 |
| KEYMAN_VERSION_ISO639_3 | 2024-05-22 |
| KEYMAN_VERSION_JAVA | 21 |
| KEYMAN_VERSION_LANGTAGS | 2025-02-18 |
| KEYMAN_VERSION_LANGTAGS | 2026-02-03 |
| KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY | 2025-03-10 |
| KEYMAN_VERSION_UNICODE | 17.0.0 |

View file

@ -1,3 +1,9 @@
keyman (18.0.246-1) unstable; urgency=medium
* New upstream release.
-- Eberhard Beilharz <eb1@sil.org> Wed, 04 Feb 2026 13:48:31 +0100
keyman (18.0.245-1) unstable; urgency=medium
* New upstream release.

View file

@ -44,7 +44,7 @@ Build-Depends:
python3-xdg,
xserver-xephyr,
xvfb,
Standards-Version: 4.7.2
Standards-Version: 4.7.3
Vcs-Git: https://github.com/keymanapp/keyman.git -b beta [linux/debian]
Vcs-Browser: https://github.com/keymanapp/keyman/tree/beta/linux/debian
Homepage: https://www.keyman.com

View file

@ -6,24 +6,24 @@ Comment: Debian's licensecheck tool uses the term `Expat` for the `MIT` license.
`Unicode-3.0` is identified `Unicode-DFS-2016`.
Files: *
Copyright: 2018-2025 SIL Global
Copyright: 2018-2026 SIL Global
License: Expat
Files: linux/ibus-keyman/*
Copyright: 2004-2025 SIL Global
Copyright: 2004-2026 SIL Global
License: GPL-2+
Files: linux/ibus-keyman/src/keymanutil.c
linux/ibus-keyman/src/keymanutil.h
linux/ibus-keyman/src/kmpdetails.c
linux/ibus-keyman/src/kmpdetails.h
Copyright: 2009-2025 SIL Global
Copyright: 2009-2026 SIL Global
License: GPL-2+ or Expat
Files: linux/ibus-keyman/src/keyman-service.c
linux/ibus-keyman/src/keyman-service.h
linux/keyman-config/buildtools/help2md
Copyright: 2018-2025 SIL Global
Copyright: 2018-2026 SIL Global
License: GPL-3+
Files: linux/ibus-keyman/tests/ibusimcontext.c
@ -33,7 +33,7 @@ Copyright: 2008-2010, Peng Huang <shawn.p.huang@gmail.com>
2008-2013, Peng Huang <shawn.p.huang@gmail.com>
2008-2021, Red Hat, Inc.
2015-2021, Takao Fujiwara <takao.fujiwara1@gmail.com>
2021-2025, SIL Global
2021-2026, SIL Global
License: LGPL-2.1+
Files: linux/keyman-config/buildtools/help2man
@ -43,7 +43,7 @@ License: GPL-3+
Files: debian/com.keyman.config.appdata.xml
debian/com.keyman.ibus_keyman.metainfo.xml
Copyright: 2019 Daniel Glassey <wdg@debian.org>
2022-2025 SIL Global
2022-2026 SIL Global
License: Expat
Files: resources/standards-data/ldml-keyboards/*

View file

@ -74,14 +74,18 @@ class ViewInstalledWindowBase(Gtk.Window):
filter_text.set_name(_("KMP files"))
filter_text.add_pattern("*.kmp")
dlg.add_filter(filter_text)
response = dlg.run()
if response != Gtk.ResponseType.OK:
dlg.destroy()
return
while True:
response = dlg.run()
if response != Gtk.ResponseType.OK:
dlg.destroy()
return
file = dlg.get_filename()
dlg.destroy()
self.restart(self.install_file(file))
file = dlg.get_filename()
if file and os.path.isfile(file) and os.path.splitext(file)[1] == '.kmp':
dlg.destroy()
self.restart(self.install_file(file))
return
# Loop until we have a valid file or the user cancels the dialog
def install_file(self, kmpfile, language=None):
installDlg = InstallKmpWindow(kmpfile, viewkmp=self, language=language)

View file

@ -15,14 +15,14 @@ mkdir -p builddebs
# make the source packages
cd builddebs
vers=$(ls ../dist/keyman_*.orig.tar.gz)
vers=$(ls ../dist/keyman_*.orig.tar.xz)
#echo "vers1:${vers}"
vers=${vers##*_}
#echo "vers2:${vers}"
vers=${vers%*.orig.tar.gz}
vers=${vers%*.orig.tar.xz}
#echo "vers3:${vers}"
cp -a "../dist/keyman_${vers}.orig.tar.gz" .
tar xfz "keyman_${vers}.orig.tar.gz"
cp -a "../dist/keyman_${vers}.orig.tar.xz" .
tar xfJ "keyman_${vers}.orig.tar.xz"
cp -a ../../debian "keyman-${vers}"
cd "keyman-${vers}"
dch -v "${vers}-1" "local build"

View file

@ -4,7 +4,7 @@
# and put them in dist/
# parameters: ./dist.sh [origdist] [proj]
# origdist = create Debian orig.tar.gz
# origdist = create Debian orig.tar.xz
# proj = only make tarball for this project
set -e
@ -39,8 +39,9 @@ dch keyman --newversion "${KEYMAN_VERSION}" --force-bad-version --nomultimaint
# Create the tarball
# files and folders to include in the tarball
# shellcheck disable=2034 # to_exclude appears to be unused
# We always include these files which are the minimum files and
# folder required for Ubuntu/Debian packaging
# shellcheck disable=2034 # to_include appears to be unused
to_include=(
common/build.sh \
common/cpp \
@ -60,18 +61,61 @@ to_include=(
# files and subfolders to exclude from paths included in 'to_include',
# i.e. the exceptions to 'to_include'.
# shellcheck disable=2034 # to_exclude appears to be unused
to_exclude=(
build \
common/test/keyboards/baseline/kmcomp-*.zip \
core/build \
linux/build \
linux/builddebs \
linux/docs/help \
linux/keyman-config/keyman_config/version.py \
linux/keyman-config/buildtools/build-langtags.py \
linux/keyman-system-service/build
)
if [[ -z "${create_origdist+x}" ]]; then
# If we build a full source tarball we include additional files
# so that it's possible to run `${KEYMAN_ROOT}/build.sh` on Linux
# shellcheck disable=2034 # to_include appears to be unused
to_include+=(
common/tools/hextobin \
common/web/keyman-version \
common/web/langtags \
common/web/types \
common/windows/cpp \
common/windows/include \
developer/src/common/include \
developer/src/common/web \
developer/src/ext/json \
developer/src/kmc \
developer/src/kmc-analyze \
developer/src/kmc-copy \
developer/src/kmc-generate \
developer/src/kmc-keyboard-info \
developer/src/kmc-kmn \
developer/src/kmc-ldml \
developer/src/kmc-model \
developer/src/kmc-model-info \
developer/src/kmc-package \
developer/src/kmcmplib \
docs/minimum-versions.md.in
resources/build \
resources/standards-data \
)
# additional files and subfolders to exclude from paths included in 'to_include',
# i.e. the exceptions to 'to_include'.
# shellcheck disable=2034 # to_exclude appears to be unused
to_exclude+=(
*.exe \
resources/build/history \
resources/build/l10n \
resources/build/mac \
resources/build/win \
resources/build/*.lua \
)
fi
# array to store list of --tar-ignore parameters generated from to_include and to_exclude.
ignored_files=()
@ -101,19 +145,19 @@ dpkg-source \
\
"${ignored_files[@]}" \
\
-Zgzip -b .
-Zxz -b .
mv ../keyman_"${KEYMAN_VERSION}".tar.gz linux/dist/keyman-"${KEYMAN_VERSION}".tar.gz
mv ../keyman_"${KEYMAN_VERSION}".tar.xz linux/dist/keyman-"${KEYMAN_VERSION}".tar.xz
echo "3.0 (quilt)" > debian/source/format
cd "${BASEDIR}"
# create orig.tar.gz
# create orig.tar.xz
if [[ ! -z "${create_origdist+x}" ]]; then
cd dist
cd "${KEYMAN_ROOT}/linux/dist"
pkgvers="keyman-${KEYMAN_VERSION}"
tar xfz keyman-"${KEYMAN_VERSION}".tar.gz
tar xfJ keyman-"${KEYMAN_VERSION}".tar.xz
mv -v keyman "${pkgvers}" 2>/dev/null || mv -v "$(find . -mindepth 1 -maxdepth 1 -type d)" "${pkgvers}"
tar cfz "keyman_${KEYMAN_VERSION}.orig.tar.gz" "${pkgvers}"
rm "keyman-${KEYMAN_VERSION}.tar.gz"
tar cfJ "keyman_${KEYMAN_VERSION}.orig.tar.xz" "${pkgvers}"
rm "keyman-${KEYMAN_VERSION}.tar.xz"
rm -rf "${pkgvers}"
fi

View file

@ -42,8 +42,8 @@ function downloadSource() {
uscan || (echo "ERROR: No new version available for ${proj}" >&2 && exit 1)
cd ..
mv "${proj}-${version}" "${BASEDIR}/${packageDir}"
mv "${proj}_${version}.orig.tar.gz" "${BASEDIR}/${packageDir}"
mv "${proj}-${version}.tar.gz" "${BASEDIR}/${packageDir}"
mv "${proj}_${version}.orig.tar.xz" "${BASEDIR}/${packageDir}"
mv "${proj}-${version}.tar.xz" "${BASEDIR}/${packageDir}"
mv "${proj}"*.asc "${BASEDIR}/${packageDir}"
rm "${proj}"*.debian.tar.xz
cd "${BASEDIR}/${packageDir}" || exit
@ -91,78 +91,135 @@ function generate_tar_ignore_list() {
local list_var="$4"
local prefix="$5"
local includes_array="${includes_var}[@]"
# shellcheck disable=SC2034
local includes=("${!includes_array}")
local excludes_array="${excludes_var}[@]"
local excludes=("${!excludes_array}")
local dir all_dirs found_match inc
mapfile -t all_dirs < <(find "${directory}" -mindepth 1 -maxdepth 1 -type d | sort)
for dir in "${all_dirs[@]}"; do
found_match=false
for inc in "${includes[@]}"; do
if [[ "./${inc}" =~ ^${dir} ]]; then
found_match=true
if [[ "./${inc}" != "${dir}" ]]; then
# check subdirectories
generate_tar_ignore_list "${dir}" "${includes_var}" "${excludes_var}" "${list_var}" "${prefix}"
fi
# check if files/subdir in $dir are in excludes list
_generate_excludes_for_dir "${dir}" "${includes_var}" "${excludes_var}" "${list_var}"
break
fi
done
if ! ${found_match}; then
_add_to_list "${list_var}" "${dir}"
# Loop through excludes and put all without path in single_excludes
local single_excludes=()
for item in "${excludes[@]}"; do
if [[ ${item} != */* ]]; then
single_excludes+=("${item}")
fi
done
local ignore_list=()
_process_directory "${directory}" false
for item in "${ignore_list[@]}"; do
eval "${list_var}+=(\"--tar-ignore=${item}\")"
done
}
function _generate_excludes_for_dir() {
function _process_directory() {
local directory="$1"
local includes_var="$2"
local excludes_var="$3"
local list_var="$4"
local includes_array="${includes_var}[@]"
local includes=("${!includes_array}")
local excludes_array="${excludes_var}[@]"
local excludes=("${!excludes_array}")
local file all_files excluded included is_match
local isParentIncluded="$2"
local all_items item
mapfile -t all_files < <(find "${directory}" -mindepth 1 -maxdepth 1 | sort)
is_match=false
for file in "${all_files[@]}"; do
for included in "${includes[@]}"; do
if [[ "${file}" == ./${included} ]]; then
is_match=true
break
mapfile -t all_items < <(find "${directory}" -mindepth 1 -maxdepth 1 | sort)
for item in "${all_items[@]}"; do
debug "Checking item: ${item}"
if _is_exact_match "includes" "${item}"; then
debug " Including (full match): ${item}"
if [[ -d "${item}" ]]; then
_process_directory "${item}" true
fi
done
if ${is_match} ; then
if [[ "${file}" != ./${included} ]] && [[ -f "${file}" ]]; then
_add_to_list "${list_var}" "${file}"
elif _starts_with "includes" "${item}"; then
debug " Including (partial match): ${item}"
if [[ -d "${item}" ]]; then
_process_directory "${item}" "${isParentIncluded}"
fi
elif _is_exact_match "excludes" "${item}"; then
debug " Excluding (full match): ${item}"
_add_to_list "${item}"
elif _ends_with "single_excludes" "${item}" || _is_wildcard_match "single_excludes" "${item}"; then
debug " Excluding (single exclude): ${item}"
_add_to_list "${item}"
elif [[ "${isParentIncluded}" == "false" ]]; then
debug " Excluding (not included): ${item}"
_add_to_list "${item}"
else
for excluded in "${excludes[@]}"; do
if [[ "${file}" == ./${excluded} ]]; then
_add_to_list "${list_var}" "${file}"
break
elif [[ "./${excluded}" =~ ^${file} ]]; then
# check subdirectories
_generate_excludes_for_dir "${file}" "${includes_var}" "${excludes_var}" "${list_var}"
break
fi
done
debug " Including (parent included): ${item}"
if [[ -d "${item}" ]]; then
_process_directory "${item}" "${isParentIncluded}"
fi
fi
done
}
function _add_to_list() {
local list_var="$1"
local filename="$2"
# Note: the files end up in subdirectories under `keyman` (or rather
# the directory name of $KEYMAN_ROOT), so we can
# include that when matching files and directories to ignore.
# shellcheck disable=SC2154
eval "${list_var}+=(\"--tar-ignore=${prefix}/${filename#./}\")"
local item="$1"
ignore_list+=("${prefix}/${item#./}")
}
# Returns true if one of the values in the $1 array equals ${file} ($2)
# Example: will return true for array=(path1/path2) file=./path1/path2
function _is_exact_match() {
local array_var="$1"
local file="$2"
local array_name="${array_var}[@]"
local haystack=("${!array_name}")
local item
for item in "${haystack[@]}"; do
if [[ "./${item#./}" == "${file}" ]]; then
return 0
fi
done
return 1
}
# Returns true if one of the values in the $1 array starts with ${file} ($2)
# Example: will return true for array=(path1/path2) file=./path1
function _starts_with() {
local array_var="$1"
local file="$2"
local array_name="${array_var}[@]"
local array_values=("${!array_name}")
local array_item
for array_item in "${array_values[@]}"; do
if [[ "./${array_item#./}" == ${file}* ]]; then
return 0
fi
done
return 1
}
# Returns true if ${file} ($2) ends with one of the values in the $1 array
# Example: will return true for array=(path2) file=./path1/path2
function _ends_with() {
local array_var="$1"
local file="$2"
local array_name="${array_var}[@]"
local array_values=("${!array_name}")
local array_item
for array_item in "${array_values[@]}"; do
if [[ "${file}" == */${array_item#./} ]]; then
return 0
fi
done
return 1
}
# Returns true if ${file} ($2) matches one of the values of the $1 array.
# These values may contain wildcards.
# Example: will return true for array=(*.sh) file=./path1/build.sh
function _is_wildcard_match() {
local array_var="$1"
local file="$2"
local array_name="${array_var}[@]"
local array_values=("${!array_name}")
local array_item
for array_item in "${array_values[@]}"; do
array_item=${array_item/./\\.}
if [[ "${file}" =~ /${array_item/\*/.\*}$ ]]; then
return 0
fi
done
return 1
}
function debug() {
# echo "$@"
return 0
}

View file

@ -16,61 +16,105 @@ function setup_file() {
# Create directory structure for testing
# /
# /subdir1
# /build/foo
# /build.sh
# /README.md
# /path1
# /abc.build
# /build/foo
# /build.sh
# /README.md
# /x/z.txt
# /xy/z.txt
# /path2
# /build/foo
# /build.sh
# /path3
# /build/foo
# /build.sh
# /subdir2
# /build/foo
# /build.sh
# /path1
# /build/foo
# /subdir3
# /build/foo
# /build.sh
# /path1
# /build/foo
# /path2
# /build/foo
# /path3
# /subdir4
# /build/foo
# /build.sh
# /path1
# /build.sh
# /path2
# /subpath1
# /build/foo
# /build.sh
# /subpath2
# /build/foo
# /build.sh
# /path3
# /build.sh
# /build.sh
# subdir1/path2, subdir2, subdir4/path1, subdir4/path2/subpath1,
# and subdir4/path3 will be ignored
mkdir -p "${temp_dir}/subdir1/path1" # include
touch "${temp_dir}/subdir1/build.sh"
# all build/ directories will be ignored with the exception of
# subdir3/build.
mkdir -p "${temp_dir}/subdir1/build" # exclude
touch "${temp_dir}/subdir1/build/foo" # exclude
mkdir -p "${temp_dir}/subdir1/path1/build"
touch "${temp_dir}/subdir1/path1/build/foo" # exclude
mkdir -p "${temp_dir}/subdir1/path1/x"
touch "${temp_dir}/subdir1/path1/x/z.txt"
mkdir -p "${temp_dir}/subdir1/path1/xy"
touch "${temp_dir}/subdir1/path1/xy/z.txt"
touch "${temp_dir}/subdir1/build.sh" # include
touch "${temp_dir}/subdir1/README.md" # exclude
touch "${temp_dir}/subdir1/path1/abc.build"
touch "${temp_dir}/subdir1/path1/build.sh" # include
touch "${temp_dir}/subdir1/path1/README.md" # exclude
mkdir -p "${temp_dir}/subdir1/path2" # exclude
touch "${temp_dir}/subdir1/path2/build.sh"
mkdir -p "${temp_dir}/subdir1/path3" # include
touch "${temp_dir}/subdir1/path3/build.sh"
mkdir -p "${temp_dir}/subdir2/path1" # exclude
mkdir -p "${temp_dir}/subdir1/path2/build"
touch "${temp_dir}/subdir1/path2/build/foo" # exclude
touch "${temp_dir}/subdir1/path2/build.sh" # exclude
mkdir -p "${temp_dir}/subdir1/path3/build"
touch "${temp_dir}/subdir1/path2/build/foo" # exclude
touch "${temp_dir}/subdir1/path3/build.sh" # include
mkdir -p "${temp_dir}/subdir2/build" # exclude
touch "${temp_dir}/subdir2/build/foo" # exclude
mkdir -p "${temp_dir}/subdir2/path1/build" # exclude
touch "${temp_dir}/subdir2/path1/build/foo" # exclude
touch "${temp_dir}/subdir2/build.sh"
mkdir -p "${temp_dir}/subdir3/build" # include
touch "${temp_dir}/subdir3/build/foo" # include
mkdir -p "${temp_dir}/subdir3/path1" # include
touch "${temp_dir}/subdir3/build.sh"
mkdir -p "${temp_dir}/subdir3/path1/build" # exclude
touch "${temp_dir}/subdir3/path1/build/foo" # exclude
mkdir -p "${temp_dir}/subdir3/path2" # include
mkdir -p "${temp_dir}/subdir3/path2/build" # exclude
touch "${temp_dir}/subdir3/path2/build/foo" # exclude
mkdir -p "${temp_dir}/subdir3/path3" # include
mkdir -p "${temp_dir}/subdir4/build" # exclude
touch "${temp_dir}/subdir4/build/foo" # exclude
mkdir -p "${temp_dir}/subdir4/path1" # exclude
touch "${temp_dir}/subdir4/build.sh"
touch "${temp_dir}/subdir4/path1/build.sh"
mkdir -p "${temp_dir}/subdir4/path2/subpath1" # exclude
touch "${temp_dir}/subdir4/path2/subpath1/build.sh"
mkdir -p "${temp_dir}/subdir4/path2/subpath1/build" # exclude
touch "${temp_dir}/subdir4/path2/subpath1/build/foo" # exclude
mkdir -p "${temp_dir}/subdir4/path2/subpath2" # include
touch "${temp_dir}/subdir4/path2/subpath2/build.sh"
mkdir -p "${temp_dir}/subdir4/path2/subpath2/build" # exclude
touch "${temp_dir}/subdir4/path2/subpath2/build/foo" # exclude
mkdir -p "${temp_dir}/subdir4/path3" # exclude
touch "${temp_dir}/subdir4/path3/build.sh"
touch "${temp_dir}/subdir4/path3/other.txt"
touch "${temp_dir}/build.sh"
}
function teardown_file() {
@ -89,9 +133,14 @@ function test__generate_tar_ignore_list__basic() {
# Verify
expected=(
--tar-ignore=test1/build.sh
--tar-ignore=test1/subdir1/build
--tar-ignore=test1/subdir1/build.sh
--tar-ignore=test1/subdir1/path2
--tar-ignore=test1/subdir1/README.md
--tar-ignore=test1/subdir2
--tar-ignore=test1/subdir4/build
--tar-ignore=test1/subdir4/build.sh
--tar-ignore=test1/subdir4/path1
--tar-ignore=test1/subdir4/path2/subpath1
--tar-ignore=test1/subdir4/path3
@ -113,6 +162,9 @@ function test__generate_tar_ignore_list__path_wildcard_in_exclude() {
# Verify
expected=(
--tar-ignore=foo/build.sh # not in include
--tar-ignore=foo/subdir1/build # not in include
--tar-ignore=foo/subdir1/build.sh # not in include
--tar-ignore=foo/subdir1/path1/README.md # in exclude
--tar-ignore=foo/subdir1/path2 # not in include
--tar-ignore=foo/subdir1/README.md # not in include
@ -136,10 +188,12 @@ function test__generate_tar_ignore_list__file_wildcard_in_exclude() {
# Verify
expected=(
--tar-ignore=keyman/build.sh # not in include
--tar-ignore=keyman/subdir1/build # not in include
--tar-ignore=keyman/subdir1/build.sh # not in include
--tar-ignore=keyman/subdir1/path1/build.sh # in exclude
--tar-ignore=keyman/subdir1/path2 # not in include
--tar-ignore=keyman/subdir1/path3/build.sh # in exclude
--tar-ignore=keyman/subdir1/build.sh # not in include
--tar-ignore=keyman/subdir1/README.md # not in include
--tar-ignore=keyman/subdir2 # not in include
--tar-ignore=keyman/subdir3/build.sh # in exclude
@ -162,13 +216,17 @@ function test__generate_tar_ignore_list__file_wildcard_in_include() {
# Verify
expected=(
--tar-ignore=xyz/build.sh # not in include
--tar-ignore=xyz/subdir1/build # not in include
--tar-ignore=xyz/subdir1/build.sh # not in include
--tar-ignore=xyz/subdir1/path2 # not in include
--tar-ignore=xyz/subdir1/README.md # in exclude
--tar-ignore=xyz/subdir2 # not in include
--tar-ignore=xyz/subdir4/build # not in include
--tar-ignore=xyz/subdir4/build.sh # not in include
--tar-ignore=xyz/subdir4/path1 # not in include
--tar-ignore=xyz/subdir4/path2 # not in include
--tar-ignore=xyz/subdir4/path3/other.txt # not in include
--tar-ignore=xyz/subdir4/path2/subpath1 # not necessary, but in exclude
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
@ -187,9 +245,14 @@ function test__generate_tar_ignore_list__path_wildcard_in_include() {
# Verify
expected=(
--tar-ignore=bar/build.sh # not in include
--tar-ignore=bar/subdir1/build # not in include
--tar-ignore=bar/subdir1/build.sh # not in include
--tar-ignore=bar/subdir1/path2 # not in include
--tar-ignore=bar/subdir1/README.md # not in include
--tar-ignore=bar/subdir2 # not in include
--tar-ignore=bar/subdir4/build # not in include
--tar-ignore=bar/subdir4/build.sh # not in include
--tar-ignore=bar/subdir4/path1 # not in include
--tar-ignore=bar/subdir4/path2/subpath1 # not in include
--tar-ignore=bar/subdir4/path3 # not in include
@ -210,15 +273,370 @@ function test__generate_tar_ignore_list__exclude_subsubdir() {
# Verify
expected=(
--tar-ignore=baz/subdir1
--tar-ignore=baz/subdir2
--tar-ignore=baz/subdir3
--tar-ignore=baz/subdir4/path2/subpath2
--tar-ignore=baz/build.sh # not in include
--tar-ignore=baz/subdir1 # not in include
--tar-ignore=baz/subdir2 # not in include
--tar-ignore=baz/subdir3 # not in include
--tar-ignore=baz/subdir4/path2/subpath2 # in exclude
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
}
function test__generate_tar_ignore_list__exclude_build_Dirs() {
to_include=(subdir1/path1 subdir1/path3 subdir3 subdir3/build subdir4/path2)
to_exclude=(subdir1/README.md subdir4/path2/subpath1 subdir4/path2/subpath3 build)
cd "${temp_dir}"
ignored_files=()
# Execute
generate_tar_ignore_list "./" to_include to_exclude ignored_files wildcard
# Verify
expected=(
--tar-ignore=wildcard/build.sh # not in include
--tar-ignore=wildcard/subdir1/build # in exclude
--tar-ignore=wildcard/subdir1/build.sh # not in include
--tar-ignore=wildcard/subdir1/path1/build # in exclude
--tar-ignore=wildcard/subdir1/path2 # not in include
--tar-ignore=wildcard/subdir1/path3/build # in exclude
--tar-ignore=wildcard/subdir1/README.md # in exclude
--tar-ignore=wildcard/subdir2 # not in include
--tar-ignore=wildcard/subdir3/path1/build # in exclude
--tar-ignore=wildcard/subdir3/path2/build # in exclude
--tar-ignore=wildcard/subdir4/build # in exclude
--tar-ignore=wildcard/subdir4/build.sh # not in include
--tar-ignore=wildcard/subdir4/path1 # not in include
--tar-ignore=wildcard/subdir4/path2/subpath1 # in exclude
--tar-ignore=wildcard/subdir4/path2/subpath2/build # in exclude
--tar-ignore=wildcard/subdir4/path3 # not in include
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
}
function test__generate_tar_ignore_list__exclude_xy() {
to_include=(subdir1 subdir2 subdir3 subdir4)
to_exclude=(xy)
cd "${temp_dir}"
ignored_files=()
# Execute
generate_tar_ignore_list "./" to_include to_exclude ignored_files wildcard
# Verify
expected=(
--tar-ignore=wildcard/build.sh # not in include
--tar-ignore=wildcard/subdir1/path1/xy # in exclude
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
}
function test__generate_tar_ignore_list__exclude_x_doesnot_exclude_xy() {
to_include=(subdir1 subdir2 subdir3 subdir4)
to_exclude=(x)
cd "${temp_dir}"
ignored_files=()
# Execute
generate_tar_ignore_list "./" to_include to_exclude ignored_files wildcard
# Verify
expected=(
--tar-ignore=wildcard/build.sh # not in include
--tar-ignore=wildcard/subdir1/path1/x # in exclude
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
}
function test__generate_tar_ignore_list__can_include_toplevel_files() {
to_include=(subdir1 subdir3 ./build.sh)
to_exclude=(*.sh)
cd "${temp_dir}"
ignored_files=()
# Execute
generate_tar_ignore_list "./" to_include to_exclude ignored_files top
# Verify
expected=(
--tar-ignore=top/subdir1/build.sh # in exclude
--tar-ignore=top/subdir1/path1/build.sh # in exclude
--tar-ignore=top/subdir1/path2/build.sh # in exclude
--tar-ignore=top/subdir1/path3/build.sh # in exclude
--tar-ignore=top/subdir2 # not in include
--tar-ignore=top/subdir3/build.sh # in exclude
--tar-ignore=top/subdir4 # not in include
)
assert-equal "${ignored_files[*]}" "${expected[*]}"
}
# Tests for _starts_with function
function test__starts_with__match_parent() {
# Setup
includes=(subdir1/path1 subdir3)
# Verify
assert-true _starts_with includes ./subdir1
}
function test__starts_with__exact_match() {
# Setup
includes=(subdir1/path1 subdir3)
# Verify
assert-true _starts_with includes ./subdir1/path1
}
function test__starts_with__exact_match_localdir() {
# Setup
includes=(./subdir1/path1 ./subdir3)
# Verify
assert-true _starts_with includes ./subdir1/path1
}
function test__starts_with__no_match() {
# Setup
includes=(subdir1/path1 subdir3)
# Verify
assert-false _starts_with includes ./subdir2
}
function test__starts_with__normalized_paths() {
# Setup
includes=(path1 path2)
# Verify
assert-true _starts_with includes ./path1
}
function test__starts_with__subdir() {
# Setup
includes=(path1 path2)
# Verify
assert-false _starts_with includes ./path1/subdir
}
function test__starts_with__no_match_different_name() {
# Setup
includes=(path1 path2)
# Verify
assert-false _starts_with includes ./path3
}
# Tests for _ends_with function
function test__ends_with__exact_filename_first() {
# Setup
local array=(README.md test.txt)
# Verify
assert-true _ends_with array ./subdir/README.md
}
function test__ends_with__exact_filename_second() {
# Setup
local array=(README.md test.txt)
# Verify
assert-true _ends_with array ./path/to/test.txt
}
function test__ends_with__not_matching_similar_extension() {
# Setup
local array=(README.md test.txt)
# Verify
assert-false _ends_with array ./README.md.bak
}
function test__ends_with__no_match_other_filename() {
# Setup
local array=(README.md test.txt)
# Verify
assert-false _ends_with array ./other.txt
}
# Tests for _is_exact_match function
function test__is_exact_match__full_path_first() {
# Setup
includes=(./subdir1/path1 subdir3)
# Verify
assert-true _is_exact_match includes ./subdir1/path1
}
function test__is_exact_match__full_path_second() {
# Setup
includes=(./subdir1/path1 subdir3)
# Verify
assert-true _is_exact_match includes ./subdir3
}
function test__is_exact_match__partial_path_no_match() {
# Setup
includes=(./subdir1/path1 subdir3)
# Verify
assert-false _is_exact_match includes ./subdir1
}
function test__is_exact_match__sibling_path_no_match() {
# Setup
includes=(./subdir1/path1 subdir3)
# Verify
assert-false _is_exact_match includes ./subdir1/path2
}
function test__is_exact_match__normalized_paths_first() {
# Setup
includes=(subdir1 subdir2)
# Verify
assert-true _is_exact_match includes ./subdir1
}
function test__is_exact_match__normalized_paths_second() {
# Setup
includes=(subdir1 subdir2)
# Verify
assert-true _is_exact_match includes ./subdir2
}
function test__is_exact_match__empty_array() {
# Setup
includes=()
# Verify
assert-false _is_exact_match includes ./anything
}
# Tests for _is_wildcard_match function
function test__is_wildcard_match__file() {
# Setup
local array=(sh)
# Verify
assert-true _is_wildcard_match array ./config/sh
}
function test__is_wildcard_match__dot_file() {
# Setup
local array=(sh)
# Verify
assert-false _is_wildcard_match array ./config/.sh
}
function test__is_wildcard_match__dot_files_nested() {
# Setup
local array=(.sh)
# Verify
assert-true _is_wildcard_match array ./deeply/nested/build/.sh
}
function test__is_wildcard_match__no_dot_prefix() {
# Setup
local array=(sh)
# Verify
assert-false _is_wildcard_match array ./build.sh
}
function test__is_wildcard_match__toplevel_file() {
# Setup
local array=(sh)
# Verify
assert-true _is_wildcard_match array ./sh
}
function test__is_wildcard_match__multiple_extensions_first() {
# Setup
local array=(tmp log)
# Verify
assert-true _is_wildcard_match array ./path/tmp
}
function test__is_wildcard_match__multiple_extensions_second() {
# Setup
local array=(tmp log)
# Verify
assert-true _is_wildcard_match array ./path/log
}
function test__is_wildcard_match__dot_in_wildcard_first() {
# Setup
local array=(.tmp .log)
# Verify
assert-true _is_wildcard_match array ./path/.tmp
}
function test__is_wildcard_match__dot_in_wildcard_second() {
# Setup
local array=(.tmp .log)
# Verify
assert-true _is_wildcard_match array ./path/.log
}
function test__is_wildcard_match__dot_in_wildcard_no_dotfile() {
# Setup
local array=(.tmp .log)
# Verify
assert-false _is_wildcard_match array ./path/xtmp
}
function test__is_wildcard_match__wildcard_match_dotfile() {
# Setup
local array=(\*.sh)
# Verify
assert-true _is_wildcard_match array ./path/.sh
}
function test__is_wildcard_match__wildcard_match_file() {
# Setup
local array=(\*.sh)
# Verify
assert-true _is_wildcard_match array ./path/subdir/build.sh
}
function test__is_wildcard_match__wildcard_no_matches_file() {
# Setup
local array=(\*.sh)
# Verify
assert-false _is_wildcard_match array ./path/old
}
function test__is_wildcard_match__wildcard_no_matches_path() {
# Setup
local array=(\*.sh)
# Verify
assert-false _is_wildcard_match array ./path/build.sh/foo
}
# shellcheck disable=2119
run_tests

View file

@ -1,5 +1,5 @@
# Uncomment the next line to define a global platform for your project
platform :osx, '10.13'
platform :osx, '11.0'
use_frameworks!
target 'Keyman' do
@ -7,11 +7,11 @@ target 'Keyman' do
# use_frameworks!
# Pods for Keyman
pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.38.0'
pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.57.3'
target 'KeymanTests' do
inherit! :search_paths
pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.38.0'
pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.57.3'
use_frameworks!
# Pods for testing
end

View file

@ -1,24 +1,24 @@
PODS:
- Sentry (8.38.0-beta.1):
- Sentry/Core (= 8.38.0-beta.1)
- Sentry/Core (8.38.0-beta.1)
- Sentry (8.57.3):
- Sentry/Core (= 8.57.3)
- Sentry/Core (8.57.3)
DEPENDENCIES:
- Sentry (from `https://github.com/getsentry/sentry-cocoa.git`, tag `8.38.0`)
- Sentry (from `https://github.com/getsentry/sentry-cocoa.git`, tag `8.57.3`)
EXTERNAL SOURCES:
Sentry:
:git: https://github.com/getsentry/sentry-cocoa.git
:tag: 8.38.0
:tag: 8.57.3
CHECKOUT OPTIONS:
Sentry:
:git: https://github.com/getsentry/sentry-cocoa.git
:tag: 8.38.0
:tag: 8.57.3
SPEC CHECKSUMS:
Sentry: 4d6027fbfde9ddc35e5c368292843097d039db5f
Sentry: c643eb180df401dd8c734c5036ddd9dd9218daa6
PODFILE CHECKSUM: 19b128c35d9c5e59f90d09522c053de65096696a
PODFILE CHECKSUM: d45d4bde6c75c2c91314777c88c421e5e604d84d
COCOAPODS: 1.15.2

View file

@ -60,7 +60,7 @@ fv,fv_ojibwa_rdot,ᐁᓂᔑᓇᐯᒧᐏᓐ (a-finals right w-dot),Eastern Subarc
fv,fv_ojibwa_ifinal,ᐊᓂᔑᓇᐯᒧᐎᣙ (i-finals),Eastern Subarctic,fv_ojibwa_ifinal_kmw-9.0.js,1.0.3,oj,Ojibwa
fv,fv_ojibwa_ifinal_rdot,ᐊᓂᔑᓇᐯᒧᐏᣙ (i-finals right w-dot),Eastern Subarctic,fv_ojibwa_ifinal_rdot_kmw-9.0.js,1.0.3,oj,Ojibwa
fv,fv_naskapi,ᓇᔅᑲᐱ (Naskapi),Eastern Subarctic,fv_naskapi_kmw-9.0.js,9.3.1,nsk-Cans,Naskapi (Unified Canadian Aboriginal Syllabics)
sil,sil_euro_latin,English,European,european2-1.6.js,3.0.3,en,English
sil,sil_euro_latin,English,European,european2-1.6.js,3.0.4,en,English
basic,basic_kbdcan,Français,European,canadian_french-1.0.js,1.1.1,fr-CA,French (Canada)
fv,fv_anishinaabemowin,Anishinaabemowin,Great Lakes - St. Lawrence,fv_anishinaabemowin_kmw-9.0.js,10.2,oj,Ojibwa
fv,fv_bodewadminwen,Bodéwadminwen-Nishnabémwen,Great Lakes - St. Lawrence,fv_bodewadminwen_kmw-9.0.js,9.1.1,pot-Latn,Potawatomi (Latin)
@ -102,5 +102,6 @@ fv,fv_shihgotine_yati,Shıhgot'ı̨nę́ Yatı̨́,Western Subarctic,fv_shihgot
fv,fv_southern_tutchone,Southern Tutchone,Western Subarctic,fv_southern_tutchone_kmw-9.0.js,9.3,tce-Latn,Southern Tutchone (Latin)
fv,fv_tagizi_dene,Tāgizi Dene,Western Subarctic,fv_tagizi_dene_kmw-9.0.js,9.4,tgx-Latn,Tagish (Latin)
fv,fv_tlicho_yatii,ı̨chǫ Yatıı̀,Western Subarctic,fv_tlicho_yatii_kmw-9.0.js,9.1.1,dgr-Latn,Dogrib (Latin)
fv,fv_tlingityooxatangi,Taku Tlingit Yóo Xátangí,Western Subarctic,fv_tlingityooxatangi-9.0.js,1.0,tli,Taku Tlingit Yóo Xátangí
fv,fv_dene_mb,ᑌᓀ ᔭᕠᐁ (Dene MB),Western Subarctic,fv_dene_mb_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics)
fv,fv_dene_nt,ᑌᓀ ᔭᕱᐁ (Dene NT),Western Subarctic,fv_dene_nt_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics)

1 Shortname ID Name Region 9.0 Web Keyboard Version Language ID Language Name
60 fv fv_ojibwa_ifinal ᐊᓂᔑᓇᐯᒧᐎᣙ (i-finals) Eastern Subarctic fv_ojibwa_ifinal_kmw-9.0.js 1.0.3 oj Ojibwa
61 fv fv_ojibwa_ifinal_rdot ᐊᓂᔑᓇᐯᒧᐏᣙ (i-finals right w-dot) Eastern Subarctic fv_ojibwa_ifinal_rdot_kmw-9.0.js 1.0.3 oj Ojibwa
62 fv fv_naskapi ᓇᔅᑲᐱ (Naskapi) Eastern Subarctic fv_naskapi_kmw-9.0.js 9.3.1 nsk-Cans Naskapi (Unified Canadian Aboriginal Syllabics)
63 sil sil_euro_latin English European european2-1.6.js 3.0.3 3.0.4 en English
64 basic basic_kbdcan Français European canadian_french-1.0.js 1.1.1 fr-CA French (Canada)
65 fv fv_anishinaabemowin Anishinaabemowin Great Lakes - St. Lawrence fv_anishinaabemowin_kmw-9.0.js 10.2 oj Ojibwa
66 fv fv_bodewadminwen Bodéwadminwen-Nishnabémwen Great Lakes - St. Lawrence fv_bodewadminwen_kmw-9.0.js 9.1.1 pot-Latn Potawatomi (Latin)
102 fv fv_southern_tutchone Southern Tutchone Western Subarctic fv_southern_tutchone_kmw-9.0.js 9.3 tce-Latn Southern Tutchone (Latin)
103 fv fv_tagizi_dene Tāgizi Dene Western Subarctic fv_tagizi_dene_kmw-9.0.js 9.4 tgx-Latn Tagish (Latin)
104 fv fv_tlicho_yatii Tłı̨chǫ Yatıı̀ Western Subarctic fv_tlicho_yatii_kmw-9.0.js 9.1.1 dgr-Latn Dogrib (Latin)
105 fv fv_tlingityooxatangi T’aku Tlingit Yóo X’átangí Western Subarctic fv_tlingityooxatangi-9.0.js 1.0 tli T’aku Tlingit Yóo X’átangí
106 fv fv_dene_mb ᑌᓀ ᔭᕠᐁ (Dene MB) Western Subarctic fv_dene_mb_kmw-9.0.js 9.2.1 chp-Cans Chipewyan (Unified Canadian Aboriginal Syllabics)
107 fv fv_dene_nt ᑌᓀ ᔭᕱᐁ (Dene NT) Western Subarctic fv_dene_nt_kmw-9.0.js 9.2.1 chp-Cans Chipewyan (Unified Canadian Aboriginal Syllabics)

8
package-lock.json generated
View file

@ -243,7 +243,7 @@
"dependencies": {
"@keymanapp/common-types": "*",
"eventemitter3": "^5.0.0",
"fast-xml-parser": "^5.2.2",
"fast-xml-parser": "^5.3.4",
"path-browserify": "^1.0.1",
"restructure": "^3.0.1",
"sax": ">=0.6.0",
@ -7841,9 +7841,9 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.2.tgz",
"integrity": "sha512-ZaCmslH75Jkfowo/x44Uq8KT5SutC5BFxHmY61nmTXPccw11PVuIXKUqC2hembMkJ3nPwTkQESXiUlsKutCbMg==",
"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==",
"funding": [
{
"type": "github",

View file

@ -48,6 +48,6 @@ KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER=noble # Ubuntu 24.04 Noble
# Data versions -- see resources/standards-data/readme.md
KEYMAN_VERSION_CLDR=46 # LDML Keyboards version
KEYMAN_VERSION_ISO639_3=2024-05-22 # Date of last import
KEYMAN_VERSION_LANGTAGS=2025-02-18 # _version value
KEYMAN_VERSION_LANGTAGS=2026-02-03 # _version value
KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY=2025-03-10 # Date from first line of language-subtag-registry
KEYMAN_VERSION_UNICODE=17.0.0 # UCD + related data

View file

@ -77,6 +77,31 @@ assert-succeeded() {
fi
}
_callfunc() {
local command="$1"
shift
"${command}" "$@"
}
assert-true() {
# shellcheck disable=SC2310
if ! _callfunc "$@"; then
messages+=("- Expected test to return true but it returned false
")
((test_failures++))
fi
}
assert-false() {
# shellcheck disable=SC2310
if _callfunc "$@"; then
messages+=("- Expected test to return false but it returned true
")
((test_failures++))
fi
}
# setup() will run before each test
# can be overriden in test script
setup() {

View file

@ -164,6 +164,74 @@ EOF
assert-contains "${test_output_contents}" "PASS: test_succeeding1"
}
# Helper functions for testing assert-true and assert-false
function returns_true() {
return 0
}
function returns_false() {
return 1
}
## Tests for assert-true function
function test__assert_true__with_function_that_returns_true() {
# Execute and Verify - should pass
assert-true returns_true
}
function test__assert_true__with_builtin_true() {
# Execute and Verify - should pass
assert-true true
}
function test__assert_true__with_function_with_args_success() {
# Execute and Verify - file exists
assert-true test -f /etc/hostname
}
function test__assert_true__increments_failure_on_false() {
# Setup
test_failures=0
messages=()
# Execute function that returns false
returns_false
local result=$?
# Verify it failed
assert-equal "${result}" "1"
}
## Tests for assert-false function
function test__assert_false__with_function_that_returns_false() {
# Execute and Verify - should pass
assert-false returns_false
}
function test__assert_false__with_builtin_false() {
# Execute and Verify - should pass
assert-false false
}
function test__assert_false__with_function_with_args_success() {
# Execute and Verify - file does not exist
assert-false test -f /nonexistent/file/that/does/not/exist
}
function test__assert_false__increments_failure_on_true() {
# Setup
test_failures=0
messages=()
# Execute function that returns true
returns_true
local result=$?
# Verify it succeeded
assert-equal "${result}" "0"
}
# shellcheck disable=SC2031
if [[ "${1:-}" != "--recursive" ]]; then

View file

@ -14,7 +14,7 @@ _utils_inc_sh=1
# 1: UPLOAD_DIR Directory where artifact can be found
# 2: ARTIFACT_FILENAME Filename (without path) of artifact
# 3: ARTIFACT_NAME Descriptive name of artifact
# 4: ARTIFACT_TYPE File extension of artifact, without initial period (e.g. tar.gz)
# 4: ARTIFACT_TYPE File extension of artifact, without initial period (e.g. tar.xz)
# 5: PLATFORM Target platform for artifact
#
# TODO: Move to CI include?

View file

@ -135,6 +135,10 @@ wrap-signcode() {
}
wrap-symstore() {
local target="$1"
local slasht="$2"
local product="$3"
if builder_is_ci_build && builder_is_ci_build_level_build; then
builder_echo "Skipping symstore - buildLevel=build"
return 0
@ -147,10 +151,10 @@ wrap-symstore() {
"$ProgramFilesx86/Windows Kits/10/Debuggers/x64/symstore.exe" \
add \
//s "$KEYMAN_SYMSTOREPATH" \
//s "$(cygpath -w "$KEYMAN_SYMSTOREPATH")" \
//v "$KEYMAN_VERSION_WIN" \
//c "Version: $KEYMAN_VERSION_WITH_TAG" \
//compress //f "$@"
//compress //f "$(cygpath -w "$target")" "$slasht" "$product"
}
wrap-mt() {

File diff suppressed because it is too large Load diff

View file

@ -60,10 +60,10 @@ function _make_release_source_tarball() {
./scripts/reconf.sh
PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh
mkdir -p "upload/${KEYMAN_VERSION}"
cp -a dist/*.tar.gz "upload/${KEYMAN_VERSION}"
cp -a dist/*.tar.xz "upload/${KEYMAN_VERSION}"
(
cd "upload/${KEYMAN_VERSION}"
sha256sum ./*.tar.gz > SHA256SUMS
sha256sum ./*.tar.xz > SHA256SUMS
builder_echo end "make source tarball" success "Make source tarball"
)
}
@ -75,7 +75,7 @@ function _sign_source_tarball() {
eval "$(gpg-agent -vv --daemon --allow-preset-passphrase --debug-level 9)"
/usr/lib/gnupg/gpg-preset-passphrase --passphrase "${GPGKEYPW}" --preset "${GPGKEYGRIP}"
for f in ./*.tar.gz; do gpg --output "${f}.asc" -a --detach-sig "${f}"; done
for f in ./*.tar.xz; do gpg --output "${f}.asc" -a --detach-sig "${f}"; done
/usr/lib/gnupg/gpg-preset-passphrase --forget "${GPGKEYGRIP}" || true
builder_echo end "sign source tarball" success "Sign source tarball"
)
@ -84,10 +84,10 @@ function _sign_source_tarball() {
function _publish_to_downloads() {
builder_echo start "publish to downloads" "Publish to downloads.keyman.com"
local UPLOAD_DIR KEYMAN_TGZ
local UPLOAD_DIR KEYMAN_TXZ
UPLOAD_DIR="upload/${KEYMAN_VERSION}"
KEYMAN_TGZ="keyman-${KEYMAN_VERSION}.tar.gz"
KEYMAN_TXZ="keyman-${KEYMAN_VERSION}.tar.xz"
# Set permissions as required on download site
builder_echo "Setting upload file permissions for downloads.keyman.com"
@ -96,7 +96,7 @@ function _publish_to_downloads() {
chmod g+w "${UPLOAD_DIR}"/*
chmod a+r "${UPLOAD_DIR}"/*
write_download_info "${UPLOAD_DIR}" "${KEYMAN_TGZ}" "Keyman for Linux" tar.gz linux
write_download_info "${UPLOAD_DIR}" "${KEYMAN_TXZ}" "Keyman for Linux" tar.xz linux
tc_rsync_upload "${UPLOAD_DIR}" "linux/${KEYMAN_TIER}"
builder_echo end "publish to downloads" success "Publish to downloads.keyman.com"

View file

@ -85,7 +85,8 @@
},
"imports": {
"#gesture-tools": "./src/engine/osk/gesture-processor/build/tools/obj/index.js",
"#recorder": "./build/tools/testing/recorder/obj/index.js"
"#recorder": "./build/tools/testing/recorder/obj/index.js",
"#test-resources/*.js": "./build/test/resources/*.js"
},
"repository": {
"type": "git",

View file

@ -1,15 +1,12 @@
import { assert } from 'chai'
import sinon from 'sinon';
import * as PromiseStatusModule from 'promise-status-async';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
import { assertingPromiseStatus as promiseStatus } from '../../../../../resources/assertingPromiseStatus.js';
import { InputSample, gestures, GestureDebugPath } from '@keymanapp/gesture-recognizer';
import { assertingPromiseStatus as promiseStatus } from '#test-resources/assertingPromiseStatus.js';
import { TouchpathTurtle } from '#gesture-tools';
import { simulateMultiSourceMatcherInput } from "../../../../../resources/simulateMultiSourceInput.js";
import { simulateMultiSourceMatcherInput } from "#test-resources/simulateMultiSourceInput.js";
import {
FlickEndModel,
@ -27,6 +24,8 @@ import {
MainLongpressSourceModel
} from './isolatedPathSpecs.js';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
type PathInheritanceType = gestures.specs.ContactModel<string>['pathInheritance'];
function dummyInheritanceMatcher(inheritanceType: PathInheritanceType): gestures.specs.GestureModel<string> {
return {

View file

@ -1,10 +1,14 @@
import { assert } from 'chai'
import sinon from 'sinon';
import * as PromiseStatusModule from 'promise-status-async';
import { assertingPromiseStatus as promiseStatus } from '../../../../../resources/assertingPromiseStatus.js';
import { GestureModelDefs, buildGestureMatchInspector, gestures } from '@keymanapp/gesture-recognizer';
import { ManagedPromise, timedPromise } from '@keymanapp/web-utils';
import { HeadlessInputEngine, TouchpathTurtle } from '#gesture-tools';
import { assertingPromiseStatus as promiseStatus } from '#test-resources/assertingPromiseStatus.js';
import { assertGestureSequence, SequenceAssertion } from "#test-resources/sequenceAssertions.js";
const { matchers } = gestures;
// Huh... gotta do BOTH here? One for constructor use, the other for generic-parameter use?
@ -16,10 +20,7 @@ type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
const getGestureModelSet = gestures.specs.getGestureModelSet;
const modelSetForAction = gestures.matchers.modelSetForAction;
import { HeadlessInputEngine, TouchpathTurtle } from '#gesture-tools';
import { ManagedPromise, timedPromise } from '@keymanapp/web-utils';
import { assertGestureSequence, SequenceAssertion } from "../../../../../resources/sequenceAssertions.js";
import {
LongpressModel,

View file

@ -2,18 +2,13 @@ import { assert } from 'chai'
import sinon from 'sinon';
import * as PromiseStatusModule from 'promise-status-async';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
import { assertingPromiseStatus as promiseStatus } from '../../../../../resources/assertingPromiseStatus.js';
import { simulateMultiSourceMatcherInput, simulateSelectorInput } from "../../../../../resources/simulateMultiSourceInput.js";
import { timedPromise } from '@keymanapp/web-utils';
import { gestures } from '@keymanapp/gesture-recognizer';
import { TouchpathTurtle } from '#gesture-tools';
type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
type GestureModel<Type> = gestures.specs.GestureModel<Type>;
import { assertingPromiseStatus as promiseStatus } from '#test-resources/assertingPromiseStatus.js';
import { simulateMultiSourceMatcherInput, simulateSelectorInput } from "#test-resources/simulateMultiSourceInput.js";
import {
LongpressModel,
@ -26,6 +21,10 @@ import {
LongpressDistanceThreshold
} from './isolatedPathSpecs.js';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
type GestureModel<Type> = gestures.specs.GestureModel<Type>;
describe("MatcherSelector", function () {
beforeEach(function() {
this.fakeClock = sinon.useFakeTimers();

View file

@ -1,13 +1,12 @@
import { assert } from 'chai'
import sinon from 'sinon';
import * as PromiseStatusModule from 'promise-status-async';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
import { assertingPromiseStatus as promiseStatus } from '../../../../../resources/assertingPromiseStatus.js';
import { InputSample, GestureSource, gestures, CumulativePathStats } from '@keymanapp/gesture-recognizer';
import { timedPromise } from '@keymanapp/web-utils';
import { assertingPromiseStatus as promiseStatus } from '#test-resources/assertingPromiseStatus.js';
import {
InstantRejectionModel,
InstantResolutionModel,
@ -22,6 +21,8 @@ import {
FlickEndThreshold
} from './isolatedPathSpecs.js';
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
async function simulateSequence(
samples: InputSample<string>[],
fakeClock: sinon.SinonFakeTimers,

View file

@ -1,20 +1,14 @@
import { assert } from 'chai'
import sinon from 'sinon';
import * as PromiseStatusModule from 'promise-status-async';
import { assertingPromiseStatus as promiseStatus } from '../../../../../resources/assertingPromiseStatus.js';
import { PROMISE_PENDING } from 'promise-status-async';
import { GestureModelDefs, GestureSource, gestures, TouchpointCoordinator } from '@keymanapp/gesture-recognizer';
const { matchers } = gestures;
// Huh... gotta do BOTH here? One for constructor use, the other for generic-parameter use?
const { GestureSequence } = matchers;
type GestureSequence<Type> = gestures.matchers.GestureSequence<Type>;
import { HeadlessInputEngine, TouchpathTurtle } from '#gesture-tools';
import { ManagedPromise, timedPromise } from '@keymanapp/web-utils';
import { assertGestureSequence, SequenceAssertion } from "../../../../../resources/sequenceAssertions.js";
import { HeadlessInputEngine, TouchpathTurtle } from '#gesture-tools';
import { assertingPromiseStatus as promiseStatus } from '#test-resources/assertingPromiseStatus.js';
import { assertGestureSequence, SequenceAssertion } from "#test-resources/sequenceAssertions.js";
import {
LongpressModel,
@ -25,9 +19,13 @@ import {
SubkeySelectModel
} from './isolatedGestureSpecs.js';
const LongpressDurationThreshold = LongpressModel.contacts[0].model.timer.duration;
const { matchers } = gestures;
import { PROMISE_PENDING } from 'promise-status-async';
// Huh... gotta do BOTH here? One for constructor use, the other for generic-parameter use?
const { GestureSequence } = matchers;
type GestureSequence<Type> = gestures.matchers.GestureSequence<Type>;
const LongpressDurationThreshold = LongpressModel.contacts[0].model.timer.duration;
const TestGestureModelDefinitions: GestureModelDefs<string> = {
gestures: [

View file

@ -774,7 +774,8 @@ end;
procedure UpdateAvailableState.HandleInstallNow;
begin
bucStateContext.SetApplyNow(True);
ChangeState(DownloadingState);
// A new kmshell process will be used to download
StartDownloadProcess;
end;
{ DownloadingState }

View file

@ -839,7 +839,12 @@ begin
FResult := TUtilExecute.Shell(0, ShellPath, '', '-an');
if not FResult then
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
'TrmfMain: Shell Execute Update_ApplyNow Failed');
'TrmfMain: Shell Execute Update_ApplyNow Failed')
else
ModalResult := mrAbort;
// If a splash screen is currently open when "Install Now" is executed,
// setting mrAbort ensures the splash screen is closed on the
// return of "Keyman Configuration".
end;
end;

View file

@ -70,7 +70,7 @@ uses
Windows, Controls, SysUtils, Classes, ErrorControlledRegistry, Forms, MessageIdentifiers, MessageIdentifierConsts, keymanapi_TLB;
procedure Run;
procedure Main(Owner: TComponent = nil);
function Main(Owner: TComponent = nil): TModalResult;
type
@ -164,10 +164,11 @@ procedure ShowKeyboardWelcome(PackageName: WideString); forward; // I2569
procedure PrintKeyboard(KeyboardName: WideString); forward; // I2329
function ProcessBackgroundUpdate(FMode: TKMShellMode; FSilent: Boolean): Boolean; forward;
procedure Main(Owner: TComponent = nil);
function Main(Owner: TComponent = nil): TModalResult;
var
frmMain: TfrmMain;
begin
Result := mrNone;
if not Assigned(Owner) then
begin
UfrmWebContainer.CreateForm(TfrmMain, frmMain);
@ -180,7 +181,7 @@ begin
begin
with TfrmMain.Create(Owner) do
try
ShowModal;
Result := ShowModal;
finally
Free;
end;

View file

@ -63,6 +63,7 @@ type
FShowConfigurationOnLoad: Boolean;
procedure WMUser_FormShown(var Message: TMessage); message WM_USER_FormShown;
procedure WMUser(var Message: TMessage); message WM_USER;
procedure ShowConfiguration;
protected
procedure FireCommand(const command: WideString; params: TStringList);
override;
@ -112,6 +113,17 @@ begin
end;
end;
procedure TfrmSplash.ShowConfiguration;
var
configFrmResult: Integer;
begin
configFrmResult := Main(Self);
if configFrmResult = mrAbort then
Command_Exit
else
Do_Content_Render;
end;
procedure TfrmSplash.TntFormActivate(Sender: TObject);
begin
inherited;
@ -152,15 +164,17 @@ begin
if FShowConfigurationOnLoad then
begin
Main(Self);
Do_Content_Render;
ShowConfiguration;
end;
end;
procedure TfrmSplash.FireCommand(const command: WideString; params: TStringList);
begin
if command = 'start' then Command_Start
else if command = 'config' then begin Main(Self); Do_Content_Render; end // I4393 // I4396
else if command = 'config' then
begin
ShowConfiguration;
end // I4393 // I4396
else if command = 'hidesplash' then FShouldDisplay := False
else if command = 'showsplash' then FShouldDisplay := True
else if command = 'exit' then Command_Exit