diff --git a/docs/BACKUP.md b/docs/BACKUP.md
new file mode 100644
index 00000000..4f5c8857
--- /dev/null
+++ b/docs/BACKUP.md
@@ -0,0 +1,64 @@
+# Chara Backup Format, Version 1
+
+JSON Schema files: TBD
+
+Chara backups are serialized using JSON, in this general format:
+
+```json
+{
+ "charaVersion": 1,
+ "type": "..."
+}
+```
+
+The presence of the key `charaVersion` uniquely identifies the JSON file as a chara backup file and serves
+as a discriminator against other generic JSON files. This key is mandatory for that reason.
+
+## Type `layout`
+
+```json
+{
+ "charaVersion": 1,
+ "type": "layout",
+ "device": "one",
+ "layers": [[], [], []]
+}
+```
+
+Devices at the current point in time may be identified as either `lite` or `one`, more to come in the future.
+
+Layers are serialized as an array of `[layer1, layer2, layer3]` in the internal order of the key, each specifying
+an action code. Action codes of `0` are considered unassigned.
+
+## Type `chords`
+
+```json
+{
+ "charaVersion": 1,
+ "type": "chords",
+ "chords": [
+ [
+ [1, 2, 3],
+ [3, 4, 5]
+ ],
+ [
+ [6, 7, 8],
+ [9, 10, 11]
+ ]
+ ]
+}
+```
+
+Chords are serialized using a key-value mapping of chord action codes to actions.
+
+## Type `settings`
+
+```json
+{
+ "charaVersion": 1,
+ "type": "settings",
+ "settings": [0, 1, 3, 6]
+}
+```
+
+Settings are serialized as an array of the values in the way they appear on the device.
diff --git a/icons.config.ts b/icons.config.ts
index 0a3d8154..d9f2c21d 100644
--- a/icons.config.ts
+++ b/icons.config.ts
@@ -67,6 +67,8 @@ const config: IconsConfig = {
"bolt",
"undo",
"redo",
+ "navigate_before",
+ "navigate_next",
],
codePoints: {
speed: "e9e4",
diff --git a/src/i18n/de/index.ts b/src/i18n/de/index.ts
index 9bcd411d..952b2a11 100644
--- a/src/i18n/de/index.ts
+++ b/src/i18n/de/index.ts
@@ -62,6 +62,14 @@ const de = {
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
DOWNLOAD_APP: "Desktop-app herunterladen",
},
+ changes: {
+ TITLE: "Änderungen anwenden",
+ CHORD_ADD: "{0} Akkord{{|e}} hinzugefügt",
+ CHORD_EDIT: "{0} Akkord{{|e}} bearbeitet",
+ CHORD_DELETE: "{0} Akkord{{|e}} entfernt",
+ SETTING_CHANGE: "{0} Einstellung{{|en}} geändert",
+ LAYOUT_CHANGE: "{0} Layout-belegung{{|en}} geändert",
+ },
configure: {
chords: {
TITLE: "Akkorde",
diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts
index bbc2738a..052cc552 100644
--- a/src/i18n/en/index.ts
+++ b/src/i18n/en/index.ts
@@ -60,6 +60,14 @@ const en = {
INFO_BROWSER_SUFFIX: ".",
DOWNLOAD_APP: "Download the desktop app",
},
+ changes: {
+ TITLE: "Apply changes",
+ CHORD_ADD: "{0} chord{{|s}} added",
+ CHORD_EDIT: "{0} chord{{|s}} edited",
+ CHORD_DELETE: "{0} chord{{|s}} deleted",
+ SETTING_CHANGE: "{0} setting{{|s}} changed",
+ LAYOUT_CHANGE: "{0} layout key{{|s}} changed",
+ },
configure: {
chords: {
TITLE: "Chords",
diff --git a/src/lib/components/ActionStringEdit.svelte b/src/lib/components/ActionStringEdit.svelte
new file mode 100644
index 00000000..ba61d0d1
--- /dev/null
+++ b/src/lib/components/ActionStringEdit.svelte
@@ -0,0 +1,113 @@
+
+
+
+
+
+ {#each actions as char}
+ {@const action = KEYMAP_CODES[char]}
+ {#if action?.id && /^\w$/.test(action.id)}
+ {KEYMAP_CODES[char].id}
+ {:else if action}
+ {action.icon || action.id}
+ {:else}
+ {action}
+ {/if}
+ {/each}
+
+
+
+
diff --git a/src/lib/components/Tooltip.svelte b/src/lib/components/Tooltip.svelte
index b8153143..ca126d30 100644
--- a/src/lib/components/Tooltip.svelte
+++ b/src/lib/components/Tooltip.svelte
@@ -4,7 +4,7 @@
{#if title}
-
{title}
+ {@html title}
{/if}
{#if shortcut}
diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts
index 97860cf0..eda23baa 100644
--- a/src/lib/serial/connection.ts
+++ b/src/lib/serial/connection.ts
@@ -25,7 +25,7 @@ export const layout = persistentWritable(
export interface Change {
layout?: Record>
- chords?: never
+ chords?: Array>
settings?: Record
}
diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts
index c2451f73..03dba120 100644
--- a/src/lib/serial/device.ts
+++ b/src/lib/serial/device.ts
@@ -42,6 +42,7 @@ export class CharaDevice {
!manual && ports.length === 1
? ports[0]
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
+
await this.port.open({baudRate: this.baudRate})
const info = this.port.getInfo()
serialLog.update(it => {
@@ -53,7 +54,27 @@ export class CharaDevice {
})
return it
})
+ await this.port.close()
+ const [version] = await this.send("VERSION")
+ this.version = version.split(".").map(Number) as [number, number, number]
+ const [company, device, chipset] = await this.send("ID")
+ this.company = company as "CHARACHORDER"
+ this.device = device as "ONE" | "LITE"
+ this.chipset = chipset as "M0" | "S2"
+ }
+
+ private async suspend() {
+ await this.reader.cancel()
+ await this.streamClosed.catch(() => {
+ /** noop */
+ })
+ this.reader.releaseLock()
+ await this.port.close()
+ }
+
+ private async wake() {
+ await this.port.open({baudRate: this.baudRate})
const decoderStream = new TextDecoderStream()
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
signal: this.abortController1.signal,
@@ -64,13 +85,6 @@ export class CharaDevice {
signal: this.abortController2.signal,
})
.getReader()
-
- const [version] = await this.send("VERSION")
- this.version = version.split(".").map(Number) as [number, number, number]
- const [company, device, chipset] = await this.send("ID")
- this.company = company as "CHARACHORDER"
- this.device = device as "ONE" | "LITE"
- this.chipset = chipset as "M0" | "S2"
}
private async internalRead() {
@@ -105,19 +119,9 @@ export class CharaDevice {
}
async forget() {
- await this.disconnect()
await this.port.forget()
}
- async disconnect() {
- await this.reader.cancel()
- await this.streamClosed.catch(() => {
- /** noop */
- })
- this.reader.releaseLock()
- await this.port.close()
- }
-
/**
* Read/write to serial port
*/
@@ -132,8 +136,10 @@ export class CharaDevice {
const exec = new Promise(async resolve => {
let result!: T
try {
+ await this.wake()
result = await callback(send, read)
} finally {
+ await this.suspend()
this.lock = undefined
resolve(result)
}
@@ -252,7 +258,6 @@ export class CharaDevice {
*/
async reboot() {
await this.send("RST")
- await this.disconnect()
// TODO: reconnect
}
@@ -261,7 +266,6 @@ export class CharaDevice {
*/
async bootloader() {
await this.send("RST BOOTLOADER")
- await this.disconnect()
// TODO: more...
}
diff --git a/src/lib/serial/keymap-codes.ts b/src/lib/serial/keymap-codes.ts
index d56f9940..ec32a91b 100644
--- a/src/lib/serial/keymap-codes.ts
+++ b/src/lib/serial/keymap-codes.ts
@@ -19,3 +19,17 @@ export const KEYMAP_CODES: Record = Object.fromEntries(
]),
),
)
+
+export const KEYMAP_IDS: Map = new Map(
+ keymaps
+ .flatMap(category =>
+ Object.entries(category.actions).map(
+ ([code, action]) => [action.id!, {...action, code: Number(code), category}] as const,
+ ),
+ )
+ .filter(([id]) => id !== undefined),
+)
+
+export const specialKeycodes = new Map([
+ [" ", 32], // Space
+])
diff --git a/src/lib/share/chara-file.ts b/src/lib/share/chara-file.ts
index 43b6c858..aed79eb3 100644
--- a/src/lib/share/chara-file.ts
+++ b/src/lib/share/chara-file.ts
@@ -9,7 +9,11 @@ export interface CharaLayoutFile extends CharaFile<"layout"> {
}
export interface CharaChordFile extends CharaFile<"chords"> {
- chords: [number[], number[]]
+ chords: [number[], number[]][]
+}
+
+export interface CharaChordSettings extends CharaFile<"settings"> {
+ settings: number[]
}
export type CharaFiles = CharaLayoutFile | CharaChordFile
diff --git a/src/lib/style/form/_button.scss b/src/lib/style/form/_button.scss
index 88069056..62e83103 100644
--- a/src/lib/style/form/_button.scss
+++ b/src/lib/style/form/_button.scss
@@ -28,6 +28,8 @@ button {
transition: all 250ms ease;
&.icon {
+ display: inline-flex;
+
aspect-ratio: 1;
padding-block: 0;
padding-inline: 0;
@@ -41,6 +43,10 @@ button {
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
}
+
+ &.compact {
+ height: 32px;
+ }
}
label:has(input):hover,
diff --git a/src/routes/EditActions.svelte b/src/routes/EditActions.svelte
index 2ff7c5be..d1fbc903 100644
--- a/src/routes/EditActions.svelte
+++ b/src/routes/EditActions.svelte
@@ -40,8 +40,14 @@
{#if $changes.length !== 0}
- bolt{$LL.saveActions.APPLY()}
{/if}
diff --git a/src/routes/config/chords/+page.svelte b/src/routes/config/chords/+page.svelte
index 84a5b117..bd3da764 100644
--- a/src/routes/config/chords/+page.svelte
+++ b/src/routes/config/chords/+page.svelte
@@ -1,9 +1,29 @@
@@ -35,35 +58,32 @@
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
on:input={search}
/>
+ {page + 1} / {lastPage + 1}
+
+
-
-
-
+
- {#each items.slice(0, 50) as [{ phrase, actions }, i]}
-
+ {#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]}
+
|
- {#each phrase as char}
- {KEYMAP_CODES[char].id}
- {/each}
+
|
- {#each actions as action}
- {@const keyInfo = KEYMAP_CODES[action]}
- {#if keyInfo}
- {keyInfo.icon || keyInfo.id}
- {:else}
- {action}
- {/if}
- {/each}
+
+ |
+
+
+
|
{/each}
@@ -98,16 +118,11 @@
}
section {
- --scrollbar-color: var(--md-sys-color-surface-variant);
-
- scrollbar-gutter: stable;
-
position: relative;
- overflow-x: hidden;
- overflow-y: auto;
- display: flex;
+ overflow: hidden;
+ height: 100%;
padding-inline: 8px;
border-radius: 16px;
@@ -143,10 +158,12 @@
text-align: start;
}
- td {
- display: flex;
- gap: 4px;
- align-items: stretch;
- justify-content: flex-end;
+ .table-buttons {
+ opacity: 0;
+ transition: opacity 75ms ease;
+ }
+
+ tr:hover > .table-buttons {
+ opacity: 1;
}