Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .eslintignore

This file was deleted.

19 changes: 0 additions & 19 deletions .eslintrc.js

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [18, 20, 22, 24]
node-version: [20, 22, 24]

steps:
- uses: actions/checkout@v4
Expand Down
677 changes: 227 additions & 450 deletions bun.lock

Large diffs are not rendered by default.

237 changes: 119 additions & 118 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,138 +9,139 @@ import { generateKcm } from "./generator/generateKcm"
import { generateChr_background } from "./generator/generateChr_background"
import { generateChr_manifest } from "./generator/generateChr_manifest"
import { fixUnicode } from "./utils"
import { argv } from "yargs"

// eslint-disable-next-line @typescript-eslint/no-var-requires
prompts.override(require("yargs").argv)
prompts.override(argv)

const filenames = fs.readdirSync("input")
.filter(name => name.endsWith(".json") && !name.endsWith(".json.bak"))
const filenames = fs
.readdirSync("input")
.filter((name) => name.endsWith(".json") && !name.endsWith(".json.bak"))
.sort((a, b) => a.localeCompare(b))
const choices = filenames.map((filename) => ({
title: filename,
value: filename,
}))

; (async () => {
const response = await prompts({
type: "select",
name: "input",
message: "Pick input JSON file",
choices,
})

const jsonInput = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "input", response.input), "utf8")
)

let jsonName: string;
let layoutName: string;
let safeLayoutName: string;
let dir: string;

try {
jsonName = response.input.split(".").slice(0, -1).join(".");
layoutName = jsonInput.name;
// Replace spaces with underscores for safe use in IDs or filenames for some system
safeLayoutName = layoutName.replace(/\s+/g, "_").replace(/\./g, "_");
dir = `output/${jsonName}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
} catch (e) {
console.error(e)
process.exit(1)
;(async () => {
const response = await prompts({
type: "select",
name: "input",
message: "Pick input JSON file",
choices,
})

const jsonInput = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "input", response.input), "utf8"),
)

let jsonName: string
let layoutName: string
let safeLayoutName: string
let dir: string

try {
jsonName = response.input.split(".").slice(0, -1).join(".")
layoutName = jsonInput.name
// Replace spaces with underscores for safe use in IDs or filenames for some system
safeLayoutName = layoutName.replace(/\s+/g, "_").replace(/\./g, "_")
dir = `output/${jsonName}`
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}

// Keylayout
try {
const keylayoutXml = await generateKeylayout(jsonInput)
const bundleDir = `${dir}/${layoutName}.bundle`
const contentsDir = `${bundleDir}/Contents`
const resourceDir = `${contentsDir}/Resources`
const lprojDir = `${resourceDir}/th.lproj`
if (!fs.existsSync(lprojDir)) {
fs.mkdirSync(lprojDir, { recursive: true })
}
const outputFilename = `${resourceDir}/${layoutName}.keylayout`
fs.writeFileSync(outputFilename, keylayoutXml, "utf-8")
fixUnicode(outputFilename)

console.log(`Output : ${outputFilename}`)

const iconName = jsonInput.icon ? `icon_${jsonInput.icon}` : 'icon';
fs.copyFileSync(
`src/iconMac/${iconName}.icns`,
path.join(resourceDir, `${layoutName}.icns`)
);

const files = await generateMacBundle(jsonInput)
for (const [filename, content] of Object.entries(files)) {
const filePath = path.join(contentsDir, filename)
fs.writeFileSync(filePath, content, "utf-8")
}
} catch (e) {
console.error(e)
process.exit(1)
} catch (e) {
console.error(e)
process.exit(1)
}

// Keylayout
try {
const keylayoutXml = await generateKeylayout(jsonInput)
const bundleDir = `${dir}/${layoutName}.bundle`
const contentsDir = `${bundleDir}/Contents`
const resourceDir = `${contentsDir}/Resources`
const lprojDir = `${resourceDir}/th.lproj`
if (!fs.existsSync(lprojDir)) {
fs.mkdirSync(lprojDir, { recursive: true })
}
const outputFilename = `${resourceDir}/${layoutName}.keylayout`
fs.writeFileSync(outputFilename, keylayoutXml, "utf-8")
fixUnicode(outputFilename)

console.log(`Output : ${outputFilename}`)

// Klc
try {
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
const outputFilename = `${dir}/`+jsonInput.os.windows.installerName+`.klc`
await generateKlc(jsonInput, outputFilename)
const iconName = jsonInput.icon ? `icon_${jsonInput.icon}` : "icon"
fs.copyFileSync(
`src/iconMac/${iconName}.icns`,
path.join(resourceDir, `${layoutName}.icns`),
)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
const files = await generateMacBundle(jsonInput)
for (const [filename, content] of Object.entries(files)) {
const filePath = path.join(contentsDir, filename)
fs.writeFileSync(filePath, content, "utf-8")
}

// Xkb
try {
const outputFilename = `${dir}/${safeLayoutName}_xkb`
await generateXkb(jsonInput, outputFilename)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
} catch (e) {
console.error(e)
process.exit(1)
}

// Klc
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}

// Kmc
try {
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
const outputFilename = `${dir}/${safeLayoutName}.kcm`
await generateKcm(jsonInput, outputFilename)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
const outputFilename =
`${dir}/` + jsonInput.os.windows.installerName + `.klc`
await generateKlc(jsonInput, outputFilename)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
}

// Xkb
try {
const outputFilename = `${dir}/${safeLayoutName}_xkb`
await generateXkb(jsonInput, outputFilename)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
}

// Kmc
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}

// Chr___OS
try {
const chrDir = `${dir}/${jsonInput.os.windows.installerName.toLowerCase()}`
if (!fs.existsSync(chrDir)){
fs.mkdirSync(chrDir);
}
const outputManifest = chrDir+`/manifest.json`
await generateChr_manifest(jsonInput, outputManifest)
console.log(`Output : ${outputManifest}`)
const outputBackground = chrDir+`/background.js`
await generateChr_background(jsonInput, outputBackground)
console.log(`Output : ${outputBackground}`)
fs.readdirSync('src/iconChr').forEach(f =>
fs.copyFileSync(path.join('src/iconChr', f), path.join(chrDir, f))
)
} catch (e) {
console.error(e)
process.exit(1)
const outputFilename = `${dir}/${safeLayoutName}.kcm`
await generateKcm(jsonInput, outputFilename)

console.log(`Output : ${outputFilename}`)
} catch (e) {
console.error(e)
process.exit(1)
}

// Chr___OS
try {
const chrDir = `${dir}/${jsonInput.os.windows.installerName.toLowerCase()}`
if (!fs.existsSync(chrDir)) {
fs.mkdirSync(chrDir)
}
})()
const outputManifest = chrDir + `/manifest.json`
await generateChr_manifest(jsonInput, outputManifest)
console.log(`Output : ${outputManifest}`)
const outputBackground = chrDir + `/background.js`
await generateChr_background(jsonInput, outputBackground)
console.log(`Output : ${outputBackground}`)
fs.readdirSync("src/iconChr").forEach((f) =>
fs.copyFileSync(path.join("src/iconChr", f), path.join(chrDir, f)),
)
} catch (e) {
console.error(e)
process.exit(1)
}
})()
17 changes: 17 additions & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import js from "@eslint/js"
import globals from "globals"
import tseslint from "typescript-eslint"
import { defineConfig, globalIgnores } from "eslint/config"
import eslintConfigPrettier from "eslint-config-prettier/flat"

export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: { ...globals.browser, ...globals.node } },
},
tseslint.configs.recommended,
eslintConfigPrettier,
globalIgnores(["dist", "coverage", "output"]),
])
4 changes: 0 additions & 4 deletions jest.config.js

This file was deleted.

23 changes: 13 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"generate": "ts-node cli.ts",
"start": "ts-node .",
"test": "jest",
"test": "vitest",
"lint": "eslint .",
"prepare": "husky install"
},
Expand All @@ -22,20 +22,23 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0",
"@types/node": "^20.19.9",
"@types/prompts": "^2.4.9",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.5.3",
"@types/yargs": "^17.0.33",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "^3.2.4",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.4.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jiti": "^2.6.1",
"prettier": "3.0.0",
"ts-jest": "^29.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
"typescript": "^5.9.3",
"typescript-eslint": "^8.45.0",
"vitest": "^3.2.4"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
1 change: 1 addition & 0 deletions test/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs"
import path from "path"
import { js2xml, xml2js } from "xml-js"
import { describe, it, expect } from 'vitest'
import { validateLayout } from "../main"
import { generateKeylayout } from "../generator/generateKeylayout"
import { generateKlc } from "../generator/generateKlc"
Expand Down
7 changes: 7 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'node',
},
})
Loading
Loading