{
if(result.artifacts[filename]) {
return true;
}
@@ -388,6 +403,10 @@ export class KeymanProjectCopier implements KeymanCompiler {
return false;
}
+ if(KeymanFileTypes.binaryTypeFromFilename(filename) != null) {
+ outputPath = this.callbacks.path.join(this.outPath, 'build');
+ }
+
result.artifacts[filename] = {
data,
filename: this.generateNewFilename(filename, outputPath)
@@ -578,6 +597,7 @@ export class KeymanProjectCopier implements KeymanCompiler {
*/
private generateNewFilename(filename: string, outputPath: string): string {
const {ext, base} = this.splitFilename(filename);
+
const newFilename = base == this.sourceId
? this.callbacks.path.join(outputPath, this.outputId + ext)
: this.callbacks.path.join(outputPath, base + ext);
diff --git a/developer/src/kmc-copy/src/cloud.ts b/developer/src/kmc-copy/src/cloud.ts
index 7b3ef1ff11..a65284933e 100644
--- a/developer/src/kmc-copy/src/cloud.ts
+++ b/developer/src/kmc-copy/src/cloud.ts
@@ -63,9 +63,10 @@ export class KeymanCloudSource {
}
public async downloadFolderFromGitHub(ref: GitHubRef): Promise<{filename:string,type:'dir'|'file'}[]> {
- // this.callbacks.reportMessage(CopierMessages.Info_DownloadingFile({ref})); // TODO-COPY: verbose mode support
const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/contents/${ref.path}?ref=${ref.branch}`;
+ this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFolder({path:ref.path, url}));
+
let folder: any;
try {
folder = await this.callbacks.net.fetchJSON(url);
@@ -94,6 +95,7 @@ export class KeymanCloudSource {
}
const url = `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/refs/heads/${ref.branch}${ref.path}`;
+ this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFile({filename:this.callbacks.path.basename(ref.path), url}));
try {
return await this.callbacks.net.fetchBlob(url);
diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts
index 78a948ec79..8afff79149 100644
--- a/developer/src/kmc-copy/src/copier-messages.ts
+++ b/developer/src/kmc-copy/src/copier-messages.ts
@@ -7,6 +7,7 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.Copier;
+const SevVerbose = CompilerErrorSeverity.Verbose | Namespace;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
// const SevHint = CompilerErrorSeverity.Hint | Namespace;
const SevWarn = CompilerErrorSeverity.Warn | Namespace;
@@ -198,4 +199,36 @@ export class CopierMessages {
the provided error details for more details.`
);
+ static VERBOSE_DownloadingFile = SevVerbose | 0x001C;
+ static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m(
+ this.VERBOSE_DownloadingFile,
+ `Downloading '${def(o.filename)}' from '${def(o.url)}'`,
+ );
+
+ static VERBOSE_DownloadingFolder = SevVerbose | 0x001D;
+ static Verbose_DownloadingFolder = (o:{path: string, url: string}) => m(
+ this.VERBOSE_DownloadingFolder,
+ `Downloading folder '${def(o.path)}' from '${def(o.url)}'`,
+ );
+
+ // See also generator-messages.ERROR_InvalidKeymanKeyboardId
+ static ERROR_InvalidKeyboardId = SevError | 0x001E;
+ static Error_InvalidKeyboardId = (o:{id: string}) => m(
+ this.ERROR_InvalidKeyboardId,
+ `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a keyboard id or filename.`,
+ );
+
+ // See also generator-messages.ERROR_InvalidLexicalModelId
+ static ERROR_InvalidLexicalModelId = SevError | 0x001F;
+ static Error_InvalidLexicalModelId = (o:{id:string}) => m(
+ this.ERROR_InvalidLexicalModelId,
+ `The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.`,
+ );
+
+ static WARN_FilenameCollides = SevWarn | 0x0020;
+ static Warn_FilenameCollides = (o:{filename:string}) => m(
+ this.WARN_FilenameCollides,
+ `The output file '${def(o.filename)}' has two different possible source files.`,
+ );
+
};
diff --git a/developer/src/kmc-copy/test/copier-messages.tests.ts b/developer/src/kmc-copy/test/copier-messages.tests.ts
new file mode 100644
index 0000000000..0312910860
--- /dev/null
+++ b/developer/src/kmc-copy/test/copier-messages.tests.ts
@@ -0,0 +1,54 @@
+/*
+ * Keyman is copyright (C) SIL International. MIT License.
+ */
+
+import 'mocha';
+// import * as path from 'path';
+// import * as fs from 'fs';
+// import { fileURLToPath } from 'url';
+import { assert } from 'chai';
+import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
+import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
+import { CopierMessages } from '../src/copier-messages.js';
+import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js';
+import { makePathToFixture } from './helpers/index.js';
+
+describe('CopierMessages', function () {
+ const callbacks = new TestCompilerCallbacks(makePathToFixture('online'));
+
+ this.beforeEach(function() {
+ callbacks.clear();
+ });
+
+ this.afterEach(function() {
+ if(this.currentTest?.isFailed()) {
+ callbacks.printMessages();
+ }
+ });
+
+ it('should have a valid CopierMessages object', function() {
+ return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier);
+ });
+
+ it('should generate ERROR_InvalidKeyboardId if the output keyboard id is invalid', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: '0invalid'}))
+ assert.isNull(await copier.run('cloud:jawa'));
+ assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidKeyboardId));
+ });
+
+ it('should generate ERROR_InvalidLexicalModelId if the output lexical model id is invalid', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'nrc.str-x-sencoten.sencoten'}))
+ assert.isNull(await copier.run('cloud:nrc.str.sencoten'));
+ assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidLexicalModelId));
+ });
+
+ it('should generate WARN_FilenameCollision if a copied file will collide', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'collided'}))
+ const result = await copier.run(makePathToFixture('projects/collision/collision.kpj'));
+ assert.isNotNull(result);
+ assert.isTrue(callbacks.hasMessage(CopierMessages.WARN_FilenameCollides));
+ });
+});
diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts
index 6d583862ae..6a2a8cbb1e 100644
--- a/developer/src/kmc-copy/test/copier.tests.ts
+++ b/developer/src/kmc-copy/test/copier.tests.ts
@@ -306,7 +306,6 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
- // TODO-COPY rename: false,
}));
const result = await copier.run(fullSource);
@@ -316,7 +315,7 @@ describe('KeymanProjectCopier', function() {
//
assert.isOk(result);
- assert.isEmpty(callbacks.messages);
+ assert.isEmpty(callbacks.filteredMessages());
if(TEST_SAVE_ARTIFACTS) {
assert.isTrue(await copier.write(result.artifacts));
@@ -370,7 +369,6 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
- // TODO-COPY rename: false,
}));
// armenian_mnemonic selected because (a) small, and (b) has v2.0 project, so
@@ -379,7 +377,7 @@ describe('KeymanProjectCopier', function() {
// We should have no messages and a successful result
assert.isOk(result);
- assert.isEmpty(callbacks.messages);
+ assert.isEmpty(callbacks.filteredMessages());
// TODO-COPY: verify outcome using pattern above
@@ -400,14 +398,13 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
- // TODO-COPY rename: false,
}));
const result = await copier.run('cloud:'+id);
// We should have no messages and a successful result
assert.isOk(result);
- assert.isEmpty(callbacks.messages);
+ assert.isEmpty(callbacks.filteredMessages());
// TODO-COPY: verify outcome using pattern above
@@ -492,8 +489,41 @@ describe('KeymanProjectCopier', function() {
});
}));
+ it('should copy a disorganized project into current structure', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {
+ dryRun: false,
+ outPath: 'organized',
+ }));
+
+ const result = await copier.run(makePathToFixture('projects','disorganized','alephwithbeth.kpj'));
+ assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [
+ 'organized', // kmc-copy:outputPath
+ 'organized/organized.kpj',
+ 'organized/HISTORY.md',
+ 'organized/LICENSE.md',
+ 'organized/README.md',
+ // primary source files get moved to 'source/'
+ 'organized/source/organized.kps',
+ 'organized/source/organized.kmn',
+ 'organized/source/organized.ico',
+ 'organized/source/organized.kvks',
+ // files in 'other/' get moved to 'source/other/'
+ 'organized/source/other/organized.keyman-touch-layout',
+ 'organized/source/other/readme.htm',
+ 'organized/source/other/welcome/welcome.htm',
+ 'organized/source/other/welcome/KeyboardLayout.png',
+ 'organized/source/other/welcome/KeyboardLayoutAlt.png',
+ 'organized/source/other/welcome/KeyboardLayoutShift.png',
+ // build artifact files get moved to build/
+ 'organized/build/organized.js',
+ 'organized/build/organized.kvk',
+ 'organized/build/organized.kmx',
+ // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well?
+ ]);
+ });
+
// TODO-COPY: additional tests
- it.skip('should copy a disorganized project into current structure', async function() {});
it.skip('should copy a standalone .kmn into a new project', async function() {});
it.skip('should copy a standalone .kmn and .kps into a new project', async function() {});
});
diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj b/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj
new file mode 100644
index 0000000000..fffdca635e
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj
@@ -0,0 +1,7 @@
+
+
+
+ keyboard
+ 2.0
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico
new file mode 100644
index 0000000000..84b57c5aeb
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico
new file mode 100644
index 0000000000..84b57c5aeb
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn
new file mode 100644
index 0000000000..121e224d89
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn
@@ -0,0 +1,7 @@
+store(&TARGETS) 'any'
+store(&NAME) 'Collision'
+store(&BITMAP) 'collision.ico'
+
+begin Unicode > use(main)
+
+group(main) using keys
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md
new file mode 100644
index 0000000000..65dd7e13c7
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md
@@ -0,0 +1,35 @@
+alephwithbeth Change History
+====================
+
+1.0.6 (2024-06-11)
+------------------
+* Remove source files from package
+
+1.0.5 (2023-02-28)
+------------------
+* replaced hyphen with maqef
+* added a touch-optimized keyboard layout for phone
+
+1.0.4 (2023-02-13)
+------------------
+* Removed unnecessary version numbers.
+* added description
+
+1.0.3 (2023-02-11)
+------------------
+* Removed the faulty auto correct feature.
+* Captialized all words of the title.
+* Added title to .kmn
+
+1.0.2 (2023-02-09)
+------------------
+* Renamed keyboard to "Aleph with Beth"
+
+1.0.1 (2023-02-08)
+----------------
+* Updated author and website (in kps)
+* Added copyrights where needed.
+
+1.0 (2023-01-28)
+----------------
+* Created by Cameron Briggs
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md
new file mode 100644
index 0000000000..7f84a306c0
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © 2023-2024 Cameron Briggs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md
new file mode 100644
index 0000000000..16edd8db20
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md
@@ -0,0 +1,25 @@
+alephwithbeth keyboard
+==============
+
+Description
+-----------
+A Hebrew keyboard designed to intuitively match the standard QWERTY layout.
+
+Copyright
+---------
+See [LICENSE.md](LICENSE.md)
+
+Supported Platforms
+-------------------
+ * Windows
+ * macOS
+ * Linux
+ * Web
+ * iPhone
+ * iPad
+ * Android phone
+ * Android tablet
+ * Mobile devices
+ * Desktop devices
+ * Tablet devices
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico
new file mode 100644
index 0000000000..84b57c5aeb
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js
new file mode 100644
index 0000000000..9466d76b85
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js
@@ -0,0 +1 @@
+KeymanWeb.KR(new Keyboard_alephwithbeth());function Keyboard_alephwithbeth(){this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;this.KI="Keyboard_alephwithbeth";this.KN="Aleph With Beth";this.KMINVER="14.0";this.KV={F:' 1em "Arial"',K102:0};this.KV.KLS={"shift": ["","!","֘","֨","֜","֞","֔","֬","֝","(",")","_","+","","","","קּ","וּ","ֵ","רּ","תּ","יּ","ֻ","","וֹ","פּ","{","}","|","","","","ָ","סּ","דּ","טּ","גּ","הּ","וַיּ","כּ","לּ","״","\"","","","","","","","זּ","צּ","שּׂ","שּׁ","בּ","נּ","מּ","","ּ","?","","","","","",""],"default": ["֑","1","2","3","4","5","6","7","8","9","0","־","=","","","","ק","ו","ֶ","ר","ת","י","ע","ִ","ֹ","פ","[","]","\\","","","","ַ","ס","ד","ט","ג","ה","ח","כ","ל","ְ","'","","","","","","","ז","צ","שׂ","שׁ","ב","נ","מ","א",".","/","","","","","",""],"alt": ["","֗","֮","֙","֠","֩","֟","֡","֕","֓","֯","ֿ","◌","","","","","","ֱ","","","","","ֽ","ֳ","ף","֔","֒","֫","","","","ֲ","","","","•","̊","","ך","","׃","ׄ","","","","","","","","ץ","","","","ן","ם","֫","̈","״","","","","","",""]};this.KV.BK=(function(x){var e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH="Aleph With Beth";this.KM=0;this.KBVER="1.0.6";this.KMBM=0x70;this.KRTL=1;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]},"phone":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]}};this.s7="כמנפצ";this.s8="ךםןףץ";this.s9="כּפּ";this.s10="ךּףּ";this.s11="ּ";this.s12="מּנּצּ";this.s13="םןץ";this.s14="ֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯־ֿ׀ׄ";this.s15="...";this.KVER="18.0.170.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,9)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,13)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,32)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}else if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16432,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16448,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,49)) {if(1){r=m=1;k.KO(0,t,"!");}}else if(k.KKM(e,16400,222)) {if(1){r=m=1;k.KO(0,t,"\"");}}else if(k.KKM(e,16400,51)) {if(1){r=m=1;k.KO(0,t,"֨");}}else if(k.KKM(e,16400,52)) {if(1){r=m=1;k.KO(0,t,"֜");}}else if(k.KKM(e,16400,53)) {if(1){r=m=1;k.KO(0,t,"֞");}}else if(k.KKM(e,16400,55)) {if(1){r=m=1;k.KO(0,t,"֬");}}else if(k.KKM(e,16384,222)) {if(1){r=m=1;k.KO(0,t,"'");}}else if(k.KKM(e,16448,222)) {if(1){r=m=1;k.KO(0,t,"ׄ");}}else if(k.KKM(e,16400,57)) {if(1){r=m=1;k.KO(0,t,"(");}}else if(k.KKM(e,16400,48)) {if(1){r=m=1;k.KO(0,t,")");}}else if(k.KKM(e,16400,56)) {if(1){r=m=1;k.KO(0,t,"֝");}}else if(k.KKM(e,16400,187)) {if(1){r=m=1;k.KO(0,t,"+");}}else if(k.KKM(e,16448,188)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,188)) {if(1){r=m=1;k.KO(0,t,"א");}}else if(k.KKM(e,16384,189)) {if(1){r=m=1;k.KO(0,t,"־");}}else if(k.KKM(e,16448,189)) {if(1){r=m=1;k.KO(0,t,"ֿ");}}else if(k.KKM(e,16384,190)) {if(1){r=m=1;k.KO(0,t,".");}}else if(k.KKM(e,16448,190)) {if(1){r=m=1;k.KO(0,t,"̈");}}else if(k.KKM(e,16384,191)) {if(1){r=m=1;k.KO(0,t,"/");}}else if(k.KKM(e,16448,191)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16384,48)) {if(1){r=m=1;k.KO(0,t,"0");}}else if(k.KKM(e,16448,48)) {if(1){r=m=1;k.KO(0,t,"֯");}}else if(k.KKM(e,16384,49)) {if(1){r=m=1;k.KO(0,t,"1");}}else if(k.KKM(e,16448,49)) {if(1){r=m=1;k.KO(0,t,"֗");}}else if(k.KKM(e,16384,50)) {if(1){r=m=1;k.KO(0,t,"2");}}else if(k.KKM(e,16448,50)) {if(1){r=m=1;k.KO(0,t,"֮");}}else if(k.KKM(e,16384,51)) {if(1){r=m=1;k.KO(0,t,"3");}}else if(k.KKM(e,16448,51)) {if(1){r=m=1;k.KO(0,t,"֙");}}else if(k.KKM(e,16384,52)) {if(1){r=m=1;k.KO(0,t,"4");}}else if(k.KKM(e,16448,52)) {if(1){r=m=1;k.KO(0,t,"֠");}}else if(k.KKM(e,16384,53)) {if(1){r=m=1;k.KO(0,t,"5");}}else if(k.KKM(e,16448,53)) {if(1){r=m=1;k.KO(0,t,"֩");}}else if(k.KKM(e,16384,54)) {if(1){r=m=1;k.KO(0,t,"6");}}else if(k.KKM(e,16448,54)) {if(1){r=m=1;k.KO(0,t,"֟");}}else if(k.KKM(e,16384,55)) {if(1){r=m=1;k.KO(0,t,"7");}}else if(k.KKM(e,16448,55)) {if(1){r=m=1;k.KO(0,t,"֡");}}else if(k.KKM(e,16384,56)) {if(1){r=m=1;k.KO(0,t,"8");}}else if(k.KKM(e,16448,56)) {if(1){r=m=1;k.KO(0,t,"֕");}}else if(k.KKM(e,16384,57)) {if(1){r=m=1;k.KO(0,t,"9");}}else if(k.KKM(e,16448,57)) {if(1){r=m=1;k.KO(0,t,"֓");}}else if(k.KKM(e,16400,186)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16448,186)) {if(1){r=m=1;k.KO(0,t,"׃");}}else if(k.KKM(e,16384,186)) {if(1){r=m=1;k.KO(0,t,"ְ");}}else if(k.KKM(e,16400,188)) {if(1){r=m=1;}}else if(k.KKM(e,16384,187)) {if(1){r=m=1;k.KO(0,t,"=");}}else if(k.KKM(e,16448,187)) {if(1){r=m=1;k.KO(0,t,"◌");}}else if(k.KKM(e,16400,190)) {if(1){r=m=1;k.KO(0,t,"ּ");}}else if(k.KKM(e,16400,191)) {if(1){r=m=1;k.KO(0,t,"?");}}else if(k.KKM(e,16400,50)) {if(1){r=m=1;k.KO(0,t,"֘");}}else if(k.KKM(e,16400,65)) {if(1){r=m=1;k.KO(0,t,"ָ");}}else if(k.KKM(e,16400,66)) {if(1){r=m=1;k.KO(0,t,"בּ");}}else if(k.KKM(e,16400,67)) {if(1){r=m=1;k.KO(0,t,"שּׂ");}}else if(k.KKM(e,16400,68)) {if(1){r=m=1;k.KO(0,t,"דּ");}}else if(k.KKM(e,16400,69)) {if(1){r=m=1;k.KO(0,t,"ֵ");}}else if(k.KKM(e,16400,70)) {if(1){r=m=1;k.KO(0,t,"טּ");}}else if(k.KKM(e,16400,71)) {if(1){r=m=1;k.KO(0,t,"גּ");}}else if(k.KKM(e,16400,72)) {if(1){r=m=1;k.KO(0,t,"הּ");}}else if(k.KKM(e,16400,73)) {if(1){r=m=1;}}else if(k.KKM(e,16400,74)) {if(1){r=m=1;k.KO(0,t,"וַיּ");}}else if(k.KKM(e,16400,75)) {if(1){r=m=1;k.KO(0,t,"כּ");}}else if(k.KKM(e,16400,76)) {if(1){r=m=1;k.KO(0,t,"לּ");}}else if(k.KKM(e,16400,77)) {if(1){r=m=1;k.KO(0,t,"מּ");}}else if(k.KKM(e,16400,78)) {if(1){r=m=1;k.KO(0,t,"נּ");}}else if(k.KKM(e,16400,79)) {if(1){r=m=1;k.KO(0,t,"וֹ");}}else if(k.KKM(e,16400,80)) {if(1){r=m=1;k.KO(0,t,"פּ");}}else if(k.KKM(e,16400,81)) {if(1){r=m=1;k.KO(0,t,"קּ");}}else if(k.KKM(e,16400,82)) {if(1){r=m=1;k.KO(0,t,"רּ");}}else if(k.KKM(e,16400,83)) {if(1){r=m=1;k.KO(0,t,"סּ");}}else if(k.KKM(e,16400,84)) {if(1){r=m=1;k.KO(0,t,"תּ");}}else if(k.KKM(e,16400,85)) {if(1){r=m=1;k.KO(0,t,"ֻ");}}else if(k.KKM(e,16400,86)) {if(1){r=m=1;k.KO(0,t,"שּׁ");}}else if(k.KKM(e,16400,87)) {if(1){r=m=1;k.KO(0,t,"וּ");}}else if(k.KKM(e,16400,88)) {if(1){r=m=1;k.KO(0,t,"צּ");}}else if(k.KKM(e,16400,89)) {if(1){r=m=1;k.KO(0,t,"יּ");}}else if(k.KKM(e,16400,90)) {if(1){r=m=1;k.KO(0,t,"זּ");}}else if(k.KKM(e,16384,219)) {if(1){r=m=1;k.KO(0,t,"[");}}else if(k.KKM(e,16448,219)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16384,220)) {if(1){r=m=1;k.KO(0,t,"\\");}}else if(k.KKM(e,16448,220)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,221)) {if(1){r=m=1;k.KO(0,t,"]");}}else if(k.KKM(e,16448,221)) {if(1){r=m=1;k.KO(0,t,"֒");}}else if(k.KKM(e,16400,54)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16400,189)) {if(1){r=m=1;k.KO(0,t,"_");}}else if(k.KKM(e,16448,192)) {if(1){r=m=1;}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KO(0,t,"֑");}}else if(k.KKM(e,16448,65)) {if(1){r=m=1;k.KO(0,t,"ֲ");}}else if(k.KKM(e,16384,65)) {if(1){r=m=1;k.KO(0,t,"ַ");}}else if(k.KKM(e,16448,66)) {if(1){r=m=1;}}else if(k.KKM(e,16384,66)) {if(1){r=m=1;k.KO(0,t,"ב");}}else if(k.KKM(e,16448,67)) {if(1){r=m=1;}}else if(k.KKM(e,16384,67)) {if(1){r=m=1;k.KO(0,t,"שׂ");}}else if(k.KKM(e,16448,68)) {if(1){r=m=1;}}else if(k.KKM(e,16384,68)) {if(1){r=m=1;k.KO(0,t,"ד");}}else if(k.KKM(e,16448,69)) {if(1){r=m=1;k.KO(0,t,"ֱ");}}if(m) {}else if(k.KKM(e,16384,69)) {if(1){r=m=1;k.KO(0,t,"ֶ");}}else if(k.KKM(e,16448,70)) {if(1){r=m=1;}}else if(k.KKM(e,16384,70)) {if(1){r=m=1;k.KO(0,t,"ט");}}else if(k.KKM(e,16448,71)) {if(1){r=m=1;k.KO(0,t,"•");}}else if(k.KKM(e,16384,71)) {if(1){r=m=1;k.KO(0,t,"ג");}}else if(k.KKM(e,16448,72)) {if(1){r=m=1;k.KO(0,t,"̊");}}else if(k.KKM(e,16384,72)) {if(1){r=m=1;k.KO(0,t,"ה");}}else if(k.KKM(e,16448,73)) {if(1){r=m=1;k.KO(0,t,"ֽ");}}else if(k.KKM(e,16384,73)) {if(1){r=m=1;k.KO(0,t,"ִ");}}else if(k.KKM(e,16448,74)) {if(1){r=m=1;}}else if(k.KKM(e,16384,74)) {if(1){r=m=1;k.KO(0,t,"ח");}}else if(k.KKM(e,16448,75)) {if(1){r=m=1;k.KO(0,t,"ך");}}else if(k.KKM(e,16384,75)) {if(1){r=m=1;k.KO(0,t,"כ");}}else if(k.KKM(e,16448,76)) {if(1){r=m=1;}}else if(k.KKM(e,16384,76)) {if(1){r=m=1;k.KO(0,t,"ל");}}else if(k.KKM(e,16448,77)) {if(1){r=m=1;k.KO(0,t,"ם");}}else if(k.KKM(e,16384,77)) {if(1){r=m=1;k.KO(0,t,"מ");}}else if(k.KKM(e,16448,78)) {if(1){r=m=1;k.KO(0,t,"ן");}}else if(k.KKM(e,16384,78)) {if(1){r=m=1;k.KO(0,t,"נ");}}else if(k.KKM(e,16448,79)) {if(1){r=m=1;k.KO(0,t,"ֳ");}}else if(k.KKM(e,16384,79)) {if(1){r=m=1;k.KO(0,t,"ֹ");}}else if(k.KKM(e,16448,80)) {if(1){r=m=1;k.KO(0,t,"ף");}}else if(k.KKM(e,16384,80)) {if(1){r=m=1;k.KO(0,t,"פ");}}else if(k.KKM(e,16448,81)) {if(1){r=m=1;}}else if(k.KKM(e,16384,81)) {if(1){r=m=1;k.KO(0,t,"ק");}}else if(k.KKM(e,16448,82)) {if(1){r=m=1;}}else if(k.KKM(e,16384,82)) {if(1){r=m=1;k.KO(0,t,"ר");}}else if(k.KKM(e,16448,83)) {if(1){r=m=1;}}else if(k.KKM(e,16384,83)) {if(1){r=m=1;k.KO(0,t,"ס");}}else if(k.KKM(e,16448,84)) {if(1){r=m=1;}}else if(k.KKM(e,16384,84)) {if(1){r=m=1;k.KO(0,t,"ת");}}else if(k.KKM(e,16448,85)) {if(1){r=m=1;}}else if(k.KKM(e,16384,85)) {if(1){r=m=1;k.KO(0,t,"ע");}}else if(k.KKM(e,16448,86)) {if(1){r=m=1;}}else if(k.KKM(e,16384,86)) {if(1){r=m=1;k.KO(0,t,"שׁ");}}else if(k.KKM(e,16448,87)) {if(1){r=m=1;}}else if(k.KKM(e,16384,87)) {if(1){r=m=1;k.KO(0,t,"ו");}}else if(k.KKM(e,16448,88)) {if(1){r=m=1;k.KO(0,t,"ץ");}}else if(k.KKM(e,16384,88)) {if(1){r=m=1;k.KO(0,t,"צ");}}else if(k.KKM(e,16448,89)) {if(1){r=m=1;}}else if(k.KKM(e,16384,89)) {if(1){r=m=1;k.KO(0,t,"י");}}else if(k.KKM(e,16448,90)) {if(1){r=m=1;}}else if(k.KKM(e,16384,90)) {if(1){r=m=1;k.KO(0,t,"ז");}}else if(k.KKM(e,16400,219)) {if(1){r=m=1;k.KO(0,t,"{");}}else if(k.KKM(e,16400,220)) {if(1){r=m=1;k.KO(0,t,"|");}}else if(k.KKM(e,16400,221)) {if(1){r=m=1;k.KO(0,t,"}");}}else if(k.KKM(e,16400,192)) {if(1){r=m=1;}}return r;};}
\ No newline at end of file
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn
new file mode 100644
index 0000000000..8ab7ab2901
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn
@@ -0,0 +1,215 @@
+store(&TARGETS) 'any'
+store(&NAME) 'Aleph With Beth'
+store(&BITMAP) 'alephwithbeth.ico'
+store(&VISUALKEYBOARD) 'alephwithbeth.kvks'
+
+store(letters-with-final-forms) "כמנפצ"
+store(sofit-forms-of-letters) "ךםןףץ"
+store(presentation-dageshed-letters-with-final-forms) "כּפּ"
+store(presentation-dageshed-sofit-forms-of-letters) "ךּףּ"
+store(dagesh) 'ּ'
+c store(vowels) "ְֱֲֳִֵֶַׇׇָֹֻׅ"
+
+store(normal-dageshed-letters-with-final-forms) "מּנּצּ"
+store(normal-undageshed-sofit-forms-of-letters) "םןץ"
+
+store(punctuation) U+0591 .. U+05AF U+05BD .. U+05BF U+05C0 U+05C4
+store(whitespace) [K_SPACE] [K_ENTER] [K_TAB]
+
+store(&KMW_RTL) '1'
+store(&LAYOUTFILE) 'other\alephwithbeth.keyman-touch-layout'
+store(©RIGHT) '© Cameron Briggs'
+store(&KEYBOARDVERSION) '1.0.6'
+store(&KMW_HELPTEXT) 'Aleph With Beth'
+
+begin Unicode > use(main)
+
+group(main) using keys
++ [SHIFT K_I] > nul
++ [ALT K_BKQUOTE] > nul
++ [ALT K_B] > nul
++ [ALT K_V] > nul
++ [ALT K_C] > nul
++ [ALT K_Z] > nul
++ [ALT K_L] > nul
++ [ALT K_J] > nul
++ [ALT K_F] > nul
++ [ALT K_D] > nul
++ [ALT K_S] > nul
++ [ALT K_U] > nul
++ [ALT K_Y] > nul
++ [ALT K_T] > nul
++ [ALT K_R] > nul
++ [ALT K_W] > nul
++ [ALT K_Q] > nul
++ [SHIFT K_BKSLASH] > '|'
++ [SHIFT K_EQUAL] > '+'
++ [SHIFT K_HYPHEN] > '_'
++ [SHIFT K_0] > ')'
++ [SHIFT K_9] > '('
++ [SHIFT K_SLASH] > '?'
++ [SHIFT K_QUOTE] > '"'
++ [SHIFT K_BKQUOTE] > nul
++ [K_EQUAL] > '='
++ [K_HYPHEN] > '־'
++ [K_0] > '0'
++ [K_9] > '9'
++ [K_8] > '8'
++ [K_7] > '7'
++ [K_6] > '6'
++ [K_5] > '5'
++ [K_4] > '4'
++ [K_3] > '3'
++ [K_2] > '2'
++ [K_1] > '1'
++ [K_PERIOD] > '.'
++ [K_SLASH] > '/'
++ [K_QUOTE] > U+0027
++ [SHIFT K_COLON] > '״'
++ [ALT K_A] > 'ֲ'
++ [SHIFT K_RBRKT] > '}'
++ [SHIFT K_LBRKT] > '{'
++ [K_BKSLASH] > '\'
++ [K_RBRKT] > ']'
++ [K_LBRKT] > '['
++ [SHIFT K_R] > 'רּ'
++ [SHIFT CTRL K_SPACE] > ' '
++ [SHIFT K_SPACE] > ' '
++ [K_SPACE] > ' '
++ [ALT K_SPACE] > ' '
++ [SHIFT K_8] > '֝'
++ [SHIFT K_7] > '֬'
++ [SHIFT K_6] > '֔'
++ [SHIFT K_5] > '֞'
++ [SHIFT K_4] > '֜'
++ [SHIFT K_3] > '֨'
++ [SHIFT K_2] > '֘'
++ [SHIFT K_1] > '!'
++ [ALT K_SLASH] > '״'
++ [ALT K_PERIOD] > '̈'
++ [ALT K_COMMA] > '֫'
++ [ALT K_QUOTE] > 'ׄ'
++ [ALT K_COLON] > '׃'
++ [ALT K_H] > '̊'
++ [ALT K_G] > '•'
++ [ALT K_BKSLASH] > '֫'
++ [ALT K_RBRKT] > '֒'
++ [ALT K_LBRKT] > '֔'
++ [ALT K_I] > 'ֽ'
++ [ALT K_O] > 'ֳ'
++ [ALT K_E] > 'ֱ'
++ [ALT K_EQUAL] > '◌'
++ [ALT K_HYPHEN] > 'ֿ'
++ [ALT K_0] > '֯'
++ [ALT K_9] > '֓'
++ [ALT K_8] > '֕'
++ [ALT K_7] > '֡'
++ [ALT K_6] > '֟'
++ [ALT K_5] > '֩'
++ [ALT K_4] > '֠'
++ [ALT K_3] > '֙'
++ [ALT K_2] > '֮'
++ [ALT K_1] > '֗'
++ [ALT K_N] > 'ן'
++ [ALT K_P] > 'ף'
++ [ALT K_K] > 'ך'
++ [ALT K_M] > 'ם'
++ [ALT K_X] > 'ץ'
++ [SHIFT K_PERIOD] > 'ּ'
++ [SHIFT K_COMMA] > nul
++ [SHIFT K_M] > 'מּ'
++ [SHIFT K_N] > 'נּ'
++ [SHIFT K_B] > 'בּ'
++ [SHIFT K_V] > 'שּׁ'
++ [SHIFT K_C] > 'שּׂ'
++ [SHIFT K_X] > 'צּ'
++ [SHIFT K_Z] > 'זּ'
++ [SHIFT K_L] > 'לּ'
++ [SHIFT K_K] > 'כּ'
++ [SHIFT K_J] > 'וַיּ'
++ [SHIFT K_H] > 'הּ'
++ [SHIFT K_G] > 'גּ'
++ [SHIFT K_F] > 'טּ'
++ [SHIFT K_D] > 'דּ'
++ [SHIFT K_S] > 'סּ'
++ [SHIFT K_A] > 'ָ'
++ [SHIFT K_P] > 'פּ'
++ [SHIFT K_O] > 'וֹ'
++ [SHIFT K_U] > 'ֻ'
++ [SHIFT K_Y] > 'יּ'
++ [SHIFT K_T] > 'תּ'
++ [SHIFT K_E] > 'ֵ'
++ [SHIFT K_W] > 'וּ'
++ [SHIFT K_Q] > 'קּ'
++ [K_BKQUOTE] > '֑'
++ [K_COMMA] > 'א'
++ [K_M] > 'מ'
++ [K_N] > 'נ'
++ [K_B] > 'ב'
++ [K_V] > 'שׁ'
++ [K_C] > 'שׂ'
++ [K_X] > 'צ'
++ [K_Z] > 'ז'
++ [K_COLON] > 'ְ'
++ [K_L] > 'ל'
++ [K_K] > 'כ'
++ [K_J] > 'ח'
++ [K_H] > 'ה'
++ [K_G] > 'ג'
++ [K_F] > 'ט'
++ [K_D] > 'ד'
++ [K_S] > 'ס'
++ [K_A] > 'ַ'
++ [K_P] > 'פ'
++ [K_O] > 'ֹ'
++ [K_I] > 'ִ'
++ [K_U] > 'ע'
++ [K_Y] > 'י'
++ [K_T] > 'ת'
++ [K_R] > 'ר'
++ [K_E] > 'ֶ'
++ [K_W] > 'ו'
++ [K_Q] > 'ק'
+c Transform final letters
+any(letters-with-final-forms) + any(whitespace) > index(sofit-forms-of-letters,1)
+
+c Transform final letters with a dagesh
+any(letters-with-final-forms) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh
+any(presentation-dageshed-letters-with-final-forms) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1)
+any(normal-dageshed-letters-with-final-forms) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh
+
+c Transform final letters with a vowel
+c any(letters-with-final-forms) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2)
+
+c Transform final letters with a vowel and a dagesh
+c any(letters-with-final-forms) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh
+c any(letters-with-final-forms) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3)
+c any(presentation-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2)
+c any(normal-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2)
+
+c Transform final letters with punctuation
+any(letters-with-final-forms) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2)
+
+c Transform final letters with punctuation and a dagesh
+any(letters-with-final-forms) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh
+any(letters-with-final-forms) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3)
+any(presentation-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2)
+any(normal-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2)
+
+c Transfrom final letters with a punctuation and a vowel
+c any(letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
+c any(letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
+
+c Transform final letters with punctuation, a vowel, and a dagesh (phew!)
+c any(letters-with-final-forms) any(punctuation) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) $dagesh
+c any(letters-with-final-forms) any(punctuation) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh index(vowels,4)
+c any(letters-with-final-forms) any(vowels) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) $dagesh
+c any(letters-with-final-forms) any(vowels) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh index(punctuation,4)
+c any(letters-with-final-forms) $dagesh any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) index(vowels,4)
+c any(letters-with-final-forms) $dagesh any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) index(punctuation,4)
+
+
+c any(presentation-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
+c any(presentation-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
+c any(normal-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) index(vowels,3)
+c any(normal-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) index(punctuation,3)
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp
new file mode 100644
index 0000000000..d44bf1edca
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx
new file mode 100644
index 0000000000..867213cedd
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj
new file mode 100644
index 0000000000..03565f5141
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj
@@ -0,0 +1,133 @@
+
+
+
+ $PROJECTPATH
+ True
+ True
+
+
+
+ id_ede98e4633e239f933cbfd1f4e1b766c
+ HISTORY.md
+ HISTORY.md
+
+ .md
+
+
+ id_53e892b8b41cc4caece1cfd5ef21d6e7
+ LICENSE.md
+ LICENSE.md
+
+ .md
+
+
+ id_0730bb7c2e8f9ea2438b52e419dd86c9
+ README.md
+ README.md
+
+ .md
+
+
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+ alephwithbeth.kps
+ alephwithbeth.kps
+
+ .kps
+
+ Aleph With Beth
+ © Cameron Briggs
+
+
+
+ id_e72bb7d9956f6973ce3de0362e47b0d3
+ alephwithbeth.kmn
+ alephwithbeth.kmn
+ 1.0.6
+ .kmn
+
+ Aleph With Beth
+ © Cameron Briggs
+
+
+
+ id_dc799c273ed24ff1ca06a2429e5af8b4
+ alephwithbeth.ico
+ alephwithbeth.ico
+
+ .ico
+ id_e72bb7d9956f6973ce3de0362e47b0d3
+
+
+ id_8da344c4cea6f467013357fe099006f5
+ readme.htm
+ other\readme.htm
+
+ .htm
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_724e5b4c63f10bc0abf7077f7c3172fc
+ welcome.htm
+ other\welcome\welcome.htm
+
+ .htm
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_101067b24e270db6fdd8b52ac6c77297
+ alephwithbeth.js
+ alephwithbeth.js
+
+ .js
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_cf57b9fb0e29a5e6b4ff4b9a55fc7ef3
+ alephwithbeth.kvk
+ alephwithbeth.kvk
+
+ .kvk
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_dfa1b7bb444da6106e7ffcce1b6ae59d
+ alephwithbeth.kmx
+ alephwithbeth.kmx
+
+ .kmx
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_92d25e21c1bb82cab07440032ea15ca1
+ KeyboardLayout.png
+ other\welcome\KeyboardLayout.png
+
+ .png
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_a751525f488287b36c872b48e3c385b8
+ KeyboardLayoutAlt.png
+ other\welcome\KeyboardLayoutAlt.png
+
+ .png
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_a260e1bca7e99f6955c706b3697f842c
+ KeyboardLayoutShift.png
+ other\welcome\KeyboardLayoutShift.png
+
+ .png
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+ id_c296b5df3dc126e219e0173fcbc1baec
+ LICENSE.md
+ LICENSE.md
+
+ .md
+ id_195752eb7ec06cfb1f90ed508d2c5b54
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps
new file mode 100644
index 0000000000..93a49e397e
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps
@@ -0,0 +1,95 @@
+
+
+
+ 17.0.325.0
+ 7.0
+
+
+
+ other\readme.htm
+ LICENSE.md
+ other\welcome\welcome.htm
+
+
+
+
+
+
+
+
+
+
+ Aleph With Beth
+ © Cameron Briggs
+ Cameron Briggs
+ https://freehebrew.online
+ alephwithbeth keyboard
+
+
+
+ other\readme.htm
+ File readme.htm
+ 0
+ .htm
+
+
+ other\welcome\welcome.htm
+ File welcome.htm
+ 0
+ .htm
+
+
+ alephwithbeth.js
+ File alephwithbeth.js
+ 0
+ .js
+
+
+ alephwithbeth.kvk
+ File alephwithbeth.kvk
+ 0
+ .kvk
+
+
+ alephwithbeth.kmx
+ Keyboard alephwithbeth
+ 0
+ .kmx
+
+
+ other\welcome\KeyboardLayout.png
+ File KeyboardLayout.png
+ 0
+ .png
+
+
+ other\welcome\KeyboardLayoutAlt.png
+ File KeyboardLayoutAlt.png
+ 0
+ .png
+
+
+ other\welcome\KeyboardLayoutShift.png
+ File KeyboardLayoutShift.png
+ 0
+ .png
+
+
+ LICENSE.md
+
+ 0
+ .md
+
+
+
+
+ Aleph With Beth
+ alephwithbeth
+ 1.0.4
+
+ Hebrew
+
+
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk
new file mode 100644
index 0000000000..7772d84d9e
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks
new file mode 100644
index 0000000000..6b3cf3eeb9
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks
@@ -0,0 +1,138 @@
+
+
+
+ 10.0
+ alephwithbeth
+
+
+
+
+ !
+ "
+ ֨
+ ֜
+ ֞
+ ֬
+ (
+ )
+ ֝
+ +
+ ״
+ ּ
+ ?
+ ֘
+ ָ
+ בּ
+ שּׂ
+ דּ
+ ֵ
+ טּ
+ גּ
+ הּ
+ וַיּ
+ כּ
+ לּ
+ מּ
+ נּ
+ וֹ
+ פּ
+ קּ
+ רּ
+ סּ
+ תּ
+ ֻ
+ שּׁ
+ וּ
+ צּ
+ יּ
+ זּ
+ ֔
+ _
+ {
+ |
+ }
+
+
+ '
+ א
+ .
+ /
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ ְ
+ =
+ [
+ \
+ ]
+ ֑
+ ַ
+ ב
+ שׂ
+ ד
+ ֶ
+ ט
+ ג
+ ה
+ ִ
+ ח
+ כ
+ ל
+ מ
+ נ
+ ֹ
+ פ
+ ק
+ ר
+ ס
+ ת
+ ע
+ שׁ
+ ו
+ צ
+ י
+ ז
+ ־
+ 0
+
+
+ ׄ
+ ֫
+ ֿ
+ ̈
+ ״
+ ֯
+ ֗
+ ֮
+ ֙
+ ֠
+ ֩
+ ֟
+ ֡
+ ֕
+ ֓
+ ׃
+ ◌
+ ֔
+ ֫
+ ֒
+ ֲ
+ ֱ
+ •
+ ̊
+ ֽ
+ ך
+ ם
+ ן
+ ֳ
+ ף
+ ץ
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout
new file mode 100644
index 0000000000..05db57c7f9
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout
@@ -0,0 +1,1646 @@
+{
+ "tablet": {
+ "displayUnderlying": false,
+ "layer": [
+ {
+ "id": "default",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "1"
+ },
+ {
+ "id": "K_2",
+ "text": "2"
+ },
+ {
+ "id": "K_3",
+ "text": "3"
+ },
+ {
+ "id": "K_4",
+ "text": "4"
+ },
+ {
+ "id": "K_5",
+ "text": "5"
+ },
+ {
+ "id": "K_6",
+ "text": "6"
+ },
+ {
+ "id": "K_7",
+ "text": "7"
+ },
+ {
+ "id": "K_8",
+ "text": "8"
+ },
+ {
+ "id": "K_9",
+ "text": "9"
+ },
+ {
+ "id": "K_0",
+ "text": "0"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "־"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "="
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "ק",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "ו"
+ },
+ {
+ "id": "K_E",
+ "text": "ֶ"
+ },
+ {
+ "id": "K_R",
+ "text": "ר"
+ },
+ {
+ "id": "K_T",
+ "text": "ת"
+ },
+ {
+ "id": "K_Y",
+ "text": "י"
+ },
+ {
+ "id": "K_U",
+ "text": "ע"
+ },
+ {
+ "id": "K_I",
+ "text": "ִ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "["
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "]"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": "֑"
+ },
+ {
+ "id": "K_A",
+ "text": "ַ"
+ },
+ {
+ "id": "K_S",
+ "text": "ס"
+ },
+ {
+ "id": "K_D",
+ "text": "ד"
+ },
+ {
+ "id": "K_F",
+ "text": "ט"
+ },
+ {
+ "id": "K_G",
+ "text": "ג"
+ },
+ {
+ "id": "K_H",
+ "text": "ה"
+ },
+ {
+ "id": "K_J",
+ "text": "ח"
+ },
+ {
+ "id": "K_K",
+ "text": "כ"
+ },
+ {
+ "id": "K_L",
+ "text": "ל"
+ },
+ {
+ "id": "K_COLON",
+ "text": "ְ"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "'"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "\\"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "ז"
+ },
+ {
+ "id": "K_X",
+ "text": "צ"
+ },
+ {
+ "id": "K_C",
+ "text": "שׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "ב"
+ },
+ {
+ "id": "K_N",
+ "text": "נ"
+ },
+ {
+ "id": "K_M",
+ "text": "מ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "א"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "."
+ },
+ {
+ "id": "K_SLASH",
+ "text": "/"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "shift",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "!"
+ },
+ {
+ "id": "K_2",
+ "text": "֘"
+ },
+ {
+ "id": "K_3",
+ "text": "֨"
+ },
+ {
+ "id": "K_4",
+ "text": "֜"
+ },
+ {
+ "id": "K_5",
+ "text": "֞"
+ },
+ {
+ "id": "K_6",
+ "text": "֔"
+ },
+ {
+ "id": "K_7",
+ "text": "֬"
+ },
+ {
+ "id": "K_8",
+ "text": "֝"
+ },
+ {
+ "id": "K_9",
+ "text": "("
+ },
+ {
+ "id": "K_0",
+ "text": ")"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "_"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "+"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "קּ",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "וּ"
+ },
+ {
+ "id": "K_E",
+ "text": "ֵ"
+ },
+ {
+ "id": "K_R",
+ "text": "רּ"
+ },
+ {
+ "id": "K_T",
+ "text": "תּ"
+ },
+ {
+ "id": "K_Y",
+ "text": "יּ"
+ },
+ {
+ "id": "K_U",
+ "text": "ֻ"
+ },
+ {
+ "id": "K_I",
+ "text": ""
+ },
+ {
+ "id": "K_O",
+ "text": "וֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פּ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "{"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "}"
+ },
+ {
+ "id": "T_new_362",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ָ"
+ },
+ {
+ "id": "K_S",
+ "text": "סּ"
+ },
+ {
+ "id": "K_D",
+ "text": "דּ"
+ },
+ {
+ "id": "K_F",
+ "text": "טּ"
+ },
+ {
+ "id": "K_G",
+ "text": "גּ"
+ },
+ {
+ "id": "K_H",
+ "text": "הּ"
+ },
+ {
+ "id": "K_J",
+ "text": "וַיּ"
+ },
+ {
+ "id": "K_K",
+ "text": "כּ"
+ },
+ {
+ "id": "K_L",
+ "text": "לּ"
+ },
+ {
+ "id": "K_COLON",
+ "text": "״"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "\""
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "|"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "זּ"
+ },
+ {
+ "id": "K_X",
+ "text": "צּ"
+ },
+ {
+ "id": "K_C",
+ "text": "שּׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שּׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "בּ"
+ },
+ {
+ "id": "K_N",
+ "text": "נּ"
+ },
+ {
+ "id": "K_M",
+ "text": "מּ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": ""
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "ּ"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "?"
+ },
+ {
+ "id": "T_new_388",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "alt",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "֗"
+ },
+ {
+ "id": "K_2",
+ "text": "֮"
+ },
+ {
+ "id": "K_3",
+ "text": "֙"
+ },
+ {
+ "id": "K_4",
+ "text": "֠"
+ },
+ {
+ "id": "K_5",
+ "text": "֩"
+ },
+ {
+ "id": "K_6",
+ "text": "֟"
+ },
+ {
+ "id": "K_7",
+ "text": "֡"
+ },
+ {
+ "id": "K_8",
+ "text": "֕"
+ },
+ {
+ "id": "K_9",
+ "text": "֓"
+ },
+ {
+ "id": "K_0",
+ "text": "֯"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "ֿ"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "◌"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": ""
+ },
+ {
+ "id": "K_E",
+ "text": "ֱ"
+ },
+ {
+ "id": "K_R",
+ "text": ""
+ },
+ {
+ "id": "K_T",
+ "text": ""
+ },
+ {
+ "id": "K_Y",
+ "text": ""
+ },
+ {
+ "id": "K_U",
+ "text": ""
+ },
+ {
+ "id": "K_I",
+ "text": "ֽ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֳ"
+ },
+ {
+ "id": "K_P",
+ "text": "ף"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "֔"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "֒"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ֲ"
+ },
+ {
+ "id": "K_S",
+ "text": ""
+ },
+ {
+ "id": "K_D",
+ "text": ""
+ },
+ {
+ "id": "K_F",
+ "text": ""
+ },
+ {
+ "id": "K_G",
+ "text": "•"
+ },
+ {
+ "id": "K_H",
+ "text": "̊"
+ },
+ {
+ "id": "K_J",
+ "text": ""
+ },
+ {
+ "id": "K_K",
+ "text": "ך"
+ },
+ {
+ "id": "K_L",
+ "text": ""
+ },
+ {
+ "id": "K_COLON",
+ "text": "׃"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "ׄ"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "֫"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift",
+ "layer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": ""
+ },
+ {
+ "id": "K_X",
+ "text": "ץ"
+ },
+ {
+ "id": "K_C",
+ "text": ""
+ },
+ {
+ "id": "K_V",
+ "text": ""
+ },
+ {
+ "id": "K_B",
+ "text": ""
+ },
+ {
+ "id": "K_N",
+ "text": "ן"
+ },
+ {
+ "id": "K_M",
+ "text": "ם"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "֫"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "̈"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "״"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "default",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "phone": {
+ "displayUnderlying": false,
+ "layer": [
+ {
+ "id": "default",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "1"
+ },
+ {
+ "id": "K_2",
+ "text": "2"
+ },
+ {
+ "id": "K_3",
+ "text": "3"
+ },
+ {
+ "id": "K_4",
+ "text": "4"
+ },
+ {
+ "id": "K_5",
+ "text": "5"
+ },
+ {
+ "id": "K_6",
+ "text": "6"
+ },
+ {
+ "id": "K_7",
+ "text": "7"
+ },
+ {
+ "id": "K_8",
+ "text": "8"
+ },
+ {
+ "id": "K_9",
+ "text": "9"
+ },
+ {
+ "id": "K_0",
+ "text": "0"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "־"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "="
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "ק",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "ו"
+ },
+ {
+ "id": "K_E",
+ "text": "ֶ"
+ },
+ {
+ "id": "K_R",
+ "text": "ר"
+ },
+ {
+ "id": "K_T",
+ "text": "ת"
+ },
+ {
+ "id": "K_Y",
+ "text": "י"
+ },
+ {
+ "id": "K_U",
+ "text": "ע"
+ },
+ {
+ "id": "K_I",
+ "text": "ִ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "["
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "]"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": "֑"
+ },
+ {
+ "id": "K_A",
+ "text": "ַ"
+ },
+ {
+ "id": "K_S",
+ "text": "ס"
+ },
+ {
+ "id": "K_D",
+ "text": "ד"
+ },
+ {
+ "id": "K_F",
+ "text": "ט"
+ },
+ {
+ "id": "K_G",
+ "text": "ג"
+ },
+ {
+ "id": "K_H",
+ "text": "ה"
+ },
+ {
+ "id": "K_J",
+ "text": "ח"
+ },
+ {
+ "id": "K_K",
+ "text": "כ"
+ },
+ {
+ "id": "K_L",
+ "text": "ל"
+ },
+ {
+ "id": "K_COLON",
+ "text": "ְ"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "'"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "\\"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "ז"
+ },
+ {
+ "id": "K_X",
+ "text": "צ"
+ },
+ {
+ "id": "K_C",
+ "text": "שׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "ב"
+ },
+ {
+ "id": "K_N",
+ "text": "נ"
+ },
+ {
+ "id": "K_M",
+ "text": "מ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "א"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "."
+ },
+ {
+ "id": "K_SLASH",
+ "text": "/"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "shift",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "!"
+ },
+ {
+ "id": "K_2",
+ "text": "֘"
+ },
+ {
+ "id": "K_3",
+ "text": "֨"
+ },
+ {
+ "id": "K_4",
+ "text": "֜"
+ },
+ {
+ "id": "K_5",
+ "text": "֞"
+ },
+ {
+ "id": "K_6",
+ "text": "֔"
+ },
+ {
+ "id": "K_7",
+ "text": "֬"
+ },
+ {
+ "id": "K_8",
+ "text": "֝"
+ },
+ {
+ "id": "K_9",
+ "text": "("
+ },
+ {
+ "id": "K_0",
+ "text": ")"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "_"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "+"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "קּ",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "וּ"
+ },
+ {
+ "id": "K_E",
+ "text": "ֵ"
+ },
+ {
+ "id": "K_R",
+ "text": "רּ"
+ },
+ {
+ "id": "K_T",
+ "text": "תּ"
+ },
+ {
+ "id": "K_Y",
+ "text": "יּ"
+ },
+ {
+ "id": "K_U",
+ "text": "ֻ"
+ },
+ {
+ "id": "K_I",
+ "text": ""
+ },
+ {
+ "id": "K_O",
+ "text": "וֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פּ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "{"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "}"
+ },
+ {
+ "id": "T_new_362",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ָ"
+ },
+ {
+ "id": "K_S",
+ "text": "סּ"
+ },
+ {
+ "id": "K_D",
+ "text": "דּ"
+ },
+ {
+ "id": "K_F",
+ "text": "טּ"
+ },
+ {
+ "id": "K_G",
+ "text": "גּ"
+ },
+ {
+ "id": "K_H",
+ "text": "הּ"
+ },
+ {
+ "id": "K_J",
+ "text": "וַיּ"
+ },
+ {
+ "id": "K_K",
+ "text": "כּ"
+ },
+ {
+ "id": "K_L",
+ "text": "לּ"
+ },
+ {
+ "id": "K_COLON",
+ "text": "״"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "\""
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "|"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "זּ"
+ },
+ {
+ "id": "K_X",
+ "text": "צּ"
+ },
+ {
+ "id": "K_C",
+ "text": "שּׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שּׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "בּ"
+ },
+ {
+ "id": "K_N",
+ "text": "נּ"
+ },
+ {
+ "id": "K_M",
+ "text": "מּ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": ""
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "ּ"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "?"
+ },
+ {
+ "id": "T_new_388",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "alt",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "֗"
+ },
+ {
+ "id": "K_2",
+ "text": "֮"
+ },
+ {
+ "id": "K_3",
+ "text": "֙"
+ },
+ {
+ "id": "K_4",
+ "text": "֠"
+ },
+ {
+ "id": "K_5",
+ "text": "֩"
+ },
+ {
+ "id": "K_6",
+ "text": "֟"
+ },
+ {
+ "id": "K_7",
+ "text": "֡"
+ },
+ {
+ "id": "K_8",
+ "text": "֕"
+ },
+ {
+ "id": "K_9",
+ "text": "֓"
+ },
+ {
+ "id": "K_0",
+ "text": "֯"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "ֿ"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "◌"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": ""
+ },
+ {
+ "id": "K_E",
+ "text": "ֱ"
+ },
+ {
+ "id": "K_R",
+ "text": ""
+ },
+ {
+ "id": "K_T",
+ "text": ""
+ },
+ {
+ "id": "K_Y",
+ "text": ""
+ },
+ {
+ "id": "K_U",
+ "text": ""
+ },
+ {
+ "id": "K_I",
+ "text": "ֽ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֳ"
+ },
+ {
+ "id": "K_P",
+ "text": "ף"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "֔"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "֒"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ֲ"
+ },
+ {
+ "id": "K_S",
+ "text": ""
+ },
+ {
+ "id": "K_D",
+ "text": ""
+ },
+ {
+ "id": "K_F",
+ "text": ""
+ },
+ {
+ "id": "K_G",
+ "text": "•"
+ },
+ {
+ "id": "K_H",
+ "text": "̊"
+ },
+ {
+ "id": "K_J",
+ "text": ""
+ },
+ {
+ "id": "K_K",
+ "text": "ך"
+ },
+ {
+ "id": "K_L",
+ "text": ""
+ },
+ {
+ "id": "K_COLON",
+ "text": "׃"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "ׄ"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "֫"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift",
+ "layer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": ""
+ },
+ {
+ "id": "K_X",
+ "text": "ץ"
+ },
+ {
+ "id": "K_C",
+ "text": ""
+ },
+ {
+ "id": "K_V",
+ "text": ""
+ },
+ {
+ "id": "K_B",
+ "text": ""
+ },
+ {
+ "id": "K_N",
+ "text": "ן"
+ },
+ {
+ "id": "K_M",
+ "text": "ם"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "֫"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "̈"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "״"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "default",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png
new file mode 100644
index 0000000000..d0998e288b
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png
new file mode 100644
index 0000000000..f740eec1e3
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png
new file mode 100644
index 0000000000..9b243d4dd2
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php
new file mode 100644
index 0000000000..48a2c2e7c8
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php
@@ -0,0 +1,22 @@
+
+
+
+Start Using Aleph with Beth
+
+Desktop Layout
+
+Base layout
+
+Layout when shift is pressed
+
+Layout when alt is pressed
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm
new file mode 100644
index 0000000000..f187388c1e
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm
@@ -0,0 +1,22 @@
+
+
+
+
+
+ alephwithbeth
+
+
+
+
+Aleph With Beth
+
+A Hebrew keyboard designed to intuitively match the standard QWERTY layout.
+
+© Cameron Briggs
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png
new file mode 100644
index 0000000000..d0998e288b
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png
new file mode 100644
index 0000000000..f740eec1e3
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png
new file mode 100644
index 0000000000..9b243d4dd2
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm
new file mode 100644
index 0000000000..717e7bedeb
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Start Using alephwithbeth
+
+
+
+
+Start Using Aleph With Beth
+
+Keyboard Layouts
+
+Base layout
+
+Layout when shift is pressed
+
+Layout when alt is pressed
+
+
+
+
diff --git a/developer/src/kmc-copy/test/messages.tests.ts b/developer/src/kmc-copy/test/messages.tests.ts
deleted file mode 100644
index aca98c3ea7..0000000000
--- a/developer/src/kmc-copy/test/messages.tests.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Keyman is copyright (C) SIL International. MIT License.
- */
-
-import 'mocha';
-// import * as path from 'path';
-// import * as fs from 'fs';
-// import { fileURLToPath } from 'url';
-// import { assert } from 'chai';
-import { CopierMessages } from '../src/copier-messages.js';
-import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
-import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
-
-describe('CopierMessages', function () {
- it('should have a valid CopierMessages object', function() {
- return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier);
- });
-
- // it('should generate ERROR_OutputPathAlreadyExists if output path already exists', async function () {
- // const ag = new AbstractGenerator();
- // const callbacks = new TestCompilerCallbacks();
- // const options: GeneratorOptions = {
- // id: 'ERROR_OutputPathAlreadyExists',
- // outPath: path.dirname(fileURLToPath(import.meta.url)),
- // };
- // assert(await ag.init(callbacks, options));
- // const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'ERROR_OutputPathAlreadyExists');
- // if(!fs.existsSync(dir))
- // fs.mkdirSync(dir);
- // assert.isFalse(await ag.write({}));
- // assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_OutputPathAlreadyExists),
- // `messageId ERROR_OutputPathAlreadyExists not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
-
- // fs.rmdirSync(dir);
- // });
-
- // it('should generate ERROR_CannotWriteOutputFile if it cannot create a folder, e.g. invalid filename', async function () {
- // const ag = new AbstractGenerator();
- // const callbacks = new TestCompilerCallbacks();
- // const options: GeneratorOptions = {
- // id: 'ERROR_CannotWriteOutputFile',
- // outPath: path.dirname(fileURLToPath(import.meta.url)),
- // };
- // assert(await ag.init(callbacks, options));
- // assert.isFalse(await ag.write({
- // '.': {filename: '.', data: new Uint8Array([1,2,3])}
- // }));
- // assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile),
- // `messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
- // });
-});
diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json
index a8e0810338..ba488ff575 100644
--- a/developer/src/kmc-generate/package.json
+++ b/developer/src/kmc-generate/package.json
@@ -54,7 +54,8 @@
"exclude-after-remap": true,
"exclude": [
"test/",
- "src/template/"
+ "src/template/",
+ "src/main.ts"
]
},
"repository": {
diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts
index 6cb8b1e43e..5a6ed46cc0 100644
--- a/developer/src/kmc-generate/src/abstract-generator.ts
+++ b/developer/src/kmc-generate/src/abstract-generator.ts
@@ -53,11 +53,14 @@ export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */
* name of the author of the keyboard
*/
author?: string;
- /**
+
+ // TODO-GENERATE: icon support
+ /* *
* set to true to generate an icon for a Keyman keyboard with the first
* characters of the first specified BCP 47 tag
*/
- icon?: boolean;
+ // icon?: boolean;
+
/**
* description of the keyboard, Markdown permissible
*/
diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts
index cb69ef5fdd..fe5a5cd304 100644
--- a/developer/src/kmc-generate/src/basic-generator.ts
+++ b/developer/src/kmc-generate/src/basic-generator.ts
@@ -7,6 +7,7 @@
import { KeymanTargets } from "@keymanapp/common-types";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js";
+import { GeneratorMessages } from "./generator-messages.js";
/**
* @internal
@@ -28,7 +29,19 @@ export class BasicGenerator extends AbstractGenerator {
this.languageTags = this.options.languageTags.length
? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString())
: [this.DEFAULT_LOCALE];
- //TODO-GENERATE: validate targets
+
+ // Verify that targets passed in are valid
+ let failed = false;
+ for(const target of this.options.targets) {
+ if(!KeymanTargets.AllKeymanTargets.includes(target)) {
+ this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target}));
+ failed = true;
+ }
+ }
+ if(failed) {
+ return false;
+ }
+
if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) {
this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any);
} else {
@@ -50,6 +63,8 @@ export class BasicGenerator extends AbstractGenerator {
dt.getDate().toString().padStart(2, '0');
this.tokenMap['$PACKAGE_LANGUAGES'] = this.generateLanguageListForPackage();
this.tokenMap['$PLATFORMS_DOTLIST_README'] = this.getPlatformDotListForReadme();
+
+ return true;
}
protected generate(artifacts: GeneratorArtifacts): boolean {
@@ -89,6 +104,8 @@ export class BasicGenerator extends AbstractGenerator {
sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile);
const sourceData = this.callbacks.loadFile(sourceFile);
+
+ /* c8 ignore next 4 */
if(sourceData == null) {
// internal error -- source file should be readable
throw new Error(`source file ${sourceFile} does not exist`);
diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts
index cda0d82fd2..6c9ccfa7d8 100644
--- a/developer/src/kmc-generate/src/generator-messages.ts
+++ b/developer/src/kmc-generate/src/generator-messages.ts
@@ -17,30 +17,67 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
* @internal
*/
export class GeneratorMessages {
- static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error');
static FATAL_UnexpectedException = SevFatal | 0x0001;
+ static Fatal_UnexpectedException = (o:{e: any}) => m(
+ this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'
+ );
- static Info_GeneratingProject = (o:{type: string, id: string}) => m(this.INFO_GeneratingProject,
- `Generating project of type ${def(o.type)} with id ${def(o.id)}`);
static INFO_GeneratingProject = SevInfo | 0x0002;
+ static Info_GeneratingProject = (o:{type: string, id: string}) => m(
+ this.INFO_GeneratingProject,
+ `Generating project of type ${def(o.type)} with id ${def(o.id)}`
+ );
static ERROR_CannotCreateFolder = SevError | 0x0003;
- static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotCreateFolder, null,
- `Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`);
+ static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(
+ this.ERROR_CannotCreateFolder, null,
+ `Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`
+ );
static ERROR_OutputPathAlreadyExists = SevError | 0x0004;
static Error_OutputPathAlreadyExists = (o:{outPath:string}) => m(
this.ERROR_OutputPathAlreadyExists,
- `Output path ${def(o.outPath)} already exists, not overwriting`);
+ `Output path ${def(o.outPath)} already exists, not overwriting`
+ );
static ERROR_CannotWriteOutputFile = SevError | 0x0005;
- static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotWriteOutputFile, null,
- `Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`);
+ static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(
+ this.ERROR_CannotWriteOutputFile, null,
+ `Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`
+ );
// See also PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions
static WARN_ModelIdDoesNotFollowLexicalModelConventions = SevWarn | 0x0006;
- static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
+ static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(
+ this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
`The id ${def(o.id)} does not follow the recommended model id conventions. The id should be all lower case, `+
`include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+
- `..`);
+ `..`
+ );
+
+ static ERROR_InvalidTarget = SevError | 0x0007;
+ static Error_InvalidTarget = (o:{target:string}) => m(
+ this.ERROR_InvalidTarget,
+ `The specified target platform '${def(o.target)}' is not a valid target.`
+ );
+
+ // See also copier-messages.ERROR_InvalidKeyboardId
+ static ERROR_InvalidKeymanKeyboardId = SevError | 0x0008;
+ static Error_InvalidKeymanKeyboardId = (o:{id:string}) => m(
+ this.ERROR_InvalidKeymanKeyboardId,
+ `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a .kmn keyboard id or filename.`
+ );
+
+ static ERROR_InvalidLdmlKeyboardId = SevError | 0x0009;
+ static Error_InvalidLdmlKeyboardId = (o:{id:string}) => m(
+ this.ERROR_InvalidLdmlKeyboardId,
+ `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for an LDML .xml keyboard id or filename.`
+ );
+
+ // See also copier-messages.ERROR_InvalidLexicalModelId
+ static ERROR_InvalidLexicalModelId = SevError | 0x000A;
+ static Error_InvalidLexicalModelId = (o:{id:string}) => m(
+ this.ERROR_InvalidLexicalModelId,
+ `The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.`
+ );
};
diff --git a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts
index c3d5c7665a..13591fb472 100644
--- a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts
+++ b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts
@@ -5,9 +5,10 @@
*/
import { KeymanFileTypes, KeymanTargets } from '@keymanapp/common-types';
-import { KeymanCompiler, } from '@keymanapp/developer-utils';
+import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
+import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@@ -39,7 +40,15 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise {
- this.preGenerate();
+ if(!ValidIds.isValidKeymanKeyboardId(this.options.id)) {
+ this.callbacks.reportMessage(GeneratorMessages.Error_InvalidKeymanKeyboardId({id:this.options.id}));
+ return null;
+ }
+
+ if(!this.preGenerate()) {
+ // errors will have been reported in preGenerate
+ return null;
+ }
const artifacts: GeneratorArtifacts = this.defaultArtifacts();
@@ -63,9 +72,10 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.TouchLayout;
}
- if(this.hasIcon()) {
- this.includedPrefixes.push('Icon');
- }
+ // TODO-GENERATE: icon support
+ // if(this.hasIcon()) {
+ // this.includedPrefixes.push('Icon');
+ // }
if(this.hasKMX()) {
this.includedPrefixes.push('KMX');
@@ -82,9 +92,9 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
// Special case for creating icon, run after successful creation of other
// project bits and pieces
- if(this.hasIcon()) {
- this.writeIcon(artifacts);
- }
+ // if(this.hasIcon()) {
+ // this.writeIcon(artifacts);
+ // }
return {artifacts};
}
@@ -98,13 +108,13 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
private readonly hasKMX = () => this.targetIncludes(KeymanTargets.KMXKeymanTargets);
private readonly hasTouchLayout = () => this.targetIncludes(KeymanTargets.TouchKeymanTargets);
- // TODO-GENERATE, once writeIcon is implemented:
+ // TODO-GENERATE: icon support
// hasIcon = () => this.options.icon && this.targetIncludes(KeymanTargets.KMXKeymanTargets);
- private readonly hasIcon = () => false;
+ // private readonly hasIcon = () => false;
- private writeIcon(artifacts: GeneratorArtifacts) {
- // TODO-GENERATE: this will require some effort
- // proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon
- // research for .ico writer in node
- }
+ // TODO-GENERATE: icon support
+ // private writeIcon(artifacts: GeneratorArtifacts) {
+ // proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon
+ // research for .ico writer in node
+ // }
}
diff --git a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts
index e9885e7fb3..f8afb49013 100644
--- a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts
+++ b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts
@@ -5,9 +5,10 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
-import { KeymanCompiler, } from '@keymanapp/developer-utils';
+import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
+import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@@ -28,7 +29,15 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise {
- this.preGenerate();
+ if(!ValidIds.isValidLdmlKeyboardId(this.options.id)) {
+ this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLdmlKeyboardId({id:this.options.id}));
+ return null;
+ }
+
+ if(!this.preGenerate()) {
+ // errors will have been reported in preGenerate
+ return null;
+ }
const artifacts: GeneratorArtifacts = this.defaultArtifacts();
this.templatePath = 'ldml-keyboard';
diff --git a/developer/src/kmc-generate/src/lexical-model-generator.ts b/developer/src/kmc-generate/src/lexical-model-generator.ts
index f78807798c..86316549cf 100644
--- a/developer/src/kmc-generate/src/lexical-model-generator.ts
+++ b/developer/src/kmc-generate/src/lexical-model-generator.ts
@@ -5,7 +5,7 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
-import { KeymanCompiler, SourceFilenamePatterns } from '@keymanapp/developer-utils';
+import { KeymanCompiler, SourceFilenamePatterns, ValidIds } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
@@ -31,7 +31,15 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise {
- this.preGenerate();
+ if(!ValidIds.isValidLexicalModelId(this.options.id)) {
+ this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLexicalModelId({id:this.options.id}));
+ return null;
+ }
+
+ if(!this.preGenerate()) {
+ // errors will have been reported in preGenerate
+ return null;
+ }
const artifacts: GeneratorArtifacts = this.defaultArtifacts();
diff --git a/developer/src/kmc-generate/test/basic-generator.tests.ts b/developer/src/kmc-generate/test/basic-generator.tests.ts
index b1eb1e420d..f5cff4044a 100644
--- a/developer/src/kmc-generate/test/basic-generator.tests.ts
+++ b/developer/src/kmc-generate/test/basic-generator.tests.ts
@@ -2,22 +2,71 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+
import 'mocha';
import { assert } from 'chai';
-import { BasicGenerator } from '../src/basic-generator.js';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
+import { BasicGenerator } from '../src/basic-generator.js';
+import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js';
import { options } from './shared-options.js';
+import { makePathToFixture } from './helpers/index.js';
+
+// this class is here to make it possible to access protected members
+class BasicGeneratorTest extends BasicGenerator {
+ static files() {
+ return [
+ 'sample/' + AbstractGenerator.SFile_WelcomeHTM,
+ 'sample/' + AbstractGenerator.SFile_ReadmeHTM,
+ 'sample/' + AbstractGenerator.SFile_HistoryMD,
+ 'sample/' + AbstractGenerator.SFile_LicenseMD,
+ 'sample/' + AbstractGenerator.SFile_ReadmeMD,
+ 'sample/' + AbstractGenerator.SFile_GitIgnore,
+ ];
+ };
+
+ setup() {
+ this.templatePath = 'kmn-keyboard';
+ }
+}
describe('BasicGenerator', function () {
it('should configure default settings', async function() {
const bg = new BasicGenerator();
const callbacks = new TestCompilerCallbacks();
- assert(await bg.init(callbacks, options));
- bg.test_preGenerate();
+ assert.isTrue(await bg.init(callbacks, options));
+ assert.isTrue(bg.test_preGenerate());
assert.equal(bg.test_tokenMap['$NAME'], 'Sample Project');
});
- // TODO-GENERATE: test transform
+ it('should successfully transform common project files', async function() {
+ const bg = new BasicGeneratorTest();
+ bg.setup();
+
+ const callbacks = new TestCompilerCallbacks();
+ assert.isTrue(await bg.init(callbacks, options));
+ assert.isTrue(bg.test_preGenerate());
+
+ const artifacts: GeneratorArtifacts = { "kmc-generate:outputPath": { data: null, filename: '' } };
+ assert.isTrue(bg.test_generate(artifacts));
+
+ // standard artifacts are listed in AbstractGenerator
+ const keys = [].concat(
+ 'kmc-generate:outputPath',
+ BasicGeneratorTest.files().map(f => f.replaceAll('/', path.sep))
+ );
+
+ assert.hasAllKeys(artifacts, keys);
+
+ // pick one file and compare it
+ const filename = BasicGeneratorTest.files()[0].replaceAll('/', path.sep); // welcome.htm
+ assert.equal(artifacts[filename].filename, filename);
+ const fixturePath = makePathToFixture('basic', 'welcome.htm');
+ const fixture = fs.readFileSync(fixturePath, 'utf-8').replace(/\r\n/g, '\n');
+ const actual = new TextDecoder().decode(artifacts[filename].data);
+ assert.equal(actual, fixture);
+ });
});
diff --git a/developer/src/kmc-generate/test/fixtures/basic/welcome.htm b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm
new file mode 100644
index 0000000000..1454a604b6
--- /dev/null
+++ b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Start Using Sample Project
+
+
+
+
+Start Using Sample Project
+
+
+ # A mighty description
+
+
+Keyboard Layout
+
+
+
+
+
\ No newline at end of file
diff --git a/developer/src/kmc-generate/test/messages.tests.ts b/developer/src/kmc-generate/test/generator-messages.tests.ts
similarity index 50%
rename from developer/src/kmc-generate/test/messages.tests.ts
rename to developer/src/kmc-generate/test/generator-messages.tests.ts
index 68c7f741fa..b5d3a8837a 100644
--- a/developer/src/kmc-generate/test/messages.tests.ts
+++ b/developer/src/kmc-generate/test/generator-messages.tests.ts
@@ -2,15 +2,24 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
+import * as path from 'node:path';
+import * as fs from 'node:fs';
+
import 'mocha';
-import * as path from 'path';
-import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { assert } from 'chai';
-import { GeneratorMessages } from '../src/generator-messages.js';
+
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
+
+import { GeneratorMessages } from '../src/generator-messages.js';
import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js';
+import { BasicGenerator } from '../src/basic-generator.js';
+import { KeymanKeyboardGenerator } from '../src/keyman-keyboard-generator.js';
+import { LdmlKeyboardGenerator } from '../src/ldml-keyboard-generator.js';
+import { LexicalModelGenerator } from '../src/lexical-model-generator.js';
+
+import { options } from './shared-options.js';
describe('GeneratorMessages', function () {
const callbacks = new TestCompilerCallbacks();
@@ -62,4 +71,49 @@ describe('GeneratorMessages', function () {
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile),
`messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
+
+ it('should generate ERROR_InvalidTarget if an invalid target is passed in', async function () {
+ const testOptions: GeneratorOptions = { ...options,
+ // @ts-ignore ('invalid' could be passed by a user, even if would be a compile error here)
+ targets: ['invalid']
+ };
+ const bg = new BasicGenerator();
+ const callbacks = new TestCompilerCallbacks();
+ assert(await bg.init(callbacks, testOptions));
+ assert.isFalse(bg.test_preGenerate());
+ assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidTarget));
+ });
+
+ it('should generate ERROR_InvalidKeymanKeyboardId if an invalid keyboard id is passed in', async function () {
+ const testOptions: GeneratorOptions = { ...options,
+ id: '???invalid',
+ };
+ const kkg = new KeymanKeyboardGenerator();
+ const callbacks = new TestCompilerCallbacks();
+ assert.isTrue(await kkg.init(callbacks, testOptions));
+ assert.isNull(await kkg.run());
+ assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidKeymanKeyboardId));
+ });
+
+ it('should generate ERROR_InvalidLdmlKeyboardId if an invalid keyboard id is passed in', async function () {
+ const testOptions: GeneratorOptions = { ...options,
+ id: '???invalid',
+ };
+ const lkg = new LdmlKeyboardGenerator();
+ const callbacks = new TestCompilerCallbacks();
+ assert.isTrue(await lkg.init(callbacks, testOptions));
+ assert.isNull(await lkg.run());
+ assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLdmlKeyboardId));
+ });
+
+ it('should generate ERROR_InvalidLexicalModelId if an invalid lexical model id is passed in', async function () {
+ const testOptions: GeneratorOptions = { ...options,
+ id: 'example.???.invalid',
+ };
+ const lmg = new LexicalModelGenerator();
+ const callbacks = new TestCompilerCallbacks();
+ assert.isTrue(await lmg.init(callbacks, testOptions));
+ assert.isNull(await lmg.run());
+ assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLexicalModelId));
+ });
});
diff --git a/developer/src/kmc-generate/test/shared-options.ts b/developer/src/kmc-generate/test/shared-options.ts
index 1010c417b4..cc36789fe3 100644
--- a/developer/src/kmc-generate/test/shared-options.ts
+++ b/developer/src/kmc-generate/test/shared-options.ts
@@ -6,7 +6,8 @@ import { KeymanTargets } from "@keymanapp/common-types";
import { GeneratorOptions } from "../src/abstract-generator.js";
export const options: GeneratorOptions = {
- icon: false,
+ // TODO-GENERATE: icon support
+ // icon: false,
id: 'sample',
languageTags: ['en'],
name: 'Sample Project',
diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
index ee784429b9..3e1db45aed 100644
--- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
+++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
@@ -12,7 +12,7 @@ import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResul
import { KmpCompiler } from "@keymanapp/kmc-package";
import { SchemaValidators } from "@keymanapp/common-types";
-import { getFontFamily } from "./font-family.js";
+import { getFontFamily } from "@keymanapp/developer-utils";
const regionNames = new Intl.DisplayNames(['en'], { type: "region" });
const scriptNames = new Intl.DisplayNames(['en'], { type: "script" });
diff --git a/developer/src/kmc-keyboard-info/test/font-family.tests.ts b/developer/src/kmc-keyboard-info/test/font-family.tests.ts
index b27fad8d74..d926e75c65 100644
--- a/developer/src/kmc-keyboard-info/test/font-family.tests.ts
+++ b/developer/src/kmc-keyboard-info/test/font-family.tests.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import { assert } from 'chai';
import 'mocha';
import { makePathToFixture } from './helpers/index.js';
-import { getFontFamily } from '../src/font-family.js';
+import { getFontFamily } from '@keymanapp/developer-utils';
const AFGHAN_TURKMEN_DISPLAY_FONT = makePathToFixture('afghan_turkmen', "Lateef-Regular.ttf");
const AFGHAN_TURKMEN_OSK_FONT = makePathToFixture('afghan_turkmen', "Lateef-Bold.ttf");
diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json
index d09b25c560..124c812ecf 100644
--- a/developer/src/kmc-keyboard-info/tsconfig.json
+++ b/developer/src/kmc-keyboard-info/tsconfig.json
@@ -13,7 +13,6 @@
},
"include": [
"src/**/*.ts",
- "src/ttfmeta/lib/*.js",
"src/imports/langtags.js",
],
"references": [
diff --git a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts
index 9cdb28a3d9..d31ccdb14c 100644
--- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts
+++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts
@@ -669,7 +669,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
switch(recContext.Code) {
case KMX.KMXFile.CODE_ANY:
- Index = AdjustIndex(fkp.dpContext, ContextIndex) + 1; // I3910 // I4611
+ Index = AdjustIndex(fkp.dpContext, ContextIndex); // I3910 // I4611
Result += nlt + `k.KIO(${len},this.s${JavaScript_Name(recContext.Any.StoreIndex, recContext.Any.Store.dpName)},${Index},t);`; // I4611
break;
case KMX.KMXFile.CODE_DEADKEY:
@@ -681,7 +681,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
// Note that this is checked in compiler.cpp as well, so this error can probably never occur
throw new Error('Unexpected: notany() encountered with invalid version');
}
- Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)+1});`;
+ Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)});`;
break;
case KMX.KMXFile.CODE_IFOPT:
case KMX.KMXFile.CODE_IFSYSTEMSTORE:
@@ -759,7 +759,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
case KMX.KMXFile.CODE_CONTEXT:
if (x > 0 || len == -1) {
let xContext = 0;
- let n = 0;
+ let n = 1;
while(xContext < fkp.dpContext.length) { // I4611
if(!isGroupReadOnly(fk, fgp)) {
Result += ContextChar(n, fkp.dpContext, xContext);
@@ -767,7 +767,6 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
n++;
xContext = incxstr(fkp.dpContext, xContext);
}
- //Result := Result + Format('k.KO(%d,t,k.KC(%d,%d,t));', [len, xstrlen_printing(fkp.dpContext), xstrlen_printing(fkp.dpContext)]);
}
// else, we don't need to output anything - just don't delete the context
len = -1;
@@ -779,7 +778,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
}
if(!isGroupReadOnly(fk, fgp)) {
- Result += ContextChar(rec.ContextEx.Index, fkp.dpContext, xContext); // I4611
+ Result += ContextChar(rec.ContextEx.Index + 1, fkp.dpContext, xContext); // I4611
}
len = -1;
break;
diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn
new file mode 100644
index 0000000000..26d2164928
--- /dev/null
+++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn
@@ -0,0 +1,29 @@
+c test_index_12980 generated from template at 2025-01-22 13:25:35
+c Validates index() offsets when used with if() - see #12980
+
+store(&VERSION) '14.0'
+store(&NAME) 'test_index_12980'
+store(&TARGETS) 'web'
+
+
+begin Unicode > use(main)
+
+group(main) using keys
+
+store(a) '1'
+store(b) '2'
+store(c) '3'
+store(d) '4'
+store(e) '5'
+store(f) '6'
+store(g) '7'
+store(h) '8'
+
+if(&platform = 'web') any(a) + 'a' > context 'A'
+if(&platform = 'web') any(b) + 'b' > context(2) 'B'
+if(&platform = 'web') notany(c) + 'c' > context 'C'
+if(&platform = 'web') notany(d) + 'd' > context(2) 'D'
+nul any(e) + 'e' > context 'E'
+nul any(f) + 'f' > context(2) 'F'
+nul notany(g) + 'g' > context 'G'
+nul notany(h) + 'h' > context(2) 'H'
diff --git a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts
index d90b33c312..5060dc8757 100644
--- a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts
+++ b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts
@@ -218,6 +218,30 @@ describe('KeymanWeb Compiler', function() {
assert.lengthOf(callbacks.messages, 2);
});
+ it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard', async function() {
+ const filenames = generateTestFilenames('test_index_12980');
+
+ let result = await kmnCompiler.run(filenames.source, null);
+ assert.isNotNull(result);
+ const data = new TextDecoder('utf-8').decode(result.artifacts.js.data);
+
+ function doMatchKIO(s: string) {
+ const regex = new RegExp('k\\.KIO\\(-1,this\\.'+s+',(.+?),t\\);');
+ const m = regex.exec(data);
+ assert.isNotNull(m, `Could not find match for ${s}`);
+ assert.equal(m[1], '1', `Expected '${s}' usage to have an index parameter == 1`);
+ }
+
+ doMatchKIO('s_a_6');
+ doMatchKIO('s_b_7');
+
+ // matches the 4 notany() rules
+ const matches = [...data.matchAll(/k\.KCXO\(-1,t,1,1\);/g)];
+ assert.lengthOf(matches, 4);
+
+ doMatchKIO('s_e_10');
+ doMatchKIO('s_f_11');
+ });
});
async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string):
diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
index 607823c7dd..cd6cf6df36 100644
--- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts
+++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
@@ -18,6 +18,8 @@ import { PackageKeyboardTargetValidator } from './package-keyboard-target-valida
import { PackageMetadataUpdater } from './package-metadata-updater.js';
import { markdownToHTML } from './markdown.js';
import { PackageValidation } from './package-validation.js';
+import { getFontFamilySync } from "@keymanapp/developer-utils";
+import { setKvkFontData } from './kvk-font-facename.js';
const KMP_JSON_FILENAME = 'kmp.json';
const KMP_INF_FILENAME = 'kmp.inf';
@@ -483,36 +485,36 @@ export class KmpCompiler implements KeymanCompiler {
this.callbacks.reportMessage(PackageCompilerMessages.Warn_AbsolutePath({filename: filename}));
}
+ let memberFileData = this.getMemberFileData(kpsFilename, filename);
+ if(!memberFileData) {
+ failed = true;
+ return;
+ }
filename = this.callbacks.resolveFilename(kpsFilename, filename);
const basename = this.callbacks.path.basename(filename);
- if(!this.callbacks.fs.existsSync(filename)) {
- this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
- failed = true;
- return;
+ if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
+ if(!this.isKvkFileBinary(memberFileData)) {
+ // warn the few users who are still doing this -- non-binary .kvk should not be included in package!
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
+ } else {
+ memberFileData = this.setKvkFontData(kpsFilename, data, filename, memberFileData);
+ }
}
- let memberFileData;
- try {
- memberFileData = this.callbacks.loadFile(filename);
- } catch(e) {
- this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
- failed = true;
- return;
- }
-
- this.warnIfKvkFileIsNotBinary(filename, memberFileData);
-
zip.file(basename, memberFileData);
-
- // Remove path data from files before JSON save
- value.name = basename;
});
if(failed) {
return null;
}
+ data.files.forEach((value) => {
+ const basename = this.callbacks.path.basename(value.name);
+ // Remove path data from files before JSON save
+ value.name = basename;
+ });
+
// TODO #9477: transform .md to .htm
// Remove path data from file references in options
@@ -557,26 +559,87 @@ export class KmpCompiler implements KeymanCompiler {
return transcodeToCP1252(s);
}
+ private getMemberFileData(kpsFilename: string, filename: string) {
+ filename = this.callbacks.resolveFilename(kpsFilename, filename);
+
+ if(!this.callbacks.fs.existsSync(filename)) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
+ return null;
+ }
+
+ try {
+ return this.callbacks.loadFile(filename);
+ } catch(e) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
+ return null;
+ }
+ }
+
+ private setKvkFontData(kpsFilename: string, data: KmpJsonFile.KmpJsonFile, filename: string, kvk: Uint8Array) {
+ // find the appropriate font to set
+ const kvkId = this.callbacks.path.basename(filename.toLowerCase(), '.kvk');
+ const kbd = data.keyboards.find(k => k.id == kvkId);
+ if(!kbd) {
+ // cannot find a matching keyboard
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindMatchingKeyboardForVisualKeyboard({filename}));
+ return kvk;
+ }
+ const fontFilename = kbd.oskFont || kbd.displayFont;
+ if(!fontFilename) {
+ // no font data to set
+ return kvk;
+ }
+
+ // Look up the full font filename
+ const fontFile = data.files.find(file => this.callbacks.path.basename(file.name.toLowerCase()) == fontFilename.toLowerCase());
+ if(!fontFile) {
+ // font cannot be found
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename}));
+ return kvk;
+ }
+
+ // the font is a filename, included in the .kps
+ const fontData = this.getMemberFileData(kpsFilename, fontFile.name);
+ if(!fontData) {
+ // cannot find source font
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename: fontFile.name}));
+ return kvk;
+ }
+
+ const fontFacename = getFontFamilySync(fontData);
+ if(!fontFacename) {
+ // Font facename could not be extracted from ttf
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotReadFont({fontFilename: fontFile.name}));
+ return kvk;
+ }
+
+ const result = setKvkFontData(kvk, fontFacename);
+ if(!result) {
+ // KVK is invalid
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_VisualKeyboardFileIsInvalid({filename}));
+ return kvk;
+ }
+
+ return result;
+ }
+
/**
* Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now
- * we want that to remain the responsibility of the keyboard compiler, so we'll warn the
- * few users who are still doing this
+ * we want that to remain the responsibility of the keyboard compiler.
*/
- private warnIfKvkFileIsNotBinary(filename: string, data: Uint8Array) {
- if(!KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
- return;
- }
-
+ private isKvkFileBinary(data: Uint8Array) {
if(data.byteLength < 4) {
// TODO: Not a valid .kvk file; should we be reporting this?
- return;
+ return false;
}
+ // Must start with 'KVKF'
if(data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] ||
data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] ||
data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] ||
data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) {
- this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
+ return false;
}
+ return true;
}
}
diff --git a/developer/src/kmc-package/src/compiler/kvk-font-facename.ts b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts
new file mode 100644
index 0000000000..b7c69da8e8
--- /dev/null
+++ b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts
@@ -0,0 +1,16 @@
+import { KvkFileReader, KvkFileWriter } from "@keymanapp/common-types";
+
+export function setKvkFontData(fileData: Uint8Array, fontFacename: string) {
+ const reader = new KvkFileReader();
+ const kvk = reader.read(fileData);
+ if(!kvk) {
+ // kvk is invalid
+ return null;
+ }
+
+ kvk.header.ansiFont.name = fontFacename;
+ kvk.header.unicodeFont.name = fontFacename;
+
+ const writer = new KvkFileWriter();
+ return writer.write(kvk);
+}
\ No newline at end of file
diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
index 9a593a3a11..6545b7d963 100644
--- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
+++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
@@ -164,5 +164,25 @@ export class PackageCompilerMessages {
static Error_RequiredParameterMissing = (o:{param: string}) => m(
this.ERROR_RequiredParameterMissing, `Source parameter '${def(o.param)}' is required.`
);
+
+ static WARN_CannotFindMatchingKeyboardForVisualKeyboard = SevWarn | 0x0023;
+ static Warn_CannotFindMatchingKeyboardForVisualKeyboard = (o:{filename: string}) => m(
+ this.WARN_CannotFindMatchingKeyboardForVisualKeyboard, `Cannot find matching keyboard for visual keyboard file '${def(o.filename)}'.`
+ );
+
+ static WARN_CannotFindFontForKeyboard = SevWarn | 0x0024;
+ static Warn_CannotFindFontForKeyboard = (o:{id: string, fontFilename: string}) => m(
+ this.WARN_CannotFindFontForKeyboard, `Cannot find font file '${def(o.fontFilename)}' for keyboard '${def(o.id)}'.`
+ );
+
+ static WARN_CannotReadFont = SevWarn | 0x0025;
+ static Warn_CannotReadFont = (o:{fontFilename: string}) => m(
+ this.WARN_CannotReadFont, `Cannot parse font file '${def(o.fontFilename)}' to get facename.`
+ );
+
+ static WARN_VisualKeyboardFileIsInvalid = SevWarn | 0x0026;
+ static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m(
+ this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.`
+ );
}
diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx
new file mode 100644
index 0000000000..bd6b832cab
Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx differ
diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps
index de17212a84..1a02984beb 100644
--- a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps
+++ b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps
@@ -27,7 +27,7 @@
.kvk
- ../../invalid/basic.kmx
+ binary_kvk_file.kmx
Keyboard Basic
0
.kmx
@@ -36,7 +36,7 @@
Basic
- basic
+ binary_kvk_file
1.0
Khmer
diff --git a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts
index 6309064dd0..3109038a3a 100644
--- a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts
+++ b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts
@@ -35,7 +35,11 @@ export class BuildKeyboardInfo extends BuildActivity {
}
const keyboard = project.files.find(file => file.fileType == KeymanFileTypes.Source.KeymanKeyboard);
- const jsFilename = keyboard ? project.resolveOutputFilePath(keyboard, KeymanFileTypes.Source.KeymanKeyboard, KeymanFileTypes.Binary.WebKeyboard) : null;
+
+ // If the project has more than one keyboard, we should not list any .js files (#12853)
+ const canListJsFile = project.files.filter(file => file.fileType == KeymanFileTypes.Source.KeymanKeyboard).length == 1;
+
+ const jsFilename = canListJsFile && keyboard ? project.resolveOutputFilePath(keyboard, KeymanFileTypes.Source.KeymanKeyboard, KeymanFileTypes.Binary.WebKeyboard) : null;
const historyPath = path.join(project.projectPath, KeymanFileTypes.HISTORY_MD);
const lastCommitDate = getLastGitCommitDate(fs.existsSync(historyPath) ? historyPath : project.projectPath);
const sources = {
diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts
index 2f22b24a0a..1d4ab94dcf 100644
--- a/developer/src/kmc/src/commands/copy.ts
+++ b/developer/src/kmc/src/commands/copy.ts
@@ -19,7 +19,6 @@ export function declareCopy(program: Command) {
command
.description('Copy a Keyman keyboard or lexical model project')
.option('-o, --out-path ', 'New name and path for project')
- // TODO-COPY .option('-r, --rename', 'Rename instead of copying project')
.option('-n, --dry-run', 'Show what would happen, without making changes')
.action(copyProject)
.addHelpText('before', `
@@ -36,7 +35,6 @@ export function declareCopy(program: Command) {
function commanderOptionsToCopierOptions(options: any): CopierOptions {
const result: CopierOptions = {
outPath: options.outPath,
- // TODO-COPY rename: options.rename ?? false,
dryRun: options.dryRun ?? false,
};
return result;
diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts
index bac724d9d4..d5abcec989 100644
--- a/developer/src/kmc/src/commands/generate.ts
+++ b/developer/src/kmc/src/commands/generate.ts
@@ -30,7 +30,8 @@ function declareGenerateKmnKeyboard(command: Command) {
.option('-L, --language-tag ', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author ', 'Name of keyboard author')
- .option('-i, --icon', 'Include a generated icon', true)
+ // TODO-GENERATE: icon support
+ // .option('-i, --icon', 'Include a generated icon', true)
.option('-d, --description ', 'Short description of the project, Markdown')
.action(generateKmnKeyboard);
}
@@ -45,7 +46,7 @@ function declareGenerateLdmlKeyboard(command: Command) {
.option('-o, --out-path ', 'Output path (may exist)')
.option('-n, --name ', 'Keyboard descriptive name')
.option('-c, --copyright ', 'Copyright holder') /* © yyyy */
- .option('-v, --version ', 'Keyboard version', '1.0')
+ .option('-v, --version ', 'Keyboard version', '1.0.0')
.option('-L, --language-tag ', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author ', 'Name of keyboard author')
@@ -59,12 +60,12 @@ function declareGenerateLexicalModel(command: Command) {
modelSubCommand
.description('Generate a wordlist lexical model project')
.option('-o, --out-path ', 'Output path (may exist)')
- .option('-n, --name ', 'Keyboard descriptive name')
+ .option('-n, --name ', 'Lexical model descriptive name')
.option('-c, --copyright ', 'Copyright holder') /* © yyyy */
- .option('-v, --version ', 'Keyboard version', '1.0')
+ .option('-v, --version ', 'Lexical model version', '1.0')
.option('-L, --language-tag ', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
- .option('-a, --author ', 'Name of keyboard author')
+ .option('-a, --author ', 'Name of lexical model author')
.option('-d, --description ', 'Short description of the project, Markdown')
.action(generateLexicalModel);
@@ -73,7 +74,8 @@ function declareGenerateLexicalModel(command: Command) {
function commanderOptionsToGeneratorOptions(id: string, options: any): GeneratorOptions {
const result: GeneratorOptions = {
- icon: options.icon ?? true,
+ // TODO-GENERATE: icon support
+ // icon: options.icon ?? true,
id,
languageTags: options.languageTag ?? [],
name: options.name ?? id,
diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts
index ee568dad9c..257eacc913 100644
--- a/developer/src/kmc/src/commands/messageCommand.ts
+++ b/developer/src/kmc/src/commands/messageCommand.ts
@@ -178,7 +178,7 @@ function allMessageDetails(): CompilerMessageDetail[] {
const toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
function getMessageDetail(cls: any, id: string, escapeMarkdown: boolean): CompilerEvent {
- const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(id);
+ const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(id);
if(!o) {
throw new Error(`Unexpected compiler message ${id}, does not match message error format`);
}
diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts
index fd5f29d212..f3cdf1e7b2 100644
--- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts
+++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts
@@ -23,6 +23,8 @@ const severityColors: {[value in CompilerErrorSeverity]: chalk.Chalk} = {
[CompilerErrorSeverity.Warn]: color.hex('FFA500'), // orange
[CompilerErrorSeverity.Error]: color.redBright,
[CompilerErrorSeverity.Fatal]: color.redBright,
+ [CompilerErrorSeverity.Verbose]: color.gray,
+ [CompilerErrorSeverity.Debug]: color.blueBright,
};
/**
diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh
index c73ed853f7..e0e82e3617 100755
--- a/developer/src/server/build.sh
+++ b/developer/src/server/build.sh
@@ -113,7 +113,17 @@ function installer_server() {
# monorepo context
cat package.json | "$JQ" \
'. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version") | del(.dependencies."@keymanapp/developer-utils")' \
+ > "$PRODBUILDTEMP/package0.json"
+
+ # #12950: Merge dependencies from @keymanapp/developer-utils and Server; this
+ # is a bit messy but will mean we don't lose the needed dependencies.
+ "$JQ" -s '.[0] + { dependencies: (.[1].dependencies * .[0].dependencies) }' "$PRODBUILDTEMP/package0.json" \
+ "$KEYMAN_ROOT/developer/src/common/web/utils/package.json" > "$PRODBUILDTEMP/package1.json"
+ cat $PRODBUILDTEMP/package1.json | "$JQ" \
+ '. | del(.dependencies."@keymanapp/common-types")' \
> "$PRODBUILDTEMP/package.json"
+ rm "$PRODBUILDTEMP/package0.json"
+ rm "$PRODBUILDTEMP/package1.json"
pushd "$PRODBUILDTEMP"
npm install --omit=dev --omit=optional
diff --git a/developer/src/tike/child/UfrmKeymanWizard.pas b/developer/src/tike/child/UfrmKeymanWizard.pas
index 59e0f8a341..afb1e715ed 100644
--- a/developer/src/tike/child/UfrmKeymanWizard.pas
+++ b/developer/src/tike/child/UfrmKeymanWizard.pas
@@ -369,6 +369,8 @@ type
FLayoutSetup: Boolean;
FCheckboxGridHelper: TCheckboxGridHelper;
+ FDisplayMapFont: TFont;
+
function GetCurrentRule: TKeyboardParser_LayoutRule;
procedure EnableControls;
@@ -475,6 +477,7 @@ type
procedure OrderDetailsPanels;
function ValidateFileHasNoUnicodeCharacters(sw: WideString): Boolean;
procedure UpdateQRCode;
+ procedure SetDisplayMapFont(const Value: TFont);
protected
function GetHelpTopic: string; override;
@@ -495,6 +498,8 @@ type
function ShouldRememberFocus(Control: TWinControl): Boolean; override; // I4679
+ property DisplayMapFont: TFont read FDisplayMapFont write SetDisplayMapFont;
+
public
procedure StartDebugging(FStartTest: Boolean = False);
procedure StopDebugging;
@@ -630,6 +635,8 @@ procedure TfrmKeymanWizard.FormCreate(Sender: TObject);
begin
FLoading := True;
+ FDisplayMapFont := TFont.Create;
+
//SetWindowTheme(pages.Handle, 'Explorer', '');
cmdAddToProject.Caption := '&Add to Project';
@@ -681,6 +688,7 @@ begin
FreeAndNil(FKeyboardParser); // I4557
FreeAndNil(FDebugForm);
FreeAndNil(FCheckboxGridHelper);
+ FreeAndNil(FDisplayMapFont);
UninitSystemKeyboard;
end;
@@ -1825,6 +1833,15 @@ begin
Delete(Result.Size, Length(Result.Size)-1, 2);
end;
end;
+ kfontDisplayMap:
+ begin
+ Result.Enabled := FKeyboardParser.GetSystemStoreValue(ssDisplayMap) <> '';
+ if Result.Enabled then
+ begin
+ Result.Name := DisplayMapFont.Name;
+ Result.Size := IntToStr(TFontUtils.FontSizeInPoints(DisplayMapFont.Name, DisplayMapFont.Size)); // I4872
+ end;
+ end;
end;
end;
@@ -2535,6 +2552,8 @@ begin
FFont.Free;
end;
end;
+
+ SetFontFromString(DisplayMapFont, ProjectFile.IDEState['DisplayMapFont']);
end;
procedure TfrmKeymanWizard.SaveSettings(SaveProject: Boolean);
@@ -2548,6 +2567,7 @@ begin
else ProjectFile.IDEState['DebugDefaultFont'] := '0'; // I4702
ProjectFile.IDEState['DebugFont'] := FontAsString(FDebugForm.memo.Font); // I4702
+ ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
inherited;
end;
@@ -2781,6 +2801,13 @@ begin
TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]);
end;
+procedure TfrmKeymanWizard.SetDisplayMapFont(const Value: TFont);
+begin
+ FDisplayMapFont.Assign(Value);
+ if ProjectFile <> nil then
+ ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
+end;
+
procedure TfrmKeymanWizard.SetFontInfo(Index: TKeyboardFont; const Value: TKeyboardFontInfo); // I4057
var
f: TFont;
@@ -2814,6 +2841,11 @@ begin
CharFont.Name := Value.Name;
CharFont.Size := StrToIntDef(Value.Size, 12);
end;
+ kfontDisplayMap:
+ begin
+ DisplayMapFont.Name := Value.Name;
+ DisplayMapFont.Size := StrToIntDef(Value.Size, 12);
+ end;
kfontOSK:
begin
if FKeyboardParser.Features.ContainsKey(kfOSK) then // I4258
diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
index 9778119e9f..24c070a5e8 100644
--- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
+++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
@@ -114,7 +114,7 @@ begin
state := plsFatal;
Result := False;
end
- else // assume msgType = 'info'
+ else // assume msgType = 'info' (fine also for 'verbose', 'debug')
state := plsInfo;
FGlobalProject.Log(state, msgFilename, msgText, msgCode, msgLine);
end
diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm
index bd8ba4de5b..b874db58a8 100644
--- a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm
+++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm
@@ -2,11 +2,11 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
BorderIcons = [biSystemMenu]
BorderStyle = bsDialog
Caption = 'Keyboard Fonts'
- ClientHeight = 291
+ ClientHeight = 322
ClientWidth = 512
Position = poScreenCenter
ExplicitWidth = 518
- ExplicitHeight = 320
+ ExplicitHeight = 351
PixelsPerInch = 96
TextHeight = 13
object lblDevEnv: TLabel
@@ -147,6 +147,20 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
Height = 13
Caption = 'em'
end
+ object lblDisplayMap: TLabel
+ Left = 32
+ Top = 248
+ Width = 109
+ Height = 13
+ Caption = '&displayMap target font'
+ end
+ object Label4: TLabel
+ Left = 463
+ Top = 248
+ Width = 14
+ Height = 13
+ Caption = 'em'
+ end
object fcbCode: TscFontComboBox
Left = 176
Top = 41
@@ -260,23 +274,23 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
end
object cmdOK: TButton
Left = 162
- Top = 248
+ Top = 280
Width = 91
Height = 25
Caption = 'OK'
Default = True
- TabOrder = 12
+ TabOrder = 14
OnClick = cmdOKClick
end
object cmdCancel: TButton
Left = 259
- Top = 248
+ Top = 280
Width = 90
Height = 25
Cancel = True
Caption = 'Cancel'
ModalResult = 2
- TabOrder = 13
+ TabOrder = 15
end
object fcbTouchLayoutPhone: TscFontComboBox
Left = 176
@@ -347,4 +361,34 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
ShowPreviewFontName = False
ShowPreviewInList = False
end
+ object editDisplayMapSize: TEdit
+ Left = 391
+ Top = 245
+ Width = 66
+ Height = 21
+ TabOrder = 13
+ end
+ object fcbDisplayMap: TscFontComboBox
+ Left = 176
+ Top = 245
+ Width = 209
+ Height = 24
+ DropDownCount = 12
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -13
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ ItemHeight = 18
+ ParentFont = False
+ TabOrder = 12
+ MoveUsedToTop = False
+ ColorUsed = clWindow
+ PreviewWidth = 240
+ PreviewHeight = 60
+ FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol]
+ ShowPreview = False
+ ShowPreviewFontName = False
+ ShowPreviewInList = False
+ end
end
diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas
index 8c5bc103c2..7f5d4546bd 100644
--- a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas
+++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas
@@ -66,6 +66,10 @@ type
fcbTouchLayoutPhone: TscFontComboBox;
fcbTouchLayoutTablet: TscFontComboBox;
fcbTouchLayoutDesktop: TscFontComboBox;
+ lblDisplayMap: TLabel;
+ editDisplayMapSize: TEdit;
+ fcbDisplayMap: TscFontComboBox;
+ Label4: TLabel;
procedure FormCreate(Sender: TObject);
procedure cmdOKClick(Sender: TObject);
private
@@ -120,6 +124,10 @@ begin
FControls[kfontTouchLayoutDesktop].CaptionLabel := lblTouchLayoutDesktopFont;
FControls[kfontTouchLayoutDesktop].NameCombo := fcbTouchLayoutDesktop;
FControls[kfontTouchLayoutDesktop].SizeEdit := editTouchLayoutDesktopSize;
+
+ FControls[kfontDisplayMap].CaptionLabel := lblDisplayMap;
+ FControls[kfontDisplayMap].NameCombo := fcbDisplayMap;
+ FControls[kfontDisplayMap].SizeEdit := editDisplayMapSize;
end;
function TfrmKeyboardFonts.GetFontInfo(Index: TKeyboardFont): TKeyboardFontInfo;
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas
index 863b737dc0..10e9dcdd3c 100644
--- a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas
@@ -200,7 +200,7 @@ end;
function TkmnProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; // I4409
var
- FCompiledName: string;
+ FCompiledName, OSKFont: string;
editor: TfrmTikeEditor;
wizard: TfrmKeymanWizard;
i: TKeyboardFont;
@@ -250,7 +250,7 @@ begin
// We register all fonts that are used by the layout,
// but just once for each reference!
- for i := kfontChar to kfontTouchLayoutDesktop do
+ for i := kfontChar to kfontDisplayMap do
begin
Found := False;
for j := kfontChar to TKeyboardFont(Ord(i)-1) do
@@ -265,13 +265,18 @@ begin
if TServerUI.VerifyServerRunning then
begin
+ if Wizard.FontInfo[kfontDisplayMap].Enabled then
+ begin
+ OSKFont := Wizard.FontInfo[kfontDisplayMap].Name
+ end
+ else
+ OSKFont := Wizard.FontInfo[kfontOSK].Name;
+
TServerDebugAPI.RegisterKeyboard(
FCompiledName,
ProjectFile.FileVersion,
- // We only need to specify the char + osk fonts here
- // as the others are referenced in the touch layout definition directly
Wizard.FontInfo[kfontChar].Name,
- Wizard.FontInfo[kfontOSK].Name
+ OSKFont
);
wizard.NotifyStartedWebDebug; // I4021
diff --git a/developer/src/tike/xml/layoutbuilder/builder.js b/developer/src/tike/xml/layoutbuilder/builder.js
index 3f30b1061c..8915308d6f 100644
--- a/developer/src/tike/xml/layoutbuilder/builder.js
+++ b/developer/src/tike/xml/layoutbuilder/builder.js
@@ -348,6 +348,13 @@ $(function() {
return builder.renameSpecialKey(hint);
}
+ this.escapeFontName = function(fontName) {
+ if(!fontName) {
+ return "";
+ }
+ return JSON.stringify(fontName);
+ }
+
this.prepareLayer = function () {
var layer = KVKL[builder.lastPlatform].layer[builder.lastLayerIndex];
@@ -380,11 +387,11 @@ $(function() {
}
$('#kbd,#sub-key-groups')
- .css('font-family', KVKL[builder.lastPlatform].font)
+ .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font))
.css('font-size', KVKL[builder.lastPlatform].fontsize || "1em");
$('#inpKeyCap,#inpSubKeyCap')
- .css('font-family', KVKL[builder.lastPlatform].font)
+ .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font))
.css('font-size', '18pt');
$('#kbd div').remove();
@@ -421,7 +428,7 @@ $(function() {
.css('height', (100 * this.yscale) + 'px')
.css('margin-top', (builder.keyMargin * this.yscale) + 'px')
.css('margin-left', p + 'px')
- .css('font-family', key.font)
+ .css('font-family', this.escapeFontName(key.font))
.css('font-size', key.fontsize);
if(this.specialCharacters[text])
diff --git a/developer/src/tike/xml/layoutbuilder/prepare-key.js b/developer/src/tike/xml/layoutbuilder/prepare-key.js
index 2b7819f7ed..46c5e29839 100644
--- a/developer/src/tike/xml/layoutbuilder/prepare-key.js
+++ b/developer/src/tike/xml/layoutbuilder/prepare-key.js
@@ -140,7 +140,7 @@ $(function() {
.data('direction', key.direction) // used only by flicks
.data('default', key.default) // used only by longpress
.css('width', (100 * 0.7) + 'px')
- .css('font-family', key.font)
+ .css('font-family',builder.escapeFontName(key.font))
.css('font-size', key.fontsize);
if(builder.specialCharacters[text]) {
diff --git a/ios/docs/engine/guides/in-app/index.md b/ios/docs/engine/guides/in-app/index.md
index 3dfebbf82d..14ba8ba4c5 100644
--- a/ios/docs/engine/guides/in-app/index.md
+++ b/ios/docs/engine/guides/in-app/index.md
@@ -1,5 +1,5 @@
---
-title: Guide: build an in-app keyboard
+title: Guide: build an in-app keyboard for iPhone and iPad with Keyman Engine
---
Keyman Engine for iPhone and iPad allows you to use any Keyman touch
diff --git a/ios/docs/engine/guides/system-keyboard/index.md b/ios/docs/engine/guides/system-keyboard/index.md
index ea652360b2..4a533f08d8 100644
--- a/ios/docs/engine/guides/system-keyboard/index.md
+++ b/ios/docs/engine/guides/system-keyboard/index.md
@@ -1,5 +1,5 @@
---
-title: Guide: build a system keyboard app
+title: Guide: build a system keyboard app on iPhone and iPad with Keyman Engine
---
In [part 1](../in-app/) of this series, we looked at the steps involved
diff --git a/ios/docs/help/about/history.md b/ios/docs/help/about/history.md
index 17905b7e5a..60aba2b0f3 100644
--- a/ios/docs/help/about/history.md
+++ b/ios/docs/help/about/history.md
@@ -1,5 +1,5 @@
---
-title: Version History
+title: Version History of Keyman for iPhone and iPad
---
A frequently-updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website.
diff --git a/ios/docs/help/about/index.md b/ios/docs/help/about/index.md
index 71ead99252..d37b9ed978 100644
--- a/ios/docs/help/about/index.md
+++ b/ios/docs/help/about/index.md
@@ -1,5 +1,5 @@
---
-title: About Keyman
+title: About Keyman for iPhone and iPad
---
* [Welcome to Keyman](welcome)
diff --git a/ios/docs/help/about/welcome.md b/ios/docs/help/about/welcome.md
index deeb156a54..05246c7ca4 100644
--- a/ios/docs/help/about/welcome.md
+++ b/ios/docs/help/about/welcome.md
@@ -1,5 +1,5 @@
---
-title: Welcome to Keyman
+title: Welcome to Keyman for iPhone and iPad
---
Thank you for installing Keyman for iPhone and iPad. Whether you're a new or returning user,
diff --git a/ios/docs/help/about/whatsnew.md b/ios/docs/help/about/whatsnew.md
index b744839398..aa4cf6b333 100644
--- a/ios/docs/help/about/whatsnew.md
+++ b/ios/docs/help/about/whatsnew.md
@@ -1,5 +1,5 @@
---
-title: What's New
+title: What's New in Keyman 18.0 for iPhone and iPad
---
Here are some of the new features we have added to Keyman for iPhone and iPad 18.0:
diff --git a/ios/docs/help/basic/index.md b/ios/docs/help/basic/index.md
index b67688e03c..e93f77f3aa 100644
--- a/ios/docs/help/basic/index.md
+++ b/ios/docs/help/basic/index.md
@@ -1,5 +1,5 @@
---
-title: Basic Help
+title: Keyman for iPhone and iPad Basic Help
---
* [Using the Banner on the Keyboard](using-the-banner)
diff --git a/ios/docs/help/context/gestures.md b/ios/docs/help/context/gestures.md
new file mode 100644
index 0000000000..ab15c3203d
--- /dev/null
+++ b/ios/docs/help/context/gestures.md
@@ -0,0 +1,15 @@
+---
+title: Gesture Hints and Tips - Keyman for iPhone and iPad
+---
+
+## Using Gestures on the Keyboard Keys
+There are now several gestures available to interact with some keys on the keyboard. Keys which have gestures available will normally show them as a hint in the top right of the key:
+
+
+
+* **Long press**: Press and hold a key, and after a moment a submenu will appear. Slide the finger to the submenu to select a key. Release the finger, and the highlighted key from the submenu will be output.
+
+* **Flick**: Press and hold a key, and then slide the finger in various directions on a key to reach alternate outputs. The key will animate to show the expected output when the finger gets released. A common flick is to slide down to reach numerals on the top row of the keyboard.
+
+ An up-flick on keys that support longpresses but do not have a defined up-flick will skip the wait and immediately display their longpress submenu.
+* **Multitap**: Some keys can be pressed repeatedly and rapidly to reach alternate outputs. Tapping rapidly twice on Shift will activate Caps Lock on many keyboards.
\ No newline at end of file
diff --git a/ios/docs/help/context/index.md b/ios/docs/help/context/index.md
index 99e41dd65c..90cfbfd5f6 100644
--- a/ios/docs/help/context/index.md
+++ b/ios/docs/help/context/index.md
@@ -2,6 +2,8 @@
title: Keyman for iPhone and iPad Help
---
+[Keyboard gestures](gestures)
+
| | Keyboard Keys
(Keyboard Present) |
|---|---|
|  | Install or select another language/keyboard |
diff --git a/ios/docs/help/index.md b/ios/docs/help/index.md
index c71c703e72..994bbbda9e 100644
--- a/ios/docs/help/index.md
+++ b/ios/docs/help/index.md
@@ -18,6 +18,7 @@ title: Keyman for iPhone and iPad 18.0 Help
* [Using the Banner on the Keyboard](basic/using-the-banner)
* [Switching Between Keyboards](basic/switching-between-keyboards)
* [Using a Keyboard](basic/keyboard-usage)
+* [Keyboard Gestures](context/gestures)
* [Using the Menu](context/)
* [Using the Settings Screen](basic/config/)
* [Language Settings](basic/language-settings)
diff --git a/ios/docs/help/ios_images/gesture-hint.png b/ios/docs/help/ios_images/gesture-hint.png
new file mode 100644
index 0000000000..155ba67418
Binary files /dev/null and b/ios/docs/help/ios_images/gesture-hint.png differ
diff --git a/ios/docs/help/start/index.md b/ios/docs/help/start/index.md
index f48fc3f185..d7bb7ad72e 100644
--- a/ios/docs/help/start/index.md
+++ b/ios/docs/help/start/index.md
@@ -1,5 +1,5 @@
---
-title: Getting Started
+title: Getting Started with Keyman for iPhone and iPad
---
* [How To - Adding a Keyboard](searching-for-keyboards)
diff --git a/ios/docs/help/troubleshooting/index.md b/ios/docs/help/troubleshooting/index.md
index 2f31716c95..1509c74ee6 100644
--- a/ios/docs/help/troubleshooting/index.md
+++ b/ios/docs/help/troubleshooting/index.md
@@ -1,5 +1,5 @@
---
-title: Troubleshooting
+title: Keyman for iPhone and iPad Troubleshooting
---
* [How To - Installing fonts](installing-fonts)
diff --git a/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings
index e94c1e7da6..756eb0a49b 100644
--- a/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings
+++ b/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings
@@ -226,6 +226,24 @@
/* Text showing name of spacebar caption - language + keyboard */
"menu-settings-spacebar-item-languageKeyboard" = "Idioma y teclado";
+/* Label for the "Adjust Keyboard Height" item on the main settings screen */
+"menu-settings-adjust-keyboard-height" = "Ajustar altura del teclado";
+
+/* Title for the "Adjust Keyboard Height" settings child screen */
+"adjust-keyboard-height-title" = "Ajustar teclado";
+
+/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */
+"button-label-reset-default-keyboard-height" = "Restablecer altura de teclado predeterminada";
+
+/* Instruction text to drag keyboard to resize */
+"keyboard-drag-instructions" = "Arrastre el control de flecha para ajustar la altura del teclado.";
+
+/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */
+"portrait-keyboard-rotate-instructions" = "Gire el dispositivo para pasar al modo horizontal.";
+
+/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */
+"landscape-keyboard-rotate-instructions" = "Gire el dispositivo para pasar al modo vertical.";
+
/* Short text for notification: download failure for keyboard */
"notification-download-failure-keyboard" = "Error al descargar el teclado";
diff --git a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings
index 88dea57f70..98d3031a04 100644
--- a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings
+++ b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings
@@ -226,6 +226,24 @@
/* Text showing name of spacebar caption - language + keyboard */
"menu-settings-spacebar-item-languageKeyboard" = "Idioma e teclado";
+/* Label for the "Adjust Keyboard Height" item on the main settings screen */
+"menu-settings-adjust-keyboard-height" = "Adjust Keyboard Height";
+
+/* Title for the "Adjust Keyboard Height" settings child screen */
+"adjust-keyboard-height-title" = "Adjust Keyboard";
+
+/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */
+"button-label-reset-default-keyboard-height" = "Reset to Default Keyboard Height";
+
+/* Instruction text to drag keyboard to resize */
+"keyboard-drag-instructions" = "Drag arrow control to adjust keyboard height.";
+
+/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */
+"portrait-keyboard-rotate-instructions" = "Rotate device to adjust for landscape.";
+
+/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */
+"landscape-keyboard-rotate-instructions" = "Rotate device to adjust for portrait.";
+
/* Short text for notification: download failure for keyboard */
"notification-download-failure-keyboard" = "Download do teclado falhou";
diff --git a/linux/debian/changelog b/linux/debian/changelog
index 3a2dae55f3..99ce27f238 100644
--- a/linux/debian/changelog
+++ b/linux/debian/changelog
@@ -1,3 +1,15 @@
+keyman (17.0.334-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Eberhard Beilharz Tue, 28 Jan 2025 16:52:06 +0100
+
+keyman (17.0.333-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Eberhard Beilharz Thu, 16 Jan 2025 16:29:44 +0100
+
keyman (17.0.332-1) unstable; urgency=medium
* set environment variable for rendering of downloads dialog (#12617)
diff --git a/linux/docs/help/about/history.md b/linux/docs/help/about/history.md
index 6343e0313e..5eb733255f 100644
--- a/linux/docs/help/about/history.md
+++ b/linux/docs/help/about/history.md
@@ -1,5 +1,5 @@
---
-title: Version History
+title: Version History of Keyman for Linux
---
A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history) on the Keyman Help website.
diff --git a/linux/docs/help/about/index.md b/linux/docs/help/about/index.md
index 5064ed6a09..0bf3142c40 100644
--- a/linux/docs/help/about/index.md
+++ b/linux/docs/help/about/index.md
@@ -1,5 +1,5 @@
---
-title: About Keyman
+title: About Keyman for Linux
---
* [Welcome to Keyman](welcome)
diff --git a/linux/docs/help/about/welcome.md b/linux/docs/help/about/welcome.md
index 6dc3dd4265..eacc8c449c 100644
--- a/linux/docs/help/about/welcome.md
+++ b/linux/docs/help/about/welcome.md
@@ -1,5 +1,5 @@
---
-title: Welcome to Keyman
+title: Welcome to Keyman for Linux
---
Thank you for installing Keyman for Linux. Whether you're a new or returning user, we believe you will appreciate the significant enhancements in this latest version of Keyman for Linux.
diff --git a/linux/docs/help/about/whatsnew.md b/linux/docs/help/about/whatsnew.md
index 62228c5e20..4a4d5a30e1 100644
--- a/linux/docs/help/about/whatsnew.md
+++ b/linux/docs/help/about/whatsnew.md
@@ -1,5 +1,5 @@
---
-title: What's New
+title: What's New in Keyman 18.0 for Linux
---
Here are some of the new features we have added to Keyman for Linux 18.0:
diff --git a/linux/docs/help/common/index.md b/linux/docs/help/common/index.md
index e1bbbdc225..b2d606b38d 100644
--- a/linux/docs/help/common/index.md
+++ b/linux/docs/help/common/index.md
@@ -1,5 +1,5 @@
---
-title: Common Questions
+title: Keyman for Linux Common Questions
---
See also [Knowledge Base articles](https://help.keyman.com/knowledge-base/).
diff --git a/linux/docs/help/reference/index.md b/linux/docs/help/reference/index.md
index 7b8f704018..4553779096 100644
--- a/linux/docs/help/reference/index.md
+++ b/linux/docs/help/reference/index.md
@@ -1,8 +1,8 @@
---
-title: Keyman 17.0 for Linux Reference
+title: Keyman 18.0 for Linux Reference
---
-## Keyman 17.0 for Linux Overview
+## Keyman 18.0 for Linux Overview
Keyman for Linux makes it possible for you to type in over 2,000 languages on Linux.
diff --git a/linux/docs/help/start/index.md b/linux/docs/help/start/index.md
index b3aad00bd5..530b67016d 100644
--- a/linux/docs/help/start/index.md
+++ b/linux/docs/help/start/index.md
@@ -1,5 +1,5 @@
---
-title: Getting Started
+title: Getting Started with Keyman for Linux
---
* [How To - Installing Keyboards with Keyman for Linux](installing-keyboard)
diff --git a/linux/ibus-keyman/meson.build b/linux/ibus-keyman/meson.build
index 98281849e8..e94c90285b 100644
--- a/linux/ibus-keyman/meson.build
+++ b/linux/ibus-keyman/meson.build
@@ -18,9 +18,17 @@ conf = configuration_data()
ibus = dependency('ibus-1.0', version: '>= 1.2.0')
gtk = dependency('gtk+-3.0', version: '>= 2.4')
json_glib = dependency('json-glib-1.0', version: '>= 1.0')
-systemd = dependency('libsystemd')
icu = dependency('icu-i18n')
+systemd = dependency('libsystemd', 'libelogind', required: false)
+if systemd.found()
+ conf.set('DBUS_IMPLEMENTATION', 'SYSTEMD')
+else
+ # Gentoo can use libelogind or basu
+ systemd = dependency('basu', required: true)
+ conf.set('DBUS_IMPLEMENTATION', 'BASU')
+endif
+
core_dir = meson.current_source_dir() / '../../core'
common_dir = meson.current_source_dir() / '../../common'
diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
index 635c3ff22d..50dbccba8d 100644
--- a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
+++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
@@ -1,5 +1,10 @@
+#include "config.h"
#include
+#if DBUS_IMPLEMENTATION == SYSTEMD
#include
+#else
+#include
+#endif
#include "KeymanSystemServiceClient.h"
#define KEYMAN_BUS_NAME "com.keyman.SystemService1"
diff --git a/linux/ibus-keyman/tests/KmDbusTestServer.cpp b/linux/ibus-keyman/tests/KmDbusTestServer.cpp
index e64a9354d8..fda2a12c00 100644
--- a/linux/ibus-keyman/tests/KmDbusTestServer.cpp
+++ b/linux/ibus-keyman/tests/KmDbusTestServer.cpp
@@ -1,9 +1,14 @@
// DBus test server. The server will start and listen on a non-standard DBus.
// It runs until the DBus Exit method gets called.
+#include "config.h"
#include
#include
#include
+#if DBUS_IMPLEMENTATION == SYSTEMD
#include
+#else
+#include
+#endif
#ifndef KEYMAN_TEST_SERVICE_PATH
#warning KEYMAN_TEST_SERVICE_PATH is undefined
@@ -73,7 +78,10 @@ KmDbusTestServer::~KmDbusTestServer()
{
if (bus) sd_bus_release_name(bus, KEYMAN_TESTSVC_BUS_NAME);
if (slot) sd_bus_slot_unref(slot);
- if (bus) sd_bus_close_unref(bus);
+ if (bus) {
+ sd_bus_close(bus);
+ sd_bus_unref(bus);
+ }
g_test_dbus_down(dbus);
g_object_unref(dbus);
diff --git a/linux/ibus-keyman/tests/StopTestServer.cpp b/linux/ibus-keyman/tests/StopTestServer.cpp
index 97ce5bfbe7..2790e5b30e 100644
--- a/linux/ibus-keyman/tests/StopTestServer.cpp
+++ b/linux/ibus-keyman/tests/StopTestServer.cpp
@@ -1,8 +1,13 @@
// Call the Exit method on the km-dbus-test-server. Remember to source
// `/tmp/km-test-server.env` prior to running stop-test-server in order
// to run on our non-standard DBus.
+#include "config.h"
#include
+#if DBUS_IMPLEMENTATION == SYSTEMD
#include
+#else
+#include
+#endif
using namespace std;
diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build
index a1c51a35ae..5ae10ae211 100644
--- a/linux/ibus-keyman/tests/meson.build
+++ b/linux/ibus-keyman/tests/meson.build
@@ -40,6 +40,7 @@ dbus_test_server = executable(
'km-dbus-test-server',
'KmDbusTestServer.cpp',
dependencies: dbus_deps,
+ include_directories: ['..'],
cpp_args: [
'-DKEYMAN_TEST_SERVICE_PATH="' + system_service_dir + '"'
]
@@ -49,6 +50,7 @@ stop_test_server = executable(
'stop-test-server',
'StopTestServer.cpp',
dependencies: dbus_deps,
+ include_directories: ['..'],
)
env_file = '/tmp/env.txt'
diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
index a32fef8689..940c5ad310 100755
--- a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
+++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
@@ -251,7 +251,7 @@ function _setup_ibus() {
#shellcheck disable=SC2086
ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log
PID=$(pgrep -f "${TEMP_DATA_DIR}/test-ibus")
- if [[ "${STANDALONE}" == "--standalone" ]]; then
+ if [[ "${STANDALONE}" == "--standalone" ]] && [[ ! "${DOCKER_RUNNING:-false}" ]]; then
# manual test run
echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}"
else
diff --git a/linux/keyman-system-service/meson.build b/linux/keyman-system-service/meson.build
index 596ba48bcd..c8ff6994aa 100644
--- a/linux/keyman-system-service/meson.build
+++ b/linux/keyman-system-service/meson.build
@@ -3,8 +3,21 @@ project('keyman-system-service', 'c', 'cpp',
license: 'GPL-2+',
meson_version: '>=1.0')
+conf = configuration_data()
+
evdev = dependency('libevdev', version: '>= 1.9')
-systemd = dependency('libsystemd')
+
+systemd = dependency('libsystemd', 'libelogind', required: false)
+if systemd.found()
+ conf.set('DBUS_IMPLEMENTATION', 'SYSTEMD')
+else
+ # Gentoo can use libelogind or basu
+ systemd = dependency('basu', required: true)
+ conf.set('DBUS_IMPLEMENTATION', 'BASU')
+endif
+
+configure_file(output : 'config.h',
+ configuration : conf)
subdir('resources')
subdir('src')
diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu
new file mode 100644
index 0000000000..67dd16eb5a
--- /dev/null
+++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu
@@ -0,0 +1,7 @@
+# /usr/share/dbus-1/system-services/com.keyman.SystemService1.service
+# This version used with elogind and basu (e.g. on Gentoo)
+
+[D-BUS Service]
+Name=com.keyman.SystemService1
+Exec=/usr/libexec/systemd-keyman.service
+User=root
diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd
similarity index 78%
rename from linux/keyman-system-service/resources/com.keyman.SystemService1.service
rename to linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd
index 3966fb1bbc..65acc4067a 100644
--- a/linux/keyman-system-service/resources/com.keyman.SystemService1.service
+++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd
@@ -1,4 +1,5 @@
# /usr/share/dbus-1/system-services/com.keyman.SystemService1.service
+# This version used with systemd (e.g. on Debian)
[D-BUS Service]
Name=com.keyman.SystemService1
diff --git a/linux/keyman-system-service/resources/meson.build.in b/linux/keyman-system-service/resources/meson.build.in
index 2743889f30..3ddafdaaf3 100644
--- a/linux/keyman-system-service/resources/meson.build.in
+++ b/linux/keyman-system-service/resources/meson.build.in
@@ -1,4 +1,11 @@
# This file will be appended to meson.build by build.sh
install_data('com.keyman.SystemService1.conf', install_dir: get_option('datadir') / 'dbus-1/system.d/')
-install_data('com.keyman.SystemService1.service', install_dir: get_option('datadir') / 'dbus-1/system-services/')
+
+if systemd.name() == 'libsystemd'
+ install_data('com.keyman.SystemService1.service.systemd', install_dir: get_option('datadir') / 'dbus-1/system-services/', rename: ['com.keyman.SystemService1.service'])
+else
+ # libelogind or basu
+ install_data('com.keyman.SystemService1.service.basu', install_dir: get_option('datadir') / 'dbus-1/system-services/')
+endif
+
install_data('systemd-keyman.service', install_dir: get_option('prefix') / 'lib/systemd/system/')
diff --git a/linux/keyman-system-service/src/KeymanSystemService.cpp b/linux/keyman-system-service/src/KeymanSystemService.cpp
index deceafd0c3..03d7add533 100644
--- a/linux/keyman-system-service/src/KeymanSystemService.cpp
+++ b/linux/keyman-system-service/src/KeymanSystemService.cpp
@@ -2,6 +2,7 @@
// based on the sd-bus library, see
// https://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html
+#include "config.h"
#include
#include
#include
@@ -11,7 +12,11 @@
#include
#include
#include
+#if DBUS_IMPLEMENTATION == SYSTEMD
#include
+#else
+#include
+#endif
#include "KeymanSystemService.h"
#include "KeyboardDevice.h"
diff --git a/linux/keyman-system-service/src/KeymanSystemService.h b/linux/keyman-system-service/src/KeymanSystemService.h
index 2686bfb99f..86637f27a9 100644
--- a/linux/keyman-system-service/src/KeymanSystemService.h
+++ b/linux/keyman-system-service/src/KeymanSystemService.h
@@ -1,8 +1,13 @@
#ifndef __KEYMANSYSTEMSERVICE_H__
#define __KEYMANSYSTEMSERVICE_H__
+#include "config.h"
#include
+#if DBUS_IMPLEMENTATION == SYSTEMD
#include
+#else
+#include
+#endif
#include "KeyboardDevice.h"
using namespace std;
diff --git a/linux/keyman-system-service/src/meson.build b/linux/keyman-system-service/src/meson.build
index 51820ab9fa..3c3dd27fa6 100644
--- a/linux/keyman-system-service/src/meson.build
+++ b/linux/keyman-system-service/src/meson.build
@@ -10,6 +10,7 @@ exe = executable(
'keyman-system-service',
sources: [service_files],
dependencies: deps,
+ include_directories: ['..'],
install: true,
install_dir: get_option('libexecdir'),
)
diff --git a/linux/keyman-system-service/tests/meson.build b/linux/keyman-system-service/tests/meson.build
index f90e12d16a..749e098591 100644
--- a/linux/keyman-system-service/tests/meson.build
+++ b/linux/keyman-system-service/tests/meson.build
@@ -17,7 +17,7 @@ exe = executable(
c_args: test_c_args,
cpp_args: test_c_args,
dependencies: deps,
- include_directories: [ '../src' ]
+ include_directories: [ '..', '../src' ]
)
# we currently don't have any unit tests for keyman-system-service.
diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh
index 6e04e027d8..1b8f0dee87 100755
--- a/linux/scripts/dist.sh
+++ b/linux/scripts/dist.sh
@@ -17,7 +17,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
BASEDIR=$(pwd)
-if [[ ! -z ${1+x} ]] && [ "$1" == "origdist" ]; then
+if [[ ! -z ${1+x} ]] && [[ "$1" == "origdist" ]]; then
create_origdist=1
shift
fi
@@ -30,62 +30,69 @@ cp -a debian ../
cd ..
echo "3.0 (native)" > debian/source/format
dch keyman --newversion "${VERSION}" --force-bad-version --nomultimaint
-dpkg-source --tar-ignore=*~ \
- --tar-ignore=./.git \
- --tar-ignore=./.gitattributes \
- --tar-ignore=./.gitignore \
- --tar-ignore=experiments \
- --tar-ignore=debian \
- --tar-ignore=./.github \
- --tar-ignore=./.vscode \
- --tar-ignore=./android \
- --tar-ignore=./.devcontainer \
- --tar-ignore=./artifacts \
- \
- --tar-ignore=./common/models \
- --tar-ignore=./common/resources \
- --tar-ignore=./common/schemas \
- --tar-ignore=./common/test/keyboards/build.* \
- --tar-ignore=./common/test/resources \
- --tar-ignore=./common/web \
- --tar-ignore=./common/windows \
- \
- --tar-ignore=./core/build \
- --tar-ignore=./developer \
- --tar-ignore=./docs \
- --tar-ignore=./ios \
- --tar-ignore=./linux/keyman-config/keyman_config/version.py \
- --tar-ignore=./linux/keyman-config/buildtools/build-langtags.py \
- --tar-ignore=__pycache__ \
- --tar-ignore=./linux/docs/help \
- --tar-ignore=./mac \
- --tar-ignore=node_modules \
- --tar-ignore=./oem \
- --tar-ignore=./linux/build \
- --tar-ignore=./linux/builddebs \
- --tar-ignore=./linux/ibus-keyman/build \
- --tar-ignore=./linux/keyman-system-service/build \
- --tar-ignore=./resources/devbox \
- --tar-ignore=./resources/environment.sh \
- --tar-ignore=./resources/git-hooks \
- --tar-ignore=./resources/scopes \
- --tar-ignore=./resources/build/*.lua \
- --tar-ignore=./resources/build/jq* \
- --tar-ignore=./results \
- --tar-ignore=./tmp \
- --tar-ignore=./web \
- --tar-ignore=./windows \
- --tar-ignore=keyman_1* \
- --tar-ignore=dist \
- --tar-ignore=VERSION \
- \
- -Zgzip -b .
+
+# Create the tarball
+# Note: the files end up in subdirectories under `keyman`, so we can
+# include that when matching files and directories to ignore.
+dpkg-source \
+ --tar-ignore=*~ \
+ --tar-ignore=.git \
+ --tar-ignore=.gitattributes \
+ --tar-ignore=.gitignore \
+ --tar-ignore=experiments \
+ --tar-ignore=debian \
+ --tar-ignore=.github \
+ --tar-ignore=.vscode \
+ --tar-ignore=.devcontainer \
+ --tar-ignore=__pycache__ \
+ --tar-ignore=node_modules \
+ --tar-ignore=keyman_1* \
+ --tar-ignore=dist \
+ --tar-ignore=VERSION \
+ \
+ --tar-ignore=keyman/android \
+ --tar-ignore=keyman/artifacts \
+ \
+ --tar-ignore=keyman/common/mac \
+ --tar-ignore=keyman/common/models \
+ --tar-ignore=keyman/common/resources \
+ --tar-ignore=keyman/common/schemas \
+ --tar-ignore=keyman/common/test/keyboards/build.* \
+ --tar-ignore=keyman/common/test/resources \
+ --tar-ignore=keyman/common/web \
+ --tar-ignore=keyman/common/windows \
+ \
+ --tar-ignore=keyman/core/build \
+ --tar-ignore=keyman/developer \
+ --tar-ignore=keyman/docs \
+ --tar-ignore=keyman/ios \
+ --tar-ignore=keyman/linux/build \
+ --tar-ignore=keyman/linux/builddebs \
+ --tar-ignore=keyman/linux/docs/help \
+ --tar-ignore=keyman/linux/ibus-keyman/build \
+ --tar-ignore=keyman/linux/keyman-config/keyman_config/version.py \
+ --tar-ignore=keyman/linux/keyman-config/buildtools/build-langtags.py \
+ --tar-ignore=keyman/linux/keyman-system-service/build \
+ --tar-ignore=keyman/mac \
+ --tar-ignore=keyman/oem \
+ --tar-ignore=keyman/resources/devbox \
+ --tar-ignore=keyman/resources/environment.sh \
+ --tar-ignore=keyman/resources/git-hooks \
+ --tar-ignore=keyman/resources/scopes \
+ --tar-ignore=keyman/resources/build/*.lua \
+ --tar-ignore=keyman/resources/build/jq* \
+ --tar-ignore=keyman/results \
+ --tar-ignore=keyman/tmp \
+ --tar-ignore=keyman/web \
+ --tar-ignore=keyman/windows \
+ \
+ -Zgzip -b .
mv ../keyman_"${VERSION}".tar.gz linux/dist/keyman-"${VERSION}".tar.gz
echo "3.0 (quilt)" > debian/source/format
-cd "$BASEDIR"
+cd "${BASEDIR}"
# create orig.tar.gz
-if [ ! -z "${create_origdist+x}" ]; then
+if [[ ! -z "${create_origdist+x}" ]]; then
cd dist
pkgvers="keyman-$VERSION"
tar xfz keyman-"${VERSION}".tar.gz
diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
index 483998a16e..87b005309c 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
+++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
@@ -35,7 +35,9 @@
29BE9D872CA3C21900B67DE7 /* KMModifierMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 29BE9D862CA3C21900B67DE7 /* KMModifierMapping.m */; };
29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; };
29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; };
+ 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; };
29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */ = {isa = PBXBuildFile; fileRef = 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */; };
+ 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; };
37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; };
37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; };
37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; };
@@ -275,6 +277,8 @@
29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; };
29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = ""; };
29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; };
+ 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSentryHelper.h; sourceTree = ""; };
+ 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMSentryHelper.m; sourceTree = ""; };
29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = ""; };
29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = ""; };
29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = ""; };
@@ -433,6 +437,17 @@
path = Privacy;
sourceTree = "";
};
+ 29DBBF642D34FFE000D8E33C /* Logging */ = {
+ isa = PBXGroup;
+ children = (
+ 29B4A0D42BF7675A00682049 /* KMLogs.h */,
+ 29B4A0D32BF7675A00682049 /* KMLogs.m */,
+ 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */,
+ 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */,
+ );
+ path = Logging;
+ sourceTree = "";
+ };
2CDC487CDFFBB26D621D73BD /* Pods */ = {
isa = PBXGroup;
children = (
@@ -564,8 +579,6 @@
989C9C0A1A7876DE00A20425 /* Keyman4MacIM */ = {
isa = PBXGroup;
children = (
- 29B4A0D42BF7675A00682049 /* KMLogs.h */,
- 29B4A0D32BF7675A00682049 /* KMLogs.m */,
299ABD6F29ECE75B00AA5948 /* KeySender.m */,
299ABD7029ECE75B00AA5948 /* KeySender.h */,
D861B03D2C5747F70003675E /* KMSettingsRepository.h */,
@@ -575,6 +588,7 @@
297A501128DF4D360074EB1B /* Privacy */,
98FE105B1B4DE86300525F54 /* Categories */,
98D6DA791A799EE700B09822 /* Frameworks */,
+ 29DBBF642D34FFE000D8E33C /* Logging */,
989C9C121A7876DE00A20425 /* Images.xcassets */,
98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */,
98BF92401BF01CBE0002126A /* KMAboutWindow */,
@@ -1000,6 +1014,7 @@
9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */,
989C9C111A7876DE00A20425 /* main.m in Sources */,
290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */,
+ 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */,
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */,
98BF924F1BF02DC20002126A /* KMBarView.m in Sources */,
E240F599202DED740000067D /* KMPackage.m in Sources */,
@@ -1038,6 +1053,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */,
29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */,
29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */,
2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */,
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
index 202be65ef5..2b06db4cad 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
@@ -10,6 +10,7 @@
#import "KMDownloadKBWindowController.h"
#import "KMDataRepository.h"
#import "KMLogs.h"
+#import "KMSentryHelper.h"
@interface KMConfigurationWindowController ()
@property (nonatomic, weak) IBOutlet NSTableView *tableView;
@@ -360,21 +361,27 @@
- (void)checkBoxAction:(id)sender {
NSButton *checkBox = (NSButton *)sender;
NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag];
+ NSString * kmxFileName = [kmxFilePath lastPathComponent];
NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath];
os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath);
if (checkBox.state == NSOnState) {
- os_log_debug([KMLogs uiLog], "Adding active keyboard: %{public}@", kmxFilePath);
+ os_log_debug([KMLogs uiLog], "enabling active keyboard: %{public}@", kmxFileName);
+ NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFileName];
+ [KMSentryHelper addUserBreadCrumb:@"config" message:message];
[self.activeKeyboards addObject:partialPath];
[self saveActiveKeyboards];
}
else if (checkBox.state == NSOffState) {
- os_log_debug([KMLogs uiLog], "Disabling active keyboard: %{public}@", kmxFilePath);
+ os_log_debug([KMLogs uiLog], "disabling active keyboard: %{public}@", kmxFileName);
+ NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFileName];
+ [KMSentryHelper addUserBreadCrumb:@"config" message:message];
[self.activeKeyboards removeObject:partialPath];
[self saveActiveKeyboards];
}
}
- (void)infoAction:(id)sender {
+ [KMSentryHelper addUserBreadCrumb:@"user" message:@"getting package information"];
NSButton *infoButton = (NSButton *)sender;
NSString *packagePath = [self packagePathAtIndex:infoButton.tag];
if (packagePath != nil) {
@@ -389,6 +396,7 @@
}
- (void)helpAction:(id)sender {
+ [KMSentryHelper addUserBreadCrumb:@"user" message:@"displaying help"];
NSButton *helpButton = (NSButton *)sender;
NSString *packagePath = [self packagePathAtIndex:helpButton.tag];
if (packagePath != nil) {
@@ -406,22 +414,31 @@
NSButton *deleteButton = (NSButton *)sender;
NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag];
NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil);
-
- if ([info objectForKey:@"HeaderTitle"] != nil)
- [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:@"HeaderTitle"]]];
- else
- [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:kKMKeyboardNameKey]]];
-
+ NSString *keyboardName = nil;
+
+ if ([info objectForKey:@"HeaderTitle"] != nil) {
+ keyboardName = [info objectForKey:@"HeaderTitle"];
+ } else {
+ keyboardName = [info objectForKey:kKMKeyboardNameKey];
+ }
+ [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
+ os_log_debug([KMLogs configLog], "entered removeAction for keyboardName: %{public}@", keyboardName);
+
[self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
if (returnCode == NSAlertFirstButtonReturn) {
os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed");
[self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]];
+
+ os_log_debug([KMLogs configLog], "removeAction for keyboardName: %{public}@", keyboardName);
+ [KMSentryHelper addUserBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]];
}
self.deleteAlertView = nil;
}];
}
- (IBAction)downloadAction:(id)sender {
+ [KMSentryHelper addUserBreadCrumb:@"user" message:@"download keyboard"];
+
if (self.AppDelegate.infoWindow_.window != nil)
[self.AppDelegate.infoWindow_ close];
@@ -453,7 +470,8 @@
- (void)installPackageFile:(NSString *)kmpFile {
// kmpFile could be a temp file (in fact, it always is!), so don't display the name.
os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile);
-
+ [KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"];
+
BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile];
if (!didUnzip) {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings
index b6bbd130a0..def43607c3 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings
@@ -17,7 +17,7 @@
"JOK-JV-n8w.title" = "Voltar";
/* Keyboards button text */
-"MPN-9N-wWc.label" = "Keyboards";
+"MPN-9N-wWc.label" = "Teclados";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Quando a opção de registro detalhada estiver ativada, o Keyman irá registrar ações que podem ajudar o suporte ao Keyman a diagnosticar um problema. O programa Console pode ser usado para ver um registro de mensagens do Keyman. Em Console, filtro para mostrar todas as mensagens do processo \"Keyman\". Este log pode ser exportado e enviado para o suporte a Keyman, se necessário.";
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
index e97be3e389..dd53b49622 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
@@ -13,7 +13,7 @@
#import "KMLogs.h"
#import "InputMethodKit/InputMethodKit.h"
#import "KMInputMethodLifecycle.h"
-
+#import "KMSentryHelper.h"
@implementation KMInputController
@@ -25,7 +25,7 @@ KMInputMethodEventHandler* _eventHandler;
- (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient
{
- os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]);
+ os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]);
self = [super initWithServer:server delegate:delegate client:inputClient];
if (self) {
@@ -84,7 +84,7 @@ KMInputMethodEventHandler* _eventHandler;
if (_eventHandler != nil) {
[_eventHandler deactivate];
}
- _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client];
+ _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client];
}
@@ -111,17 +111,21 @@ KMInputMethodEventHandler* _eventHandler;
NSInteger itag = mItem.tag;
os_log_debug([KMLogs uiLog], "Keyman menu clicked - tag: %lu", itag);
if (itag == CONFIG_MENUITEM_TAG) {
+ [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Configuration..."];
[self showConfigurationWindow:sender];
}
else if (itag == OSK_MENUITEM_TAG) {
+ [KMSentryHelper addUserBreadCrumb:@"menu" message:@"On-screen Keyboard"];
[KMSettingsRepository.shared writeShowOskOnActivate:YES];
os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES");
[self.appDelegate showOSK];
}
else if (itag == ABOUT_MENUITEM_TAG) {
+ [KMSentryHelper addUserBreadCrumb:@"menu" message:@"About"];
[self.appDelegate showAboutWindow];
}
else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) {
+ [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Selected Keyboard"];
[self.appDelegate selectKeyboardFromMenu:itag];
}
}
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
index fe06fa603a..eade3d6d2f 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
@@ -124,6 +124,7 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0;
- (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent;
- (KeymanVersionInfo)versionInfo;
- (void)registerConfigurationWindow:(NSWindowController *)window;
+- (BOOL)canForceSentryEvents;
@end
#endif /* KMInputMethodAppDelegate_h */
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index b938279197..0c4199959e 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -17,8 +17,17 @@
#import "KMPackageInfo.h"
#import "PrivacyConsent.h"
#import "KMLogs.h"
+#import "KMSentryHelper.h"
@import Sentry;
+#if TARGET_CPU_ARM64
+NSString *processorType = @"ARM";
+#elif TARGET_CPU_X86_64
+NSString *processorType = @"Intel";
+#else
+NSString *processorType = @"Unknown";
+#endif
+
NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";
@implementation NSString (VersionNumbers)
@@ -48,6 +57,7 @@ NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboard
@property (nonatomic, strong) KMPackageReader *packageReader;
@property BOOL receivedKeyDownFromOsk;
@property NSEventModifierFlags oskEventModifiers;
+@property (nonatomic, assign) BOOL sentryTestingEnabled;
@end
@implementation KMInputMethodAppDelegate
@@ -124,10 +134,12 @@ id _lastServerWithOSKShowing = nil;
- (void)inputMethodDeactivated:(NSNotification *)notification {
if ([self.oskWindow.window isVisible]) {
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"];
[self.oskWindow.window setIsVisible:NO];
} else {
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden");
}
+ [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
if (self.lowLevelEventTap) {
os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
@@ -136,16 +148,19 @@ id _lastServerWithOSKShowing = nil;
}
/**
- * When the input method is activated, show the OSK and enable the low-level event tap
+ * When the input method is activated, enable the low-level event tap and show the OSK
*/
- (void)inputMethodActivated:(NSNotification *)notification {
if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) {
os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap...");
CGEventTapEnable(self.lowLevelEventTap, YES);
}
+
+ os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false");
- if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) {
+ if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) {
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
[self showOSK];
}
}
@@ -181,7 +196,7 @@ id _lastServerWithOSKShowing = nil;
[self setDefaultKeymanMenuItems];
[self updateKeyboardMenuItems];
[self setPostLaunchKeymanSentryTags];
- // [SentrySDK captureMessage:@"Starting Keyman [test message]"];
+ self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError];
}
- (void)startSentry {
@@ -192,32 +207,47 @@ id _lastServerWithOSKShowing = nil;
options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522";
options.releaseName = releaseName;
options.environment = keymanVersionInfo.sentryEnvironment;
- //options.debug = YES;
}];
}
+/**
+ * Determine whether to force sentry events.
+ * When true, then typing certain characters will cause Sentry events or crashes to occur.
+ *
+ * To enable, set the flag in UserDefaults
+ * `defaults write keyman.inputmethod.Keyman KMForceSentryError 1`
+ * Also, use the SIL Euro Latin keyboard and type in the Stickies app
+ */
+- (BOOL)canForceSentryEvents {
+ NSString* const testKeyboardName = @"sil_euro_latin.kmx";
+ NSString* const testClientApplicationId = @"com.apple.Stickies";
+ BOOL canForce = NO;
+
+ if (self.sentryTestingEnabled) {
+ NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId];
+ NSString * kmxFileName = [self getKmxFileName];
+ canForce = [kmxFileName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
+ os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxFileName, applicationId, canForce?@"YES":@"NO");
+ }
+
+ return canForce;
+}
+
+- (NSString*) getKmxFileName {
+ return [[self.kmx filePath] lastPathComponent];
+}
+
- (void)setPostLaunchKeymanSentryTags {
NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
- NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"Yes":@"No";
-
- // assign custom keyboard tag in Sentry to initial keyboard
- [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
- [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"];
- [scope setTagValue:keyboardFileName forKey:@"keyboard"];
- }];
+ // assign custom keyboard tags in Sentry
+ [KMSentryHelper addKeyboardTag:keyboardFileName];
+ [KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]];
+ [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
+ [KMSentryHelper addArchitectureTag:processorType];
}
-#ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE
-- (BOOL)alertShowHelp:(NSAlert *)alert {
- os_log_error([KMLogs startupLog], "Sentry - KME: Got call to force crash from engine");
- [SentrySDK crash];
- os_log_error([KMLogs startupLog], "Sentry - KME: should not have gotten this far!");
- return NO;
-}
-#endif
-
- (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent {
[self processURL:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
@@ -317,6 +347,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
os_log_debug([KMLogs eventsLog], "*** kKeymanEventKeyCode = 0xFF");
} else {
if ([OSKView isOskKeyDownEvent:event]) {
+ [KMSentryHelper addInfoBreadCrumb:@"event" message:@"processing OSK-generated keydown event"];
NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event];
appDelegate.receivedKeyDownFromOsk = YES;
appDelegate.oskEventModifiers = oskEventModifiers;
@@ -406,22 +437,26 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
_kmx = kmx;
CoreKeyboardInfo *keyboardInfo = [self.kme loadKeyboardFromKmxFile:kmx];
- os_log_info([KMLogs keyboardLog], "setKmx loaded keyboard, keyboard info: %{public}@", keyboardInfo);
-
+ os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo);
+ [KMSentryHelper addInfoBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]];
+ [KMSentryHelper addKeyboardTag:[self getKmxFileName]];
+
_modifierMapping = [[KMModifierMapping alloc] init:keyboardInfo];
os_log_info([KMLogs keyboardLog], "modifierMapping bothOptionKeysGenerateRightAlt: %d", self.modifierMapping.bothOptionKeysGenerateRightAlt);
-
- // assign custom keyboard tag in Sentry to default keyboard
- [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
- [scope setTagValue:keyboardInfo.keyboardId forKey:@"keyboard"];
- }];
}
- (void)setKvk:(KVKFile *)kvk {
+ if (kvk != nil) {
+ os_log_info([KMLogs lifecycleLog], "*** setKvk, kvk.associatedKeyboard: %{public}@", kvk.associatedKeyboard);
+ } else {
+ os_log_info([KMLogs lifecycleLog], "*** setKvk, setting to nil");
+ }
_kvk = kvk;
- if (_oskWindow != nil)
+
+ if (_oskWindow != nil) {
[_oskWindow resetOSK];
+ }
}
- (void)setKeyboardName:(NSString *)keyboardName {
@@ -616,6 +651,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
if (!_activeKeyboards) {
os_log_debug([KMLogs dataLog], "initializing activeKeyboards");
_activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy];
+
+ [KMSentryHelper addActiveKeyboardCountTag:_activeKeyboards.count];
}
return _activeKeyboards;
@@ -626,6 +663,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
[KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards];
[self resetActiveKeyboards];
[self updateKeyboardMenuItems];
+ [KMSentryHelper addActiveKeyboardCountTag:self.activeKeyboards.count];
}
- (void)clearActiveKeyboards {
@@ -825,6 +863,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
[self setSelectedKeyboard:keyboardName inMenuItem:menuItem];
[self setSelectedKeyboard:keyboardName];
[self setContextBuffer:nil];
+
+ [KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]];
}
- (void) addKeyboardPlaceholderMenuItem {
@@ -926,8 +966,11 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
}
- (NSWindowController *)oskWindow {
- if (!_oskWindow)
+ if (!_oskWindow) {
_oskWindow = [[OSKWindowController alloc] initWithWindowNibName:@"OSKWindowController"];
+
+ os_log_debug([KMLogs oskLog], "Loaded oskWindow from Nib, isVisible: %{public}@ readShowOskOnActivate: %{public}@", [_oskWindow.window isVisible]?@"true":@"false", [KMSettingsRepository.shared readShowOskOnActivate]?@"true":@"false");
+ }
return _oskWindow;
}
@@ -948,6 +991,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
[[self.oskWindow window] makeKeyAndOrderFront:nil];
[[self.oskWindow window] setLevel:NSStatusWindowLevel];
[[self.oskWindow window] setTitle:self.oskWindowTitle];
+ [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
}
- (void)showAboutWindow {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index 2e38dd4bc3..d4ab67d10f 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
@@ -12,6 +12,7 @@
#import "TextApiCompliance.h"
#import "KMSettingsRepository.h"
#import "KMLogs.h"
+#import "KMSentryHelper.h"
@import Sentry;
@interface KMInputMethodEventHandler ()
@@ -22,6 +23,7 @@
@property (nonatomic, retain) NSString* clientApplicationId;
@property NSString *queuedText;
@property BOOL contextChanged;
+@property BOOL sentryTestingEnabled;
@end
@implementation KMInputMethodEventHandler
@@ -33,9 +35,6 @@ const NSTimeInterval kQueuedTextEventDelayInSeconds = 0.1;
CGKeyCode _keyCodeOfOriginalEvent;
CGEventSourceRef _sourceFromOriginalEvent = nil;
CGEventSourceRef _sourceForGeneratedEvent = nil;
-NSMutableString* _easterEggForSentry = nil;
-NSString* const kEasterEggText = @"Sentry force now";
-NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
// This is the public initializer.
- (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
@@ -44,24 +43,12 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
_generatedBackspaceCount = 0;
_lowLevelBackspaceCount = 0;
_queuedText = nil;
-
- BOOL forceSentryCrash = [KMSettingsRepository.shared readForceSentryError];
-
- // In Xcode, if Keyman is the active IM and the settings include the
- // forceSentryCrash flag and "English plus Spanish" is the current keyboard
- // and you type "Sentry force now", it will force a simulated crash to
- // test reporting to sentry.keyman.com
- if (forceSentryCrash && [clientAppId isEqual: @"com.apple.dt.Xcode"]) {
- os_log_debug([KMLogs testLog], "initWithClient, preparing to force Sentry crash.");
- _easterEggForSentry = [[NSMutableString alloc] init];
- }
- else
- _easterEggForSentry = nil;
-
- [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
- [scope setTagValue:clientAppId forKey:@"clientAppId"];
- }];
+ _apiCompliance = [[TextApiCompliance alloc]initWithClient:sender applicationId:clientAppId];
+ os_log_info([KMLogs lifecycleLog], "KMInputMethodEventHandler initWithClient, clientAppId: %{public}@", clientAppId);
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]];
+
+ [KMSentryHelper addClientAppIdTag:clientAppId];
return self;
}
@@ -110,9 +97,15 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
- (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender {
CoreKeyOutput *output = nil;
+ if ([self.appDelegate canForceSentryEvents]) {
+ if ([self forceSentryEvent:event]) {
+ return NO;
+ }
+ }
+
output = [self processEventWithKeymanEngine:event in:sender];
+
if (output == nil) {
- [self checkEventForSentryEasterEgg:event];
return NO;
}
@@ -139,28 +132,28 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
return coreKeyOutput;
}
-- (void)checkEventForSentryEasterEgg:(NSEvent *)event {
- if (_easterEggForSentry != nil) {
- NSString * kmxName = [[self.kme.kmx filePath] lastPathComponent];
- os_log_debug([KMLogs testLog], "Sentry - KMX name: %{public}@", kmxName);
- if ([kmxName isEqualToString:kEasterEggKmxName]) {
- NSUInteger len = [_easterEggForSentry length];
- os_log_debug([KMLogs testLog], "Sentry - Processing character(s): %{public}@", [event characters]);
- if ([[event characters] characterAtIndex:0] == [kEasterEggText characterAtIndex:len]) {
- NSString *characterToAdd = [kEasterEggText substringWithRange:NSMakeRange(len, 1)];
- os_log_debug([KMLogs testLog], "Sentry - Adding character to Easter Egg code string: %{public}@", characterToAdd);
- [_easterEggForSentry appendString:characterToAdd];
- if ([_easterEggForSentry isEqualToString:kEasterEggText]) {
- os_log_debug([KMLogs testLog], "Sentry - Forcing crash now");
- [SentrySDK crash];
- }
- }
- else if (len > 0) {
- os_log_debug([KMLogs testLog], "Sentry - Clearing Easter Egg code string.");
- [_easterEggForSentry setString:@""];
- }
- }
+/**
+ * When in the mode to test sentry, this method will force certain Sentry APIs to be called,
+ * such as forcing a crash or capturing a message, depending on the key being typed.
+ */
+- (BOOL)forceSentryEvent:(NSEvent *)event {
+ BOOL forcedEvent = YES;
+
+ NSString *sentryCommand = event.characters;
+ if ([sentryCommand isEqualToString:@"{"]) {
+ os_log_debug([KMLogs testLog], "forceSentryEvent: forcing crash now");
+ NSBeep();
+ [SentrySDK crash];
+ } else if ([sentryCommand isEqualToString:@"["]) {
+ os_log_debug([KMLogs testLog], "forceSentryEvent: forcing message now");
+ NSBeep();
+ [SentrySDK captureMessage:@"Forced test message"];
+ } else {
+ os_log_debug([KMLogs testLog], "forceSentryEvent: unrecognized command");
+ forcedEvent = NO;
}
+
+ return forcedEvent;
}
/**
@@ -174,7 +167,8 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
*/
- (void)handleBackspace:(NSEvent *)event {
os_log_debug([KMLogs eventsLog], "KMInputMethodEventHandler handleBackspace, event = %{public}@", event);
-
+ [KMSentryHelper addInfoBreadCrumb:@"user" message:@"handle backspace for non-compliant app"];
+
if (self.generatedBackspaceCount > 0) {
self.generatedBackspaceCount--;
self.lowLevelBackspaceCount++;
@@ -194,13 +188,10 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
//MARK: Core-related key processing
- (void)checkTextApiCompliance:(id)client {
- // If the TextApiCompliance object is nil or the app or the context has changed,
- // then create a new object for the current application and context.
+ // If the TextApiCompliance object is stale, create a new one for the current application and context.
// The text api compliance may vary from one text field to another of the same app.
- if ((self.apiCompliance == nil) ||
- (![self.apiCompliance.clientApplicationId isEqualTo:self.clientApplicationId]) ||
- self.contextChanged) {
+ if ([self textApiComplianceIsStale]) {
self.apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:self.clientApplicationId];
os_log_debug([KMLogs complianceLog], "KMInputMethodHandler initWithClient checkTextApiCompliance: %{public}@", _apiCompliance);
} else if (self.apiCompliance.isComplianceUncertain) {
@@ -209,6 +200,33 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
}
}
+- (BOOL)textApiComplianceIsStale {
+ BOOL stale = false;
+ NSString *complianceAppId = nil;
+ TextApiCompliance *currentApiCompliance = self.apiCompliance;
+
+ // test for three scenarios in which the api compliance is stale
+ if (currentApiCompliance == nil) { // if we have no previous api compliance object
+ stale = true;
+ } else { // if we have one but for a different client
+ complianceAppId = self.apiCompliance.clientApplicationId;
+ if ([complianceAppId isNotEqualTo:self.clientApplicationId]) {
+ stale = true;
+ } else {
+ stale = false;
+ }
+ }
+ if (self.contextChanged) { // if the context has changed
+ stale = true;
+ }
+
+ NSString *message = [NSString stringWithFormat:@"textApiComplianceIsStale = %@ for appId: %@, apiCompliance: %p, complianceAppId: %@", stale?@"true":@"false", self.clientApplicationId, currentApiCompliance, complianceAppId];
+ [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message];
+ os_log_debug([KMLogs complianceLog], "%{public}@", message);
+
+ return stale;
+}
+
- (void) handleContextChangedByLowLevelEvent {
if (self.appDelegate.contextChangedByLowLevelEvent) {
if (!self.contextChanged) {
@@ -230,6 +248,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
[self handleContextChangedByLowLevelEvent];
if (event.type == NSEventTypeKeyDown) {
+ [KMSentryHelper addDebugBreadCrumb:@"event" message:@"handling keydown event"];
// indicates that our generated backspace event(s) are consumed
// and we can insert text that followed the backspace(s)
if (event.keyCode == kKeymanEventKeyCode) {
@@ -422,6 +441,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx";
NSString *value = [options objectForKey:key];
if(key && value) {
os_log_debug([KMLogs keyLog], "persistOptions, key: %{public}@, value: %{public}@", key, value);
+ [KMSentryHelper addInfoBreadCrumb:@"event" message:@"persist options"];
[[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value];
}
else {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h
index 62a3aac35d..e467e645bd 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h
@@ -15,7 +15,8 @@ extern NSString *const kInputMethodClientChangeNotification;
@interface KMInputMethodLifecycle : NSObject
+ (KMInputMethodLifecycle *)shared;
-+ (NSString*)getClientApplicationId;
++ (NSString*)getRunningApplicationId;
+@property NSString *clientApplicationId;
- (void)startLifecycle;
- (void)activateClient:(id)client;
- (void)deactivateClient:(id)client;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
index 9b877a530e..c60fbaebf4 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
@@ -10,7 +10,7 @@
*/
/**
- * This class is needed because many activateServer and deactivateServer messages sent from macOS
+ * This class is needed because many activateServer and deactivateServer messages are sent from macOS
* to KMInputController, but they are not particularly reliable. Keyman receives some messages when it
* is not active and should not become active. It also receives messages when it is active, but there is no
* need to change state. For example, when a menu is clicked with Keyman active, macOS will send a
@@ -36,6 +36,7 @@
#import
#import "KMSettingsRepository.h"
#import
+#import "KMSentryHelper.h"
NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification";
NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification";
@@ -61,11 +62,12 @@ typedef enum {
@property LifecycleState lifecycleState;
@property NSString *inputSourceId;
-@property NSString *clientApplicationId;
@end
@implementation KMInputMethodLifecycle
+@synthesize lifecycleState = _lifecycleState;
+
+ (KMInputMethodLifecycle *)shared {
static KMInputMethodLifecycle *shared = nil;
static dispatch_once_t onceToken;
@@ -86,11 +88,40 @@ typedef enum {
return self;
}
+- (void)setLifecycleState:(LifecycleState)state {
+ _lifecycleState = state;
+
+ // whenever the state is changed, update the Sentry tag
+ [self addLifecycleStateSentryTag];
+}
+
+- (void)addLifecycleStateSentryTag {
+ NSString *stateString = @"Unknown";
+ switch (self.lifecycleState) {
+ case Started:
+ stateString = @"Started";
+ break;
+ case Active:
+ stateString = @"Active";
+ break;
+ case Inactive:
+ stateString = @"Inactive";
+ break;
+ }
+ [KMSentryHelper addLifecycleStateTag:stateString];
+ os_log_info([KMLogs lifecycleLog], "setLifecycleState: %{public}@", stateString);
+}
+
+
+- (LifecycleState)lifecycleState {
+ return _lifecycleState;
+}
+
/**
* called from Application Delgate during init
*/
- (void)startLifecycle {
- _lifecycleState = Started;
+ self.lifecycleState = Started;
}
/**
@@ -106,9 +137,10 @@ typedef enum {
/**
* Get the bundle ID of the currently active text input client..
*/
-+ (NSString*)getClientApplicationId {
++ (NSString*)getRunningApplicationId {
NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
NSString *clientAppId = [currentApp bundleIdentifier];
+ os_log_debug([KMLogs lifecycleLog], "getRunningApplicationId, frontmost: %{public}@", clientAppId);
return clientAppId;
}
@@ -156,7 +188,7 @@ typedef enum {
*/
- (void)performTransition:(id)client {
NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId];
- NSString *currentClientAppId = [KMInputMethodLifecycle getClientApplicationId];
+ NSString *currentClientAppId = [KMInputMethodLifecycle getRunningApplicationId];
TransitionType transition = [self determineTransition:currentInputSource withAppId:currentClientAppId];
[self saveNewInputMethodState:currentInputSource withAppId:currentClientAppId];
@@ -166,7 +198,8 @@ typedef enum {
os_log_info([KMLogs lifecycleLog], "performTransition: None, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId);
break;
case Activate:
- os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId);
+ os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new application ID: %{public}@", currentClientAppId);
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"activated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]];
/**
* Perform two actions when activating the input method.
* Change the client first which prepares the event handler.
@@ -177,10 +210,12 @@ typedef enum {
break;
case Deactivate:
os_log_info([KMLogs lifecycleLog], "performTransition: Deactivate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId);
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"deactivated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]];
[self deactivateInputMethod];
break;
case ChangeClients:
os_log_info([KMLogs lifecycleLog], "performTransition: ChangeClients, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId);
+ [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]];
[self changeClient];
break;
}
@@ -214,7 +249,7 @@ typedef enum {
*/
- (void)activateInputMethod {
os_log_debug([KMLogs lifecycleLog], "activateInputMethod");
- _lifecycleState = Active;
+ self.lifecycleState = Active;
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self];
}
@@ -223,7 +258,7 @@ typedef enum {
*/
- (void)deactivateInputMethod {
os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod");
- _lifecycleState = Inactive;
+ self.lifecycleState = Inactive;
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self];
}
@@ -231,7 +266,7 @@ typedef enum {
* Does not change lifecycleState, just fires notification so that InputController knows to change the event handler
*/
- (void)changeClient {
- os_log_debug([KMLogs lifecycleLog], "changeClient");
+ os_log_debug([KMLogs lifecycleLog], "changeClient, posting kInputMethodClientChangeNotification");
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self];
}
@@ -243,11 +278,10 @@ typedef enum {
}
/**
- * Returns true if lifecycleState is Active and the Settings require us to show the OSK
+ * Returns true the Settings require us to show the OSK on activation
*/
- (BOOL)shouldShowOskOnActivate {
- return [KMSettingsRepository.shared readShowOskOnActivate]
- && (self.lifecycleState == Active);
+ return [KMSettingsRepository.shared readShowOskOnActivate];
}
@end
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h
similarity index 84%
rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h
rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h
index 002bc5023c..f7f83702ec 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h
@@ -1,4 +1,4 @@
-/**
+/*
* Keyman is copyright (C) SIL International. MIT License.
*
* KMLogs.h
@@ -6,7 +6,6 @@
*
* Created by Shawn Schantz on 2024-05-16.
*
- * Contains methods to get singleton logger objects and constants for subsystem and category names.
*/
#import
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m
similarity index 99%
rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m
rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m
index f2d4dc4d53..6a8a5cd957 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m
@@ -1,4 +1,4 @@
-/**
+/*
* Keyman is copyright (C) SIL International. MIT License.
*
* KMLogs.m
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
new file mode 100644
index 0000000000..75a5c27282
--- /dev/null
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -0,0 +1,28 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by Shawn Schantz on 2025-01-13.
+ *
+ */
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KMSentryHelper : NSObject
+
++ (void)addLifecycleStateTag: (NSString *)value;
++ (void)addApiComplianceTag: (NSString *)value;
++ (void)addArchitectureTag: (NSString *)value;
++ (void)addOskVisibleTag:(BOOL)value;
++ (void)addClientAppIdTag:(NSString *)value;
++ (void)addKeyboardTag:(NSString *)value;
++ (void)addHasAccessibilityTag:(BOOL)value;
++ (void)addActiveKeyboardCountTag:(NSUInteger)value;
++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
new file mode 100644
index 0000000000..7c37be9ad9
--- /dev/null
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
@@ -0,0 +1,98 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by Shawn Schantz on 2025-01-13.
+ *
+ * Description
+ */
+
+#import "KMSentryHelper.h"
+@import Sentry;
+
+@implementation KMSentryHelper
+
+NSString * const kApiComplianceTagName = @"apiCompliance";
+NSString * const kArchitectureTagName = @"architecture";
+NSString * const kOskVisibleTagName = @"oskVisible";
+NSString * const kClientAppIdTagName = @"clientAppId";
+NSString * const kKeyboardTagName = @"keyboard";
+NSString * const kHasAccessibilityTagName = @"accessibilityEnabled";
+NSString * const kActiveKeyboardCountTagName = @"activeKeyboardCount";
+NSString * const kLifecycleStateTagName = @"lifecycleState";
+
++ (void)addLifecycleStateTag: (NSString *)value {
+ [self addCustomTag:kLifecycleStateTagName withValue:value];
+}
+
++ (void)addApiComplianceTag: (NSString *)value {
+ [self addCustomTag:kApiComplianceTagName withValue:value];
+}
+
++ (void)addArchitectureTag: (NSString *)value {
+ [self addCustomTag:kArchitectureTagName withValue:value];
+}
+
++ (void)addOskVisibleTag:(BOOL)value {
+ [self addCustomTag:kOskVisibleTagName withValue:value?@"true":@"false"];
+}
+
++ (void)addClientAppIdTag:(NSString *)value {
+ [self addCustomTag:kClientAppIdTagName withValue:value];
+}
+
++ (void)addKeyboardTag:(NSString *)value {
+ [self addCustomTag:kKeyboardTagName withValue:value];
+}
+
++ (void)addHasAccessibilityTag:(BOOL)value {
+ [self addCustomTag:kHasAccessibilityTagName withValue:value?@"true":@"false"];
+}
+
++ (void)addActiveKeyboardCountTag:(NSUInteger)value {
+ [self addCustomTag:kActiveKeyboardCountTagName withValue:[[NSNumber numberWithUnsignedLong:value] stringValue]];
+}
+
+/**
+ *assign custom keyboard tag in Sentry for the given name and value
+ */
++ (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value {
+ [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
+ [scope setTagValue:value forKey:tagName];
+ }];
+}
+
+/**
+ *add a Sentry breadcrumb message of level Info with the specified category
+ */
++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText {
+ SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+ crumb.level = kSentryLevelInfo;
+ crumb.category = category;
+ crumb.message = messageText;
+ [SentrySDK addBreadcrumb:crumb];
+}
+
+/**
+ *add a Sentry breadcrumb message of level debug with the specified category
+ */
++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText {
+ SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+ crumb.level = kSentryLevelDebug;
+ crumb.category = category;
+ crumb.message = messageText;
+ [SentrySDK addBreadcrumb:crumb];
+}
+
+/**
+ *add a Sentry breadcrumb message of type 'user' and level debug with the specified category
+ */
++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText {
+ SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+ crumb.type = @"user";
+ crumb.level = kSentryLevelInfo;
+ crumb.category = category;
+ crumb.message = messageText;
+ [SentrySDK addBreadcrumb:crumb];
+}
+
+@end
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
index b3824fa897..d4f457c754 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
@@ -11,6 +11,7 @@
#import "KMInputMethodLifecycle.h"
#import "KMSettingsRepository.h"
#import "KMLogs.h"
+#import "KMSentryHelper.h"
@interface OSKWindowController ()
@property (nonatomic, strong) NSButton *helpButton;
@@ -76,6 +77,7 @@
// whenever the OSK is closing clear all of its modifier keys
[self.oskView clearOskModifiers];
+ [KMSentryHelper addOskVisibleTag:NO];
}
- (void)helpAction:(id)sender {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib
index b753e43802..6a55541cb5 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib
+++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib
@@ -14,7 +14,7 @@
-
+
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
index e83439794b..81f50c5edd 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
@@ -36,6 +36,7 @@
#import
#import "KMInputMethodAppDelegate.h"
#import "KMLogs.h"
+#import "KMSentryHelper.h"
// this is the user managed list of non-compliant apps persisted in User Defaults
NSString *const kKMLegacyApps = @"KMLegacyApps";
@@ -62,6 +63,10 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
_clientApplicationId = appId;
_complianceUncertain = YES;
_initialSelection = NSMakeRange(NSNotFound, NSNotFound);
+
+ NSString *message = [NSString stringWithFormat:@"TextApiCompliance initWithClient, client: %p applicationId: %@", client, appId];
+ [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message];
+ os_log_debug([KMLogs complianceLog], "%{public}@", message);
// if we do not have hard-coded noncompliance, then test the app
if (![self applyNoncompliantAppLists:appId]) {
@@ -76,6 +81,16 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientApplicationId: %@, client: %@", self.complianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client];
}
+/**
+ * For Sentry, create a short description as the tag value is limited to 200 characters.
+ * We don't know how long the clientAppId will be, but this should not be well below 200 characters.
+ * If the text were to exceed 200 characters, then Sentry would display an error rather than truncating.
+ */
+-(NSString *)shortSentryTagDescription
+{
+ return [NSString stringWithFormat:@"uncertain: %@, compliant: %@, clientAppId: %@, ", self.complianceUncertain?@"true":@"false", self.hasCompliantSelectionApi?@"true":@"false", _clientApplicationId];
+}
+
/** test to see if the API selectedRange functions properly for the text input client */
-(void) checkCompliance:(id) client {
// confirm that the API actually exists (this always seems to return true)
@@ -91,6 +106,7 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
[self checkComplianceUsingInitialSelection];
}
os_log_debug([KMLogs complianceLog], "checkCompliance workingSelectionApi for app %{public}@: set to %{public}@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO");
+ [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription];
}
-(void) checkComplianceUsingInitialSelection {
@@ -135,6 +151,7 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
}
os_log_info([KMLogs complianceLog], "checkComplianceAfterInsert, self.hasWorkingSelectionApi = %{public}@ for app %{public}@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId);
+ [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription];
}
- (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete {
diff --git a/mac/docs/help/about/history.md b/mac/docs/help/about/history.md
index 3a0b9af3a6..76320b31e5 100644
--- a/mac/docs/help/about/history.md
+++ b/mac/docs/help/about/history.md
@@ -1,5 +1,5 @@
---
-title: Version History
+title: Version History of Keyman for macOS
---
A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website.
diff --git a/mac/docs/help/about/requirements.md b/mac/docs/help/about/requirements.md
index d92272a730..07745f9efc 100644
--- a/mac/docs/help/about/requirements.md
+++ b/mac/docs/help/about/requirements.md
@@ -1,5 +1,5 @@
---
-title: System Requirements
+title: System Requirements - Keyman for macOS Help
---
## Supported Mac Operating Systems
diff --git a/mac/docs/help/about/welcome.md b/mac/docs/help/about/welcome.md
index 5bd92f2fd8..ec67a871e8 100644
--- a/mac/docs/help/about/welcome.md
+++ b/mac/docs/help/about/welcome.md
@@ -1,5 +1,5 @@
---
-title: Welcome to Keyman
+title: Welcome to Keyman for macOS
---
Thank you for installing Keyman for macOS. Whether you're a new or returning user,
diff --git a/mac/docs/help/about/whatsnew.md b/mac/docs/help/about/whatsnew.md
index f4b11b973c..73ccb4d49b 100644
--- a/mac/docs/help/about/whatsnew.md
+++ b/mac/docs/help/about/whatsnew.md
@@ -1,5 +1,5 @@
---
-title: What's New
+title: What's New in Keyman 18.0 for macOS
---
Here are some of the new features we have added to Keyman 18.0 for macOS:
diff --git a/mac/docs/help/basic/index.md b/mac/docs/help/basic/index.md
index 8b3d0df07a..e09ff4f7b5 100644
--- a/mac/docs/help/basic/index.md
+++ b/mac/docs/help/basic/index.md
@@ -1,5 +1,5 @@
---
-title: Basic Help
+title: Keyman for macOS Basic Help
---
* [Keyman Configuration](config/)
diff --git a/mac/docs/help/start/install-keyboard.md b/mac/docs/help/start/install-keyboard.md
index bf9310c8bb..e22b3fad56 100644
--- a/mac/docs/help/start/install-keyboard.md
+++ b/mac/docs/help/start/install-keyboard.md
@@ -1,5 +1,5 @@
---
-title: Install a Keyman Keyboard
+title: Install a Keyman Keyboard on macOS
---
You must have a Keyman keyboard installed to type with Keyman for macOS in your language.
diff --git a/mac/docs/help/troubleshooting/index.md b/mac/docs/help/troubleshooting/index.md
index 2190a7c3cb..9325371dae 100644
--- a/mac/docs/help/troubleshooting/index.md
+++ b/mac/docs/help/troubleshooting/index.md
@@ -1,5 +1,5 @@
---
-title: Troubleshooting
+title: Keyman for macOS Troubleshooting
---
* [Configure macOS security for Keyman](configure-security)
diff --git a/mac/docs/help/troubleshooting/uninstall-keyman.md b/mac/docs/help/troubleshooting/uninstall-keyman.md
index 64ba2648e0..e1add66120 100644
--- a/mac/docs/help/troubleshooting/uninstall-keyman.md
+++ b/mac/docs/help/troubleshooting/uninstall-keyman.md
@@ -1,5 +1,5 @@
---
-title: Uninstalling Keyman
+title: Uninstalling Keyman for macOS
---
To uninstall Keyman for macOS:
diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh
index da459776eb..3be11dba1f 100755
--- a/resources/build/increment-version.sh
+++ b/resources/build/increment-version.sh
@@ -179,7 +179,7 @@ if [ "$action" == "commit" ]; then
# In order to avoid potential git conflicts, we run the history collater
# again on the master HISTORY.md. Note that the script always exits 1 to
# indicate it hasn't updated VERSION.md. We could tweak that in the future.
- node resources/build/version/lib/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true
+ node resources/build/version/build/src/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true
# If HISTORY.md has been updated, then we want to create a branch and push
# it for review
diff --git a/resources/docker-images/android/Dockerfile b/resources/docker-images/android/Dockerfile
index 91cd095a69..2b289ce077 100644
--- a/resources/docker-images/android/Dockerfile
+++ b/resources/docker-images/android/Dockerfile
@@ -22,6 +22,7 @@ RUN <> /usr/bin/bashwrapper
+RUN mkdir -p /run/user/1000 && \
+ chown build:build /run/user/1000 && \
+ chmod 700 /run/user/1000 && \
+ mkdir -p /tmp/.X11-unix && \
+ chmod 1777 /tmp/.X11-unix && \
+ echo "export XDG_RUNTIME_DIR=/run/user/1000" >> /usr/bin/bashwrapper
COPY run-tests.sh /usr/bin/run-tests.sh
diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh
index c42d6c2a02..63d299f7e3 100755
--- a/resources/docker-images/run.sh
+++ b/resources/docker-images/run.sh
@@ -36,9 +36,11 @@ run_core() {
run_linux() {
mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux"
+ mkdir -p "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux"
docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \
-v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \
-v "${KEYMAN_ROOT}/linux/build/docker-linux":/home/build/build/linux/build \
+ -v "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux":/home/build/build/linux/keyman-system-service/build \
-e DESTDIR=/tmp \
keymanapp/keyman-linux-ci:default \
"${builder_extra_params[@]}"
diff --git a/resources/docker-images/web/Dockerfile b/resources/docker-images/web/Dockerfile
index 5a27c9724d..1f08daf92c 100644
--- a/resources/docker-images/web/Dockerfile
+++ b/resources/docker-images/web/Dockerfile
@@ -12,7 +12,8 @@ LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
LABEL org.opencontainers.image.title="Keyman for Web Build Image"
USER root
-RUN apt-get install -qy git jq xvfb xserver-xephyr metacity
+RUN apt-get update && \
+ apt-get install -qy git jq xvfb xserver-xephyr metacity
# For playwright:
RUN apt-get install -qy libevent-2.1-7t64 libxslt1.1 libwoff1 libvpx9 \
libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \
diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html
index 9e1786c6d7..e7fc2768c6 100644
--- a/web/docs/engine/guide/examples/__auto-control.html
+++ b/web/docs/engine/guide/examples/__auto-control.html
@@ -20,9 +20,12 @@
+Automatic Mode Example
+
+Back to Document