diff --git a/src/lib/assets/quater-ring.svg b/src/lib/assets/quater-ring.svg deleted file mode 100644 index c3566641..00000000 --- a/src/lib/assets/quater-ring.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/lib/assets/settings.yml b/src/lib/assets/settings.yml new file mode 100644 index 00000000..6e11f464 --- /dev/null +++ b/src/lib/assets/settings.yml @@ -0,0 +1,118 @@ +settings: + 1: + title: Enable Serial Header + description: boolean 0 or 1, default is 0 + 2: + title: Enable Serial Logging + description: boolean 0 or 1, default is 0 + 3: + title: Enable Serial Debugging + description: boolean 0 or 1, default is 0 + 4: + title: Enable Serial Raw + description: boolean 0 or 1, default is 0 + 5: + title: Enable Serial Chord + description: boolean 0 or 1, default is 0 + 6: + title: Enable Serial Keyboard + description: boolean 0 or 1, default is 0 + 7: + title: Enable Serial Mouse + description: boolean 0 or 1, default is 0 + 11: + title: Enable USB HID Keyboard + description: boolean 0 or 1, default is 1 + 12: + title: Enable Character Entry + description: boolean 0 or 1 + 13: + title: GUI-CTRL Swap Mode + description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only) + 14: + title: Key Scan Duration + description: scan rate described in milliseconds; default is 2ms = 500Hz + 15: + title: Key Debounce Press Duration + description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite + 16: + title: Key Debounce Release Duration + description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite + 17: + title: Keyboard Output Character Microsecond Delays + description: delay time in microseconds (one delay for press and again for release); default is 480us; max is 10240us; increments of 40us + 21: + title: Enable USB HID Mouse + description: boolean 0 or 1; default is 1 + 22: + title: Slow Mouse Speed + description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s + 23: + title: Fast Mouse Speed + description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s + 24: + title: Enable Active Mouse + description: boolean 0 or 1; moves mouse back and forth every 60s + 25: + title: Mouse Scroll Speed + description: default is 1; polls at 1/4th the rate of the mouse move updates + 26: + title: Mouse Poll Duration + description: poll rate described in milliseconds; default is 20ms = 50Hz + 31: + title: Enable Chording + description: boolean 0 or 1 + 32: + title: Enable Chording Character Counter Timeout + description: boolean 0 or 1; default is 1 + 33: + title: Chording Character Counter Timeout Timer + description: 0-255 deciseconds; default is 40 or 4.0 seconds + 34: + title: Chord Detection Press Tolerance(ms) + description: 1-50 milliseconds + 35: + title: Chord Detection Release Tolerance(ms) + description: 1-50 milliseconds + 41: + title: Enable Spurring + description: boolean 0 or 1; default is 1 + 42: + title: Enable Spurring Character Counter Timeout + description: boolean 0 or 1; default is 1 + 43: + title: Spurring Character Counter Timeout Timer + description: 0-255 seconds; default is 240 + 51: + title: Enable Arpeggiates + description: boolean 0 or 1; default is 1 + 54: + title: Arpeggiate Tolerance + description: in milliseconds; default 800ms + 61: + title: Enable Compound Chording (coming soon) + description: boolean 0 or 1; default is 0 + 64: + title: Compound Tolerance + description: in milliseconds; default 1500ms + 81: + title: LED Brightness + description: 0-50 (CCL only); default is 5, which draws around 100 mA of current + 82: + title: LED Color Code + description: Color Codes to be listed (CCL only) + 83: + title: Enable LED Key Highlight (coming soon) + description: boolean 0 or 1 (CCL only) + 84: + title: Enable LEDs + description: boolean 0 or 1; default is 1 (CCL only) + 91: + title: Operating System + description: Operating system codes listed below + 92: + title: Enable Realtime Feedback + description: boolean 0 or 1; default is 1 + 93: + title: Enable CharaChorder Ready on startup + description: boolean 0 or 1; default is 1 diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte deleted file mode 100644 index e69de29b..00000000 diff --git a/src/lib/components/layout/GenericLayout.svelte b/src/lib/components/layout/GenericLayout.svelte index 44ce670d..5e2cd5c6 100644 --- a/src/lib/components/layout/GenericLayout.svelte +++ b/src/lib/components/layout/GenericLayout.svelte @@ -1,7 +1,7 @@ - - - {#each $layout as layer, i} - - - - - {/each} -
counter_{i + 1}
diff --git a/src/lib/components/layout/KeyText.svelte b/src/lib/components/layout/KeyText.svelte index 78c3578d..c209ec86 100644 --- a/src/lib/components/layout/KeyText.svelte +++ b/src/lib/components/layout/KeyText.svelte @@ -1,10 +1,10 @@ {#each positions as position, layer} - {@const [action, changed] = getActions(layer, key.id, $layout, $changes)} + {@const {action: actionId, isApplied} = $layout[layer][key.id]} + {@const {code, icon, id} = KEYMAP_CODES[actionId] ?? {code: actionId}} {@const isActive = layer === $activeLayer} {@const direction = [(middle[0] - margin * 3) / position[0], (middle[1] - margin * 3) / position[1]]} - {#if action.code !== 0} - {action.icon || action.id || `0x${action.code?.toString(16)}`} + {#if code !== 0} + {icon || id || `0x${code.toString(16)}`} {/if} - {#if changed} + {#if !isApplied} {/if} diff --git a/src/lib/components/layout/KeyboardKey.svelte b/src/lib/components/layout/KeyboardKey.svelte index 9df065d0..6e1bc436 100644 --- a/src/lib/components/layout/KeyboardKey.svelte +++ b/src/lib/components/layout/KeyboardKey.svelte @@ -44,7 +44,6 @@ {@const multiplier = 1.25} - import RingInput from "$lib/components/layout/RingInput.svelte" - - export let activeLayer = 0 - - -
-
-
- -
- - -
-
- - -
- -
- -
- -
- - -
-
- - -
- -
-
-
- - -
-
- - -
-
- - -
-
- - diff --git a/src/lib/components/layout/RingInput.svelte b/src/lib/components/layout/RingInput.svelte deleted file mode 100644 index 054d0d5f..00000000 --- a/src/lib/components/layout/RingInput.svelte +++ /dev/null @@ -1,186 +0,0 @@ - - -
- {#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant} - {@const actions = getActions(id, $layout, $changes)} - - {/each} -
- - diff --git a/src/lib/components/layout/get-actions.ts b/src/lib/components/layout/get-actions.ts deleted file mode 100644 index 97a95825..00000000 --- a/src/lib/components/layout/get-actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type {CharaLayout} from "$lib/serialization/layout" -import type {Change} from "$lib/serial/connection" -import {KEYMAP_CODES} from "$lib/serial/keymap-codes" -import type {KeyInfo} from "$lib/serial/keymap-codes" - -export function getActions( - layer: number, - id: number, - layout: CharaLayout, - changes: Change[], -): [KeyInfo, boolean] { - const actionId = layout?.[layer][id] - const changedId = changes.findLast(it => it?.layout?.[layer]?.[id] !== undefined)?.layout?.[layer]?.[id] - if (changedId !== undefined) { - return [KEYMAP_CODES[changedId] ?? {code: changedId}, true] - } else { - return [KEYMAP_CODES[actionId] ?? {code: actionId}, false] - } -} diff --git a/src/lib/editable-layout.ts b/src/lib/editable-layout.ts deleted file mode 100644 index 649b36be..00000000 --- a/src/lib/editable-layout.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {Action} from "svelte/action" -import ActionSelector from "$lib/components/layout/ActionSelector.svelte" -import {changes, layout} from "$lib/serial/connection" -import {get} from "svelte/store" - -export const editableLayout: Action = ( - node, - {id, activeLayer}, -) => { - let component: ActionSelector | undefined - function present() { - component?.$destroy() - component = new ActionSelector({ - target: document.body, - props: {currentAction: get(layout)[activeLayer][id]}, - }) - component.$on("close", () => { - component!.$destroy() - }) - component.$on("select", ({detail}) => { - changes.update(changes => { - changes.push({layout: {[activeLayer]: {[id]: detail}}}) - return changes - }) - component!.$destroy() - }) - } - - node.addEventListener("click", present) - return { - destroy() { - node.removeEventListener("click", present) - }, - } -} diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index eda23baa..70872e7d 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -5,6 +5,7 @@ import type {Writable} from "svelte/store" import type {CharaLayout} from "$lib/serialization/layout" import {persistentWritable} from "$lib/storage" import {userPreferences} from "$lib/preferences" +import settingInfo from "$lib/assets/settings.yml" export const serialPort = writable() @@ -15,27 +16,32 @@ export interface SerialLogEntry { export const serialLog = writable([]) -export const chords = persistentWritable("chord-library", [], () => get(userPreferences).backup) +/** + * Chords as read from the device + */ +export const deviceChords = persistentWritable( + "chord-library", + [], + () => get(userPreferences).backup, +) -export const layout = persistentWritable( +/** + * Layout as read from the device + */ +export const deviceLayout = persistentWritable( "layout", [[], [], []], () => get(userPreferences).backup, ) -export interface Change { - layout?: Record> - chords?: Array> - settings?: Record -} - -export const changes = persistentWritable("changes", []) - -export const settings = writable({}) - -export const unsavedChanges = writable(new Map()) - -export const highlightActions: Writable = writable([]) +/** + * Settings as read from the device + */ +export const deviceSettings = persistentWritable( + "device-settings", + [], + () => get(userPreferences).backup, +) export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done") @@ -45,19 +51,28 @@ export async function initSerial(manual = false) { serialPort.set(device) syncStatus.set("downloading") + const parsedSettings: number[] = [] + for (const key in settingInfo.settings) { + try { + parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key)) + } catch {} + } + deviceSettings.set(parsedSettings) + const parsedLayout: CharaLayout = [[], [], []] for (let layer = 1; layer <= 3; layer++) { + // TODO: this will fail for LITE! for (let i = 0; i < 90; i++) { parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i) } } - layout.set(parsedLayout) + deviceLayout.set(parsedLayout) const chordCount = await device.getChordCount() const chordInfo = [] for (let i = 0; i < chordCount; i++) { chordInfo.push(await device.getChord(i)) } - chords.set(chordInfo) + deviceChords.set(chordInfo) syncStatus.set("done") } diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index 03dba120..0ed32abc 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -193,7 +193,7 @@ export class CharaDevice { if (status !== "0") throw new Error(`Failed with status ${status}`) } - async deleteChord(chord: Chord) { + async deleteChord(chord: Pick) { const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`) if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`) } @@ -205,7 +205,8 @@ export class CharaDevice { * @param action the assigned action id */ async setLayoutKey(layer: number, id: number, action: number) { - const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`) + const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`) + console.log(status) if (status !== "0") throw new Error(`Failed with status ${status}`) } diff --git a/src/lib/setting.ts b/src/lib/setting.ts index b3288bea..9b85fffb 100644 --- a/src/lib/setting.ts +++ b/src/lib/setting.ts @@ -1,6 +1,5 @@ import type {Action} from "svelte/action" -import {serialPort, unsavedChanges} from "$lib/serial/connection" -import {get} from "svelte/store" +import {changes, ChangeType, settings} from "$lib/undo-redo" export const setting: Action = function ( node: HTMLInputElement, @@ -9,15 +8,20 @@ export const setting: Action { - if (port) { + const unsubscribe = settings.subscribe(async settings => { + if (id in settings) { + const {value, isApplied} = settings[id] if (type === "number") { - const value = Number(await port.getSetting(id).then(it => it.toString())) node.value = ( inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value ).toString() } else { - node.checked = await port.getSetting(id).then(it => it !== 0) + node.checked = value !== 0 + } + if (isApplied) { + node.classList.remove("pending-changes") + } else { + node.classList.add("pending-changes") } node.removeAttribute("disabled") } else { @@ -26,8 +30,7 @@ export const setting: Action { - if (originalValue === value) { - it.delete(id) - } else if (!it.has(id)) { - it.set(id, currentValue) - } - return it + changes.update(changes => { + changes.push({ + type: ChangeType.Setting, + id: id, + setting: value, + }) + return changes }) } node.addEventListener("input", listener) diff --git a/src/lib/undo-redo.ts b/src/lib/undo-redo.ts new file mode 100644 index 00000000..cc98f1cb --- /dev/null +++ b/src/lib/undo-redo.ts @@ -0,0 +1,109 @@ +import {persistentWritable} from "$lib/storage" +import {derived} from "svelte/store" +import {serializeActions} from "$lib/serial/chord" +import type {Chord} from "$lib/serial/chord" +import {deviceChords, deviceLayout, deviceSettings} from "$lib/serial/connection" + +export enum ChangeType { + Layout, + Chord, + Setting, +} + +export interface LayoutChange { + type: ChangeType.Layout + id: number + layer: number + action: number +} + +export interface ChordChange { + type: ChangeType.Chord + actions: number[] + phrase: number[] +} + +export interface SettingChange { + type: ChangeType.Setting + id: number + setting: number +} + +export interface ChangeInfo { + isApplied: boolean + isCommitted?: boolean +} + +export type Change = LayoutChange | ChordChange | SettingChange + +export const changes = persistentWritable("changes", []) + +export interface Overlay { + layout: [Map, Map, Map] + chords: Map + settings: Map +} + +export const overlay = derived(changes, changes => { + console.time("overlay building") + const overlay: Overlay = { + layout: [new Map(), new Map(), new Map()], + chords: new Map(), + settings: new Map(), + } + + for (const change of changes) { + switch (change.type) { + case ChangeType.Layout: + overlay.layout[change.layer].set(change.id, change.action) + break + case ChangeType.Chord: + overlay.chords.set(serializeActions(change.actions), change.phrase) + break + case ChangeType.Setting: + overlay.settings.set(change.id, change.setting) + break + } + } + console.timeEnd("overlay building") + + return overlay +}) + +export const settings = derived([overlay, deviceSettings], ([overlay, settings]) => + settings.map<{value: number} & ChangeInfo>((value, id) => ({ + value: overlay.settings.get(id) ?? value, + isApplied: !overlay.settings.has(id), + })), +) + +export type KeyInfo = {action: number} & ChangeInfo +export const layout = derived([overlay, deviceLayout], ([overlay, layout]) => + layout.map( + (actions, layer) => + actions.map((action, id) => ({ + action: overlay.layout[layer].get(id) ?? action, + isApplied: !overlay.layout[layer].has(id), + })) as [KeyInfo, KeyInfo, KeyInfo], + ), +) + +export type ChordInfo = Chord & ChangeInfo +export const chords = derived([overlay, deviceChords], ([overlay, chords]) => + chords.map(chord => { + const key = serializeActions(chord.actions) + if (overlay.chords.has(key)) { + return { + actions: chord.actions, + phrase: overlay.chords.get(key)!, + isApplied: false, + } + } else { + return { + actions: chord.actions, + phrase: chord.phrase, + isApplied: true, + } + } + }), +) diff --git a/src/lib/versioning.ts b/src/lib/versioning.ts new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/src/lib/versioning.ts @@ -0,0 +1 @@ +// TODO diff --git a/src/routes/BackupPopup.svelte b/src/routes/BackupPopup.svelte index 42dec62a..29cfe3eb 100644 --- a/src/routes/BackupPopup.svelte +++ b/src/routes/BackupPopup.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/EditActions.svelte b/src/routes/EditActions.svelte index 90b15741..2fe7deee 100644 --- a/src/routes/EditActions.svelte +++ b/src/routes/EditActions.svelte @@ -1,9 +1,10 @@ @@ -107,7 +154,11 @@ on:click={redo}>redo
- + {#if $changes.length !== 0}
- {#if lastPage !== -1} - {#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]} - + {#if $lastPage !== -1} + {#each $items.slice(page * $pageSize, (page + 1) * $pageSize) as [{ actions, phrase, isApplied }]} + diff --git a/src/routes/config/layout/+page.svelte b/src/routes/config/layout/+page.svelte index 9833adab..5508023a 100644 --- a/src/routes/config/layout/+page.svelte +++ b/src/routes/config/layout/+page.svelte @@ -1,6 +1,6 @@
- + - + - + changes.update(changes => { + changes.push({type: ChangeType.Chord, actions, phrase: []}) + return changes + })}>delete