feat: cookbook

This commit is contained in:
2025-11-28 14:38:51 +01:00
parent 245dd97532
commit 6895fa4a82
66 changed files with 1093 additions and 3386 deletions

View File

@@ -47,9 +47,11 @@ const config = {
"step_over",
"step_into",
"step_out",
"timer_play",
"settings_backup_restore",
"sound_detection_loud_sound",
"ring_volume",
"skillet",
"wifi",
"power_settings_circle",
"graphic_eq",

View File

@@ -1,36 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[c, p]]
output: [CAPITALIZE]
- input: [[a, b]]
output: [a, b, c]
idle: true
- press: [c, p]
keys: [c, p]
- release: [c, p]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,35 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[c, p]]
output: [CAPITALIZE, KSC_00]
- input: [[a, b]]
output: [JOIN, a, b, c, KSC_00]
idle: true
- press: [c, p]
keys: [c, p]
- release: [c, p]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true

View File

@@ -1,33 +0,0 @@
test:
- remap:
A1:
- [d, DUP]
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- press: [d]
keys: [a]
- release: [d]
- step: 16
keys: []
idle: true
- press: [c]
keys: [c]
- release: [c]
- step: 16
keys: []
idle: true
- press: [d]
keys: [c]
- release: [d]
- step: 16
keys: []
idle: true

View File

@@ -1,24 +0,0 @@
test:
- remap:
A1:
- [d, DUP]
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- press: [d]
keys: [a]
modifiers:
lshift: true
- release: [LEFT_SHIFT, d]
- step: 16
keys: []
idle: true

View File

@@ -1,25 +0,0 @@
test:
- remap:
A1:
- [d, DUP]
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [a]
keys: [a]
- press: [d]
keys: []
- keys: [a]
- release: [a, d]
- step: 16
keys: []
idle: true

View File

@@ -1,81 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [q, QUICKFIX]
addChords:
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[t, s]]
output: [s, e, t]
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, t]
keys: [s, t]
- release: [s, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [s]
- keys: [e]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [q]
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- release: [q]
step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,83 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [q, QUICKFIX]
addChords:
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[t, s]]
output: [s, e, t]
settings:
chording:
concatenation style: prepended
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, t]
keys: [s, t]
- release: [s, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [s]
- keys: [e]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [q]
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- release: [q]
step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,65 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [q, QUICKFIX]
addChords:
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[t, s]]
output: [s, e, t]
settings:
chording:
detection method: smart
idle: true
- press: [s, e, t]
- release: [s, e, t]
- step: 16
keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, t]
- release: [s, t]
- step: 16
keys: [s]
- keys: [e]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [q]
- release: [q]
step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
idle: true
- press: [s, e, t]
- release: [s, e, t]
- step: 16
keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,65 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [q, QUICKFIX]
addChords:
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[t, s]]
output: [s, e, t]
settings:
chording:
detection method: smart
concatenation style: prepended
idle: true
- press: [s, e, t]
- release: [s, e, t]
- step: 16
keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, t]
- release: [s, t]
- step: 16
keys: [SPACE]
- keys: [s]
- keys: [e]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [q]
- release: [q]
step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
idle: true
- press: [s, e, t]
- release: [s, e, t]
- step: 16
keys: [SPACE]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,79 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
settings:
arpeggiates:
enable: 1
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [x]
keys: [x]
- release: [x]
- step: 16
keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
idle: true

View File

@@ -1,74 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c, KSC_00]
settings:
arpeggiates:
enable: 1
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [x]
keys: [x]
- release: [x]
- step: 16
keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
idle: true

View File

@@ -1,65 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
settings:
arpeggiates:
enable: 1
autocorrect:
maximum attempts: 0
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,80 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
settings:
arpeggiates:
enable: 1
chording:
concatenation style: prepended
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [x]
keys: [x]
- release: [x]
- step: 16
keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
idle: true

View File

@@ -1,76 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c, KSC_00]
settings:
arpeggiates:
enable: 1
chording:
concatenation style: prepended
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [x]
keys: [x]
- release: [x]
- step: 16
keys: []
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- release: [LEFT_SHIFT]
- step: 16
keys: []
idle: true

View File

@@ -1,63 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
settings:
arpeggiates:
enable: 1
chording:
concatenation style: prepended
autocorrect:
maximum attempts: 0
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
idle: true
- press: [a, b]
keys: [a, b]
- release: [a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true

View File

@@ -1,55 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
settings:
chording:
detection method: smart
arpeggiates:
enable: 1
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [a]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [LEFT_SHIFT]
- release: [LEFT_SHIFT]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- press: [LEFT_SHIFT]
- release: [LEFT_SHIFT]
- step: 16
modifiers:
lshift: true
- modifiers: {}
idle: true

View File

@@ -1,51 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c, KSC_00]
settings:
chording:
detection method: smart
arpeggiates:
enable: 1
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [LEFT_SHIFT]
- release: [LEFT_SHIFT]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- press: [LEFT_SHIFT]
- release: [LEFT_SHIFT]
- step: 16
modifiers:
lshift: true
- modifiers: {}
idle: true

View File

@@ -1,59 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
settings:
chording:
detection method: smart
arpeggiates:
enable: 1
autocorrect:
maximum attempts: 0
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [.]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [a]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,51 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
settings:
arpeggiates:
enable: 1
chording:
detection method: smart
concatenation style: prepended
autocorrect:
maximum attempts: 0
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [.]
- keys: []
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [a]
- keys: [b]
- keys: [c]
- keys: []
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [.]
- keys: []
idle: true
- press: [a, b]
- release: [a, b]
- step: 16
keys: [SPACE]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: []
idle: true

View File

@@ -1,76 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [d, DUP]
addChords:
- input: [[h, z]]
output: [t, h, e]
- input: [[m, o, h, DUP, a, y]]
output: [m, o, t, i, v, a, t, i, o, n]
settings:
chording:
detection method: smart
concatenation style: prepended
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, z]
- release: [h, z]
- step: 16
keys: [BKSP]
- keys: [t]
- keys: [h]
- keys: [e]
- keys: []
idle: true
- step: 16
idle: true
- press: [d]
- press: [m, h, a, y]
- release: [d, m, h, a, y]
- step: 16
keys: [e, a, h, m, y]
- keys: []
idle: true
- step: 16
idle: true
- press: [d]
- press: [m, o, h, a, y]
- release: [d, m, o, h, a, y]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [m]
- keys: [o]
- keys: [t]
- keys: [i]
- keys: [v]
- keys: [a]
- keys: [t]
- keys: [i]
- keys: [o]
- keys: [n]
- keys: []
idle: true

View File

@@ -1,67 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [d, DUP]
addChords:
- input: [[h, z]]
output: [t, h, e]
- input: [[m, o, h, DUP, a, y]]
output: [m, o, t, i, v, a, t, i, o, n]
settings:
chording:
detection method: smart
concatenation style: prepended
autocorrect:
maximum attempts: 0
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, z]
- release: [h, z]
- step: 16
keys: [t]
- keys: [h]
- keys: [e]
- keys: []
idle: true
- step: 16
idle: true
- press: [d]
- press: [m, h, a, y]
- release: [d, m, h, a, y]
- step: 16
keys: [e, a, h, m, y]
- keys: []
idle: true
- step: 16
idle: true
- press: [d]
- press: [m, o, h, a, y]
- release: [d, m, o, h, a, y]
- step: 16
keys: [m]
- keys: [o]
- keys: [t]
- keys: [i]
- keys: [v]
- keys: [a]
- keys: [t]
- keys: [i]
- keys: [o]
- keys: [n]
- keys: []
idle: true

View File

@@ -1,24 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s, e]]
output: [t, e, s, t]
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,23 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s, e]]
output: [t, e, s, t, KSC_00]
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,78 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[i, n, g]]
output: [t, h, i, n, g]
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[i, n, g], [t, s, e]]
output: [t, e, s, t, i, n, g]
idle: true
- press: [i, n, g]
keys: [g, i, n]
- release: [i, n, g]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [h]
- keys: [i]
- keys: [n]
- keys: [g]
- keys: [SPACE]
- keys: []
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- press: [i, n, g]
keys: [g, i, n]
- release: [i, n, g]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [i]
- keys: [n]
- keys: [g]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,45 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [d, DUP]
addChords:
- input: [[DUP, s, e]]
output: [t, e, s, t]
settings:
autocorrect:
maximum attempts: 0
idle: true
- press: [s]
keys: [s]
- release: [s]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [s]
keys: [s]
- press: [d]
keys: []
- keys: [s]
- press: [e]
keys: [s, e]
- release: [d, e, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,27 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t]]
output: [t, e, s, t]
idle: true
- press: [t]
keys: [t]
- release: [t]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [t]
keys: [t]
- press: [LEFT_SHIFT]
keys: [t]
modifiers:
lshift: true
- release: [LEFT_SHIFT, t]
- step: 16
keys: []
idle: true

View File

@@ -1,58 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[SPACE, s]]
output: [a, b]
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- press: [SPACE]
keys: [SPACE]
- release: [SPACE]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [SPACE]
keys: [SPACE]
- press: [s]
keys: [SPACE, s]
- release: [SPACE, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [s]
keys: [s]
- press: [SPACE]
keys: [s, SPACE]
- release: [SPACE, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: [b]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,28 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, h, e]]
output: [t, h, e]
settings:
keyboard:
debounce press: 4
debounce release: 5
idle: true
- press: [t]
keys: [t]
- release: [t]
- press: [a]
keys: [t, a]
- release: [a]
- keys: [a]
- press: [t]
- keys: []
- step: 1
- step: 1
- keys: [t]
- release: [t]
- step: 1
- step: 1
- keys: []
idle: true

View File

@@ -1,34 +0,0 @@
test:
- clearChords: true
remap:
A1:
- [l, KM_3_L]
- [r, KM_3_R]
- [a, a]
A2:
- [l, KM_3_L]
- [r, KM_3_R]
- [a, b]
A3:
- [l, KM_3_L]
- [r, KM_3_R]
- [a, c]
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [l]
- press: [a]
keys: [c]
- release: [l, a]
- step: 16
keys: []
idle: true

View File

@@ -1,82 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, y]]
output: [CAPTURE, CAPITALIZE, CAPTURE]
- input: [[t, s, e]]
output: [t, e, s, t]
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, y]
keys: [h, y]
- release: [h, y]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,83 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, y]]
output: [CAPTURE, "-", CAPTURE]
- input: [[t, s, e]]
output: [t, e, s, t]
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, y]
keys: [h, y]
- release: [h, y]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: ["-"]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: ["-"]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: ["-"]
- keys: []
idle: true

View File

@@ -1,85 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, y]]
output: [CAPTURE, CAPITALIZE, CAPTURE]
- input: [[t, s, e]]
output: [t, e, s, t]
settings:
chording:
concatenation style: prepended
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, y]
keys: [h, y]
- release: [h, y]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,85 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, y]]
output: [CAPTURE, "-", CAPTURE]
- input: [[t, s, e]]
output: [t, e, s, t]
settings:
chording:
concatenation style: prepended
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, y]
keys: [h, y]
- release: [h, y]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: ["-"]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- step: 16
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: ["-"]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true

View File

@@ -1,28 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, b]]
output: [a, b, c]
idle: true
- press: [LEFT_SHIFT]
modifiers:
lshift: true
- press: [LEFT_SHIFT, a, b]
modifiers:
lshift: true
keys: [a, b]
- release: [LEFT_SHIFT, a, b]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
modifiers:
lshift: true
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,80 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[i, n, g]]
output: [t, h, i, n, g]
- input: [[t, s, e]]
output: [t, e, s, t]
- input: [[i, n, g], [t, s, e]]
output: [t, e, s, t, i, n, g]
settings:
chording:
concatenation style: prepended
idle: true
- press: [i, n, g]
keys: [g, i, n]
- release: [i, n, g]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [h]
- keys: [i]
- keys: [n]
- keys: [g]
- keys: []
idle: true
- press: [s, e, t]
keys: [e, s, t]
- release: [s, e, t]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: []
idle: true
- press: [i, n, g]
keys: [g, i, n]
- release: [i, n, g]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [SPACE]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [i]
- keys: [n]
- keys: [g]
- keys: []
idle: true

View File

@@ -1,45 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, z]]
output: [t, h, e]
settings:
chording:
detection method: smart
concatenation style: prepended
autocorrect:
maximum attempts: 0
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- press: [h, z]
- release: [h, z]
- step: 16
keys: [t]
- keys: [h]
- keys: [e]
- keys: []
idle: true
- press: [x]
- release: [x]
- step: 16
keys: [x]
- keys: []
idle: true
- step: 16
idle: true
- press: [x]
- step: 120
keys: [x]
- release: [x]
keys: []
idle: true

View File

@@ -1,49 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[h, z]]
output: [t, h, e]
settings:
chording:
detection method: smart
idle: true
- press: [ENTER]
- release: [ENTER]
- step: 16
keys: [ENTER]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, z]
- release: [h, z]
- step: 16
keys: [t]
- keys: [h]
- keys: [e]
- keys: [SPACE]
- keys: []
idle: true
- press: [ENTER]
- release: [ENTER]
- step: 16
keys: [ENTER]
- keys: []
idle: true
- step: 16
idle: true
- press: [h, z]
- release: [h, z]
- step: 16
keys: [t]
- keys: [h]
- keys: [e]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,109 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s]]
output: [t, e, s, t]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
- input: [[., .]]
output: [JOIN, ., KSC_00]
- input: [[., ., .]]
output: [JOIN, ., ., ., CAPITALIZE, JOIN]
settings:
chording:
tap dance tolerance: 175
autocorrect:
maximum attempts: 0
arpeggiates:
enable: 1
idle: true
- press: [t, s]
keys: [s, t]
- release: [t, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
- keys: [.]
- keys: []
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [t, s]
keys: [s, t]
- release: [t, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,102 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s]]
output: [t, e, s, t]
- input: [[., .]]
output: [JOIN, ., CAPITALIZE, JOIN]
- input: [[., ., .]]
output: [JOIN, ., ., ., CAPITALIZE, JOIN]
settings:
chording:
tap dance tolerance: 175
autocorrect:
maximum attempts: 0
idle: true
- press: [t, s]
keys: [s, t]
- release: [t, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
keys: [.]
- release: [.]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
- keys: [.]
- keys: []
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [t, s]
keys: [s, t]
- release: [t, s]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,103 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, a]]
output: [b, c]
- input: [[a, a, a, a, a]]
output: [d, e]
settings:
chording:
tap dance tolerance: 175
autocorrect:
maximum attempts: 0
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- step: 180
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: []
- keys: [a]
- keys: []
- keys: [a]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
idle: true
- step: 16
idle: true
- press: [a]
keys: [a]
- release: [a]
- step: 16
keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [d]
- keys: [e]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,88 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s]]
output: [t, e, s, t]
- input: [[ARPEGGIATE, .]]
output: [JOIN, ., CAPITALIZE, JOIN]
- input: [[., .]]
output: [JOIN, ., KSC_00]
- input: [[., ., .]]
output: [JOIN, ., ., ., CAPITALIZE, JOIN]
settings:
chording:
tap dance tolerance: 175
detection method: smart
autocorrect:
maximum attempts: 0
arpeggiates:
enable: 1
idle: true
- press: [t, s]
- release: [t, s]
- step: 16
keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: [.]
- keys: []
- keys: [.]
- keys: []
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [t, s]
- release: [t, s]
- step: 16
keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,85 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[t, s]]
output: [t, e, s, t]
- input: [[., .]]
output: [JOIN, ., CAPITALIZE, JOIN]
- input: [[., ., .]]
output: [JOIN, ., ., ., CAPITALIZE, JOIN]
settings:
chording:
tap dance tolerance: 175
detection method: smart
autocorrect:
maximum attempts: 0
idle: true
- press: [t, s]
- release: [t, s]
- step: 16
keys: [t]
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [.]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [.]
- release: [.]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [.]
- keys: []
- keys: [.]
- keys: []
- keys: [.]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [t, s]
- release: [t, s]
- step: 16
keys: [t]
modifiers:
lshift: true
- keys: [e]
- keys: [s]
- keys: [t]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -1,92 +0,0 @@
test:
- clearChords: true
addChords:
- input: [[a, a]]
output: [b, c]
- input: [[a, a, a, a, a]]
output: [d, e]
settings:
chording:
tap dance tolerance: 175
detection method: smart
autocorrect:
maximum attempts: 0
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [a]
- keys: []
idle: true
- step: 180
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [a]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [BKSP]
- keys: [b]
- keys: [c]
- keys: [SPACE]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [a]
- keys: []
- keys: [a]
- keys: []
- keys: [a]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [a]
- keys: []
idle: true
- step: 16
idle: true
- press: [a]
- release: [a]
- step: 16
keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: []
- keys: [BKSP]
- keys: [d]
- keys: [e]
- keys: [SPACE]
- keys: []
idle: true

View File

@@ -10,14 +10,18 @@
replay,
cursor = false,
keys = false,
paused = false,
children,
ondone,
ontick,
}: {
replay: ReplayPlayer | Replay;
cursor?: boolean;
keys?: boolean;
paused?: boolean;
children?: Snippet;
ondone?: () => void;
ontick?: (time: number) => void;
} = $props();
let replayPlayer: ReplayPlayer | undefined = $state();
@@ -45,6 +49,10 @@
$effect(() => {
if (!svg || !text) return;
if (paused) {
text.textContent = finalText ?? "";
return;
}
const player =
replay instanceof ReplayPlayer ? replay : new ReplayPlayer(replay);
replayPlayer = player;
@@ -63,6 +71,7 @@
const unsubscribePlayer = player.subscribe(apply);
textRenderer = renderer;
player.onTick = ontick;
player.onDone = ondone;
player.start();
apply();
@@ -70,8 +79,11 @@
renderer.animated = true;
});
return () => {
textRenderer = undefined;
replayPlayer = undefined;
unsubscribePlayer();
player?.destroy();
player.destroy();
renderer.destroy();
};
});
@@ -88,7 +100,7 @@
{#key replay}
<svg bind:this={svg}></svg>
{#if browser}
<span use:innerText={text}></span>
<span use:innerText={text} style:opacity={paused ? 1 : 0}></span>
{:else if !(replay instanceof ReplayPlayer)}
{finalText}
{/if}
@@ -104,7 +116,6 @@
}
span {
opacity: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
}

View File

@@ -12,7 +12,10 @@ export class ReplayPlayer {
startTime = performance.now();
private animationFrameId: number | null = null;
private animationFrameId: ReturnType<typeof requestAnimationFrame> | null =
null;
private timeoutId: ReturnType<typeof setTimeout> | null = null;
timescale = 1;
@@ -20,6 +23,8 @@ export class ReplayPlayer {
onDone?: () => void;
onTick?: (time: number) => void;
constructor(
readonly replay: Replay,
plugins: ReplayPlugin[] = [],
@@ -47,6 +52,7 @@ export class ReplayPlayer {
}
const now = performance.now() - this.startTime;
this.onTick?.(now);
while (
this.replayCursor < this.replay.keys.length &&
@@ -131,7 +137,7 @@ export class ReplayPlayer {
}
return this;
}
setTimeout(() => {
this.timeoutId = setTimeout(() => {
this.startTime = performance.now();
this.animationFrameId = requestAnimationFrame(this.updateLoop.bind(this));
}, delay);
@@ -139,6 +145,9 @@ export class ReplayPlayer {
}
destroy() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}

View File

@@ -279,6 +279,18 @@ export class TextRenderer {
}
}
destroy() {
this.cursorNode.remove();
for (const node of this.nodes.values()) {
node.remove();
}
for (const node of this.heldNodes.values()) {
node.remove();
}
this.nodes.clear();
this.heldNodes.clear();
}
private isShiny(char: TextToken, index: number) {
return (
this.shiny?.includes(index) ||

View File

@@ -0,0 +1,367 @@
<script lang="ts">
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import { onMount, tick } from "svelte";
import { scale } from "svelte/transition";
import ActionString from "$lib/components/ActionString.svelte";
import { deviceMeta, serialPort } from "$lib/serial/connection";
import { get } from "svelte/store";
import { action } from "$lib/title";
import semverGte from "semver/functions/gte";
import { inputToAction } from "../../routes/(app)/config/chords/input-converter";
import { selectAction } from "../../routes/(app)/config/chords/action-selector";
interface InteractiveProps {
interactive: true;
ondeleteaction: (at: number, count?: number) => void;
oninsertaction: (at: number, action: number) => void;
}
interface NonInteractiveProps {
interactive: false;
ondeleteaction?: never;
oninsertaction?: never;
}
let {
phrase,
edited,
interactive,
oninsertaction,
ondeleteaction,
}: { phrase: number[]; edited: boolean } & (
| NonInteractiveProps
| InteractiveProps
) = $props();
const JOIN_ACTION = 574;
const NO_CONCATENATOR_ACTION = 256;
onMount(() => {
if (interactive && phrase.length === 0) {
box?.focus();
}
});
function keypress(event: KeyboardEvent) {
if (!event.shiftKey && event.key === "ArrowUp") {
addSpecial(event);
} else if (!event.shiftKey && event.key === "ArrowLeft") {
moveCursor(cursorPosition - 1);
} else if (!event.shiftKey && event.key === "ArrowRight") {
moveCursor(cursorPosition + 1);
} else if (event.key === "Backspace") {
if (interactive) {
ondeleteaction!(cursorPosition - 1);
}
moveCursor(cursorPosition - 1);
} else if (event.key === "Delete") {
if (interactive) {
ondeleteaction!(cursorPosition);
}
} else {
if (event.key === "Shift") return;
const action = inputToAction(event, get(serialPort)?.device === "X");
if (action !== undefined) {
oninsertaction!(cursorPosition, action);
tick().then(() => moveCursor(cursorPosition + 1));
}
}
}
function moveCursor(to: number) {
if (!box) return;
cursorPosition = Math.max(0, Math.min(to, phrase.length));
const item = box.children.item(cursorPosition) as HTMLElement;
cursorOffset = item.offsetLeft + item.offsetWidth;
}
function clickCursor(event: MouseEvent) {
if (box === undefined || event.target === button) return;
const distance = (event as unknown as { layerX: number }).layerX;
let i = 0;
for (const child of box.children) {
const { offsetLeft, offsetWidth } = child as HTMLElement;
if (distance < offsetLeft + offsetWidth / 2) {
moveCursor(i - 1);
return;
}
i++;
}
moveCursor(i - 1);
}
function addSpecial(event: MouseEvent | KeyboardEvent) {
selectAction(
event,
(action) => {
oninsertaction!(cursorPosition, action);
tick().then(() => moveCursor(cursorPosition + 1));
},
() => box?.focus(),
);
}
function resolveAutospace(autospace: boolean) {
if (autospace) {
if (phrase.at(-1) === JOIN_ACTION) {
if (
phrase.every(
(action, i, arr) =>
$KEYMAP_CODES.get(action)?.printable || i === arr.length - 1,
)
) {
ondeleteaction!(phrase.length - 1);
} else {
return;
}
} else {
if (isPrintable) {
return;
} else if (phrase.at(-1) === NO_CONCATENATOR_ACTION) {
ondeleteaction!(phrase.length - 1);
} else {
oninsertaction!(phrase.length, JOIN_ACTION);
}
}
} else {
if (phrase.at(-1) === JOIN_ACTION) {
ondeleteaction!(phrase.length - 1);
} else {
if (phrase.at(-1) === NO_CONCATENATOR_ACTION) {
if (
phrase.every(
(action, i, arr) =>
$KEYMAP_CODES.get(action)?.printable || i === arr.length - 1,
)
) {
return;
} else {
ondeleteaction!(phrase.length - 1);
}
} else {
oninsertaction!(phrase.length, NO_CONCATENATOR_ACTION);
}
}
}
}
let button: HTMLButtonElement | undefined = $state();
let box: HTMLDivElement | undefined = $state();
let cursorPosition = 0;
let cursorOffset = $state(0);
let hasFocus = $state(false);
let isPrintable = $derived(
phrase.every((action) => $KEYMAP_CODES.get(action)?.printable === true),
);
let supportsAutospace = $derived(
semverGte($deviceMeta?.version ?? "0.0.0", "2.1.0"),
);
let hasAutospace = $derived(isPrintable || phrase.at(-1) === JOIN_ACTION);
let displayPhrase = $derived(
phrase.filter(
(it, i, arr) =>
!(
(i === 0 && it === JOIN_ACTION) ||
(i === arr.length - 1 &&
(it === JOIN_ACTION || it === NO_CONCATENATOR_ACTION))
),
),
);
</script>
<div
class="wrapper"
class:edited
onclick={interactive
? () => {
box.focus();
}
: undefined}
>
{#if supportsAutospace}
<label
class="auto-space-edit"
use:action={{ title: "Remove previous concatenator" }}
><span class="icon">join_inner</span><input
checked={phrase[0] === JOIN_ACTION}
disabled={!interactive}
onchange={interactive
? (event) => {
const autospace = hasAutospace;
if ((event.target as HTMLInputElement).checked) {
if (phrase[0] !== JOIN_ACTION) {
oninsertaction!(0, JOIN_ACTION);
}
} else {
if (phrase[0] === JOIN_ACTION) {
ondeleteaction!(0, 1);
}
}
tick().then(() => resolveAutospace(autospace));
}
: undefined}
type="checkbox"
/></label
>
{/if}
<div
onkeydown={interactive ? keypress : undefined}
onmousedown={interactive ? clickCursor : undefined}
role="textbox"
tabindex="0"
bind:this={box}
onfocusin={interactive ? () => (hasFocus = true) : undefined}
onfocusout={interactive
? (event) => {
if (event.relatedTarget !== button) hasFocus = false;
}
: undefined}
>
{#if hasFocus}
<div transition:scale class="cursor" style:translate="{cursorOffset}px 0">
<button class="icon" bind:this={button} onclick={addSpecial}>add</button
>
</div>
{:else}
<div></div>
<!-- placeholder for cursor placement -->
{/if}
<ActionString actions={displayPhrase} />
</div>
{#if supportsAutospace}
<label class="auto-space-edit" use:action={{ title: "Add concatenator" }}
><span class="icon">space_bar</span><input
checked={hasAutospace}
disabled={!interactive}
onchange={interactive
? (event) =>
resolveAutospace((event.target as HTMLInputElement).checked)
: undefined}
type="checkbox"
/></label
>
{/if}
<sup></sup>
</div>
<style lang="scss">
sup {
translate: 0 -40%;
opacity: 0;
transition: opacity 250ms ease;
}
.cursor {
position: absolute;
transform: translateX(-50%);
translate: 0 0;
transition: translate 50ms ease;
background: var(--md-sys-color-on-secondary-container);
width: 2px;
height: 100%;
button {
position: absolute;
top: -24px;
left: 0;
border: 2px solid currentcolor;
border-radius: 12px 12px 12px 0;
background: var(--md-sys-color-secondary-container);
padding: 0;
height: 24px;
color: var(--md-sys-color-on-secondary-container);
}
}
.edited {
color: var(--md-sys-color-primary);
sup {
opacity: 1;
}
}
.auto-space-edit {
margin-inline: 8px;
border-radius: 4px;
background: var(--md-sys-color-tertiary-container);
padding-inline: 0;
height: 1em;
color: var(--md-sys-color-on-tertiary-container);
font-size: 1.3em;
&:first-of-type:not(:has(:checked)),
&:last-of-type:has(:checked) {
opacity: 0;
}
}
.wrapper:hover .auto-space-edit {
opacity: 1;
}
.wrapper {
display: flex;
position: relative;
align-items: center;
padding-block: 4px;
height: 1em;
&::after,
&::before {
position: absolute;
bottom: -4px;
opacity: 0;
transition:
opacity 150ms ease,
scale 250ms ease;
background: currentcolor;
width: calc(100% - 8px);
height: 1px;
content: "";
}
&::after {
scale: 0 1;
transition-duration: 250ms;
}
&:hover::before {
opacity: 0.3;
}
&:has(> :focus-within)::after {
scale: 1;
opacity: 1;
}
}
[role="textbox"] {
display: flex;
position: relative;
align-items: center;
cursor: text;
white-space: pre;
&:focus-within {
outline: none;
}
}
</style>

View File

@@ -133,14 +133,16 @@
/></label
>
{#each $KEYMAP_CATEGORIES as category}
<label
>{category.name}<input
name="category"
type="radio"
value={new Set(Object.keys(category.actions).map(Number))}
bind:group={filter}
/></label
>
{#if category.name !== "Internal"}
<label
>{category.name}<input
name="category"
type="radio"
value={new Set(Object.keys(category.actions).map(Number))}
bind:group={filter}
/></label
>
{/if}
{/each}
</fieldset>
{#if currentAction !== undefined}

View File

@@ -17,7 +17,7 @@ export async function getMeta(
try {
if (!browser) return fetchMeta(device, version, fetch);
const dbRequest = indexedDB.open("version-meta", 5);
const dbRequest = indexedDB.open("version-meta", 6);
const db = await new Promise<IDBDatabase>((resolve, reject) => {
dbRequest.onsuccess = () => resolve(dbRequest.result);
dbRequest.onerror = () => reject(dbRequest.error);
@@ -130,6 +130,9 @@ async function fetchMeta(
async (load) => load().then((it) => (it as any).default),
),
)),
recipes: await (meta?.recipes
? fetch(`${path}/${meta.recipes}`).then((it) => it.json())
: undefined),
update: {
uf2:
meta?.update?.uf2 ??

View File

@@ -13,6 +13,7 @@ export interface SettingsMeta {
export interface SettingsItemMeta {
id: number;
name?: string;
description?: string;
enum?: string[];
range: [number, number];
@@ -43,6 +44,7 @@ export interface RawVersionMeta {
actions: string;
settings: string;
changelog: string;
recipes: string;
factory_defaults: {
layout: string;
settings: string;
@@ -61,6 +63,38 @@ export interface RawVersionMeta {
spi_flash: SPIFlashInfo | null;
}
export interface E2eAddChord {
input: string[][];
output: string[];
}
export interface E2eTestItem {
keys?: string[];
modifiers?: Record<string, boolean>;
press?: string[];
release?: string[];
step?: number;
idle?: boolean;
clearChords?: boolean;
addChords?: E2eAddChord[];
settings: Record<string, Record<string, string | number>>;
}
export interface E2eTest {
matrix?: string[];
test: E2eTestItem[];
}
export interface E2eDemo {
demo?: {
id?: string;
title?: string;
description?: string;
};
matrix?: string[];
tests: E2eTest[];
}
export interface VersionMeta {
version: string;
device: string;
@@ -73,6 +107,7 @@ export interface VersionMeta {
actions: KeymapCategory[];
settings: SettingsMeta[];
changelog: Changelog;
recipes?: E2eTest[];
factoryDefaults?: {
layout: CharaLayoutFile;
settings: CharaSettingsFile;

View File

@@ -11,6 +11,7 @@ async function updateLayout() {
layout.size !== currentLayout.size ||
[...layout.keys()].some((key) => layout.get(key) !== currentLayout.get(key))
) {
console.log(layout);
osLayout.set(layout);
}
}

View File

@@ -29,5 +29,10 @@ export async function fromBase64(
.replaceAll(".", "+")
.replaceAll("_", "/")
.replaceAll("-", "=")}`,
).then((it) => it.blob());
)
.then((it) => {
console.log(it);
return it;
})
.then((it) => it.blob());
}

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { page } from "$app/stores";
import { deviceMeta } from "$lib/serial/connection";
const routes = [
let routes = $derived([
[
{
href: "/config/settings/",
@@ -11,6 +12,9 @@
},
{ href: "/config/chords/", icon: "dictionary", title: "Library" },
{ href: "/config/layout/", icon: "keyboard", title: "Layout" },
...($deviceMeta?.recipes
? [{ href: "/recipes", icon: "skillet", title: "Cookbook" }]
: []),
],
[
{
@@ -47,7 +51,7 @@
wip?: boolean;
external?: boolean;
primary?: boolean;
}[][];
}[][]);
let connectButton: HTMLButtonElement;
</script>

View File

@@ -19,12 +19,7 @@
{#each data.versions as version}
{@const isPrerelease = version.name.includes("-")}
<li class:pre-release={isPrerelease}>
<a href="./{version.name}/"
>{version.name}
<time datetime={version.mtime}
>{new Date(version.mtime).toLocaleDateString()}</time
></a
>
<a href="./{version.name}/">{version.name}</a>
</li>
{/each}
</ul>
@@ -70,14 +65,6 @@
}
}
time {
opacity: 0.5;
&:before {
padding-inline: 0.4ch;
content: "•";
}
}
div.title:has(input:not(:checked)) ~ ul .pre-release {
opacity: 0;
height: 0;

View File

@@ -344,6 +344,15 @@
<section class="changelog">
<h2>Changelog</h2>
<time datetime={data.meta.date.toISOString()}
>Published {data.meta.date.toLocaleDateString()}</time
>
{#if data.meta.recipes}
<p>Includes {data.meta.recipes.length} recipes</p>
{/if}
{#if data.meta.changelog.features}
<h3>Features</h3>
<ul>

View File

@@ -1,232 +0,0 @@
<script lang="ts">
import {
compileLayout,
type VisualLayout,
} from "$lib/serialization/visual-layout";
import ccxLayout from "$lib/assets/layouts/generic/103-key.yml";
import keycodes from "./keycodes.json";
let width = $state(16);
let height = $state(16);
let layout = $state(compileLayout(ccxLayout as VisualLayout));
let layoutMargin = $state(0.2);
let timelineCanvas = $state<HTMLCanvasElement | undefined>(undefined);
interface Report {
modifiers?: number;
keys?: number[];
}
interface Tick {
ms?: number;
reports?: Report[];
keys?: number[];
}
let test: Tick[] = $state([
{ ms: 1, reports: [{ keys: [4] }], keys: [4] },
{ ms: 2, reports: [{ keys: [4, 2] }], keys: [4, 12] },
]);
function timelineData<T extends { ms: number }>(
ticks: T[],
value: (tick: T) => number[],
) {
let totalTicks = 0;
const result = new Map<number, [number, number][]>();
for (const tick of ticks) {
const key = value(tick);
}
}
let timelineData = $derived.by(() => {
const result = new Map<number, [number, number][]>();
for (const tick of test) {
if (!tick.keys) continue;
if (Array.isArray(action)) {
if (typeof action[0] === "number") {
ticks.push([action[0]]);
totalTicks++;
} else if (action.length === 0) {
ticks.push([1]);
totalTicks++;
}
}
if (typeof action !== "number") continue;
if (action >= 0) {
if (!result.has(action)) {
result.set(action, []);
}
result.get(action)!.push([totalTicks, test.length - 1]);
} else {
const value = result.get(~action)?.at(-1);
if (!value || value[1] !== test.length - 1) continue;
value[1] = totalTicks;
}
}
return {
totalTicks,
ticks,
presses: [...result.entries()].sort(([a], [b]) => a - b),
};
});
</script>
<h1>E2E Testing</h1>
{#snippet Layout(keys: Set<number>)}
<svg viewBox="0 0 {layout.size[0]} {layout.size[1]}">
{#each layout.keys as key}
{#if key.shape === "square"}
<rect
x={key.pos[0] + layoutMargin / 2}
y={key.pos[1] + layoutMargin / 2}
rx={0.5 - layoutMargin / 2}
width={key.size[0] - layoutMargin}
height={key.size[1] - layoutMargin}
fill={keys.has(key.id)
? "var(--md-sys-color-primary)"
: "var(--md-sys-color-on-surface)"}
opacity={keys.has(key.id) ? 1 : 0.1}
/>
{/if}
{/each}
</svg>
{/snippet}
<canvas bind:this={timelineCanvas}></canvas>
<div class="t">
{#each test as { ms, reports, keys }}
<div class="tick">
{ms}ms
<div class="keys">
{#each keys ?? [] as key}
<kbd>{keycodes[key] ?? key}</kbd>
{/each}
<button class="icon">+</button>
</div>
{@render Layout(new Set(keys))}
{#each reports ?? [] as report}
<div class="report">
<div class="modifiers">{report.modifiers}</div>
<div class="keys">
{#each report.keys ?? [] as key}
<kbd>{keycodes[key] ?? key}</kbd>
{/each}
</div>
</div>
{/each}
</div>
{/each}
</div>
<div class="actions">
{#each test as action, i}
{@const isActionTick = Array.isArray(action)}
{@const isActionPress = typeof action === "number" && action >= 0}
{@const isActionRelease = typeof action === "number" && action < 0}
{#if isActionTick}
<div class="tick">
<span class="icon">step_over</span>
{action[0]}ms
</div>
{#if action[1]}
<div class="report">
{#each Array.from({ length: 8 }) as _, j}
<div class="modifier">{j}</div>
{/each}
{#each action[1][1] as key}
<div class="key">
{key}
</div>
{/each}
</div>
{/if}
{:else if typeof action === "string"}
<div>Command: {action}</div>
{:else if isActionPress}
<button class="release" onclick={() => (test[i] = ~action)}
>{action}</button
>
{:else if isActionRelease}
<button class="press" onclick={() => (test[i] = ~action)}
>{~action}</button
>
{:else}
<div>Unsupported {action}</div>
{/if}
{/each}
</div>
<style lang="scss">
svg {
width: 100px;
}
$shadow-inset: 1px;
.timeline {
display: grid;
grid-template-rows: auto repeat(auto-fit, minmax(var(--height), 1fr));
}
.timeline-press {
margin-inline: calc(var(--width) / 2);
border-radius: calc(var(--height) / 2);
background-color: var(--md-sys-color-surface-variant);
height: var(--height);
}
.actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.tick {
display: flex;
position: relative;
flex-direction: column;
align-items: center;
gap: 0.5rem;
cursor: ew-resize;
padding: 0.5rem;
user-select: none;
span.icon {
position: absolute;
top: 0;
transform: translateY(-50%);
}
}
button {
cursor: pointer;
margin: 0;
border: none;
padding: 8px;
aspect-ratio: 1;
user-select: none;
&.release {
box-shadow:
inset #{$shadow-inset} #{$shadow-inset} #{$shadow-inset * 2}
rgba(0, 0, 0, 0.6),
inset -#{$shadow-inset} -#{$shadow-inset} #{$shadow-inset * 2}
rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
}
&.press {
box-shadow:
#{$shadow-inset} #{$shadow-inset} #{$shadow-inset * 2}
rgba(0, 0, 0, 0.6),
-#{$shadow-inset} -#{$shadow-inset} #{$shadow-inset * 2}
rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
}
}
</style>

View File

@@ -1,251 +0,0 @@
[
"reserved",
"esc",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"-",
"=",
"bksp",
"tab",
"q",
"w",
"e",
"r",
"t",
"y",
"u",
"i",
"o",
"p",
"[",
"]",
"enter",
"lctrl",
"a",
"s",
"d",
"f",
"g",
"h",
"j",
"k",
"l",
";",
"'",
"`",
"lshift",
"\\",
"z",
"x",
"c",
"v",
"b",
"n",
"m",
",",
".",
"/",
"rshift",
"kp*",
"lalt",
"_",
"capslock",
"f1",
"f2",
"f3",
"f4",
"f5",
"f6",
"f7",
"f8",
"f9",
"f10",
"numlock",
"scrolllock",
"kp7",
"kp8",
"kp9",
"kp-",
"kp4",
"kp5",
"kp6",
"kp+",
"kp1",
"kp2",
"kp3",
"kp0",
"kp.",
"ksc_84",
"zenkaku_hankaku",
"102nd",
"f11",
"f12",
"ro",
"katakana",
"hiragana",
"henkan",
"katakana_hiragana",
"muhenkan",
"kp,",
"kp_enter",
"rctrl",
"kp/",
"sysrq",
"ralt",
"linefeed",
"home",
"up",
"pageup",
"left",
"right",
"end",
"down",
"pagedown",
"insert",
"delete",
"macro",
"mute",
"volume_down",
"volume_up",
"power",
"kp=",
"kp+-",
"pause",
"scale",
"kp,",
"hangeul",
"hanja",
"yen",
"lmeta",
"rmeta",
"compose",
"stop",
"again",
"props",
"undo",
"front",
"copy",
"open",
"paste",
"find",
"cut",
"help",
"menu",
"calc",
"setup",
"sleep",
"wakeup",
"file",
"sendfile",
"deletefile",
"xfer",
"prog1",
"prog2",
"www",
"msdos",
"coffee",
"rotate_display",
"cyclewindows",
"mail",
"bookmarks",
"computer",
"back",
"forward",
"close_cd",
"eject_cd",
"eject_close_cd",
"next_song",
"play_pause",
"prev_song",
"stop_cd",
"record",
"rewind",
"phone",
"iso",
"config",
"homepage",
"refresh",
"exit",
"move",
"edit",
"scroll_up",
"scroll_down",
"kp_left_paren",
"kp_right_paren",
"new",
"redo",
"f13",
"f14",
"f15",
"f16",
"f17",
"f18",
"f19",
"f20",
"f21",
"f22",
"f23",
"f24",
"sc_195",
"sc_196",
"sc_197",
"sc_198",
"sc_199",
"play_cd",
"pause_cd",
"prog3",
"prog4",
"all_applications",
"suspend",
"close",
"play",
"fastforward",
"bass_boost",
"print",
"hp",
"camera",
"sound",
"question",
"email",
"chat",
"search",
"connect",
"finance",
"sport",
"shop",
"alterase",
"cancel",
"brightness_down",
"brightness_up",
"media",
"switch_video_mode",
"kbd_illum_toggle",
"kbd_illum_down",
"kbd_illum_up",
"send",
"reply",
"forward_mail",
"save",
"documents",
"battery",
"bluetooth",
"wlan",
"uwb",
"unknown",
"video_next",
"video_prev",
"brightness_cycle",
"brightness_auto",
"display_off",
"wwan",
"rfkill",
"mic_mute"
]

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
$effect(() => {
console.log(data);
});
</script>
<details>
<summary>Full Log</summary>
{#each data.data as item, i}
{#if "press" in item}
<div class="press">{item.press}</div>
{:else if "release" in item}
<div class="release">{item.release}</div>
{:else if "keys" in item}
<div class="report">
<span class="icon">keyboard</span>
<div class="modifiers">
{item.modifiers.toString(2)}
</div>
<div class="keys">{item.keys.join(", ")}</div>
</div>
{:else if "out" in item}
<pre class="out">{item.out}</pre>
{:else if "in" in item}
<pre class="in">{item.in}</pre>
{:else if "tick" in item}
<div class="tick"><span class="icon">timer_play</span>{item.tick}ms</div>
{:else}
<div>Unknown log item at index {i}</div>
{/if}
{/each}
</details>
<style lang="scss">
details {
margin-top: 1rem;
border: 1px solid var(--md-sys-color-outline);
border-radius: 0.5rem;
background-color: var(--md-sys-color-surface-variant);
padding: 1rem;
}
.report {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
border-radius: 0.25rem;
background-color: var(--md-sys-color-primary-container);
padding: 0.5rem;
color: var(--md-sys-color-on-primary-container);
}
.out {
color: var(--md-sys-color-primary);
}
.in {
color: var(--md-sys-color-secondary);
}
.tick {
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 0.25rem;
padding: 0.2rem 0.5rem;
width: fit-content;
color: var(--md-sys-color-tertiary);
font-size: 0.6rem;
}
.icon {
font-size: inherit;
}
input[type="range"] {
margin-bottom: 1rem;
width: 100%;
}
</style>

View File

@@ -0,0 +1,49 @@
import type { PageLoad } from "./$types";
import { browser } from "$app/environment";
import { fromBase64 } from "$lib/serialization/base64";
export interface ReplaySerialIn {
in: string;
}
export interface ReplaySerialOut {
out: string;
}
export interface ReplaySerialReport {
modifiers: number;
keys: number[];
}
export interface ReplaySerialPress {
press: number;
}
export interface ReplaySerialRelease {
release: number;
}
export interface ReplayTick {
tick: number;
}
export type ReplayDataItem =
| ReplayTick
| ReplaySerialIn
| ReplaySerialOut
| ReplaySerialReport
| ReplaySerialPress
| ReplaySerialRelease;
export const load = (async ({ url, fetch }) => {
const replay = browser && new URLSearchParams(url.search).get("data");
if (!replay) {
return undefined;
}
const stream = (await fromBase64(replay, fetch))
.stream()
.pipeThrough(new DecompressionStream("deflate"));
return {
data: JSON.parse(await new Response(stream).text()) as ReplayDataItem[],
};
}) satisfies PageLoad;

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { deviceMeta } from "$lib/serial/connection";
import Demo from "./Demo.svelte";
let recipes = $derived(
$deviceMeta?.recipes?.toSorted((a, b) => {
if (a.demo == null) return 1;
if (b.demo == null) return -1;
return a.demo.title.localeCompare(b.demo.title);
}),
);
</script>
<div class="page">
<nav>
{#each recipes as demo, i}
{#if demo.demo?.title}
<a href="#demo-{i}">
{demo.demo?.title}
</a>
{/if}
{/each}
</nav>
<div class="recipes">
<h1>Cookbook</h1>
{#if recipes}
{#each recipes as demo, i}
<Demo {demo} {i} />
{/each}
{/if}
</div>
</div>
<style lang="scss">
.page {
display: flex;
flex-direction: row;
gap: 2rem;
padding: 1rem;
height: 100%;
}
.recipes {
display: flex;
flex-direction: column;
gap: 60px;
overflow-y: auto;
scroll-behavior: smooth;
}
nav {
display: flex;
position: sticky;
top: 1rem;
flex-direction: column;
align-self: flex-start;
gap: 0.5rem;
padding: 1rem;
min-width: 200px;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import type { E2eDemo } from "$lib/meta/types/meta";
import Recipe from "./Recipe.svelte";
let {
demo,
i,
}: {
demo: E2eDemo;
i: number;
} = $props();
let paused = $state(true);
let config: boolean[] = $state([]);
let test = $derived.by(() => {
if (!demo.matrix) return demo.tests[0];
const configuration = demo.matrix?.filter((_, i) => config[i]);
return demo.tests.find(
(test) => test && test.matrix?.toString() === configuration?.toString(),
);
});
</script>
<section
id={"demo-" + i}
onmouseenter={() => (paused = false)}
onmouseleave={() => (paused = true)}
>
{#if demo.demo}
<h2>{demo.demo?.title}</h2>
<p>{demo.demo?.description}</p>
{/if}
{#if demo.matrix}
<div class="configuration">
{#each demo.matrix as item, i}
<label><input type="checkbox" bind:checked={config[i]} />{item}</label>
{/each}
</div>
{/if}
{#if test}
<Recipe {test} {paused} />
{/if}
</section>
<style lang="scss">
section:target h2 {
color: var(--md-sys-color-primary);
}
section {
max-width: 20cm;
scroll-margin-top: 80px;
}
section > :global(:not(h2)) {
margin-inline-start: 24px;
}
.configuration {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
margin-block: 8px;
label {
background-color: var(--md-sys-color-surface-variant);
padding-inline: 8px;
padding-block: 4px;
height: auto;
color: var(--md-sys-color-on-surface-variant);
&:has(:checked) {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
input[type="checkbox"] {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,331 @@
<script lang="ts">
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
import { ReplayPlayer } from "$lib/charrecorder/core/player";
import type { Replay } from "$lib/charrecorder/core/types";
import ActionString from "$lib/components/ActionString.svelte";
import ChordPhraseDisplay from "$lib/components/ChordPhraseDisplay.svelte";
import type { E2eTest, E2eTestItem } from "$lib/meta/types/meta";
import { osLayout } from "$lib/os-layout";
import { deviceMeta } from "$lib/serial/connection";
import { KEYMAP_IDS } from "$lib/serial/keymap-codes";
import type { ChordInfo } from "$lib/undo-redo";
let { test, paused = false }: { test: E2eTest; paused?: boolean } = $props();
let timescale = $state(10);
let idleHold = $state(500);
let pressHold = $state(250);
let replayDelay = $state(1000);
let time = $state(0);
function getKeyRaw(key: string): string {
return $KEYMAP_IDS.get(key)?.keyCode ?? key;
}
const keyMap = new Map<string, string>(
Object.entries({
ENTER: "\n",
SPACE: " ",
TAB: "\t",
BKSP: "Backspace",
}),
);
function getKeyMapped(key: string, shift: boolean): string {
let value = $osLayout.get(getKeyRaw(key)) ?? key;
value = keyMap.get(value) ?? value;
return shift ? value.toUpperCase() : value;
}
function advanceTime(
step: E2eTestItem,
timescale: number,
idleHold: number,
): number {
return (
(step.skip ?? step.step ?? 1) * timescale +
(step.idle ? idleHold : 0) +
(step.press ? pressHold : 0)
);
}
let replay: Replay = $derived.by(() => {
const replay: Replay = {
start: 0,
finish: 0,
keys: [],
};
let timeIndex = 0;
let held = new Map<string, any>();
for (const it of test.test) {
if (it.keys) {
for (const key of it.keys) {
if (held.has(key)) continue;
const raw = getKeyRaw(key);
const mapped = getKeyMapped(key, it.modifiers?.["lshift"] == true);
held.set(key, [mapped, raw, timeIndex, 0]);
replay.keys.push(held.get(key));
}
for (const [key, value] of held) {
if (!it.keys.includes(key)) {
value[3] = timeIndex - value[2];
held.delete(key);
}
}
}
timeIndex += advanceTime(it, timescale, idleHold);
}
replay.finish = timeIndex;
return replay;
});
let graph = $derived.by(() => {
const rows: string[][] = [[]];
for (const it of test.test) {
if (it.keys?.includes("BKSP")) {
if (rows.at(-1)!.at(-1) === " ") {
rows.at(-1)!.pop();
} else {
rows.push(Array.from({ length: rows.at(-1)!.length - 1 }, () => " "));
}
} else {
for (const key of it.keys ?? []) {
if (key === "SPACE") {
rows.at(-1)!.push("␣");
} else if (key === "ENTER") {
rows.at(-1)!.push("↵");
} else {
rows
.at(-1)!
.push(getKeyMapped(key, it.modifiers?.["lshift"] == true));
}
}
}
}
return rows;
});
let chords = $derived(
test.test
.flatMap((step) => step.addChords ?? [])
.map(({ input, output }) => ({
input: input.map((chord) =>
chord.map((it) => $KEYMAP_IDS.get(it)?.code ?? 0),
),
output: output.map((it) => $KEYMAP_IDS.get(it)?.code ?? 0),
})),
);
let settings = $derived(
test.test
.flatMap((step) => (step.settings ? Object.entries(step.settings) : []))
.flatMap(([key, value]) => {
const category = $deviceMeta?.settings.find((it) => it.name === key);
return Object.entries(value).map(([subkey, subvalue]) => [
category?.items.find((it) => it.name === subkey),
subvalue,
]);
}),
);
let keysUsed = $derived.by(() => {
const keys = new Map<number, number[]>();
let time = 0;
for (const step of test.test) {
for (const key of step.press ?? []) {
const keyCode = $KEYMAP_IDS.get(key)?.code ?? 0;
if (!keys.has(keyCode)) {
keys.set(keyCode, []);
}
keys.get(keyCode)!.push(time);
}
for (const key of step.release ?? []) {
const keyCode = $KEYMAP_IDS.get(key)?.code ?? 0;
keys.get(keyCode)!.push(time);
}
time += advanceTime(step, timescale, idleHold);
}
return keys;
});
function isKeyPressed(times: number[], time: number): boolean {
return (
times.findIndex(
(t, i, arr) => time >= t && (arr[i + 1] ?? Infinity) > time,
) %
2 ===
0
);
}
</script>
<div class="replay">
<CharRecorder
{replay}
{paused}
cursor={true}
keys={false}
ondone={() => setTimeout(() => (replay = { ...replay }), replayDelay)}
ontick={(t) => (time = t)}
/>
</div>
<div class="keystaff">
{#each keysUsed as [action, times]}
<div class="keystaff-item" class:active={isKeyPressed(times, time)}>
<ActionString actions={[action]} />
</div>
{/each}
</div>
<details>
<div class="graph">
{#each graph as row, i}
{#each row as char, j}
{#if char !== " "}
<div
class:deleted={(graph[i + 1]?.findIndex((it) => it !== " ") ??
Infinity) <= j}
style:grid-row={i + 1}
style:grid-column={j + 1}
>
{char}
</div>
{/if}
{/each}
{/each}
</div>
{#if chords.length > 0}
<h3>Chords Used</h3>
<div class="chords">
{#each chords as { input, output }}
<div class="input">
{#each input as actions}
<div class="compound">
<ActionString display="keys" {actions} />
</div>
{/each}
</div>
<div class="output">
<ActionString actions={output} />
</div>
{/each}
</div>
{/if}
{#if settings.length > 0}
<h3>Settings Changed</h3>
<ul>
{#each settings as [item, value]}
<li>
{item?.name ?? "Unknown Setting"}: {value?.toString()}
</li>
{/each}
</ul>
{/if}
</details>
<style lang="scss">
.chords {
display: grid;
column-gap: 1rem;
align-items: center;
justify-items: center;
width: fit-content;
.compound {
display: inline-flex;
gap: 2px;
}
.input {
display: flex;
grid-column: 1;
align-items: center;
justify-self: center;
gap: 8px;
}
.output {
grid-column: 2;
justify-self: start;
}
}
.keystaff {
$expo-out: cubic-bezier(0.16, 1, 0.3, 1);
$time: 1s;
display: flex;
gap: 4px;
width: fit-content;
.keystaff-item {
display: flex;
justify-content: center;
align-items: center;
translate: 0 -8px;
opacity: 0;
transition:
opacity $time $expo-out,
translate $time $expo-out;
border-radius: 4px;
width: 100%;
height: 24px;
&.active {
translate: 0;
opacity: 1;
transition: none;
}
}
}
.details {
position: absolute;
transform-origin: top;
scale: 1 0.5;
z-index: 1;
margin-left: -17px;
border: 1px solid var(--md-sys-color-outline);
border-top: none;
background-color: var(--md-sys-color-surface);
padding: 16px;
width: calc(100% + 2px);
}
summary {
cursor: pointer;
margin-top: 0.5rem;
font-weight: bold;
user-select: none;
}
.replay {
border-radius: 0.4rem;
background: var(--md-sys-color-surface-variant);
padding: 0.6rem;
width: fit-content;
color: var(--md-sys-color-on-surface-variant);
font-weight: bold;
font-size: 1.2rem;
}
.graph {
display: grid;
align-items: center;
justify-items: center;
width: min-content;
}
.deleted {
opacity: 0.6;
text-decoration: line-through;
}
</style>

View File

@@ -1,28 +0,0 @@
<script lang="ts">
import Recipe from "./Recipe.svelte";
const tests = import.meta.glob("$lib/assets/tests/**/*.yml");
</script>
<h1>Recipes</h1>
<p>These are example uses, taken directly from our E2E testing library.</p>
<div class="recipes">
{#each Object.entries(tests) as [path, resolver]}
{#await resolver() then module}
<section>
<Recipe test={module.default} />
</section>
{/await}
{/each}
</div>
<style lang="scss">
.recipes {
display: flex;
flex-direction: column;
gap: 2rem;
overflow-y: auto;
}
</style>

View File

@@ -1,148 +0,0 @@
<script lang="ts">
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
import type { Replay } from "$lib/charrecorder/core/types";
import type { E2eTest } from "./test-types";
let { test }: { test: E2eTest } = $props();
const replace = new Map<string, string>([
["SPACE", "Space"],
["ENTER", "Enter"],
["BKSP", "Backspace"],
["e", "KeyE"],
["t", "KeyT"],
]);
const replaceOut = new Map<string, string>([
["SPACE", " "],
["ENTER", "\n"],
["BKSP", "Backspace"],
]);
let timescale = $state(10);
let idleHold = $state(500);
let replayDelay = $state(1000);
let replay: Replay = $derived.by(() => {
const replay: Replay = {
start: 0,
finish: 0,
keys: [],
};
let timeIndex = 0;
let held = new Map<string, any>();
for (const it of test.test) {
if (it.keys) {
for (const key of it.keys) {
if (held.has(key)) continue;
held.set(key, [
replaceOut.get(key) ?? key,
replace.get(key) ?? key,
timeIndex,
0,
]);
replay.keys.push(held.get(key));
}
for (const [key, value] of held) {
if (!it.keys.includes(key)) {
value[3] = timeIndex - value[2];
held.delete(key);
}
}
}
timeIndex += timescale * (it.step ?? 1);
if (it.idle) {
timeIndex += idleHold;
}
}
replay.finish = timeIndex;
return replay;
});
let graph = $derived.by(() => {
const rows: string[][] = [[]];
for (const it of test.test) {
if (it.keys?.includes("BKSP")) {
if (rows.at(-1)!.at(-1) === " ") {
rows.at(-1)!.pop();
} else {
rows.push(Array.from({ length: rows.at(-1)!.length - 1 }, () => " "));
}
} else {
for (const key of it.keys ?? []) {
if (key === "SPACE") {
rows.at(-1)!.push("␣");
} else if (key === "ENTER") {
rows.at(-1)!.push("↵");
} else {
rows.at(-1)!.push(key);
}
}
}
}
return rows;
});
</script>
<section>
<div class="replay">
<CharRecorder
{replay}
cursor={true}
ondone={() => setTimeout(() => (replay = { ...replay }), replayDelay)}
/>
</div>
<details>
<summary>Breakdown</summary>
<div class="graph">
{#each graph as row, i}
{#each row as char, j}
{#if char !== " "}
<div
class:deleted={(graph[i + 1]?.findIndex((it) => it !== " ") ??
Infinity) <= j}
style:grid-row={i + 1}
style:grid-column={j + 1}
>
{char}
</div>
{/if}
{/each}
{/each}
</div>
</details>
</section>
<style lang="scss">
section {
font-family: monospace;
}
summary {
cursor: pointer;
margin-top: 0.5rem;
font-weight: bold;
user-select: none;
}
.replay {
background: #f0f0f0;
padding: 0.6rem;
font-weight: bold;
font-size: 1.2rem;
}
.graph {
display: grid;
align-items: center;
justify-items: center;
width: min-content;
}
.deleted {
opacity: 0.6;
text-decoration: line-through;
}
</style>

View File

@@ -1,20 +0,0 @@
export interface E2eAddChord {
input: string[][];
output: string[];
}
export interface E2eTestItem {
keys?: string[];
modifiers?: Record<string, boolean>;
press?: string[];
release?: string[];
step?: number;
idle?: boolean;
clearChords?: boolean;
addChords?: E2eAddChord[];
settings: Record<string, Record<string, string | number>>;
}
export interface E2eTest {
test: E2eTestItem[];
}