diff --git a/README.md b/README.md index 6d39c47..737662c 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,4 @@ Modify, and generate keyboard layout from single JSON file. Built with TypeScrip - `.klc` (Windows) - `.kcm` (Android Physical keyboard) - XKB (Linux) + - remap extension for Chrome OS (Manifest V3) (icons must be added manually / alphanumeric-shortcut keys will not work.) \ No newline at end of file diff --git a/cli.ts b/cli.ts index 6ce0887..3fa2670 100644 --- a/cli.ts +++ b/cli.ts @@ -5,6 +5,8 @@ import { generateKeylayout } from "./generateKeylayout" import { generateKlc } from "./generateKlc" import { generateXkb } from "./generateXkb" import { generateKcm } from "./generateKcm" +import { generateChr_background } from "./generateChr_background" +import { generateChr_manifest } from "./generateChr_manifest" import { fixUnicode } from "./utils" // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -94,4 +96,22 @@ const choices = filenames.map((filename) => ({ console.error(e) process.exit(1) } + + // Chr___OS + try { + const layoutName = response.input.split(".").slice(0, -1).join(".") + const dir = `output/${layoutName}/${jsonInput.os.windows.installerName.toLowerCase()}` + if (!fs.existsSync(dir)){ + fs.mkdirSync(dir); + } + const outputManifest = dir+`/manifest.json` + await generateChr_manifest(jsonInput, outputManifest) + console.log(`Output : ${outputManifest}`) + const outputBackground = dir+`/background.js` + await generateChr_background(jsonInput, outputBackground) + console.log(`Output : ${outputBackground}`) + } catch (e) { + console.error(e) + process.exit(1) + } })() diff --git a/generateChr_background.ts b/generateChr_background.ts new file mode 100644 index 0000000..6e1a125 --- /dev/null +++ b/generateChr_background.ts @@ -0,0 +1,192 @@ +import { + plainToClass +} from "class-transformer" +import { + validate +} from "class-validator" +import fs from "fs" +import { + Layout, + WindowsAttributes +} from "./main" + +export async function generateChr_background( + content: Record < string, unknown > , + outputPath: string, +): Promise < void > { + const layout = plainToClass(Layout, content) + const errors = await validate(layout) + + if (errors.length) { + throw new Error(errors.map((e) => e.toString()).join(", ")) + } + + const windowsErrors = await validate( + plainToClass(WindowsAttributes, layout.os.windows), + ) + + if (windowsErrors.length) { + throw new Error(windowsErrors.map((e) => e.toString()).join(", ")) + } + + function toCheck(str: string) { + // let hex, i + // let result = "" + // for (i = 0; i < str.length; i++) { + // hex = str.charCodeAt(i).toString(16) + // result += "\\u" + ("0000" + hex).slice(-4) + // } + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + } + + const klfDefaultLayout = { + "1": "Digit1", + "2": "Digit2", + "3": "Digit3", + "4": "Digit4", + "5": "Digit5", + "6": "Digit6", + "7": "Digit7", + "8": "Digit8", + "9": "Digit9", + "0": "Digit0", + "-": "Minus", + "=": "Equal", + "`": "Backquote", + q: "KeyQ", + w: "KeyW", + e: "KeyE", + r: "KeyR", + t: "KeyT", + y: "KeyY", + u: "KeyU", + i: "KeyI", + o: "KeyO", + p: "KeyP", + "[": "BracketLeft", + "]": "BracketRight", + a: "KeyA", + s: "KeyS", + d: "KeyD", + f: "KeyF", + g: "KeyG", + h: "KeyH", + j: "KeyJ", + k: "KeyK", + l: "KeyL", + ";": "Semicolon", + "'": "Quote", + "\\": "Backslash", + z: "KeyZ", + x: "KeyX", + c: "KeyC", + v: "KeyV", + b: "KeyB", + n: "KeyN", + m: "KeyM", + ",": "Comma", + ".": "Period", + "/": "Slash", + " ": "Space", + KPDL: "NumpadDecimal", + } + + const lines = `/*# License: ${layout.license} +Generated via github.com/Manoonchai/kiimo +*/ +var AltGr = { PLAIN: "plain", ALTERNATE: "alternate" }; +var Shift = { PLAIN: "plain", SHIFTED: "shifted" }; + +var contextID = -1; +var altGrState = AltGr.PLAIN; +var shiftState = Shift.PLAIN; +var lastRemappedKeyEvent = undefined; + +var lut = {` + +const endLines = ` +}; + + +chrome.input.ime.onFocus.addListener(function(context) { + contextID = context.contextID; +}); + +function updateAltGrState(keyData) { + altGrState = (keyData.code == "AltRight") ? ((keyData.type == "keydown") ? AltGr.ALTERNATE : AltGr.PLAIN) + : altGrState; +} + +function updateShiftState(keyData) { + shiftState = ((keyData.shiftKey && !(keyData.capsLock)) || (!(keyData.shiftKey) && keyData.capsLock)) ? + Shift.SHIFTED : Shift.PLAIN; +} + +function isPureModifier(keyData) { + return (keyData.key == "Shift") || (keyData.key == "Ctrl") || (keyData.key == "Alt"); +} + +function isRemappedEvent(keyData) { + // hack, should check for a sender ID (to be added to KeyData) + return lastRemappedKeyEvent != undefined && + (lastRemappedKeyEvent.key == keyData.key && + lastRemappedKeyEvent.code == keyData.code && + lastRemappedKeyEvent.type == keyData.type + ); // requestID would be different so we are not checking for it +} + + +chrome.input.ime.onKeyEvent.addListener( + function(engineID, keyData) { + var handled = false; + + if (isRemappedEvent(keyData)) { + lastRemappedKeyEvent = undefined; + return handled; + } + + updateAltGrState(keyData); + updateShiftState(keyData); + + if (lut[keyData.code]) { + //avoid hell key:process loop + if (keyData.ctrlKey === true && keyData.code != "Space") { + return; + } + var remappedKeyData = keyData; + remappedKeyData.key = lut[keyData.code][altGrState][shiftState]; + remappedKeyData.code = lut[keyData.code].code; + + if (chrome.input.ime.sendKeyEvents != undefined) { + chrome.input.ime.sendKeyEvents({"contextID": contextID, "keyData": [remappedKeyData]}); + handled = true; + lastRemappedKeyEvent = remappedKeyData; + } else if (keyData.type == "keydown" && !isPureModifier(keyData)) { + chrome.input.ime.commitText({"contextID": contextID, "text": remappedKeyData.key}); + handled = true; + } + } + + return handled; +}); +` + + const layoutLines = [""] + Object.entries(klfDefaultLayout).forEach(([key, value]) => { + const extensions = + `: { "plain": {"plain": "${toCheck(layout.keys[key][0])}"` + + `, "shifted": "${toCheck(layout.keys[key][1])}"}` + + `, "alternate": {"plain": "${toCheck(layout.keys[key][3])}"` + + `, "shifted":"${toCheck(layout.keys[key][5])}"}, ` + + `"code": "${value}"},` + + layoutLines.push([`"${value}"`, ...extensions].join("")) + }) + + fs.writeFileSync( + outputPath, + [lines, layoutLines.join("\n"),endLines].join(""), { + encoding: "utf8", + }, + ) +} \ No newline at end of file diff --git a/generateChr_manifest.ts b/generateChr_manifest.ts new file mode 100644 index 0000000..4291928 --- /dev/null +++ b/generateChr_manifest.ts @@ -0,0 +1,67 @@ +import { plainToClass } from "class-transformer" +import { validate } from "class-validator" +import fs from "fs" +import { Layout, WindowsAttributes } from "./main" + +export async function generateChr_manifest( + content: Record, + outputPath: string, +): Promise { + const layout = plainToClass(Layout, content) + const errors = await validate(layout) + + if (errors.length) { + throw new Error(errors.map((e) => e.toString()).join(", ")) + } + + const windowsErrors = await validate( + plainToClass(WindowsAttributes, layout.os.windows), + ) + + if (windowsErrors.length) { + throw new Error(windowsErrors.map((e) => e.toString()).join(", ")) + } + + const chrLocales = { + Thai: "th", + Lao: "la", + } + + const lines = +`{ + "name": "${layout.language} ${layout.name} v${layout.version}", + "version": "${layout.version}", + "manifest_version": 3, + "description": "${layout.language} ${layout.name} v${layout.version}", + "background": { + "service_worker": "background.js" + }, + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + }, + "permissions": [ + "input" + ], + "input_components": [ + { + "name": "${layout.language} ${layout.name} v${layout.version}", + "type": "ime", + "id": "${chrLocales[layout.language as keyof typeof chrLocales]}_${layout.name.toLowerCase()}_remap", + "description": "${layout.language} ${layout.name} v${layout.version}", + "language": "${chrLocales[layout.language as keyof typeof chrLocales]}", + "layouts": ["la(stea)"] + } + ] +}` +//"layouts": ["la(stea)"] due all embed-thai-layout lack of "level3(ralt_switch)", use laos instead. + + fs.writeFileSync( + outputPath, + lines, + { + encoding: "utf8", + }, + ) +} diff --git a/input/Manoonchai_Laos.json b/input/Manoonchai_Lao.json similarity index 96% rename from input/Manoonchai_Laos.json rename to input/Manoonchai_Lao.json index 834342b..06e5771 100644 --- a/input/Manoonchai_Laos.json +++ b/input/Manoonchai_Lao.json @@ -1,12 +1,12 @@ { - "name": "Manoonchai Laos Pali", + "name": "Manoonchai Lao-Pali", "version": "1.0", - "language": "Laos", + "language": "Lao", "layers": ["Base", "Shift", "Command", "AltGr", "Control", "ShiftAltGr"], "license": "MIT", "os": { "windows": { - "installerName": "LaosMnc", + "installerName": "LaoMnc", "company": "Manoonchai", "localeId": "00000454" } diff --git a/output/Manoonchai/thaimnc/background.js b/output/Manoonchai/thaimnc/background.js new file mode 100644 index 0000000..8464fd0 --- /dev/null +++ b/output/Manoonchai/thaimnc/background.js @@ -0,0 +1,125 @@ +/*# License: MIT +Generated via github.com/Manoonchai/kiimo +*/ +var AltGr = { PLAIN: "plain", ALTERNATE: "alternate" }; +var Shift = { PLAIN: "plain", SHIFTED: "shifted" }; + +var contextID = -1; +var altGrState = AltGr.PLAIN; +var shiftState = Shift.PLAIN; +var lastRemappedKeyEvent = undefined; + +var lut = { +"Digit0": { "plain": {"plain": "0", "shifted": ")"}, "alternate": {"plain": "๐", "shifted":""}, "code": "Digit0"}, +"Digit1": { "plain": {"plain": "1", "shifted": "!"}, "alternate": {"plain": "๑", "shifted":""}, "code": "Digit1"}, +"Digit2": { "plain": {"plain": "2", "shifted": "@"}, "alternate": {"plain": "๒", "shifted":""}, "code": "Digit2"}, +"Digit3": { "plain": {"plain": "3", "shifted": "#"}, "alternate": {"plain": "๓", "shifted":""}, "code": "Digit3"}, +"Digit4": { "plain": {"plain": "4", "shifted": "$"}, "alternate": {"plain": "๔", "shifted":""}, "code": "Digit4"}, +"Digit5": { "plain": {"plain": "5", "shifted": "%"}, "alternate": {"plain": "๕", "shifted":""}, "code": "Digit5"}, +"Digit6": { "plain": {"plain": "6", "shifted": "^"}, "alternate": {"plain": "๖", "shifted":""}, "code": "Digit6"}, +"Digit7": { "plain": {"plain": "7", "shifted": "&"}, "alternate": {"plain": "๗", "shifted":""}, "code": "Digit7"}, +"Digit8": { "plain": {"plain": "8", "shifted": "*"}, "alternate": {"plain": "๘", "shifted":""}, "code": "Digit8"}, +"Digit9": { "plain": {"plain": "9", "shifted": "("}, "alternate": {"plain": "๙", "shifted":""}, "code": "Digit9"}, +"Minus": { "plain": {"plain": "-", "shifted": "_"}, "alternate": {"plain": "÷", "shifted":""}, "code": "Minus"}, +"Equal": { "plain": {"plain": "=", "shifted": "+"}, "alternate": {"plain": "×", "shifted":""}, "code": "Equal"}, +"Backquote": { "plain": {"plain": "`", "shifted": "~"}, "alternate": {"plain": "`", "shifted":""}, "code": "Backquote"}, +"KeyQ": { "plain": {"plain": "ใ", "shifted": "ฒ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyQ"}, +"KeyW": { "plain": {"plain": "ต", "shifted": "ฏ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyW"}, +"KeyE": { "plain": {"plain": "ห", "shifted": "ซ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyE"}, +"KeyR": { "plain": {"plain": "ล", "shifted": "ญ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyR"}, +"KeyT": { "plain": {"plain": "ส", "shifted": "ฟ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyT"}, +"KeyY": { "plain": {"plain": "ป", "shifted": "ฉ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyY"}, +"KeyU": { "plain": {"plain": "ั", "shifted": "ึ"}, "alternate": {"plain": "ฺ", "shifted":""}, "code": "KeyU"}, +"KeyI": { "plain": {"plain": "ก", "shifted": "ธ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyI"}, +"KeyO": { "plain": {"plain": "ิ", "shifted": "ฐ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyO"}, +"KeyP": { "plain": {"plain": "บ", "shifted": "ฎ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyP"}, +"BracketLeft": { "plain": {"plain": "็", "shifted": "ฆ"}, "alternate": {"plain": "[", "shifted":"{"}, "code": "BracketLeft"}, +"BracketRight": { "plain": {"plain": "ฬ", "shifted": "ฑ"}, "alternate": {"plain": "]", "shifted":"}"}, "code": "BracketRight"}, +"KeyA": { "plain": {"plain": "ง", "shifted": "ษ"}, "alternate": {"plain": "◌", "shifted":""}, "code": "KeyA"}, +"KeyS": { "plain": {"plain": "เ", "shifted": "ถ"}, "alternate": {"plain": "๏", "shifted":""}, "code": "KeyS"}, +"KeyD": { "plain": {"plain": "ร", "shifted": "แ"}, "alternate": {"plain": "๛", "shifted":""}, "code": "KeyD"}, +"KeyF": { "plain": {"plain": "น", "shifted": "ช"}, "alternate": {"plain": "฿", "shifted":""}, "code": "KeyF"}, +"KeyG": { "plain": {"plain": "ม", "shifted": "พ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyG"}, +"KeyH": { "plain": {"plain": "อ", "shifted": "ผ"}, "alternate": {"plain": "ํ", "shifted":""}, "code": "KeyH"}, +"KeyJ": { "plain": {"plain": "า", "shifted": "ำ"}, "alternate": {"plain": "ๅ", "shifted":""}, "code": "KeyJ"}, +"KeyK": { "plain": {"plain": "่", "shifted": "ข"}, "alternate": {"plain": "ฃ", "shifted":""}, "code": "KeyK"}, +"KeyL": { "plain": {"plain": "้", "shifted": "โ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyL"}, +"Semicolon": { "plain": {"plain": "ว", "shifted": "ภ"}, "alternate": {"plain": ";", "shifted":":"}, "code": "Semicolon"}, +"Quote": { "plain": {"plain": "ื", "shifted": "\""}, "alternate": {"plain": "'", "shifted":"\""}, "code": "Quote"}, +"Backslash": { "plain": {"plain": "ฯ", "shifted": "ฌ"}, "alternate": {"plain": "\\", "shifted":"|"}, "code": "Backslash"}, +"KeyZ": { "plain": {"plain": "ุ", "shifted": "ฤ"}, "alternate": {"plain": "ฦ", "shifted":""}, "code": "KeyZ"}, +"KeyX": { "plain": {"plain": "ไ", "shifted": "ฝ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyX"}, +"KeyC": { "plain": {"plain": "ท", "shifted": "ๆ"}, "alternate": {"plain": "๚", "shifted":""}, "code": "KeyC"}, +"KeyV": { "plain": {"plain": "ย", "shifted": "ณ"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyV"}, +"KeyB": { "plain": {"plain": "จ", "shifted": "๊"}, "alternate": {"plain": "", "shifted":""}, "code": "KeyB"}, +"KeyN": { "plain": {"plain": "ค", "shifted": "๋"}, "alternate": {"plain": "ฅ", "shifted":""}, "code": "KeyN"}, +"KeyM": { "plain": {"plain": "ี", "shifted": "์"}, "alternate": {"plain": "๎", "shifted":""}, "code": "KeyM"}, +"Comma": { "plain": {"plain": "ด", "shifted": "ศ"}, "alternate": {"plain": ",", "shifted":"<"}, "code": "Comma"}, +"Period": { "plain": {"plain": "ะ", "shifted": "ฮ"}, "alternate": {"plain": ".", "shifted":">"}, "code": "Period"}, +"Slash": { "plain": {"plain": "ู", "shifted": "?"}, "alternate": {"plain": "/", "shifted":"?"}, "code": "Slash"}, +"Space": { "plain": {"plain": " ", "shifted": " "}, "alternate": {"plain": " ", "shifted":" "}, "code": "Space"}, +"NumpadDecimal": { "plain": {"plain": ".", "shifted": ","}, "alternate": {"plain": ".", "shifted":"."}, "code": "NumpadDecimal"}, +}; + + +chrome.input.ime.onFocus.addListener(function(context) { + contextID = context.contextID; +}); + +function updateAltGrState(keyData) { + altGrState = (keyData.code == "AltRight") ? ((keyData.type == "keydown") ? AltGr.ALTERNATE : AltGr.PLAIN) + : altGrState; +} + +function updateShiftState(keyData) { + shiftState = ((keyData.shiftKey && !(keyData.capsLock)) || (!(keyData.shiftKey) && keyData.capsLock)) ? + Shift.SHIFTED : Shift.PLAIN; +} + +function isPureModifier(keyData) { + return (keyData.key == "Shift") || (keyData.key == "Ctrl") || (keyData.key == "Alt"); +} + +function isRemappedEvent(keyData) { + // hack, should check for a sender ID (to be added to KeyData) + return lastRemappedKeyEvent != undefined && + (lastRemappedKeyEvent.key == keyData.key && + lastRemappedKeyEvent.code == keyData.code && + lastRemappedKeyEvent.type == keyData.type + ); // requestID would be different so we are not checking for it +} + + +chrome.input.ime.onKeyEvent.addListener( + function(engineID, keyData) { + var handled = false; + + if (isRemappedEvent(keyData)) { + lastRemappedKeyEvent = undefined; + return handled; + } + + updateAltGrState(keyData); + updateShiftState(keyData); + + if (lut[keyData.code]) { + //avoid hell key:process loop + if (keyData.ctrlKey === true && keyData.code != "Space") { + return; + } + var remappedKeyData = keyData; + remappedKeyData.key = lut[keyData.code][altGrState][shiftState]; + remappedKeyData.code = lut[keyData.code].code; + + if (chrome.input.ime.sendKeyEvents != undefined) { + chrome.input.ime.sendKeyEvents({"contextID": contextID, "keyData": [remappedKeyData]}); + handled = true; + lastRemappedKeyEvent = remappedKeyData; + } else if (keyData.type == "keydown" && !isPureModifier(keyData)) { + chrome.input.ime.commitText({"contextID": contextID, "text": remappedKeyData.key}); + handled = true; + } + } + + return handled; +}); diff --git a/output/Manoonchai/thaimnc/icon128.png b/output/Manoonchai/thaimnc/icon128.png new file mode 100644 index 0000000..5f1bb3b Binary files /dev/null and b/output/Manoonchai/thaimnc/icon128.png differ diff --git a/output/Manoonchai/thaimnc/icon16.png b/output/Manoonchai/thaimnc/icon16.png new file mode 100644 index 0000000..ea370f9 Binary files /dev/null and b/output/Manoonchai/thaimnc/icon16.png differ diff --git a/output/Manoonchai/thaimnc/icon48.png b/output/Manoonchai/thaimnc/icon48.png new file mode 100644 index 0000000..dfaa0b0 Binary files /dev/null and b/output/Manoonchai/thaimnc/icon48.png differ diff --git a/output/Manoonchai/thaimnc/manifest.json b/output/Manoonchai/thaimnc/manifest.json new file mode 100644 index 0000000..c8925b6 --- /dev/null +++ b/output/Manoonchai/thaimnc/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "Thai Manoonchai v1.0", + "version": "1.0", + "manifest_version": 3, + "description": "Thai Manoonchai v1.0", + "background": { + "service_worker": "background.js" + }, + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + }, + "permissions": [ + "input" + ], + "input_components": [ + { + "name": "Thai Manoonchai v1.0", + "type": "ime", + "id": "th_manoonchai_remap", + "description": "Thai Manoonchai v1.0", + "language": "th", + "layouts": ["la(stea)"] + } + ] +} \ No newline at end of file