Skip to content
Open
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
38 changes: 25 additions & 13 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import KeycodePicker from "@/components/KeycodePicker";
import LayerSelector from "@/components/LayerSelector";
import Toolbar from "@/components/Toolbar";
import { parseViaDefinition } from "@/lib/parser";
import { createDefaultKeymap, exportKeymap, importKeymap, downloadJson } from "@/lib/keymap";
import { createDefaultKeymap, exportKeymap, exportToVIA, importAnyKeymap, downloadJson } from "@/lib/keymap";
import { ParsedLayout, Keymap, KeyboardDefinition } from "@/types/keyboard";

import defaultKb from "../../keyboards/60-percent.json";
Expand Down Expand Up @@ -37,25 +37,36 @@ export default function Home() {
[selectedKey, activeLayer]
);

const handleExport = useCallback(() => {
const json = exportKeymap("My Keymap", kbName, keymap);
downloadJson(json, "keymap.json");
const handleExportVIA = useCallback(() => {
try {
const json = exportToVIA(keymap);
downloadJson(json, "via-keymap.json");
} catch (e) {
console.error(e);
alert("Failed to export VIA keymap")
}
}, [keymap]);

const handleExportKeylab = useCallback(() => {
try {
const json = exportKeymap("My Keymap", kbName, keymap);
downloadJson(json, "zumap-keymap.json");
} catch (e) {
console.error(e);
alert("Failed to export keymap")
}
}, [keymap, kbName]);

const handleImport = useCallback(
(json: string) => {
try {
const file = importKeymap(json);
// Pad layers to 4 if needed
while (file.layers.length < 4) {
file.layers.push(
Array.from({ length: layout.keys.length }, () => "KC_NO")
);
}
setKeymap(file.layers);
const layers = importAnyKeymap(json, layout.keys.length);

setKeymap(layers);
setSelectedKey(null);
setActiveLayer(0);
} catch (e) {
console.error(e);
alert("Invalid keymap file");
}
},
Expand Down Expand Up @@ -98,7 +109,8 @@ export default function Home() {
</p>
</div>
<Toolbar
onExport={handleExport}
onExportVIA={handleExportVIA}
onExportKeylab={handleExportKeylab}
onImport={handleImport}
onLoadKeyboard={handleLoadKeyboard}
/>
Expand Down
17 changes: 13 additions & 4 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { useRef } from "react";

interface Props {
onExport: () => void;
onExportVIA: () => void;
onExportKeylab: () => void;
onImport: (json: string) => void;
onLoadKeyboard: (json: string) => void;
}

export default function Toolbar({ onExport, onImport, onLoadKeyboard }: Props) {
export default function Toolbar({ onExportVIA, onExportKeylab, onImport, onLoadKeyboard }: Props) {
const keymapInputRef = useRef<HTMLInputElement>(null);
const kbInputRef = useRef<HTMLInputElement>(null);

Expand All @@ -27,11 +28,19 @@ export default function Toolbar({ onExport, onImport, onLoadKeyboard }: Props) {
return (
<div className="flex flex-wrap items-center gap-2">
<button
onClick={onExport}
onClick={onExportVIA}
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors"
>
Export Keymap
Export VIA
</button>

<button
onClick={onExportKeylab}
className="rounded bg-zinc-700 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors"
>
Export Keylab
</button>

<button
onClick={() => keymapInputRef.current?.click()}
className="rounded bg-zinc-700 px-4 py-2 text-sm font-medium text-zinc-200 hover:bg-zinc-600 transition-colors"
Expand Down
88 changes: 82 additions & 6 deletions src/lib/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ export function createDefaultKeymap(layout: ParsedLayout): Keymap {
);
}

function trimEmptyLayers(layers: Keymap): Keymap {
const trimmed = layers.filter(layer =>
layer.some(key => key !== "KC_NO")
);

return trimmed.length > 0 ? trimmed : layers.slice(0,1);
}

/** Export to VIA format */
export function exportToVIA(layers: Keymap): string {
if (!Array.isArray(layers)) {
throw new Error("Invalid keymap format")
}

//validating again
validateLayers(layers)

const viaFormat = {
version: 1,
layers: trimEmptyLayers(layers),
}

return JSON.stringify(viaFormat, null, 2);
}

/** Export keymap as a downloadable JSON file */
export function exportKeymap(
name: string,
Expand All @@ -23,13 +48,64 @@ export function exportKeymap(
return JSON.stringify(file, null, 2);
}

/** Parse an imported keymap JSON string */
export function importKeymap(json: string): KeymapFile {
const parsed = JSON.parse(json);
if (!parsed.layers || !Array.isArray(parsed.layers)) {
throw new Error("Invalid keymap file: missing layers array");
/** Detect what type of JSON is being imported and then update */

export function importAnyKeymap(
json: string,
keyCount: number
): Keymap {
let parsed;

try {
parsed = JSON.parse(json);
} catch {
throw new Error("Invalid JSON.");
}

if (Array.isArray(parsed.layers)) {
validateLayers(parsed.layers);

// Normalization logic
const normalizedLayers: Keymap = parsed.layers.map((layer: string[]) => {
const result = [...layer];
while (result.length < keyCount) {
result.push("KC_NO");
}

return result.slice(0, keyCount);
})

// Ensure minimum layers
while (normalizedLayers.length < NUM_LAYERS) {
normalizedLayers.push(
Array.from({ length: keyCount }, () => "KC_NO")
);
}

return normalizedLayers;
}

throw new Error("Invalid keymap format");
}

/** Function to validate the layers of the JSON file */

function validateLayers(layers: unknown): asserts layers is Keymap {
if (!Array.isArray(layers)) {
throw new Error("Layers must be an array");
}

for (const layer of layers) {
if (!Array.isArray(layer)) {
throw new Error("Each layer must be an array");
}

for (const key of layer) {
if (typeof key !== "string") {
throw new Error("Keycodes must be strings");
}
}
}
return parsed as KeymapFile;
}

/** Download a string as a file */
Expand Down