Merge branch 'master' into fix/windows/15333-simplify-keyboard-refresh

This commit is contained in:
Marc Durdin 2026-09-01 15:44:05 +07:00 • committed by GitHub
commit 3c44f8ab5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 505 additions and 289 deletions

View file

@ -1,5 +1,21 @@
# Keyman Version History
## 19.0.276 alpha 2026-08-28
* fix(windows): add manifest to insthelper and set trustinfo to asInvoker (#16430)
* fix(windows): resync caps lock state when Keyman keyboard is activated (#16423)
## 19.0.275 alpha 2026-08-27
* docs(mac): refresh build/setup docs and remove APPSTORECONNECT_PROVIDER (#16432)
* chore(mac): update setup document (#16415)
* change(web): enhance missing-stubs error (#16437)
## 19.0.274 alpha 2026-08-24
* maint(windows): move to vs2026 arm runner for GHA (#16418)
* chore(web): skip guide-examples.tests.ts until race is fixed (#16427)
## 19.0.273 alpha 2026-08-22
* feat(linux): use display language for keyboard search (#16393)

View file

@ -1 +1 @@
19.0.274
19.0.277

View file

@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh"
# ################################ Main script ################################
@ -19,6 +20,7 @@ builder_describe "Builds Keyman Engine for Android." \
"configure" \
"build" \
"test Runs lint and unit tests." \
"publish-symbols Publishes symbols to Sentry." \
":engine Builds Engine"
builder_parse "$@"
@ -79,6 +81,13 @@ do_test() {
./gradlew $GRADLE_DAEMON $TEST_FLAGS
}
builder_run_action clean:engine rm -rf build app/build
builder_run_action build:engine do_build
builder_run_action test:engine do_test
do_publish_symbols() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
sentry_upload_web "${KEYMAN_ROOT}/web/build/app/webview/release/"
fi
}
builder_run_action clean:engine rm -rf build app/build
builder_run_action build:engine do_build
builder_run_action test:engine do_test
builder_run_action publish-symbols do_publish_symbols

View file

@ -326,10 +326,20 @@ int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) {
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) {
if(ch == 0 || !ctx) {
return NULL;
}
if (!p) {
p = *ctx;
if (!p)
if (!p) {
return NULL;
}
}
// skip initial delimiter
while (*p == ch) {
p++;
}
KMX_WCHAR* q = p;
@ -356,10 +366,20 @@ KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) {
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx) {
if(!ctx || !delimiters || !(*delimiters)) {
return NULL;
}
if (!p) {
p = *ctx;
if (!p)
if (!p) {
return NULL;
}
}
// skip initial delimiters
while (*p && u16chr(delimiters, *p)) {
p++;
}
KMX_WCHAR* q = p;

View file

@ -896,22 +896,36 @@ end;
procedure TfrmCharacterMapNew.gridMouseWheelDown(Sender: TObject;
Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin
Handled := True;
if ssCtrl in Shift then
begin
Handled := True;
// Zoom
if tbSize.Position < tbSize.Max then
tbSize.Position := tbSize.Position + tbSize.PageSize;
end
else
begin
// Scroll
if grid.TopRow < grid.RowCount - grid.VisibleRowCount then
grid.TopRow := grid.TopRow + 1;
end;
end;
procedure TfrmCharacterMapNew.gridMouseWheelUp(Sender: TObject;
Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin
Handled := True;
if ssCtrl in Shift then
begin
Handled := True;
// Zoom
if tbSize.Position > tbSize.Min then
tbSize.Position := tbSize.Position - tbSize.PageSize;
end
else
begin
// Scroll
if grid.TopRow > 0 then
grid.TopRow := grid.TopRow - 1;
end;
end;

View file

@ -1,10 +1,15 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
import 'mocha';
import { assert } from 'chai';
import { KmnCompiler } from '../src/main.js';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { KmxFileReader } from '@keymanapp/common-types';
import { compileTestKeyboard } from './helpers/index.js';
import { KmnCompiler } from '../src/main.js';
const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/');
const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/';
@ -13,6 +18,16 @@ const baselineDir = keyboardsDir + 'baseline/';
describe('Compiler class', function() {
const callbacks = new TestCompilerCallbacks(this);
this.beforeEach(function() {
callbacks.clear();
});
this.afterEach(function() {
if(this.currentTest?.isFailed()) {
callbacks.printMessages();
}
});
it('should throw on failure', async function() {
const compiler = new KmnCompiler();
const callbacks : any = null; // ERROR
@ -129,4 +144,20 @@ describe('Compiler class', function() {
assert.deepEqual(kvkData, kvkFixtureData);
});
it('should trim all whitespace for `&targets` store', async function() {
const result = await compileTestKeyboard(callbacks, ['keyboards', 'targets-with-whitespace.kmn']);
assert.isNotNull(result);
// Verify implictly that `&targets` store was interpreted correctly as 'any'
// because the compiler generated both JS and KMX targets (#13721)
assert.isNotNull(result.artifacts.js);
assert.isNotNull(result.artifacts.kmx);
// Then verify directly that the `&targets` store was trimmed by looking at
// the final value in the kmx data
const reader = new KmxFileReader();
const keyboard = reader.read(result.artifacts.kmx.data);
assert.equal(keyboard.targets, 'any');
});
});

View file

@ -0,0 +1,11 @@
store(&NAME) 'targets_with_whitespace'
store(&VERSION) '10.0'
c this used to generate KM0207B: At least one compile target must be specified
store(&TARGETS) ' any'
begin unicode > use(main)
group(main) using keys
+ 'x' > 'y'

View file

@ -1,8 +1,13 @@
/**
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Helpers and utilities for the Mocha tests.
*/
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { assert } from 'chai';
import { CompilerCallbacks } from '@keymanapp/developer-utils';
import { KmnCompiler } from '../../src/compiler/compiler.js';
/**
* Builds a path to the fixture with the given path components.
@ -15,3 +20,13 @@ import { fileURLToPath } from 'url';
export function makePathToFixture(...components: string[]): string {
return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url));
}
export async function compileTestKeyboard(callbacks: CompilerCallbacks, fixture: string[]) {
const compiler = new KmnCompiler();
assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false}));
assert(compiler.verifyInitialized());
const kmnPath = makePathToFixture(...fixture);
return await compiler.run(kmnPath, null);
}

View file

@ -138,7 +138,7 @@ KMX_BOOL IsSameToken(PKMX_WCHAR *p, KMX_WCHAR const * token);
KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, int offset, int IsUnicode);
PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags);
KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isVKey, int isUnicode);
KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets);
KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets);
int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p);
@ -1331,7 +1331,16 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s
return TRUE;
}
KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets) {
/**
* Extract the compile targets from the &targets store, and rewrite the
* &targets dpString value to remove unnecessary whitespace. Does not
* reallocate sp->dpString, but overwrites its value with a string the
* same length or shorter.
* @param store store value to rewrite
* @param targets (output)
* @return FALSE if no targets found or invalid targets found
*/
KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets) {
// Compile to .kmx
const std::vector<std::u16string> KMXKeymanTargets{
u"windows", u"macosx", u"linux", u"desktop"
@ -1347,10 +1356,17 @@ KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets)
targets = 0;
auto p = new KMX_WCHAR[u16len(store)+1];
u16cpy(p, store);
auto q = p;
*p = 0;
KMX_WCHAR* ctx;
auto token = u16tok(p, u" ", &ctx);
auto token = u16tok(store, u" ", &ctx);
while(token) {
if(q > p) {
// Insert a delimiter between tokens, if
// more than one token
*q++ = ' ';
*q = 0;
}
bool found = false;
if(*token) {
if(AnyTarget == token) {
@ -1378,9 +1394,16 @@ KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets)
targets = 0;
return FALSE;
}
// Append the token to the output, we know it is long enough
// because the buffer is the same length as the input
u16cpy(q, token);
q = const_cast<KMX_WCHAR*>(u16chr(q, 0));
*q = 0;
}
token = u16tok(nullptr, u" ", &ctx);
}
u16cpy(store, p);
delete[] p;
if(targets == 0) {
@ -2079,7 +2102,7 @@ int LineTokenType(PKMX_WCHAR *str)
switch (towupper(*p))
{
case 'C':
if (iswspace(*(p + 1))) return T_COMMENT;
if (iswspace(*(p + 1)) || *(p + 1) == 0) return T_COMMENT;
break;
case 0:
return T_BLANK;

View file

@ -71,66 +71,90 @@ void test_kmcmp_CompileKeyboard(char *kmn_file) {
unlink(kmn_file);
}
extern KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets);
extern KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR* buf, int &targets);
KMX_BOOL do_GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets, const KMX_WCHAR* expected = nullptr) {
KMX_WCHAR* buf;
buf = new KMX_WCHAR[u16len(store)+1];
u16cpy(buf, store);
KMX_BOOL result = GetCompileTargetsFromTargetsStore(buf, targets);
if(expected) {
test_assert(u16cmp(expected, buf) == 0);
}
delete[] buf;
return result;
}
void test_GetCompileTargetsFromTargetsStore() {
int targets = 0;
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"any", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"any", targets, u"any"));
test_assert(error_vec.size() == 0);
test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS));
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"windows", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"windows", targets, u"windows"));
test_assert(error_vec.size() == 0);
test_assert(targets == COMPILETARGETS_KMX);
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"desktop", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop", targets, u"desktop"));
test_assert(error_vec.size() == 0);
test_assert(targets == COMPILETARGETS_KMX);
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"mobile", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"mobile", targets, u"mobile"));
test_assert(error_vec.size() == 0);
test_assert(targets == COMPILETARGETS_JS);
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"web", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"web", targets, u"web"));
test_assert(error_vec.size() == 0);
test_assert(targets == COMPILETARGETS_JS);
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"desktop mobile", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop mobile", targets, u"desktop mobile"));
test_assert(error_vec.size() == 0);
test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS));
setup();
test_assert(GetCompileTargetsFromTargetsStore(u"desktop tablet", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop tablet", targets, u"desktop tablet"));
test_assert(error_vec.size() == 0);
test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS));
setup();
test_assert(!GetCompileTargetsFromTargetsStore(u"foo bar baz", targets));
test_assert(do_GetCompileTargetsFromTargetsStore(u" desktop tablet", targets, u"desktop tablet"));
test_assert(error_vec.size() == 0);
test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS));
setup();
test_assert(do_GetCompileTargetsFromTargetsStore(u" windows androidphone ", targets, u"windows androidphone"));
test_assert(error_vec.size() == 0);
test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS));
setup();
test_assert(!do_GetCompileTargetsFromTargetsStore(u"foo bar baz", targets));
test_assert(error_vec.size() == 1);
test_assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget);
test_assert(targets == 0);
setup();
test_assert(!GetCompileTargetsFromTargetsStore(u"windows chromeos", targets));
test_assert(!do_GetCompileTargetsFromTargetsStore(u"windows chromeos", targets));
test_assert(error_vec.size() == 1);
test_assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget);
test_assert(targets == 0);
setup();
test_assert(!GetCompileTargetsFromTargetsStore(u" ", targets));
test_assert(!do_GetCompileTargetsFromTargetsStore(u" ", targets));
test_assert(error_vec.size() == 1);
test_assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified);
test_assert(targets == 0);
setup();
test_assert(!GetCompileTargetsFromTargetsStore(u"", targets));
test_assert(!do_GetCompileTargetsFromTargetsStore(u"", targets));
test_assert(error_vec.size() == 1);
test_assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified);
test_assert(targets == 0);

View file

@ -663,7 +663,7 @@ TEST_F(CompilerTest, LineTokenType_test) {
// comment without following space ... potential bug, but ReadLine() currently ensures following space
u16cpy(str, u"c");
p = str;
EXPECT_EQ(T_UNKNOWN, LineTokenType(&p));
EXPECT_EQ(T_COMMENT, LineTokenType(&p));
EXPECT_EQ(0, p - str);
// T_KEYTOKEY

View file

@ -32,6 +32,11 @@ TEST(km_u16_Test, u16tok_char_delim) {
KMX_WCHAR str[LINESIZE];
KMX_WCHAR *ctx = nullptr;
// invalid parameters
EXPECT_EQ(nullptr, u16tok(str, ' ', nullptr));
EXPECT_EQ(nullptr, u16tok(str, (KMX_WCHAR)0, &ctx));
EXPECT_EQ(nullptr, u16tok(nullptr, ' ', nullptr));
// sequence of tokens
u16cpy(str, u"test a space and two");
ctx = nullptr;
@ -55,6 +60,13 @@ TEST(km_u16_Test, u16tok_char_delim) {
EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, ' ', &ctx)));
EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx));
// delimiters at start
u16cpy(str, u" a b");
ctx = nullptr;
EXPECT_TRUE(!u16cmp(u"a", u16tok(str, ' ', &ctx)));
EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, ' ', &ctx)));
EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx));
// no string, no context
ctx = nullptr;
EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx));
@ -73,6 +85,12 @@ TEST(km_u16_Test, u16tok_str_delim) {
KMX_WCHAR str[LINESIZE];
KMX_WCHAR *ctx = nullptr;
// invalid parameters
EXPECT_EQ(nullptr, u16tok(str, u" ", nullptr));
EXPECT_EQ(nullptr, u16tok(str, nullptr, &ctx));
EXPECT_EQ(nullptr, u16tok(str, u"", &ctx));
EXPECT_EQ(nullptr, u16tok(nullptr, u" ", nullptr));
// sequence of tokens
u16cpy(str, u"test a space and two");
ctx = nullptr;
@ -96,6 +114,13 @@ TEST(km_u16_Test, u16tok_str_delim) {
EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, u" ", &ctx)));
EXPECT_EQ(nullptr, u16tok(nullptr, u" ", &ctx));
// delimiters at start
u16cpy(str, u" a b");
ctx = nullptr;
EXPECT_TRUE(!u16cmp(u"a", u16tok(str, u" ", &ctx)));
EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, u" ", &ctx)));
EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx));
// no string, no context
ctx = nullptr;
EXPECT_EQ(nullptr, u16tok(nullptr, u"", &ctx));
@ -130,6 +155,9 @@ TEST(km_u16_Test, u16tok_str_compare_to_strtok) {
// Compare behaviour of strtok:
char str[LINESIZE];
// Note: strtok behavior is undefined with invalid parameters so we don't do a reference test
// of invalid parameters
// sequence of tokens
strcpy(str, "test a space and two");
EXPECT_TRUE(!strcmp("test", strtok(str, " ")));
@ -150,6 +178,12 @@ TEST(km_u16_Test, u16tok_str_compare_to_strtok) {
EXPECT_TRUE(!strcmp("b", strtok(nullptr, " ")));
EXPECT_EQ(nullptr, strtok(nullptr, " "));
// delimiters at start
strcpy(str, " a b");
EXPECT_TRUE(!strcmp("a", strtok(str, " ")));
EXPECT_TRUE(!strcmp("b", strtok(nullptr, " ")));
EXPECT_EQ(nullptr, strtok(nullptr, " "));
// multiple delimiters
strcpy(str, "abc<def>ghi");
const char *delim = "<>";

View file

@ -1,5 +1,20 @@
import { extractVersionData } from './version-data.js';
// TODO: environment should be just KEYMAN_VERSION
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Environmental variables and paths
*/
import path from 'node:path';
import KEYMAN_VERSION from "@keymanapp/keyman-version";
export const environment = extractVersionData(KEYMAN_VERSION.VERSION_WITH_TAG);
/**
* @returns base path for the running server -- where index.js is stored
*/
export function serverBasePath() {
return path.join(import.meta.dirname, '../../build/src');
}
/**
* @returns path where the site public files can be found -- index.html
*/
export function serverSitePath() {
return path.join(serverBasePath(), 'site');
}

View file

@ -1,15 +1,21 @@
import { KeymanUrls } from '@keymanapp/developer-utils';
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* API: return list of packages under testing and urls for installing in mobile
* apps
*/
import * as express from 'express';
import { KeymanUrls } from '@keymanapp/developer-utils';
import KEYMAN_VERSION from '@keymanapp/keyman-version';
import { data } from "../../data.js";
import { environment } from '../../environment.js';
export default function handleIncPackagesJson (req: express.Request, res: express.Response) {
const packages = Object.keys(data.packages).map(id => { return { id: id, filename: id+'.kmp', name: data.packages[id].name} });
res.send({
packages: packages,
urls: {
installLinkAndroid: KeymanUrls.KeymanDeveloper_KeymanForAndroidDownload(environment.versionRelease),
installLinkIos: KeymanUrls.KeymanDeveloper_KeymanForIosDownload(environment.versionRelease),
installLinkAndroid: KeymanUrls.KeymanDeveloper_KeymanForAndroidDownload(KEYMAN_VERSION.VERSION_RELEASE),
installLinkIos: KeymanUrls.KeymanDeveloper_KeymanForIosDownload(KEYMAN_VERSION.VERSION_RELEASE),
}
});
}

View file

@ -1,12 +1,17 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Keyman Developer Server main program
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import chalk from 'chalk';
import express from 'express';
import multer from 'multer';
import * as ws from 'ws';
import KEYMAN_VERSION from '@keymanapp/keyman-version';
import { KeymanSentry } from './KeymanSentry.js';
import { standardPaths } from './standardPaths.js';
import { environment } from './environment.js';
import setupRoutes from './routes.js';
import { shutdown } from './shutdown.js';
import { initTray } from './tray.js';
@ -23,7 +28,7 @@ const options = {
// set of options if an error occurs.
await loadOptions();
console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${getOption('web host port')}.`);
console.log(`Starting Keyman Developer Server ${KEYMAN_VERSION.VERSION_WITH_TAG}, listening on port ${getOption('web host port')}.`);
KeymanSentry.init();
try {
@ -74,7 +79,7 @@ export async function run() {
/* Setup routes */
setupRoutes(app, upload, wsServer, environment);
setupRoutes(app, upload, wsServer);
/* Start the web server */

View file

@ -1,6 +1,12 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Setup URL routes for Keyman Developer Server
*/
import * as express from 'express';
import * as ws from 'ws';
import * as multer from 'multer';
import KEYMAN_VERSION from '@keymanapp/keyman-version';
import handleIncKeyboardsJs from './handlers/inc/keyboards-js.js';
import { data, DebugFont, DebugKeyboard, DebugModel, DebugObject, DebugPackage, isValidId } from './data.js';
import apiGet from './handlers/api/debugobject/get.js';
@ -11,22 +17,20 @@ import apiUnregister from './handlers/api/debugobject/unregister.js';
import handleIncPackagesJson from './handlers/inc/packages-json.js';
import apiPackageRegister from './handlers/api/package/register.js';
import handleIncKeyboardsCss from './handlers/inc/keyboards-css.js';
import { Environment } from './version-data.js';
import { standardPaths } from './standardPaths.js';
import chalk from 'chalk';
import { shutdown } from './shutdown.js';
import { getOption } from './options.js';
import { serverSitePath } from './environment.js';
export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) {
export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer ) {
/* Middleware - JSON and logging */
app.use(express.json()); // for parsing application/json
app.use(function (req, _res, next) {
// if(environment.environment == 'local') {
console.log(req.method + ' ' + req.path);
// }
console.log(req.method + ' ' + req.path);
next();
});
@ -51,7 +55,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer,
/* All routes */
app.use('/', express.static('build/src/site'));
app.use('/', express.static(serverSitePath()));
app.post('/upload', localhostOnly, upload.single('file'), (req, res, next) => {
const name = req.file.originalname;
@ -93,7 +97,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer,
app.get('/inc/packages.json', handleIncPackagesJson);
app.get('/api-public/version', (req,res,next)=>{
res.json({version: environment.versionWithTag, isApiAvailable: isLocalhost(req)});
res.json({version: KEYMAN_VERSION.VERSION_WITH_TAG, isApiAvailable: isLocalhost(req)});
next();
});

View file

@ -1,52 +0,0 @@
/*
Version information from builder-basic.inc.sh:
# KEYMAN_VERSION: Full current build version, e.g. "14.0.1"
# [KEYMAN_VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0"]
# KEYMAN_VERSION_RELEASE: Current release version, e.g. "14.0"
# KEYMAN_VERSION_MAJOR: Major version, e.g. "14"
# KEYMAN_VERSION_MINOR: Minor version, e.g. "0"
# KEYMAN_VERSION_PATCH: Patch version, e.g. "1"
# KEYMAN_TIER: Current tier, one of "alpha", "beta" or "stable"
# KEYMAN_VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local]
# KEYMAN_VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test"
# [KEYMAN_ROOT: fully resolved root path of Keyman repository]
# KEYMAN_VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable
# KEYMAN_VERSION_GIT_TAG: Git tag for the release, "release@$KEYMAN_VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234"
*/
export interface Environment {
version: string;
versionRelease: string;
versionMajor: string;
versionMinor: string;
versionPatch: string;
tier: string;
versionTag: string;
versionWithTag: string;
versionEnvironment: string;
versionGitTag: string;
// Pull Request Data
pr: string;
}
export function extractVersionData(version: string): Environment {
const versionData = /^(\d+)\.(\d+)\.(\d+)((?:-(alpha|beta))?(?:-(test|local))?(?:-(\d+))?)$/.exec(version);
if(!versionData) return null;
return {
version: `${versionData[1]}.${versionData[2]}.${versionData[3]}`,
versionRelease: `${versionData[1]}.${versionData[2]}`,
versionMajor: versionData[1],
versionMinor: versionData[2],
versionPatch: versionData[3],
tier: versionData[5] || 'stable',
versionTag: versionData[4],
versionWithTag: version,
versionEnvironment: versionData[6] || versionData[5] || 'stable',
versionGitTag: 'release@'+version,
pr: versionData[7] || ''
};
};

View file

@ -71,6 +71,11 @@ export class Win32Tray {
open(ngrokAddress);
break;
case 'item-id-show-console':
// #9694 - the first time, this has to be called twice for the console
// window to appear. This could probably be fixed in the source
// node-hide-console-window component, but this works well enough and
// has little in the way of side-effects.
WindowsConsole.showConsole();
WindowsConsole.showConsole();
break;
case 'item-id-hide-console':

View file

@ -0,0 +1,20 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import {assert} from 'chai';
import 'mocha';
import { serverBasePath, serverSitePath } from '../src/environment.js';
describe('serverBasePath', function() {
it('should find index.js in the base path', function() {
assert.isTrue(fs.existsSync(path.join(serverBasePath(), 'index.js')));
});
});
describe('serverSitePath', function() {
it('should find index.html in the base path', function() {
assert.isTrue(fs.existsSync(path.join(serverSitePath(), 'index.html')));
});
});

View file

@ -1,55 +0,0 @@
import {assert} from 'chai';
import 'mocha';
import { extractVersionData } from '../src/version-data.js';
describe('extractVersionData', function() {
it('should parse version strings', function() {
let v = extractVersionData('14.0.283');
assert.equal(v.version, '14.0.283');
assert.equal(v.versionRelease, '14.0');
assert.equal(v.versionMajor, '14');
assert.equal(v.versionMinor, '0');
assert.equal(v.versionPatch, '283');
assert.equal(v.tier, 'stable');
assert.equal(v.versionTag, '');
assert.equal(v.versionWithTag, '14.0.283');
assert.equal(v.versionEnvironment, 'stable');
assert.equal(v.pr, '');
v = extractVersionData('14.0.283-beta');
assert.equal(v.version, '14.0.283');
assert.equal(v.versionRelease, '14.0');
assert.equal(v.versionMajor, '14');
assert.equal(v.versionMinor, '0');
assert.equal(v.versionPatch, '283');
assert.equal(v.tier, 'beta');
assert.equal(v.versionTag, '-beta');
assert.equal(v.versionWithTag, '14.0.283-beta');
assert.equal(v.versionEnvironment, 'beta');
assert.equal(v.pr, '');
v = extractVersionData('14.0.283-alpha-local');
assert.equal(v.version, '14.0.283');
assert.equal(v.versionRelease, '14.0');
assert.equal(v.versionMajor, '14');
assert.equal(v.versionMinor, '0');
assert.equal(v.versionPatch, '283');
assert.equal(v.tier, 'alpha');
assert.equal(v.versionTag, '-alpha-local');
assert.equal(v.versionWithTag, '14.0.283-alpha-local');
assert.equal(v.versionEnvironment, 'local');
assert.equal(v.pr, '');
v = extractVersionData('14.0.283-alpha-test-1234');
assert.equal(v.version, '14.0.283');
assert.equal(v.versionRelease, '14.0');
assert.equal(v.versionMajor, '14');
assert.equal(v.versionMinor, '0');
assert.equal(v.versionPatch, '283');
assert.equal(v.tier, 'alpha');
assert.equal(v.versionTag, '-alpha-test-1234');
assert.equal(v.versionWithTag, '14.0.283-alpha-test-1234');
assert.equal(v.versionEnvironment, 'test');
assert.equal(v.pr, '1234');
});
});

16
docs/build/macos.md vendored
View file

@ -28,6 +28,8 @@ The following projects **cannot** be built on macOS:
## System Requirements
* Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0
(For using Xcode 26.2 or 26.3 macOS Sequoia 15.6 or later is required.
Make sure bash 4.0 is installed and used as modern macOS defaults to Zsh as its primary system shell)
**Note:** to make a fully M1-compatible release build of Keyman for macOS (for
the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier
@ -39,8 +41,9 @@ Rosetta 2 installed.
Many dependencies are only required for specific projects.
* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for macOS and Keyman
for iOS.
* XCode (iOS, macOS) 26.2 or 26.3 (not higher at this time, see #16416)
is needed only for Keyman for macOS and Keyman for iOS ().
* Install from App Store
* Accept the Xcode license: `sudo xcodebuild -license accept`
@ -105,7 +108,7 @@ PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"
## KeymanWeb Dependencies
* node.js, emscripten 3.1.46 or later
* node.js 20.0, emscripten 3.1.64
### node.js
@ -141,7 +144,8 @@ If you are updating an existing install of Emscripten:
cd emsdk
git pull
emsdk install 3.1.58
emsdk activate 3.1.58
./emsdk install 3.1.64
./emsdk activate 3.1.64
cd upstream/emscripten
npm install
```
@ -204,7 +208,9 @@ components such as emulator images and SDK updates.
```shell
brew install node emscripten
```
* node.js 20.0, emscripten 3.1.64
See section KeymanWeb Dependencies for installation
## Optional Tools

View file

@ -32,6 +32,7 @@ builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices
"configure" \
"build" \
"test" \
"publish-symbols Publishes symbols to Sentry" \
":engine Builds KeymanEngine.xcframework, usable by our main app and by third-party apps" \
":app=keyman Builds the Keyman app for iOS platforms" \
":help Online documentation" \
@ -50,3 +51,4 @@ function do_test_help() {
}
builder_run_action test:help do_test_help
builder_run_child_actions publish-symbols

View file

@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "$KEYMAN_ROOT/resources/build/mac/mac.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-download-resources.sh"
. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh"
builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad." \
"@/web/src/app/webview build" \
@ -16,7 +17,8 @@ builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad.
"clean" \
"configure" \
"build" \
"--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build"
"publish-symbols Publishes symbols to Sentry." \
"--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build"
builder_parse "$@"
@ -138,6 +140,13 @@ function build_engine() {
fi
}
builder_run_action clean do_clean
builder_run_action configure do_configure
builder_run_action build build_engine
do_publish_symbols() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
sentry_upload_web "${KEYMAN_ROOT}/web/build/app/webview/release/"
fi
}
builder_run_action clean do_clean
builder_run_action configure do_configure
builder_run_action build build_engine
builder_run_action publish-symbols do_publish_symbols

View file

@ -9,94 +9,121 @@ Keyman for macOS can be built from a command line (preferred) or Xcode.
### Setting up your code signing and notarization environment.
With macOS 10.14 and later, Keyman must be notarized in order to be permitted to interact with
keyboard input. You have two options for local builds:
With macOS 10.14 and later, Keyman must be notarized in order to be permitted to
interact with keyboard input. You have two options for local builds:
1. You can disable security checks for the system with the command:
`sudo spctl --master-disable`
`sudo spctl --master-disable`
This has obvious security implications and the risk is up to you. However, builds are
much, much, faster than with the alternative option below, and for extensive local
debugging is far less painful.
This has obvious security implications and the risk is up to you. However,
builds are much, much, faster than with the alternative option below, and for
extensive local debugging is far less painful.
2. Or, you must sign and notarize every build. See below. (Use `-deploy local`)
2. Or, you must sign and notarize every build. This requires an Apple Developer
Program subscription or access to Developer ID code signing certificates -- a
self-signed certificate will not work.
### Signing and notarizing builds
Keyman must be signed then notarized by Apple, even for local test builds. This requires additional
configuration for your build environment.
Unless security checks are disabled, Keyman must be signed then notarized by
Apple, even for local test builds. This requires additional configuration for
your build environment.
1. First, open XCode, Settings, Accounts, and select Manage Certificates for the identity
you wish to use for signing. Click **+** and select **Apple Development**. A
certificate will then be generated and listed in your Keychain.
1. First, open XCode, Settings, Apple Accounts, `<apple id>`, and select Manage
Certificates for the identity you wish to use for signing. Find the
appropriate Development ID certificate.
2. Find the SHA-1 hash. To find the certificate in terminal:
`security find-certificate -Z -c "<your-apple-id>" -a`
`security find-certificate -Z -c "<apple-id>" -a`
(If you have more than one, you may need to use Keychain Access to differentiate).
Copy the SHA-1 hash from this command's output.
(If you have more than one, you may need to use Keychain Access to
differentiate). Copy the SHA-1 hash from this command's output.
Take note also of the Development Team ID, found in parentheses at the end of the
`labl` blob line.
Take note also of the Development Team ID, found in parentheses at the end of
the `labl` blob line.
2. Determine the Apple ID details in order to run a build. You may wish to create an
App-Specific Password at https://appleid.apple.com/ and use this. Your Shortname will
be either your 10-digit Team Identifier or a shortname that can be extracted with
the following command:
2. Determine the Apple ID details in order to run a build. You will need to
create an App-Specific Password at https://appleid.apple.com/.
`/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter -m provider -u '<Username>' -p '<Password>' -account_type itunes_connect -v off`
Note: Use your Apple ID for `<Username>` and the app-specific password you
generated above for `<Password>`. You'll use these in the next step.
Note: Use your Apple ID for `<Username>` and the app-specific password you generated above
for `<Password>`. You'll use these in the next step as well.
3. Add the following environment variables to `mac/localenv.sh` (or to
`~/.bashrc`), replacing with the values you collected in the previous steps:
3. Add the following environment variables to mac/localenv.sh (or to your .bashrc file), replacing with the
values you collected in the previous steps:
export CERTIFICATE_ID=<SHA1-Fingerprint>
export APPSTORECONNECT_PROVIDER=<Shortname>
export APPSTORECONNECT_USERNAME=<Username>
export APPSTORECONNECT_PASSWORD=<Password>
export DEVELOPMENT_TEAM=<TeamID>
```sh
export CERTIFICATE_ID=<SHA1-Fingerprint>
export APPSTORECONNECT_USERNAME=<Username>
export APPSTORECONNECT_PASSWORD=<Password>
export DEVELOPMENT_TEAM=<TeamID>
```
### Compiling from Command Line
To build Keyman for macOS, do the following:
1. Open a Terminal window.
2. Build using `mac/build.sh clean configure build`
Note: If Carthage prompts you to allow it access to your github credentials, it's fine to click Deny.
Note: If Carthage prompts you to allow it access to your github credentials,
it's fine to click Deny.
### Running Keyman
1. Deploy Keyman locally using `mac/build.sh install`.
* This will notarize the app, signing with your local credentials if not already signed, and copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app** to **~/Library/Input Methods**
2. If running for the first time, follow the installation instructions at [Install Keyman for macOS].
1. Deploy Keyman locally:
You can also use `mac/build.sh build install` to do a single-step build, notarize, and deploy (see above for faster options).
* Without notarization (requires `spctl --master-disable`), recommended:
`mac/build.sh install --quick`
This will copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app**
to **~/Library/Input Methods**
* With notarization (requires appropriate certificates / Apple Developer
subscription):
`mac/build.sh install`
This will notarize the app, signing with your local credentials if not
already signed, and copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app**
to **~/Library/Input Methods**
2. If running for the first time, follow the installation instructions at
[Install Keyman for macOS].
You can also use `mac/build.sh build install [--quick]` to do a single-step
build, notarize, and deploy (see above for faster options).
### Compiling from Xcode
To build using Xcode, you will need to build KeymanEngine4Mac first and then build Keyman4MacIM. The very _first_ time after getting the source code (and any time the Podfile is edited; e.g., to install new pods), you need to go to **keyman/mac/Keyman4MacIM** and run "pod install" in Terminal.
To build using Xcode, you will need to build KeymanEngine4Mac first and then
build Keyman4MacIM. The very _first_ time after getting the source code (and any
time the Podfile is edited; e.g., to install new pods), you need to go to
**keyman/mac/Keyman4MacIM** and run `pod install` in Terminal.
1. Launch Xcode
2. Open **keyman/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj**
3. Build the project: Product > Build (or Cmd-B)
4. Open **keyman/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj**
5. If you do not have signing credentials from the core development team, disable code signing in Xcode.
1. Open the Project Navigator: View > Navigators > Show Project Navigator (Cmd-1)
5. If you do not have signing credentials from the core development team,
disable code signing in Xcode.
1. Open the Project Navigator: View > Navigators > Show Project Navigator
(Cmd-1)
2. Select Keyman4MacIM and click Build Settings
3. In the Signing section, change Code Signing Identity to Don't Code Sign. This will modify
**Keyman4MacIM.xcodeproj**. Do not commit the change.
6. Build the project. Refer to [Running Keyman](#running-keyman) on how to install the app.
3. In the Signing section, change Code Signing Identity to Don't Code Sign.
This will modify **Keyman4MacIM.xcodeproj**. Do not commit the change.
6. Build the project. Refer to [Running Keyman](#running-keyman) on how to
install the app.
### Testing
The Keyman4Mac project builds a test-bed app that can be used to test keyboards without installing the input method.
It can also be used as reference for the usage of Keyman Engine.
The Keyman4Mac project builds a test-bed app that can be used to test keyboards
without installing the input method. It can also be used as reference for the
usage of Keyman Engine.
Keyman4Mac tests are run using `./build.sh -test -no-codesign`.
Keyman4Mac tests are run using `./build.sh test`.
### A note about Homebrew and xcodebuild

View file

@ -87,9 +87,8 @@ UPLOAD_SENTRY=false
# Import local environment variables for build
#
# /mac/localenv.sh can be used to define CERTIFICATE_ID,
# APPSTORECONNECT_PROVIDER, APPSTORECONNECT_USERNAME,
# APPSTORECONNECT_PASSWORD, DEVELOPMENT_TEAM variables;
# see /mac/README.md for details.
# APPSTORECONNECT_USERNAME, APPSTORECONNECT_PASSWORD, DEVELOPMENT_TEAM
# variables; see /mac/README.md for details.
#
if [[ -f "$THIS_SCRIPT_PATH/localenv.sh" ]]; then
. "$THIS_SCRIPT_PATH/localenv.sh"
@ -223,7 +222,7 @@ do_notarize() {
builder_die "Notarization and signed executable is required for deployment, even locally. Specify CERTIFICATE_ID environment variable for custom certificate."
fi
if [ -z "${APPSTORECONNECT_PROVIDER+x}" ] || [ -z "${APPSTORECONNECT_USERNAME+x}" ] || [ -z "${APPSTORECONNECT_PASSWORD+x}" ]; then
if [ -z "${APPSTORECONNECT_USERNAME+x}" ] || [ -z "${APPSTORECONNECT_PASSWORD+x}" ]; then
builder_die "Appstoreconnect Apple ID credentials must be configured in environment. See README.md for details."
fi

View file

@ -12,7 +12,7 @@
function isSentryConfigured() {
if [ -z "${SENTRY_AUTH_TOKEN-}" ] || [ -z "${SENTRY_ORG-}" ] || [ -z "${SENTRY_URL-}" ]; then
echo "WARNING: Sentry environment variables SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_URL must be configured."
builder_warn "WARNING: Sentry environment variables SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_URL must be configured."
return 1
fi
return 0
@ -20,7 +20,7 @@ function isSentryConfigured() {
function isSentryCliAvailable() {
which sentry-cli > /dev/null && return 0
echo "WARNING: sentry-cli could not be found. Skipping all sentry integration."
builder_warn "WARNING: sentry-cli could not be found. Skipping all sentry integration."
return 1
}
@ -28,15 +28,35 @@ function makeSentryRelease() {
if isSentryConfigured; then
if isSentryCliAvailable; then
# This version tag matches the repository version tag release@x.y.z
echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG"
builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli releases new -p keyman-android -p keyman-developer -p keyman-ios -p keyman-linux -p keyman-mac -p keyman-web -p keyman-windows $KEYMAN_VERSION_GIT_TAG
echo "Setting commits for release tag $KEYMAN_VERSION_GIT_TAG"
builder_echo "Setting commits for release tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli releases set-commits --auto $KEYMAN_VERSION_GIT_TAG
echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG"
builder_echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli releases finalize "$KEYMAN_VERSION_GIT_TAG"
fi
fi
}
#
# Upload sourcemaps to Sentry
#
# Parameters:
# 1: Directory containing sourcemaps to upload
#
function sentry_upload_web () {
local SOURCEMAP_DIR="$1"
if ! isSentryConfigured; then
builder_warn "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset."
return
fi
builder_echo "Uploading ${SOURCEMAP_DIR} to Sentry..."
sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix \
"${SOURCEMAP_DIR}" --rewrite --ext js --ext map --ext ts
builder_echo "Upload successful."
}

View file

@ -3,13 +3,18 @@
ios_build() {
builder_echo start "build" "Building KeymanEngine + Keyman for iOS"
# shellcheck disable=SC2154
"${KEYMAN_ROOT}/ios/ci.sh" build
builder_launch /ios/ci.sh build
builder_echo end "build" success "Finished building KeymanEngine + Keyman for iOS"
}
ios_capture_build_artifacts() {
builder_echo start "prep-release" "Using prep-release script to capture build artifacts"
"${KEYMAN_ROOT}/ios/tools/prepRelease.sh"
builder_launch /ios/tools/prepRelease.sh
builder_echo end "prep-release" success "Finished capturing build artifacts"
}
ios_publish_symbols() {
builder_echo start "publish to Sentry" "Publishing source map to Sentry"
builder_launch /ios/build.sh "publish-symbols"
builder_echo end "publish to Sentry" success "Finished publishing source map to Sentry"
}

View file

@ -243,6 +243,7 @@ function do_build() {
function do_publish() {
_publish_to_downloads_keyman_com
ios_publish_symbols
tc_upload_help "Keyman for iOS" ios
if [[ "${KEYMAN_TIER}" == "stable" ]]; then

View file

@ -15,6 +15,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${KEYMAN_ROOT}/resources/build/utils.inc.sh"
. "${KEYMAN_ROOT}/resources/build/zip.inc.sh"
. "${KEYMAN_ROOT}/resources/build/ci/pull-requests.inc.sh"
. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh"
# This script runs from its own folder
cd "${THIS_SCRIPT_PATH}"
@ -42,21 +43,6 @@ builder_parse "$@"
KEYMAN_TIER=$(cat ../TIER.md)
BUILD_NUMBER=$(cat ../VERSION.md)
function web_sentry_upload () {
if [[ -z "${SENTRY_ORG:-}" ]] || [[ -z "${SENTRY_PROJECT:-}" ]]; then
echo "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset."
return
fi
echo "Uploading $1 to Sentry..."
# --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects
# the most common prefix instead.
sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \
--rewrite --ext js --ext map --ext ts
echo "Upload successful."
}
function build_action() {
# Build step: since CI builds start (and should start) from scratch, run the following
# three actions:
@ -73,11 +59,10 @@ function build_action() {
for sourcemap in "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/"*.map; do
node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --clean
done
web_sentry_upload "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/"
sentry_upload_web "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/"
# And, of course, the main build-products too
web_sentry_upload "${KEYMAN_ROOT}/web/build/app/webview/release/"
web_sentry_upload "${KEYMAN_ROOT}/web/build/publish/release/"
sentry_upload_web "${KEYMAN_ROOT}/web/build/publish/release/"
fi
}

View file

@ -327,7 +327,8 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
if(!requestedStub) {
if(keyboardId) {
throw new Error("No matching stub has been registered.");
const availableStubList = this.keyboardCache.getStubList().map(stub => `${stub.KI}@${stub.KLC}`);
throw new Error(`No matching stub has been registered for keyboard ${keyboardId}. Available stubs: ${JSON.stringify(availableStubList)}`);
} else {
return {
keyboard: Promise.resolve(null),

View file

@ -9,7 +9,7 @@ async function setTimeoutAndLoadPage(page: Page, url: string): Promise<void> {
await loadPage(page, url);
}
test.describe('First example from the guide', function () {
test.describe.skip('First example from the guide', function () {
const beforeEach = async (page: Page) => {
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__first-example.html');
}
@ -41,7 +41,7 @@ test.describe('First example from the guide', function () {
});
});
test.describe('Auto-control example from the guide', function () {
test.describe.skip('Auto-control example from the guide', function () {
const beforeEach = async (page: Page) => {
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__auto-control.html');
}
@ -69,7 +69,7 @@ test.describe('Auto-control example from the guide', function () {
});
});
test.describe('Control-by-control example from the guide', function () {
test.describe.skip('Control-by-control example from the guide', function () {
const beforeEach = async (page: Page) => {
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__control-by-control.html');
}
@ -119,7 +119,7 @@ test.describe('Control-by-control example from the guide', function () {
});
});
test.describe('Full manual control example from the guide', function () {
test.describe.skip('Full manual control example from the guide', function () {
const beforeEach = async (page: Page) => {
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__full-manual-control.html');
}
@ -203,7 +203,7 @@ test.describe('Full manual control example from the guide', function () {
});
});
test.describe('Manual control example from the guide', function () {
test.describe.skip('Manual control example from the guide', function () {
const beforeEach = async (page: Page) => {
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__manual-control.html');
}

View file

@ -26,8 +26,7 @@ builder_describe_outputs \
function do_build() {
create-windows-output-folders
build_version.res
# TODO: why no manifest?
# build_manifest.res
build_manifest.res
delphi_msbuild insthelp.dproj "//p:Platform=Win32"
sentrytool_delphiprep "$WIN32_TARGET" insthelp.dpr
tds2dbg "$WIN32_TARGET"
@ -38,8 +37,7 @@ function do_build() {
function do_publish() {
# test that (a) linked manifest exists and correct
# TODO: no manifest included?
# wrap-mt -nologo -inputresource:"$WINDOWS_PROGRAM_APP/insthelp.exe" -validate_manifest
wrap-mt -nologo -inputresource:"$WINDOWS_PROGRAM_APP/insthelp.exe" -validate_manifest
wrap-signcode //d "Keyman for Windows Install Helper" "$WINDOWS_PROGRAM_APP/insthelp.exe"
wrap-symstore "$WINDOWS_PROGRAM_APP/insthelp.exe" //t keyman-windows

View file

@ -11,7 +11,7 @@ uses
UserMessages in '..\..\..\..\common\windows\delphi\general\UserMessages.pas';
{$R version.res}
{-R manifest.res}
{$R manifest.res}
begin
CoInitializeEx(nil, COINIT_APARTMENTTHREADED);

View file

@ -6,15 +6,13 @@
version="$VersionWin"
processorArchitecture="x86"/>
<description>Keyman Installation Assistant</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<!-- Switch on various compatibility values -->
@ -27,4 +25,4 @@
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><!-- Windows 10 -->
</application>
</compatibility>
</assembly>
</assembly>

View file

@ -69,6 +69,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPActivateKeyboard(GUID *profile)
if(!_td) return FALSE;
RefreshThreadKeyboardsIfRequired();
RefreshToggleState();
if(profile != NULL) {
for(int i = 0; i < _td->nKeyboards; i++) {

View file

@ -29,3 +29,21 @@
BOOL IsCapsLockOn(void) {
return GetKeyState(VK_CAPITAL) & 1;
}
/**
* Resync the Caps Lock and Num Lock state cache.
*
* Use when state may be stale after focus changes or when the toggle keys
* changed while this Keyman engine instance was not processing key events.
*/
void RefreshToggleState(void) {
DWORD previousShiftState = Globals::get_ShiftState();
if (GetKeyState(VK_CAPITAL) & 1) *Globals::ShiftState() |= CAPITALFLAG;
else *Globals::ShiftState() &= ~CAPITALFLAG;
if (GetKeyState(VK_NUMLOCK) & 1) *Globals::ShiftState() |= NUMLOCKFLAG;
else *Globals::ShiftState() &= ~NUMLOCKFLAG;
SendDebugMessageFormat("Enter: %x Exit: %x", previousShiftState, Globals::get_ShiftState());
}

View file

@ -20,5 +20,6 @@
#define __CAPSSTATE_H
BOOL IsCapsLockOn(void);
void RefreshToggleState(void);
#endif

View file

@ -68,7 +68,7 @@
void ProcessWMKeymanControlInternal(HWND hwnd, WPARAM wParam, LPARAM lParam);
void ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam);
void ProcessWMKeyman(HWND hwnd, WPARAM wParam, LPARAM lParam);
void GetCapsAndNumlockState();
void RefreshModifierState();
/*
BOOL SysTabCtrl(HWND hwnd)
@ -354,7 +354,7 @@ void ProcessWMKeyman(HWND hwnd, WPARAM wParam, LPARAM lParam)
if(IsFocusedThread())
{
if(_td->app) _td->app->ResetQueue();
GetCapsAndNumlockState();
RefreshModifierState();
UpdateActiveWindows();
}
}
@ -413,20 +413,19 @@ ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam) {
#endif
}
/*
GetCapsAndNumlockState:
/**
* Refresh Keyman's internal keyboard modifier and toggle state according
* to the Windows current keyboard state (not async state). The state may
* have been reset while Keyman was not aware of it. This is used only when
* focus changes. Note that the global values are per-architecture, so
* these may track separately.
*
* @see ProcessModifierChange()
*/
void RefreshModifierState() { // I4793
DWORD previousShiftState = Globals::get_ShiftState();
Updates the global caps and numlock state when a window is focused, because it
may have been reset while Keyman was not aware of it
*/
void GetCapsAndNumlockState() { // I4793
DWORD n = Globals::get_ShiftState();
if(GetKeyState(VK_NUMLOCK) & 1) *Globals::ShiftState() |= NUMLOCKFLAG;
else *Globals::ShiftState() &= ~NUMLOCKFLAG;
if(GetKeyState(VK_CAPITAL) & 1) *Globals::ShiftState() |= CAPITALFLAG;
else *Globals::ShiftState() &= ~CAPITALFLAG;
RefreshToggleState();
if(GetKeyState(VK_SHIFT) < 0) *Globals::ShiftState() |= K_SHIFTFLAG;
else *Globals::ShiftState() &= ~K_SHIFTFLAG;
@ -443,18 +442,19 @@ void GetCapsAndNumlockState() { // I4793
if(GetKeyState(VK_RMENU) < 0) *Globals::ShiftState() |= RALTFLAG;
else *Globals::ShiftState() &= ~RALTFLAG;
SendDebugMessageFormat("Enter: %x Exit: %x", n, Globals::get_ShiftState());
SendDebugMessageFormat("Enter: %x Exit: %x", previousShiftState, Globals::get_ShiftState());
}
/*
Update the Keyman shift state based on the key event. This has to be
done from both the GetMessage hook and the TSF methods, because we don't
consistently receive modifier events in some applications (e.g. ALT in Firefox)
via TSF, and we don't receive the notifications via the GetMessage hook when
in UWP apps (#4369).
TODO: test whether we still need the GetMessage hook for reading modifier state.
*/
/**
* Update the Keyman shift state based on the key event. This has to be done
* from both the GetMessage hook and the TSF methods, because we don't
* consistently receive modifier events in some applications (e.g. ALT in
* Firefox) via TSF, and we don't receive the notifications via the GetMessage
* hook when in UWP apps (#4369).
*
* TODO: test whether we still need the GetMessage hook for reading modifier
* state.
*/
void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended) { // I4793
UINT flag = 0;